Sfoglia il codice sorgente

CMake GUI: Add environment editor

Kyle Edwards 5 anni fa
parent
commit
85f5009d27

+ 4 - 0
Help/release/dev/cmake-gui-environment.rst

@@ -0,0 +1,4 @@
+cmake-gui-environment
+---------------------
+
+* The :manual:`CMake GUI <cmake-gui(1)>` now has an environment variable editor.

+ 4 - 0
Source/QtDialog/CMakeLists.txt

@@ -84,6 +84,8 @@ set(SRCS
   CMakeSetupDialog.cxx
   CMakeSetupDialog.h
   Compilers.h
+  EnvironmentDialog.cxx
+  EnvironmentDialog.h
   FirstConfigure.cxx
   FirstConfigure.h
   QCMake.cxx
@@ -102,6 +104,7 @@ qt5_wrap_ui(UI_SRCS
   Compilers.ui
   CrossCompiler.ui
   AddCacheEntry.ui
+  EnvironmentDialog.ui
   RegexExplorer.ui
   WarningMessagesDialog.ui
   )
@@ -109,6 +112,7 @@ qt5_wrap_cpp(MOC_SRCS
   AddCacheEntry.h
   Compilers.h
   CMakeSetupDialog.h
+  EnvironmentDialog.h
   FirstConfigure.h
   QCMake.h
   QCMakeCacheView.h

+ 17 - 0
Source/QtDialog/CMakeSetupDialog.cxx

@@ -16,6 +16,7 @@
 #include <QMenuBar>
 #include <QMessageBox>
 #include <QMimeData>
+#include <QProcessEnvironment>
 #include <QProgressBar>
 #include <QSettings>
 #include <QShortcut>
@@ -35,6 +36,7 @@
 #include "cmVersion.h"
 
 #include "AddCacheEntry.h"
+#include "EnvironmentDialog.h"
 #include "FirstConfigure.h"
 #include "RegexExplorer.h"
 #include "WarningMessagesDialog.h"
@@ -292,6 +294,9 @@ void CMakeSetupDialog::initialize()
   QObject::connect(this->AddEntry, &QToolButton::clicked, this,
                    &CMakeSetupDialog::addCacheEntry);
 
+  QObject::connect(this->Environment, &QToolButton::clicked, this,
+                   &CMakeSetupDialog::editEnvironment);
+
   QObject::connect(this->WarnUninitializedAction, &QAction::triggered,
                    this->CMakeThread->cmakeInstance(),
                    &QCMake::setWarnUninitializedMode);
@@ -742,6 +747,7 @@ void CMakeSetupDialog::setEnabledState(bool enabled)
   this->ConfigureAction->setEnabled(enabled);
   this->AddEntry->setEnabled(enabled);
   this->RemoveEntry->setEnabled(false); // let selection re-enable it
+  this->Environment->setEnabled(enabled);
 }
 
 bool CMakeSetupDialog::setupFirstConfigure()
@@ -1075,6 +1081,17 @@ void CMakeSetupDialog::enterState(CMakeSetupDialog::State s)
   }
 }
 
+void CMakeSetupDialog::editEnvironment()
+{
+  EnvironmentDialog dialog(this->CMakeThread->cmakeInstance()->environment(),
+                           this);
+  if (dialog.exec() == QDialog::Accepted) {
+    QMetaObject::invokeMethod(
+      this->CMakeThread->cmakeInstance(), "setEnvironment",
+      Q_ARG(QProcessEnvironment, dialog.environment()));
+  }
+}
+
 void CMakeSetupDialog::addCacheEntry()
 {
   QDialog dialog(this);

+ 1 - 0
Source/QtDialog/CMakeSetupDialog.h

@@ -65,6 +65,7 @@ protected slots:
   void setCacheModified();
   void removeSelectedCacheEntries();
   void selectionChanged();
+  void editEnvironment();
   void addCacheEntry();
   void startSearch();
   void setDebugOutput(bool);

+ 64 - 12
Source/QtDialog/CMakeSetupDialog.ui

@@ -11,7 +11,16 @@
    </rect>
   </property>
   <layout class="QGridLayout">
-   <property name="margin">
+   <property name="leftMargin">
+    <number>9</number>
+   </property>
+   <property name="topMargin">
+    <number>9</number>
+   </property>
+   <property name="rightMargin">
+    <number>9</number>
+   </property>
+   <property name="bottomMargin">
     <number>9</number>
    </property>
    <property name="spacing">
@@ -19,7 +28,16 @@
    </property>
    <item row="0" column="0">
     <layout class="QGridLayout">
-     <property name="margin">
+     <property name="leftMargin">
+      <number>0</number>
+     </property>
+     <property name="topMargin">
+      <number>0</number>
+     </property>
+     <property name="rightMargin">
+      <number>0</number>
+     </property>
+     <property name="bottomMargin">
       <number>0</number>
      </property>
      <property name="spacing">
@@ -90,7 +108,16 @@
        <property name="spacing">
         <number>6</number>
        </property>
-       <property name="margin">
+       <property name="leftMargin">
+        <number>0</number>
+       </property>
+       <property name="topMargin">
+        <number>0</number>
+       </property>
+       <property name="rightMargin">
+        <number>0</number>
+       </property>
+       <property name="bottomMargin">
         <number>0</number>
        </property>
        <item>
@@ -98,7 +125,16 @@
          <property name="spacing">
           <number>6</number>
          </property>
-         <property name="margin">
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
           <number>0</number>
          </property>
          <item>
@@ -191,6 +227,13 @@
            </property>
           </widget>
          </item>
+         <item>
+          <widget class="QPushButton" name="Environment">
+           <property name="text">
+            <string>E&amp;nvironment...</string>
+           </property>
+          </widget>
+         </item>
         </layout>
        </item>
        <item>
@@ -224,7 +267,16 @@
          <property name="spacing">
           <number>6</number>
          </property>
-         <property name="margin">
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
           <number>0</number>
          </property>
          <item>
@@ -241,13 +293,13 @@
            </property>
           </widget>
          </item>
-          <item>
-            <widget class="QPushButton" name="OpenProjectButton">
-              <property name="text">
-                <string>Open &amp;Project</string>
-              </property>
-            </widget>
-          </item>
+         <item>
+          <widget class="QPushButton" name="OpenProjectButton">
+           <property name="text">
+            <string>Open &amp;Project</string>
+           </property>
+          </widget>
+         </item>
          <item>
           <widget class="QLabel" name="Generator">
            <property name="text">

+ 194 - 0
Source/QtDialog/EnvironmentDialog.cxx

@@ -0,0 +1,194 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "EnvironmentDialog.h"
+
+#include <QDialogButtonBox>
+#include <QGridLayout>
+#include <QItemSelectionModel>
+#include <QLabel>
+#include <QLineEdit>
+#include <QMessageBox>
+#include <QStandardItem>
+
+EnvironmentItemModel::EnvironmentItemModel(
+  const QProcessEnvironment& environment, QObject* parent)
+  : QStandardItemModel(parent)
+{
+  this->clear();
+  for (auto const& key : environment.keys()) {
+    auto value = environment.value(key);
+    this->appendVariable(key, value);
+  }
+}
+
+QProcessEnvironment EnvironmentItemModel::environment() const
+{
+  QProcessEnvironment env;
+  for (int i = 0; i < this->rowCount(); ++i) {
+    auto name = this->data(this->index(i, 0), Qt::DisplayRole).toString();
+    auto value = this->data(this->index(i, 1), Qt::DisplayRole).toString();
+    env.insert(name, value);
+  }
+  return env;
+}
+
+void EnvironmentItemModel::clear()
+{
+  this->QStandardItemModel::clear();
+
+  QStringList labels;
+  labels << tr("Name") << tr("Value");
+  this->setHorizontalHeaderLabels(labels);
+}
+
+QModelIndex EnvironmentItemModel::buddy(const QModelIndex& index) const
+{
+  if (index.column() == 0) {
+    return this->index(index.row(), index.column() + 1, index.parent());
+  }
+  return index;
+}
+
+void EnvironmentItemModel::appendVariable(const QString& key,
+                                          const QString& value)
+{
+  this->insertVariable(this->rowCount(), key, value);
+}
+
+void EnvironmentItemModel::insertVariable(int row, const QString& key,
+                                          const QString& value)
+{
+  for (int i = 0; i < this->rowCount(); ++i) {
+    if (this->data(this->index(i, 0), Qt::DisplayRole) == key) {
+      this->setData(this->index(i, 1), value, Qt::DisplayRole);
+      return;
+    }
+  }
+
+  auto* keyItem = new QStandardItem(key);
+  auto* valueItem = new QStandardItem(value);
+  this->insertRow(row, { keyItem, valueItem });
+}
+
+EnvironmentSearchFilter::EnvironmentSearchFilter(QObject* parent)
+  : QSortFilterProxyModel(parent)
+{
+}
+
+bool EnvironmentSearchFilter::filterAcceptsRow(int row,
+                                               const QModelIndex& parent) const
+{
+  auto* model = this->sourceModel();
+  auto key =
+    model->data(model->index(row, 0, parent), Qt::DisplayRole).toString();
+  return key.contains(this->filterRegExp());
+}
+
+EnvironmentDialog::EnvironmentDialog(const QProcessEnvironment& environment,
+                                     QWidget* parent)
+  : QDialog(parent)
+{
+  this->setupUi(this);
+
+  this->RemoveEntry->setEnabled(false);
+
+  this->m_model = new EnvironmentItemModel(environment, this);
+  this->m_filter = new EnvironmentSearchFilter(this);
+  this->m_filter->setSourceModel(this->m_model);
+  this->Environment->setModel(this->m_filter);
+
+  this->Environment->setUniformRowHeights(true);
+  this->Environment->setRootIsDecorated(false);
+  this->Environment->setSelectionMode(QAbstractItemView::ExtendedSelection);
+  this->Environment->setSelectionBehavior(QAbstractItemView::SelectRows);
+
+  QObject::connect(this->AddEntry, &QToolButton::clicked, this,
+                   &EnvironmentDialog::addEntry);
+  QObject::connect(this->RemoveEntry, &QToolButton::clicked, this,
+                   &EnvironmentDialog::removeSelectedEntries);
+  QObject::connect(this->Search, &QLineEdit::textChanged, this->m_filter,
+                   &EnvironmentSearchFilter::setFilterFixedString);
+  QObject::connect(this->Environment->selectionModel(),
+                   &QItemSelectionModel::selectionChanged, this,
+                   &EnvironmentDialog::selectionChanged);
+}
+
+QProcessEnvironment EnvironmentDialog::environment() const
+{
+  return this->m_model->environment();
+}
+
+void EnvironmentDialog::addEntry()
+{
+  // Build the dialog manually because it's simple enough
+  QDialog dialog(this);
+  dialog.setWindowTitle("Add Environment Variable");
+
+  auto* layout = new QGridLayout;
+  dialog.setLayout(layout);
+
+  auto* nameLabel = new QLabel;
+  nameLabel->setText("Name:");
+  layout->addWidget(nameLabel, 0, 0);
+
+  auto* nameEdit = new QLineEdit;
+  nameEdit->setObjectName("name");
+  layout->addWidget(nameEdit, 0, 1);
+
+  auto* valueLabel = new QLabel;
+  valueLabel->setText("Value:");
+  layout->addWidget(valueLabel, 1, 0);
+
+  auto* valueEdit = new QLineEdit;
+  valueEdit->setObjectName("value");
+  layout->addWidget(valueEdit, 1, 1);
+
+  auto* buttons = new QDialogButtonBox;
+  buttons->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+  QObject::connect(
+    buttons, &QDialogButtonBox::accepted, &dialog,
+    [this, &dialog, nameEdit]() {
+      auto text = nameEdit->text();
+      if (text.isEmpty()) {
+        QMessageBox::critical(&dialog, "Error", "Name must be non-empty.");
+        return;
+      }
+
+      auto* model = this->Environment->model();
+      for (int i = 0; i < model->rowCount(); ++i) {
+        if (model->data(model->index(i, 0), Qt::DisplayRole) == text) {
+          QMessageBox::critical(
+            &dialog, "Error",
+            tr("Environment variable \"%1\" already exists.").arg(text));
+          return;
+        }
+      }
+
+      dialog.accept();
+    });
+  QObject::connect(buttons, &QDialogButtonBox::rejected, &dialog,
+                   &QDialog::reject);
+  layout->addWidget(buttons, 2, 0, 1, 2);
+
+  if (dialog.exec() == QDialog::Accepted) {
+    this->m_model->insertVariable(0, nameEdit->text(), valueEdit->text());
+  }
+}
+
+void EnvironmentDialog::removeSelectedEntries()
+{
+  QModelIndexList idxs = this->Environment->selectionModel()->selectedRows();
+  QList<QPersistentModelIndex> pidxs;
+  foreach (QModelIndex const& i, idxs) {
+    pidxs.append(i);
+  }
+  foreach (QPersistentModelIndex const& pi, pidxs) {
+    this->Environment->model()->removeRow(pi.row(), pi.parent());
+  }
+}
+
+void EnvironmentDialog::selectionChanged()
+{
+  auto selected = this->Environment->selectionModel()->selectedRows();
+  this->RemoveEntry->setEnabled(!selected.isEmpty());
+}

+ 59 - 0
Source/QtDialog/EnvironmentDialog.h

@@ -0,0 +1,59 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include <QDialog>
+#include <QObject>
+#include <QProcessEnvironment>
+#include <QSortFilterProxyModel>
+#include <QStandardItemModel>
+
+#include "ui_EnvironmentDialog.h"
+
+class EnvironmentItemModel : public QStandardItemModel
+{
+  Q_OBJECT
+public:
+  EnvironmentItemModel(const QProcessEnvironment& environment,
+                       QObject* parent = nullptr);
+
+  QProcessEnvironment environment() const;
+  void clear();
+
+  QModelIndex buddy(const QModelIndex& index) const override;
+
+public slots:
+  void appendVariable(const QString& key, const QString& value);
+  void insertVariable(int row, const QString& key, const QString& value);
+};
+
+class EnvironmentSearchFilter : public QSortFilterProxyModel
+{
+  Q_OBJECT
+public:
+  EnvironmentSearchFilter(QObject* parent = nullptr);
+
+protected:
+  bool filterAcceptsRow(int row, const QModelIndex& parent) const override;
+};
+
+class EnvironmentDialog
+  : public QDialog
+  , public Ui::EnvironmentDialog
+{
+  Q_OBJECT
+public:
+  EnvironmentDialog(const QProcessEnvironment& environment,
+                    QWidget* parent = nullptr);
+
+  QProcessEnvironment environment() const;
+
+protected slots:
+  void addEntry();
+  void removeSelectedEntries();
+  void selectionChanged();
+
+private:
+  EnvironmentItemModel* m_model;
+  EnvironmentSearchFilter* m_filter;
+};

+ 130 - 0
Source/QtDialog/EnvironmentDialog.ui

@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>EnvironmentDialog</class>
+ <widget class="QDialog" name="EnvironmentDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Environment Editor</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>S&amp;earch:</string>
+       </property>
+       <property name="buddy">
+        <cstring>Search</cstring>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLineEdit" name="Search"/>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeType">
+        <enum>QSizePolicy::Minimum</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>12</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QToolButton" name="AddEntry">
+       <property name="text">
+        <string>&amp;Add Entry</string>
+       </property>
+       <property name="icon">
+        <iconset resource="CMakeSetup.qrc">
+         <normaloff>:/Icons/Plus16.png</normaloff>:/Icons/Plus16.png</iconset>
+       </property>
+       <property name="toolButtonStyle">
+        <enum>Qt::ToolButtonTextBesideIcon</enum>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QToolButton" name="RemoveEntry">
+       <property name="text">
+        <string>&amp;Remove Entry</string>
+       </property>
+       <property name="icon">
+        <iconset resource="CMakeSetup.qrc">
+         <normaloff>:/Icons/Delete16.png</normaloff>:/Icons/Delete16.png</iconset>
+       </property>
+       <property name="toolButtonStyle">
+        <enum>Qt::ToolButtonTextBesideIcon</enum>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QTreeView" name="Environment"/>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources>
+  <include location="CMakeSetup.qrc"/>
+ </resources>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>EnvironmentDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>EnvironmentDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>

+ 62 - 25
Source/QtDialog/QCMake.cxx

@@ -19,10 +19,12 @@
 
 QCMake::QCMake(QObject* p)
   : QObject(p)
+  , Environment(QProcessEnvironment::systemEnvironment())
 {
   this->WarnUninitializedMode = false;
   qRegisterMetaType<QCMakeProperty>();
   qRegisterMetaType<QCMakePropertyList>();
+  qRegisterMetaType<QProcessEnvironment>();
 
   cmSystemTools::DisableRunCommandOutput();
   cmSystemTools::SetRunCommandHideConsole(true);
@@ -151,34 +153,46 @@ void QCMake::setToolset(const QString& toolset)
   }
 }
 
+void QCMake::setEnvironment(const QProcessEnvironment& environment)
+{
+  this->Environment = environment;
+}
+
 void QCMake::configure()
 {
+  int err;
+  {
+    cmSystemTools::SaveRestoreEnvironment restoreEnv;
+    this->setUpEnvironment();
+
 #ifdef Q_OS_WIN
-  UINT lastErrorMode = SetErrorMode(0);
+    UINT lastErrorMode = SetErrorMode(0);
 #endif
 
-  this->CMakeInstance->SetHomeDirectory(
-    this->SourceDirectory.toLocal8Bit().data());
-  this->CMakeInstance->SetHomeOutputDirectory(
-    this->BinaryDirectory.toLocal8Bit().data());
-  this->CMakeInstance->SetGlobalGenerator(
-    this->CMakeInstance->CreateGlobalGenerator(
-      this->Generator.toLocal8Bit().data()));
-  this->CMakeInstance->SetGeneratorPlatform(
-    this->Platform.toLocal8Bit().data());
-  this->CMakeInstance->SetGeneratorToolset(this->Toolset.toLocal8Bit().data());
-  this->CMakeInstance->LoadCache();
-  this->CMakeInstance->SetWarnUninitialized(this->WarnUninitializedMode);
-  this->CMakeInstance->PreLoadCMakeFiles();
-
-  InterruptFlag = 0;
-  cmSystemTools::ResetErrorOccuredFlag();
-
-  int err = this->CMakeInstance->Configure();
+    this->CMakeInstance->SetHomeDirectory(
+      this->SourceDirectory.toLocal8Bit().data());
+    this->CMakeInstance->SetHomeOutputDirectory(
+      this->BinaryDirectory.toLocal8Bit().data());
+    this->CMakeInstance->SetGlobalGenerator(
+      this->CMakeInstance->CreateGlobalGenerator(
+        this->Generator.toLocal8Bit().data()));
+    this->CMakeInstance->SetGeneratorPlatform(
+      this->Platform.toLocal8Bit().data());
+    this->CMakeInstance->SetGeneratorToolset(
+      this->Toolset.toLocal8Bit().data());
+    this->CMakeInstance->LoadCache();
+    this->CMakeInstance->SetWarnUninitialized(this->WarnUninitializedMode);
+    this->CMakeInstance->PreLoadCMakeFiles();
+
+    InterruptFlag = 0;
+    cmSystemTools::ResetErrorOccuredFlag();
+
+    err = this->CMakeInstance->Configure();
 
 #ifdef Q_OS_WIN
-  SetErrorMode(lastErrorMode);
+    SetErrorMode(lastErrorMode);
 #endif
+  }
 
   emit this->propertiesChanged(this->properties());
   emit this->configureDone(err);
@@ -186,18 +200,24 @@ void QCMake::configure()
 
 void QCMake::generate()
 {
+  int err;
+  {
+    cmSystemTools::SaveRestoreEnvironment restoreEnv;
+    this->setUpEnvironment();
+
 #ifdef Q_OS_WIN
-  UINT lastErrorMode = SetErrorMode(0);
+    UINT lastErrorMode = SetErrorMode(0);
 #endif
 
-  InterruptFlag = 0;
-  cmSystemTools::ResetErrorOccuredFlag();
+    InterruptFlag = 0;
+    cmSystemTools::ResetErrorOccuredFlag();
 
-  int err = this->CMakeInstance->Generate();
+    err = this->CMakeInstance->Generate();
 
 #ifdef Q_OS_WIN
-  SetErrorMode(lastErrorMode);
+    SetErrorMode(lastErrorMode);
 #endif
+  }
 
   emit this->generateDone(err);
   checkOpenPossible();
@@ -373,6 +393,18 @@ void QCMake::stderrCallback(std::string const& msg)
   QCoreApplication::processEvents();
 }
 
+void QCMake::setUpEnvironment() const
+{
+  auto env = QProcessEnvironment::systemEnvironment();
+  for (auto const& key : env.keys()) {
+    cmSystemTools::UnsetEnv(key.toLocal8Bit().data());
+  }
+
+  for (auto const& var : this->Environment.toStringList()) {
+    cmSystemTools::PutEnv(var.toLocal8Bit().data());
+  }
+}
+
 QString QCMake::binaryDirectory() const
 {
   return this->BinaryDirectory;
@@ -388,6 +420,11 @@ QString QCMake::generator() const
   return this->Generator;
 }
 
+QProcessEnvironment QCMake::environment() const
+{
+  return this->Environment;
+}
+
 std::vector<cmake::GeneratorInfo> const& QCMake::availableGenerators() const
 {
   return AvailableGenerators;

+ 8 - 0
Source/QtDialog/QCMake.h

@@ -18,6 +18,7 @@
 #include <QList>
 #include <QMetaType>
 #include <QObject>
+#include <QProcessEnvironment>
 #include <QString>
 #include <QStringList>
 #include <QVariant>
@@ -55,6 +56,7 @@ using QCMakePropertyList = QList<QCMakeProperty>;
 // allow QVariant to be a property or list of properties
 Q_DECLARE_METATYPE(QCMakeProperty)
 Q_DECLARE_METATYPE(QCMakePropertyList)
+Q_DECLARE_METATYPE(QProcessEnvironment)
 
 /// Qt API for CMake library.
 /// Wrapper like class allows for easier integration with
@@ -78,6 +80,8 @@ public slots:
   void setPlatform(const QString& platform);
   /// set the desired generator to use
   void setToolset(const QString& toolset);
+  /// set the configure and generate environment
+  void setEnvironment(const QProcessEnvironment& environment);
   /// do the configure step
   void configure();
   /// generate the files
@@ -125,6 +129,8 @@ public:
   QString sourceDirectory() const;
   /// get the current generator
   QString generator() const;
+  /// get the configure and generate environment
+  QProcessEnvironment environment() const;
   /// get the available generators
   std::vector<cmake::GeneratorInfo> const& availableGenerators() const;
   /// get whether to do debug output
@@ -170,6 +176,7 @@ protected:
   void messageCallback(std::string const& msg, const char* title);
   void stdoutCallback(std::string const& msg);
   void stderrCallback(std::string const& msg);
+  void setUpEnvironment() const;
 
   bool WarnUninitializedMode;
   QString SourceDirectory;
@@ -180,4 +187,5 @@ protected:
   std::vector<cmake::GeneratorInfo> AvailableGenerators;
   QString CMakeExecutable;
   QAtomicInt InterruptFlag;
+  QProcessEnvironment Environment;
 };

+ 6 - 0
Tests/CMakeGUI/CMakeGUITest.cmake

@@ -112,3 +112,9 @@ run_cmake_gui_test(sourceBinaryArgs:noExistConfigExists
 
 run_cmake_gui_test(simpleConfigure:success)
 run_cmake_gui_test(simpleConfigure:fail)
+
+unset(ENV{ADDED_VARIABLE})
+set(ENV{KEPT_VARIABLE} "Kept variable")
+set(ENV{CHANGED_VARIABLE} "This variable will be changed")
+set(ENV{REMOVED_VARIABLE} "Removed variable")
+run_cmake_gui_test(environment)

+ 29 - 0
Tests/CMakeGUI/CMakeGUITest.cxx

@@ -143,6 +143,35 @@ void CMakeGUITest::simpleConfigure_data()
                         << -1;
 }
 
+void CMakeGUITest::environment()
+{
+  auto* cmake = this->m_window->findChild<QCMakeThread*>()->cmakeInstance();
+
+  this->m_window->SourceDirectory->setText(CMakeGUITest_BINARY_DIR
+                                           "/environment/src");
+  this->m_window->BinaryDirectory->setCurrentText(CMakeGUITest_BINARY_DIR
+                                                  "/environment/build");
+
+  // We are already testing EnvironmentDialog, so just trust that it's
+  // connected correctly and modify the environment directly.
+  auto env = cmake->environment();
+  env.insert("ADDED_VARIABLE", "Added variable");
+  env.insert("CHANGED_VARIABLE", "Changed variable");
+  env.remove("REMOVED_VARIABLE");
+  cmake->setEnvironment(env);
+
+  // Wait a bit for everything to update
+  loopSleep();
+
+  this->tryConfigure();
+
+  auto penv = QProcessEnvironment::systemEnvironment();
+  QVERIFY(!penv.contains("ADDED_VARIABLE"));
+  QCOMPARE(penv.value("KEPT_VARIABLE"), "Kept variable");
+  QCOMPARE(penv.value("CHANGED_VARIABLE"), "This variable will be changed");
+  QCOMPARE(penv.value("REMOVED_VARIABLE"), "Removed variable");
+}
+
 void SetupDefaultQSettings()
 {
   QSettings::setDefaultFormat(QSettings::IniFormat);

+ 1 - 0
Tests/CMakeGUI/CMakeGUITest.h

@@ -22,4 +22,5 @@ private slots:
   void sourceBinaryArgs_data();
   void simpleConfigure();
   void simpleConfigure_data();
+  void environment();
 };

+ 7 - 0
Tests/CMakeGUI/CMakeLists.txt

@@ -58,6 +58,13 @@ add_cmake_gui_lib_test(CatchShow
   MOC_SOURCES
     CatchShowTest.h
   )
+add_cmake_gui_lib_test(EnvironmentDialog
+  SOURCES
+    EnvironmentDialogTest.cxx
+    EnvironmentDialogTest.h
+  MOC_SOURCES
+    EnvironmentDialogTest.h
+  )
 add_cmake_gui_lib_test(QCMakeCacheModel
   SOURCES
     QCMakeCacheModelTest.cxx

+ 142 - 0
Tests/CMakeGUI/EnvironmentDialogTest.cxx

@@ -0,0 +1,142 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "EnvironmentDialogTest.h"
+
+#include <QDialogButtonBox>
+#include <QMessageBox>
+#include <QObject>
+#include <QPushButton>
+#include <QString>
+#include <QtTest>
+
+#include "CatchShow.h"
+#include "EnvironmentDialog.h"
+
+EnvironmentDialogTest::EnvironmentDialogTest(QObject* parent)
+  : QObject(parent)
+{
+}
+
+void EnvironmentDialogTest::environmentDialog()
+{
+  CatchShow catcher;
+  catcher.setCallback<QMessageBox>([](QMessageBox* box) { box->accept(); });
+
+  QProcessEnvironment env;
+  env.insert("DELETED_VARIABLE_1", "Deleted variable 1");
+  env.insert("DELETED_VARIABLE_2", "Deleted variable 2");
+  env.insert("KEPT_VARIABLE", "Kept variable");
+  env.insert("CHANGED_VARIABLE", "This will be changed");
+
+  EnvironmentDialog dialog(env);
+
+  {
+    QStringList expected{
+      "CHANGED_VARIABLE=This will be changed",
+      "DELETED_VARIABLE_1=Deleted variable 1",
+      "DELETED_VARIABLE_2=Deleted variable 2",
+      "KEPT_VARIABLE=Kept variable",
+    };
+    QCOMPARE(dialog.environment().toStringList(), expected);
+    QCOMPARE(catcher.count(), 0);
+  }
+
+  {
+    CatchShow catcher2;
+    bool done = false;
+    catcher2.setCallback<QDialog>([&catcher, &done](QDialog* box) {
+      if (done) {
+        return;
+      }
+      done = true;
+
+      auto name = box->findChild<QLineEdit*>("name");
+      auto value = box->findChild<QLineEdit*>("value");
+      auto acceptReject = box->findChild<QDialogButtonBox*>();
+
+      name->setText("");
+      value->setText("");
+      acceptReject->button(QDialogButtonBox::Ok)->click();
+      QCOMPARE(catcher.count(), 1);
+
+      name->setText("KEPT_VARIABLE");
+      value->setText("");
+      acceptReject->button(QDialogButtonBox::Ok)->click();
+      QCOMPARE(catcher.count(), 2);
+
+      name->setText("ADDED_VARIABLE");
+      value->setText("Added variable");
+      acceptReject->button(QDialogButtonBox::Ok)->click();
+      QCOMPARE(catcher.count(), 2);
+    });
+    dialog.AddEntry->click();
+
+    QStringList expected{
+      "ADDED_VARIABLE=Added variable",
+      "CHANGED_VARIABLE=This will be changed",
+      "DELETED_VARIABLE_1=Deleted variable 1",
+      "DELETED_VARIABLE_2=Deleted variable 2",
+      "KEPT_VARIABLE=Kept variable",
+    };
+    QCOMPARE(dialog.environment().toStringList(), expected);
+    QCOMPARE(catcher.count(), 2);
+    QVERIFY(done);
+  }
+
+  {
+    CatchShow catcher2;
+    bool done = false;
+    catcher2.setCallback<QDialog>([&done](QDialog* box) {
+      if (done) {
+        return;
+      }
+      done = true;
+
+      auto name = box->findChild<QLineEdit*>("name");
+      auto value = box->findChild<QLineEdit*>("value");
+      auto acceptReject = box->findChild<QDialogButtonBox*>();
+
+      name->setText("DISCARDED_VARIABLE");
+      value->setText("Discarded variable");
+      acceptReject->button(QDialogButtonBox::Cancel)->click();
+    });
+    dialog.AddEntry->click();
+
+    QStringList expected{
+      "ADDED_VARIABLE=Added variable",
+      "CHANGED_VARIABLE=This will be changed",
+      "DELETED_VARIABLE_1=Deleted variable 1",
+      "DELETED_VARIABLE_2=Deleted variable 2",
+      "KEPT_VARIABLE=Kept variable",
+    };
+    QCOMPARE(dialog.environment().toStringList(), expected);
+    QCOMPARE(catcher.count(), 2);
+    QVERIFY(done);
+  }
+
+  {
+    auto* model = dialog.Environment->model();
+    auto* selectionModel = dialog.Environment->selectionModel();
+    for (int i = 0; i < model->rowCount(); ++i) {
+      auto index1 = model->index(i, 0);
+      auto index2 = model->buddy(index1);
+      auto name = model->data(index1, Qt::DisplayRole).toString();
+      if (name == "DELETED_VARIABLE_1" || name == "DELETED_VARIABLE_2") {
+        selectionModel->select(index1, QItemSelectionModel::Select);
+        selectionModel->select(index2, QItemSelectionModel::Select);
+      } else if (name == "CHANGED_VARIABLE") {
+        model->setData(index2, "Changed variable", Qt::DisplayRole);
+      }
+    }
+    dialog.RemoveEntry->click();
+
+    QStringList expected{
+      "ADDED_VARIABLE=Added variable",
+      "CHANGED_VARIABLE=Changed variable",
+      "KEPT_VARIABLE=Kept variable",
+    };
+    QCOMPARE(dialog.environment().toStringList(), expected);
+  }
+}
+
+QTEST_MAIN(EnvironmentDialogTest)

+ 15 - 0
Tests/CMakeGUI/EnvironmentDialogTest.h

@@ -0,0 +1,15 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include <QObject>
+
+class EnvironmentDialogTest : public QObject
+{
+  Q_OBJECT
+public:
+  EnvironmentDialogTest(QObject* parent = nullptr);
+
+private slots:
+  void environmentDialog();
+};

+ 18 - 0
Tests/CMakeGUI/environment/CMakeLists.txt.in

@@ -0,0 +1,18 @@
+cmake_minimum_required(VERSION 3.18)
+project(environment NONE)
+
+if(NOT "$ENV{KEPT_VARIABLE}" STREQUAL "Kept variable")
+  message(SEND_ERROR "KEPT_VARIABLE is \"$ENV{KEPT_VARIABLE}\", should be \"Kept variable\"")
+endif()
+
+if(NOT "$ENV{ADDED_VARIABLE}" STREQUAL "Added variable")
+  message(SEND_ERROR "ADDED_VARIABLE is \"$ENV{ADDED_VARIABLE}\", should be \"Added variable\"")
+endif()
+
+if(NOT "$ENV{CHANGED_VARIABLE}" STREQUAL "Changed variable")
+  message(SEND_ERROR "CHANGED_VARIABLE is \"$ENV{CHANGED_VARIABLE}\", should be \"Changed variable\"")
+endif()
+
+if(DEFINED ENV{REMOVED_VARIABLE})
+  message(SEND_ERROR "REMOVED_VARIABLE should not be defined")
+endif()