Răsfoiți Sursa

CMake GUI: Add presets functionality

Kyle Edwards 5 ani în urmă
părinte
comite
a4382f72d7
32 a modificat fișierele cu 1632 adăugiri și 11 ștergeri
  1. 8 0
      Source/QtDialog/CMakeLists.txt
  2. 22 3
      Source/QtDialog/CMakeSetup.cxx
  3. 84 2
      Source/QtDialog/CMakeSetupDialog.cxx
  4. 12 0
      Source/QtDialog/CMakeSetupDialog.h
  5. 19 4
      Source/QtDialog/CMakeSetupDialog.ui
  6. 52 2
      Source/QtDialog/FirstConfigure.cxx
  7. 15 0
      Source/QtDialog/FirstConfigure.h
  8. 160 0
      Source/QtDialog/QCMake.cxx
  9. 23 0
      Source/QtDialog/QCMake.h
  10. 50 0
      Source/QtDialog/QCMakePreset.cxx
  11. 30 0
      Source/QtDialog/QCMakePreset.h
  12. 64 0
      Source/QtDialog/QCMakePresetComboBox.cxx
  13. 35 0
      Source/QtDialog/QCMakePresetComboBox.h
  14. 143 0
      Source/QtDialog/QCMakePresetItemModel.cxx
  15. 45 0
      Source/QtDialog/QCMakePresetItemModel.h
  16. 38 0
      Tests/CMakeGUI/CMakeGUITest.cmake
  17. 265 0
      Tests/CMakeGUI/CMakeGUITest.cxx
  18. 3 0
      Tests/CMakeGUI/CMakeGUITest.h
  19. 21 0
      Tests/CMakeGUI/CMakeLists.txt
  20. 80 0
      Tests/CMakeGUI/QCMakePresetComboBoxTest.cxx
  21. 13 0
      Tests/CMakeGUI/QCMakePresetComboBoxTest.h
  22. 162 0
      Tests/CMakeGUI/QCMakePresetItemModelTest.cxx
  23. 17 0
      Tests/CMakeGUI/QCMakePresetItemModelTest.h
  24. 82 0
      Tests/CMakeGUI/QCMakePresetTest.cxx
  25. 14 0
      Tests/CMakeGUI/QCMakePresetTest.h
  26. 33 0
      Tests/CMakeGUI/presetArg-noPresetBinaryChange/CMakePresets.json.in
  27. 33 0
      Tests/CMakeGUI/presetArg-preset/CMakePresets.json.in
  28. 33 0
      Tests/CMakeGUI/presetArg-presetBinary/CMakePresets.json.in
  29. 39 0
      Tests/CMakeGUI/presetArg-presetBinaryChange/CMakePresets.json.in
  30. 2 0
      Tests/CMakeGUI/presetArg-presetConfigExists/CMakeLists.txt.in
  31. 33 0
      Tests/CMakeGUI/presetArg-presetConfigExists/CMakePresets.json.in
  32. 2 0
      Tests/CMakeGUI/presetArg-presetConfigExists/CMakeSetup.ini.in

+ 8 - 0
Source/QtDialog/CMakeLists.txt

@@ -92,6 +92,12 @@ set(SRCS
   QCMake.h
   QCMakeCacheView.cxx
   QCMakeCacheView.h
+  QCMakePreset.cxx
+  QCMakePreset.h
+  QCMakePresetComboBox.cxx
+  QCMakePresetComboBox.h
+  QCMakePresetItemModel.cxx
+  QCMakePresetItemModel.h
   QCMakeWidgets.cxx
   QCMakeWidgets.h
   RegexExplorer.cxx
@@ -116,6 +122,8 @@ qt5_wrap_cpp(MOC_SRCS
   FirstConfigure.h
   QCMake.h
   QCMakeCacheView.h
+  QCMakePresetComboBox.h
+  QCMakePresetItemModel.h
   QCMakeWidgets.h
   RegexExplorer.h
   WarningMessagesDialog.h

+ 22 - 3
Source/QtDialog/CMakeSetup.cxx

@@ -32,7 +32,8 @@ static const char* cmDocumentationUsage[][2] = {
     "  cmake-gui [options]\n"
     "  cmake-gui [options] <path-to-source>\n"
     "  cmake-gui [options] <path-to-existing-build>\n"
-    "  cmake-gui [options] -S <path-to-source> -B <path-to-build>\n" },
+    "  cmake-gui [options] -S <path-to-source> -B <path-to-build>\n"
+    "  cmake-gui [options] -S <path-to-source> --preset=<preset-name>\n" },
   { nullptr, nullptr }
 };
 
@@ -147,6 +148,7 @@ int main(int argc, char** argv)
   QStringList args = QApplication::arguments();
   std::string binaryDirectory;
   std::string sourceDirectory;
+  std::string presetName;
   for (int i = 1; i < args.size(); ++i) {
     const QString& arg = args[i];
     if (arg.startsWith("-S")) {
@@ -185,11 +187,28 @@ int main(int argc, char** argv)
       binaryDirectory =
         cmSystemTools::CollapseFullPath(path.toLocal8Bit().data());
       cmSystemTools::ConvertToUnixSlashes(binaryDirectory);
+    } else if (arg.startsWith("--preset=")) {
+      QString preset = arg.mid(cmStrLen("--preset="));
+      if (preset.isEmpty()) {
+        std::cerr << "No preset specified for --preset" << std::endl;
+        return 1;
+      }
+      presetName = preset.toLocal8Bit().data();
     }
   }
-  if (!sourceDirectory.empty() && !binaryDirectory.empty()) {
+  if (!sourceDirectory.empty() &&
+      (!binaryDirectory.empty() || !presetName.empty())) {
     dialog.setSourceDirectory(QString::fromLocal8Bit(sourceDirectory.c_str()));
-    dialog.setBinaryDirectory(QString::fromLocal8Bit(binaryDirectory.c_str()));
+    if (!binaryDirectory.empty()) {
+      dialog.setBinaryDirectory(
+        QString::fromLocal8Bit(binaryDirectory.c_str()));
+      if (!presetName.empty()) {
+        dialog.setStartupBinaryDirectory(true);
+      }
+    }
+    if (!presetName.empty()) {
+      dialog.setDeferredPreset(QString::fromLocal8Bit(presetName.c_str()));
+    }
   } else {
     if (args.count() == 2) {
       std::string filePath =

+ 84 - 2
Source/QtDialog/CMakeSetupDialog.cxx

@@ -21,8 +21,10 @@
 #include <QSettings>
 #include <QShortcut>
 #include <QStatusBar>
+#include <QString>
 #include <QToolButton>
 #include <QUrl>
+#include <QVector>
 
 #ifdef QT_WINEXTRAS
 #  include <QWinTaskbarButton>
@@ -263,6 +265,8 @@ void CMakeSetupDialog::initialize()
                    &CMakeSetupDialog::onBinaryDirectoryChanged);
   QObject::connect(this->SourceDirectory, &QLineEdit::textChanged, this,
                    &CMakeSetupDialog::onSourceDirectoryChanged);
+  QObject::connect(this->Preset, &QCMakePresetComboBox::presetChanged, this,
+                   &CMakeSetupDialog::onBuildPresetChanged);
 
   QObject::connect(this->CMakeThread->cmakeInstance(),
                    &QCMake::sourceDirChanged, this,
@@ -270,6 +274,13 @@ void CMakeSetupDialog::initialize()
   QObject::connect(this->CMakeThread->cmakeInstance(),
                    &QCMake::binaryDirChanged, this,
                    &CMakeSetupDialog::updateBinaryDirectory);
+  QObject::connect(this->CMakeThread->cmakeInstance(), &QCMake::presetsChanged,
+                   this, &CMakeSetupDialog::updatePresets);
+  QObject::connect(this->CMakeThread->cmakeInstance(), &QCMake::presetChanged,
+                   this, &CMakeSetupDialog::updatePreset);
+  QObject::connect(this->CMakeThread->cmakeInstance(),
+                   &QCMake::presetLoadError, this,
+                   &CMakeSetupDialog::showPresetLoadError);
 
   QObject::connect(this->CMakeThread->cmakeInstance(),
                    &QCMake::progressChanged, this,
@@ -314,9 +325,15 @@ void CMakeSetupDialog::initialize()
   QObject::connect(this->WarnUninitializedAction, &QAction::triggered,
                    this->CMakeThread->cmakeInstance(),
                    &QCMake::setWarnUninitializedMode);
+  QObject::connect(this->CMakeThread->cmakeInstance(),
+                   &QCMake::warnUninitializedModeChanged,
+                   this->WarnUninitializedAction, &QAction::setChecked);
 
-  if (!this->SourceDirectory->text().isEmpty() ||
-      !this->BinaryDirectory->lineEdit()->text().isEmpty()) {
+  if (!this->SourceDirectory->text().isEmpty() &&
+      !this->DeferredPreset.isNull()) {
+    this->onSourceDirectoryChanged(this->SourceDirectory->text());
+  } else if (!this->SourceDirectory->text().isEmpty() ||
+             !this->BinaryDirectory->lineEdit()->text().isEmpty()) {
     this->onSourceDirectoryChanged(this->SourceDirectory->text());
     this->onBinaryDirectoryChanged(this->BinaryDirectory->lineEdit()->text());
   } else {
@@ -671,6 +688,41 @@ void CMakeSetupDialog::updateBinaryDirectory(const QString& dir)
   }
 }
 
+void CMakeSetupDialog::updatePresets(const QVector<QCMakePreset>& presets)
+{
+  if (this->Preset->presets() != presets) {
+    this->Preset->blockSignals(true);
+    this->Preset->setPresets(presets);
+    this->Preset->blockSignals(false);
+  }
+
+  this->Preset->setHidden(presets.isEmpty());
+  this->PresetLabel->setHidden(presets.isEmpty());
+
+  if (!this->DeferredPreset.isNull()) {
+    this->Preset->setPresetName(this->DeferredPreset);
+    this->DeferredPreset = QString{};
+  }
+}
+
+void CMakeSetupDialog::updatePreset(const QString& name)
+{
+  if (this->Preset->presetName() != name) {
+    this->Preset->blockSignals(true);
+    this->Preset->setPresetName(name);
+    this->Preset->blockSignals(false);
+  }
+}
+
+void CMakeSetupDialog::showPresetLoadError(
+  const QString& dir, cmCMakePresetsFile::ReadFileResult result)
+{
+  QMessageBox::warning(
+    this, "Error Reading CMake Presets",
+    QString::fromLocal8Bit("Could not read presets from %1: %2")
+      .arg(dir, cmCMakePresetsFile::ResultToString(result)));
+}
+
 void CMakeSetupDialog::doBinaryBrowse()
 {
   QString dir = QFileDialog::getExistingDirectory(
@@ -686,6 +738,11 @@ void CMakeSetupDialog::setBinaryDirectory(const QString& dir)
   this->BinaryDirectory->setEditText(dir);
 }
 
+void CMakeSetupDialog::setStartupBinaryDirectory(bool startup)
+{
+  this->StartupBinaryDirectory = startup;
+}
+
 void CMakeSetupDialog::onSourceDirectoryChanged(const QString& dir)
 {
   this->Output->clear();
@@ -711,11 +768,24 @@ void CMakeSetupDialog::onBinaryDirectoryChanged(const QString& dir)
                             Q_ARG(QString, dir));
 }
 
+void CMakeSetupDialog::onBuildPresetChanged(const QString& name)
+{
+  QMetaObject::invokeMethod(this->CMakeThread->cmakeInstance(), "setPreset",
+                            Qt::QueuedConnection, Q_ARG(QString, name),
+                            Q_ARG(bool, !this->StartupBinaryDirectory));
+  this->StartupBinaryDirectory = false;
+}
+
 void CMakeSetupDialog::setSourceDirectory(const QString& dir)
 {
   this->SourceDirectory->setText(dir);
 }
 
+void CMakeSetupDialog::setDeferredPreset(const QString& preset)
+{
+  this->DeferredPreset = preset;
+}
+
 void CMakeSetupDialog::showProgress(const QString& /*msg*/, float percent)
 {
   percent = (percent * ProgressFactor) + ProgressOffset;
@@ -753,6 +823,7 @@ void CMakeSetupDialog::setEnabledState(bool enabled)
   this->CacheValues->cacheModel()->setEditEnabled(enabled);
   this->SourceDirectory->setEnabled(enabled);
   this->BrowseSourceDirectoryButton->setEnabled(enabled);
+  this->Preset->setEnabled(enabled);
   this->BinaryDirectory->setEnabled(enabled);
   this->BrowseBinaryDirectoryButton->setEnabled(enabled);
   this->ReloadCacheAction->setEnabled(enabled);
@@ -777,6 +848,17 @@ bool CMakeSetupDialog::setupFirstConfigure()
   // restore from settings
   dialog.loadFromSettings();
 
+  auto presetData = this->Preset->currentData();
+  if (presetData.isValid()) {
+    auto preset = presetData.value<QCMakePreset>();
+    dialog.setCurrentGenerator(preset.generator);
+    if (preset.setGenConfig) {
+      dialog.setPlatform(preset.architecture);
+      dialog.setToolset(preset.toolset);
+    }
+    dialog.setCompilerOption(CompilerOption::DefaultNative);
+  }
+
   if (dialog.exec() == QDialog::Accepted) {
     dialog.saveToSettings();
     this->CMakeThread->cmakeInstance()->setGenerator(dialog.getGenerator());

+ 12 - 0
Source/QtDialog/CMakeSetupDialog.h

@@ -5,12 +5,15 @@
 #include <memory>
 
 #include "QCMake.h"
+#include "QCMakePreset.h"
 #include <QEventLoop>
 #include <QMainWindow>
 #include <QThread>
+#include <QVector>
 
 #include "ui_CMakeSetupDialog.h"
 
+class QCMakePresetItemModel;
 class QCMakeThread;
 class CMakeCacheModel;
 class QProgressBar;
@@ -33,6 +36,8 @@ public:
 public slots:
   void setBinaryDirectory(const QString& dir);
   void setSourceDirectory(const QString& dir);
+  void setDeferredPreset(const QString& preset);
+  void setStartupBinaryDirectory(bool startup);
 
 protected slots:
   void initialize();
@@ -52,6 +57,10 @@ protected slots:
   void doDeleteCache();
   void updateSourceDirectory(const QString& dir);
   void updateBinaryDirectory(const QString& dir);
+  void updatePresets(const QVector<QCMakePreset>& presets);
+  void updatePreset(const QString& name);
+  void showPresetLoadError(const QString& dir,
+                           cmCMakePresetsFile::ReadFileResult result);
   void showProgress(const QString& msg, float percent);
   void setEnabledState(bool);
   bool setupFirstConfigure();
@@ -62,6 +71,7 @@ protected slots:
   void saveBuildPaths(const QStringList&);
   void onBinaryDirectoryChanged(const QString& dir);
   void onSourceDirectoryChanged(const QString& dir);
+  void onBuildPresetChanged(const QString& name);
   void setCacheModified();
   void removeSelectedCacheEntries();
   void selectionChanged();
@@ -113,6 +123,8 @@ protected:
   QAction* WarnUninitializedAction;
   QAction* InstallForCommandLineAction;
   State CurrentState;
+  QString DeferredPreset;
+  bool StartupBinaryDirectory = false;
 
   QTextCharFormat ErrorFormat;
   QTextCharFormat MessageFormat;

+ 19 - 4
Source/QtDialog/CMakeSetupDialog.ui

@@ -44,7 +44,7 @@
       <number>6</number>
      </property>
      <item row="0" column="0">
-      <widget class="QLabel" name="label">
+      <widget class="QLabel" name="SourceLabel">
        <property name="text">
         <string>Where is the source code:</string>
        </property>
@@ -61,13 +61,23 @@
       </widget>
      </item>
      <item row="1" column="0">
-      <widget class="QLabel" name="label_2">
+      <widget class="QLabel" name="PresetLabel">
        <property name="text">
-        <string>Where to build the binaries:</string>
+        <string>Preset:</string>
        </property>
       </widget>
      </item>
      <item row="1" column="1">
+      <widget class="QCMakePresetComboBox" name="Preset"/>
+     </item>
+     <item row="2" column="0">
+      <widget class="QLabel" name="BinaryLabel">
+       <property name="text">
+        <string>Where to build the binaries:</string>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="1">
       <widget class="QComboBox" name="BinaryDirectory">
        <property name="sizePolicy">
         <sizepolicy hsizetype="Ignored" vsizetype="Fixed">
@@ -83,7 +93,7 @@
        </property>
       </widget>
      </item>
-     <item row="1" column="2">
+     <item row="2" column="2">
       <widget class="QPushButton" name="BrowseBinaryDirectoryButton">
        <property name="text">
         <string>Browse &amp;Build...</string>
@@ -367,6 +377,11 @@
    <extends>QTreeView</extends>
    <header>QCMakeCacheView.h</header>
   </customwidget>
+  <customwidget>
+   <class>QCMakePresetComboBox</class>
+   <extends>QComboBox</extends>
+   <header>QCMakePresetComboBox.h</header>
+  </customwidget>
  </customwidgets>
  <resources>
   <include location="CMakeSetup.qrc"/>

+ 52 - 2
Source/QtDialog/FirstConfigure.cxx

@@ -145,6 +145,36 @@ void StartCompilerSetup::setCurrentGenerator(const QString& gen)
   }
 }
 
+void StartCompilerSetup::setPlatform(const QString& platform)
+{
+  this->PlatformOptions->setCurrentText(platform);
+}
+
+void StartCompilerSetup::setToolset(const QString& toolset)
+{
+  this->Toolset->setText(toolset);
+}
+
+void StartCompilerSetup::setCompilerOption(CompilerOption option)
+{
+  std::size_t index = 0;
+  switch (option) {
+    case CompilerOption::DefaultNative:
+      index = 0;
+      break;
+    case CompilerOption::SpecifyNative:
+      index = 1;
+      break;
+    case CompilerOption::ToolchainFile:
+      index = 2;
+      break;
+    case CompilerOption::Options:
+      index = 3;
+      break;
+  }
+  this->CompilerSetupOptions[index]->setChecked(true);
+}
+
 QString StartCompilerSetup::getGenerator() const
 {
   return this->GeneratorOptions->currentText();
@@ -482,6 +512,26 @@ void FirstConfigure::setGenerators(
   this->mStartCompilerSetupPage->setGenerators(gens);
 }
 
+void FirstConfigure::setCurrentGenerator(const QString& gen)
+{
+  this->mStartCompilerSetupPage->setCurrentGenerator(gen);
+}
+
+void FirstConfigure::setPlatform(const QString& platform)
+{
+  this->mStartCompilerSetupPage->setPlatform(platform);
+}
+
+void FirstConfigure::setToolset(const QString& toolset)
+{
+  this->mStartCompilerSetupPage->setToolset(toolset);
+}
+
+void FirstConfigure::setCompilerOption(CompilerOption option)
+{
+  this->mStartCompilerSetupPage->setCompilerOption(option);
+}
+
 QString FirstConfigure::getGenerator() const
 {
   return this->mStartCompilerSetupPage->getGenerator();
@@ -503,7 +553,7 @@ void FirstConfigure::loadFromSettings()
   // restore generator
   settings.beginGroup("Settings/StartPath");
   QString lastGen = settings.value("LastGenerator").toString();
-  this->mStartCompilerSetupPage->setCurrentGenerator(lastGen);
+  this->setCurrentGenerator(lastGen);
   settings.endGroup();
 
   // restore compiler setup
@@ -550,7 +600,7 @@ void FirstConfigure::loadFromSettings()
   //     this prevents them from being taken from environment, while the
   //     generator is taken from application settings
   if (!mDefaultGenerator.isEmpty()) {
-    this->mStartCompilerSetupPage->setCurrentGenerator(mDefaultGenerator);
+    this->setCurrentGenerator(mDefaultGenerator);
   }
 }
 

+ 15 - 0
Source/QtDialog/FirstConfigure.h

@@ -22,6 +22,14 @@ enum FirstConfigurePages
   Done
 };
 
+enum class CompilerOption
+{
+  DefaultNative,
+  SpecifyNative,
+  ToolchainFile,
+  Options,
+};
+
 //! the first page that gives basic options for what compilers setup to choose
 //! from
 class StartCompilerSetup : public QWizardPage
@@ -33,6 +41,9 @@ public:
   ~StartCompilerSetup();
   void setGenerators(std::vector<cmake::GeneratorInfo> const& gens);
   void setCurrentGenerator(const QString& gen);
+  void setToolset(const QString& toolset);
+  void setPlatform(const QString& platform);
+  void setCompilerOption(CompilerOption option);
   QString getGenerator() const;
   QString getToolset() const;
   QString getPlatform() const;
@@ -167,6 +178,10 @@ public:
   ~FirstConfigure();
 
   void setGenerators(std::vector<cmake::GeneratorInfo> const& gens);
+  void setCurrentGenerator(const QString& gen);
+  void setToolset(const QString& toolset);
+  void setPlatform(const QString& platform);
+  void setCompilerOption(CompilerOption option);
   QString getGenerator() const;
   QString getPlatform() const;
   QString getToolset() const;

+ 160 - 0
Source/QtDialog/QCMake.cxx

@@ -2,10 +2,14 @@
    file Copyright.txt or https://cmake.org/licensing for details.  */
 #include "QCMake.h"
 
+#include <algorithm>
+
 #include <cm/memory>
 
 #include <QCoreApplication>
 #include <QDir>
+#include <QString>
+#include <QVector>
 
 #include "cmExternalMakefileProjectGenerator.h"
 #include "cmGlobalGenerator.h"
@@ -19,12 +23,15 @@
 
 QCMake::QCMake(QObject* p)
   : QObject(p)
+  , StartEnvironment(QProcessEnvironment::systemEnvironment())
   , Environment(QProcessEnvironment::systemEnvironment())
 {
   this->WarnUninitializedMode = false;
   qRegisterMetaType<QCMakeProperty>();
   qRegisterMetaType<QCMakePropertyList>();
   qRegisterMetaType<QProcessEnvironment>();
+  qRegisterMetaType<QVector<QCMakePreset>>();
+  qRegisterMetaType<cmCMakePresetsFile::ReadFileResult>();
 
   cmSystemTools::DisableRunCommandOutput();
   cmSystemTools::SetRunCommandHideConsole(true);
@@ -57,6 +64,17 @@ QCMake::QCMake(QObject* p)
   for (cmake::GeneratorInfo const& gen : generators) {
     this->AvailableGenerators.push_back(gen);
   }
+
+  connect(&this->LoadPresetsTimer, &QTimer::timeout, this, [this]() {
+    this->loadPresets();
+    if (!this->PresetName.isEmpty() &&
+        this->CMakePresetsFile.Presets.find(
+          std::string(this->PresetName.toLocal8Bit())) ==
+          this->CMakePresetsFile.Presets.end()) {
+      this->setPreset(QString{});
+    }
+  });
+  this->LoadPresetsTimer.start(1000);
 }
 
 QCMake::~QCMake() = default;
@@ -73,6 +91,8 @@ void QCMake::setSourceDirectory(const QString& _dir)
   if (this->SourceDirectory != dir) {
     this->SourceDirectory = QDir::fromNativeSeparators(dir);
     emit this->sourceDirChanged(this->SourceDirectory);
+    this->loadPresets();
+    this->setPreset(QString{});
   }
 }
 
@@ -129,6 +149,56 @@ void QCMake::setBinaryDirectory(const QString& _dir)
   }
 }
 
+void QCMake::setPreset(const QString& name, bool setBinary)
+{
+  if (this->PresetName != name) {
+    this->PresetName = name;
+    emit this->presetChanged(this->PresetName);
+
+    if (!name.isNull()) {
+      std::string presetName(name.toLocal8Bit());
+      auto const& preset = this->CMakePresetsFile.Presets[presetName];
+      auto expandedPreset = this->CMakePresetsFile.ExpandMacros(preset);
+      if (expandedPreset) {
+        if (setBinary) {
+          QString binaryDir =
+            QString::fromLocal8Bit(expandedPreset->BinaryDir.data());
+          this->setBinaryDirectory(binaryDir);
+        }
+        if (expandedPreset->WarnDev) {
+          this->CMakeInstance->SetSuppressDevWarnings(
+            !*expandedPreset->WarnDev);
+        }
+        if (expandedPreset->ErrorDev) {
+          this->CMakeInstance->SetDevWarningsAsErrors(
+            *expandedPreset->ErrorDev);
+        }
+        if (expandedPreset->WarnDeprecated) {
+          this->CMakeInstance->SetSuppressDeprecatedWarnings(
+            !*expandedPreset->WarnDeprecated);
+        }
+        if (expandedPreset->ErrorDeprecated) {
+          this->CMakeInstance->SetDeprecatedWarningsAsErrors(
+            *expandedPreset->ErrorDeprecated);
+        }
+        if (expandedPreset->WarnUninitialized) {
+          this->WarnUninitializedMode = *expandedPreset->WarnUninitialized;
+          emit this->warnUninitializedModeChanged(
+            *expandedPreset->WarnUninitialized);
+        }
+        this->Environment = this->StartEnvironment;
+        for (auto const& v : expandedPreset->Environment) {
+          if (v.second) {
+            this->Environment.insert(QString::fromLocal8Bit(v.first.data()),
+                                     QString::fromLocal8Bit(v.second->data()));
+          }
+        }
+      }
+    }
+    emit this->propertiesChanged(this->properties());
+  }
+}
+
 void QCMake::setGenerator(const QString& gen)
 {
   if (this->Generator != gen) {
@@ -348,6 +418,56 @@ QCMakePropertyList QCMake::properties() const
     ret.append(prop);
   }
 
+  if (!this->PresetName.isNull()) {
+    std::string presetName(this->PresetName.toLocal8Bit());
+    auto p = this->CMakePresetsFile.ExpandMacros(
+      this->CMakePresetsFile.Presets.at(presetName));
+    if (p) {
+      for (auto const& v : p->CacheVariables) {
+        if (!v.second) {
+          continue;
+        }
+        QCMakeProperty prop;
+        prop.Key = QString::fromLocal8Bit(v.first.data());
+        prop.Value = QString::fromLocal8Bit(v.second->Value.data());
+        prop.Type = QCMakeProperty::STRING;
+        if (!v.second->Type.empty()) {
+          auto type = cmState::StringToCacheEntryType(v.second->Type);
+          switch (type) {
+            case cmStateEnums::BOOL:
+              prop.Type = QCMakeProperty::BOOL;
+              prop.Value = cmIsOn(v.second->Value);
+              break;
+            case cmStateEnums::PATH:
+              prop.Type = QCMakeProperty::PATH;
+              break;
+            case cmStateEnums::FILEPATH:
+              prop.Type = QCMakeProperty::FILEPATH;
+              break;
+            default:
+              prop.Type = QCMakeProperty::STRING;
+              break;
+          }
+        }
+
+        // QCMakeCacheModel prefers variables earlier in the list rather than
+        // later, so overwrite them if they already exist rather than simply
+        // appending
+        bool found = false;
+        for (auto& orig : ret) {
+          if (orig.Key == prop.Key) {
+            orig = prop;
+            found = true;
+            break;
+          }
+        }
+        if (!found) {
+          ret.append(prop);
+        }
+      }
+    }
+  }
+
   return ret;
 }
 
@@ -405,6 +525,46 @@ void QCMake::setUpEnvironment() const
   }
 }
 
+void QCMake::loadPresets()
+{
+  auto result = this->CMakePresetsFile.ReadProjectPresets(
+    this->SourceDirectory.toLocal8Bit().data(), true);
+  if (result != this->LastLoadPresetsResult &&
+      result != cmCMakePresetsFile::ReadFileResult::READ_OK) {
+    emit this->presetLoadError(this->SourceDirectory, result);
+  }
+  this->LastLoadPresetsResult = result;
+
+  QVector<QCMakePreset> presets;
+  for (auto const& name : this->CMakePresetsFile.PresetOrder) {
+    auto const& p = this->CMakePresetsFile.Presets[name];
+    if (p.Hidden) {
+      continue;
+    }
+
+    QCMakePreset preset;
+    preset.name = std::move(QString::fromLocal8Bit(p.Name.data()));
+    preset.displayName =
+      std::move(QString::fromLocal8Bit(p.DisplayName.data()));
+    preset.description =
+      std::move(QString::fromLocal8Bit(p.Description.data()));
+    preset.generator = std::move(QString::fromLocal8Bit(p.Generator.data()));
+    preset.architecture =
+      std::move(QString::fromLocal8Bit(p.Architecture.data()));
+    preset.toolset = std::move(QString::fromLocal8Bit(p.Toolset.data()));
+    preset.setGenConfig = !p.GeneratorConfig ||
+      p.GeneratorConfig == cmCMakePresetsFile::CMakeGeneratorConfig::Default;
+    preset.enabled = std::find_if(this->AvailableGenerators.begin(),
+                                  this->AvailableGenerators.end(),
+                                  [&p](const cmake::GeneratorInfo& g) {
+                                    return g.name == p.Generator;
+                                  }) != this->AvailableGenerators.end() &&
+      this->CMakePresetsFile.ExpandMacros(p);
+    presets.push_back(preset);
+  }
+  emit this->presetsChanged(presets);
+}
+
 QString QCMake::binaryDirectory() const
 {
   return this->BinaryDirectory;

+ 23 - 0
Source/QtDialog/QCMake.h

@@ -4,6 +4,7 @@
 
 #include "cmConfigure.h" // IWYU pragma: keep
 
+#include "cmCMakePresetsFile.h"
 #include "cmake.h"
 
 #ifdef _MSC_VER
@@ -14,6 +15,7 @@
 #include <memory>
 #include <vector>
 
+#include "QCMakePreset.h"
 #include <QAtomicInt>
 #include <QList>
 #include <QMetaType>
@@ -21,6 +23,7 @@
 #include <QProcessEnvironment>
 #include <QString>
 #include <QStringList>
+#include <QTimer>
 #include <QVariant>
 
 /// struct to represent cmake properties in Qt
@@ -57,6 +60,7 @@ using QCMakePropertyList = QList<QCMakeProperty>;
 Q_DECLARE_METATYPE(QCMakeProperty)
 Q_DECLARE_METATYPE(QCMakePropertyList)
 Q_DECLARE_METATYPE(QProcessEnvironment)
+Q_DECLARE_METATYPE(cmCMakePresetsFile::ReadFileResult)
 
 /// Qt API for CMake library.
 /// Wrapper like class allows for easier integration with
@@ -74,6 +78,8 @@ public slots:
   void setSourceDirectory(const QString& dir);
   /// set the binary directory to build in
   void setBinaryDirectory(const QString& dir);
+  /// set the preset name to use
+  void setPreset(const QString& name, bool setBinary = true);
   /// set the desired generator to use
   void setGenerator(const QString& generator);
   /// set the desired generator to use
@@ -147,6 +153,15 @@ signals:
   void sourceDirChanged(const QString& dir);
   /// signal when the binary directory changes
   void binaryDirChanged(const QString& dir);
+  /// signal when the preset list changes
+  void presetsChanged(const QVector<QCMakePreset>& presets);
+  /// signal when the selected preset changes
+  void presetChanged(const QString& name);
+  /// signal when there's an error reading the presets files
+  void presetLoadError(const QString& dir,
+                       cmCMakePresetsFile::ReadFileResult error);
+  /// signal when uninitialized warning changes
+  void warnUninitializedModeChanged(bool value);
   /// signal for progress events
   void progressChanged(const QString& msg, float percent);
   /// signal when configure is done
@@ -178,6 +193,8 @@ protected:
   void stderrCallback(std::string const& msg);
   void setUpEnvironment() const;
 
+  void loadPresets();
+
   bool WarnUninitializedMode;
   QString SourceDirectory;
   QString BinaryDirectory;
@@ -185,7 +202,13 @@ protected:
   QString Platform;
   QString Toolset;
   std::vector<cmake::GeneratorInfo> AvailableGenerators;
+  cmCMakePresetsFile CMakePresetsFile;
+  cmCMakePresetsFile::ReadFileResult LastLoadPresetsResult =
+    cmCMakePresetsFile::ReadFileResult::READ_OK;
+  QString PresetName;
   QString CMakeExecutable;
   QAtomicInt InterruptFlag;
+  QProcessEnvironment StartEnvironment;
   QProcessEnvironment Environment;
+  QTimer LoadPresetsTimer;
 };

+ 50 - 0
Source/QtDialog/QCMakePreset.cxx

@@ -0,0 +1,50 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "QCMakePreset.h"
+
+bool operator==(const QCMakePreset& lhs, const QCMakePreset& rhs)
+{
+  return lhs.name == rhs.name && lhs.displayName == rhs.displayName &&
+    lhs.description == rhs.description && lhs.generator == rhs.generator &&
+    lhs.architecture == rhs.architecture && lhs.toolset == rhs.toolset &&
+    lhs.setGenConfig == rhs.setGenConfig && lhs.enabled == rhs.enabled;
+}
+
+bool operator!=(const QCMakePreset& lhs, const QCMakePreset& rhs)
+{
+  return !(lhs == rhs);
+}
+
+bool operator<(const QCMakePreset& lhs, const QCMakePreset& rhs)
+{
+  return lhs.name < rhs.name ||
+    (lhs.name == rhs.name &&
+     (lhs.displayName < rhs.displayName ||
+      (lhs.displayName == rhs.displayName &&
+       (lhs.description < rhs.description ||
+        (lhs.description == rhs.description &&
+         (lhs.generator < rhs.generator ||
+          (lhs.generator == rhs.generator &&
+           (lhs.architecture < rhs.architecture ||
+            (lhs.architecture == rhs.architecture &&
+             (lhs.toolset < rhs.toolset ||
+              (lhs.toolset == rhs.toolset &&
+               (lhs.setGenConfig < rhs.setGenConfig ||
+                (lhs.setGenConfig == rhs.setGenConfig &&
+                 (lhs.enabled < rhs.enabled))))))))))))));
+}
+
+bool operator<=(const QCMakePreset& lhs, const QCMakePreset& rhs)
+{
+  return rhs >= lhs;
+}
+
+bool operator>(const QCMakePreset& lhs, const QCMakePreset& rhs)
+{
+  return rhs < lhs;
+}
+
+bool operator>=(const QCMakePreset& lhs, const QCMakePreset& rhs)
+{
+  return !(lhs < rhs);
+}

+ 30 - 0
Source/QtDialog/QCMakePreset.h

@@ -0,0 +1,30 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include <QString>
+#include <QVariant>
+
+#include "cmCMakePresetsFile.h"
+
+class QCMakePreset
+{
+public:
+  QString name;
+  QString displayName;
+  QString description;
+  QString generator;
+  QString architecture;
+  QString toolset;
+  bool setGenConfig;
+  bool enabled;
+};
+
+bool operator==(const QCMakePreset& lhs, const QCMakePreset& rhs);
+bool operator!=(const QCMakePreset& lhs, const QCMakePreset& rhs);
+bool operator<(const QCMakePreset& lhs, const QCMakePreset& rhs);
+bool operator<=(const QCMakePreset& lhs, const QCMakePreset& rhs);
+bool operator>(const QCMakePreset& lhs, const QCMakePreset& rhs);
+bool operator>=(const QCMakePreset& lhs, const QCMakePreset& rhs);
+
+Q_DECLARE_METATYPE(QCMakePreset)

+ 64 - 0
Source/QtDialog/QCMakePresetComboBox.cxx

@@ -0,0 +1,64 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "QCMakePresetComboBox.h"
+
+#include "QCMakePresetItemModel.h"
+
+QCMakePresetComboBox::QCMakePresetComboBox(QWidget* parent)
+  : QComboBox(parent)
+{
+  this->m_model = new QCMakePresetItemModel(this);
+  this->setModel(this->m_model);
+
+  QObject::connect(this->m_model, &QCMakePresetItemModel::modelAboutToBeReset,
+                   this, [this]() { this->m_resetting = true; });
+  QObject::connect(this->m_model, &QCMakePresetItemModel::modelReset, this,
+                   [this]() {
+                     this->setPresetName(this->m_lastPreset);
+                     this->m_resetting = false;
+                     this->emitPresetChanged();
+                   });
+  QObject::connect(
+    this,
+    static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
+    this, [this](int /*row*/) {
+      if (!this->m_resetting) {
+        this->emitPresetChanged();
+      }
+    });
+}
+
+const QVector<QCMakePreset>& QCMakePresetComboBox::presets() const
+{
+  return this->m_model->presets();
+}
+
+QString QCMakePresetComboBox::presetName() const
+{
+  auto preset = this->currentData();
+  if (preset.canConvert<QCMakePreset>()) {
+    return preset.value<QCMakePreset>().name;
+  }
+  return QString{};
+}
+
+void QCMakePresetComboBox::setPresets(const QVector<QCMakePreset>& presets)
+{
+  this->m_model->setPresets(presets);
+}
+
+void QCMakePresetComboBox::setPresetName(const QString& name)
+{
+  this->setCurrentIndex(this->m_model->presetNameToRow(name));
+  if (this->signalsBlocked()) {
+    this->m_lastPreset = this->presetName();
+  }
+}
+
+void QCMakePresetComboBox::emitPresetChanged()
+{
+  if (this->presetName() != this->m_lastPreset) {
+    emit this->presetChanged(this->presetName());
+    this->m_lastPreset = this->presetName();
+  }
+}

+ 35 - 0
Source/QtDialog/QCMakePresetComboBox.h

@@ -0,0 +1,35 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "QCMakePreset.h"
+#include <QComboBox>
+#include <QObject>
+#include <QString>
+#include <QVector>
+
+class QCMakePresetItemModel;
+
+class QCMakePresetComboBox : public QComboBox
+{
+  Q_OBJECT
+public:
+  QCMakePresetComboBox(QWidget* parent = nullptr);
+
+  const QVector<QCMakePreset>& presets() const;
+  QString presetName() const;
+
+public slots:
+  void setPresets(const QVector<QCMakePreset>& presets);
+  void setPresetName(const QString& name);
+
+signals:
+  void presetChanged(const QString& name);
+
+private:
+  QCMakePresetItemModel* m_model;
+  bool m_resetting = false;
+  QString m_lastPreset;
+
+  void emitPresetChanged();
+};

+ 143 - 0
Source/QtDialog/QCMakePresetItemModel.cxx

@@ -0,0 +1,143 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "QCMakePresetItemModel.h"
+
+#include <QFont>
+
+QCMakePresetItemModel::QCMakePresetItemModel(QObject* parent)
+  : QAbstractItemModel(parent)
+{
+}
+
+QVariant QCMakePresetItemModel::data(const QModelIndex& index, int role) const
+{
+  switch (role) {
+    case Qt::AccessibleDescriptionRole:
+      // Separators have to return "separator" for the
+      // AccessibleDescriptionRole. This was determined by looking at
+      // QComboBoxDelegate::isSeparator() (located in qcombobox_p.h.)
+      if (index.internalId() == SEPARATOR_INDEX) {
+        return QString::fromLocal8Bit("separator");
+      }
+      return QString{};
+    case Qt::DisplayRole: {
+      if (index.internalId() == CUSTOM_INDEX) {
+        return QString::fromLocal8Bit("<custom>");
+      }
+      if (index.internalId() == SEPARATOR_INDEX) {
+        return QVariant{};
+      }
+      auto const& preset = this->m_presets[index.internalId()];
+      return preset.displayName.isEmpty() ? preset.name : preset.displayName;
+    }
+    case Qt::ToolTipRole:
+      if (index.internalId() == CUSTOM_INDEX) {
+        return QString::fromLocal8Bit("Specify all settings manually");
+      }
+      if (index.internalId() == SEPARATOR_INDEX) {
+        return QVariant{};
+      }
+      return this->m_presets[index.internalId()].description;
+    case Qt::UserRole:
+      if (index.internalId() == CUSTOM_INDEX) {
+        return QVariant{};
+      }
+      if (index.internalId() == SEPARATOR_INDEX) {
+        return QVariant{};
+      }
+      return QVariant::fromValue(this->m_presets[index.internalId()]);
+    case Qt::FontRole:
+      if (index.internalId() == CUSTOM_INDEX) {
+        QFont font;
+        font.setItalic(true);
+        return font;
+      }
+      return QFont{};
+    default:
+      return QVariant{};
+  }
+}
+
+Qt::ItemFlags QCMakePresetItemModel::flags(const QModelIndex& index) const
+{
+  Qt::ItemFlags flags =
+    Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
+  if (index.internalId() != SEPARATOR_INDEX &&
+      (index.internalId() == CUSTOM_INDEX ||
+       this->m_presets[index.internalId()].enabled)) {
+    flags |= Qt::ItemIsSelectable | Qt::ItemIsEnabled;
+  }
+  return flags;
+}
+
+int QCMakePresetItemModel::rowCount(const QModelIndex& parent) const
+{
+  if (parent.isValid()) {
+    return 0;
+  }
+  if (this->m_presets.empty()) {
+    return 1;
+  }
+  return this->m_presets.size() + 2;
+}
+
+int QCMakePresetItemModel::columnCount(const QModelIndex& parent) const
+{
+  if (parent.isValid()) {
+    return 0;
+  }
+  return 1;
+}
+
+QModelIndex QCMakePresetItemModel::index(int row, int column,
+                                         const QModelIndex& parent) const
+{
+  if (parent.isValid() || column != 0 || row < 0 ||
+      row >= this->rowCount(QModelIndex{})) {
+    return QModelIndex{};
+  }
+
+  if (this->m_presets.empty() || row == this->m_presets.size() + 1) {
+    return this->createIndex(row, column, CUSTOM_INDEX);
+  }
+
+  if (row == this->m_presets.size()) {
+    return this->createIndex(row, column, SEPARATOR_INDEX);
+  }
+
+  return this->createIndex(row, column, static_cast<quintptr>(row));
+}
+
+QModelIndex QCMakePresetItemModel::parent(const QModelIndex& /*index*/) const
+{
+  return QModelIndex{};
+}
+
+QVector<QCMakePreset> const& QCMakePresetItemModel::presets() const
+{
+  return this->m_presets;
+}
+
+void QCMakePresetItemModel::setPresets(QVector<QCMakePreset> const& presets)
+{
+  this->beginResetModel();
+  this->m_presets = presets;
+  this->endResetModel();
+}
+
+int QCMakePresetItemModel::presetNameToRow(const QString& name) const
+{
+  if (this->m_presets.empty()) {
+    return 0;
+  }
+
+  int index = 0;
+  for (auto const& preset : this->m_presets) {
+    if (preset.name == name) {
+      return index;
+    }
+    index++;
+  }
+
+  return this->m_presets.size() + 1;
+}

+ 45 - 0
Source/QtDialog/QCMakePresetItemModel.h

@@ -0,0 +1,45 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include <cm/optional>
+
+#include "QCMakePreset.h"
+#include <QAbstractItemModel>
+#include <QModelIndex>
+#include <QString>
+#include <QVariant>
+#include <QVector>
+#include <QtGlobal>
+
+class QObject;
+
+class QCMakePresetItemModel : public QAbstractItemModel
+{
+  Q_OBJECT
+public:
+  QCMakePresetItemModel(QObject* parent = nullptr);
+
+  QVariant data(const QModelIndex& index, int role) const override;
+  Qt::ItemFlags flags(const QModelIndex& index) const override;
+
+  int rowCount(const QModelIndex& parent = QModelIndex{}) const override;
+  int columnCount(const QModelIndex& parent = QModelIndex{}) const override;
+
+  QModelIndex index(int row, int column,
+                    const QModelIndex& parent = QModelIndex{}) const override;
+  QModelIndex parent(const QModelIndex& index) const override;
+
+  QVector<QCMakePreset> const& presets() const;
+
+  int presetNameToRow(const QString& name) const;
+
+public slots:
+  void setPresets(QVector<QCMakePreset> const& presets);
+
+private:
+  QVector<QCMakePreset> m_presets;
+
+  static constexpr quintptr SEPARATOR_INDEX = static_cast<quintptr>(-2);
+  static constexpr quintptr CUSTOM_INDEX = static_cast<quintptr>(-1);
+};

+ 38 - 0
Tests/CMakeGUI/CMakeGUITest.cmake

@@ -27,6 +27,10 @@ function(run_cmake_gui_test name)
   if(EXISTS "${_cmakelists_in}")
     configure_file("${_cmakelists_in}" "${_workdir}/src/CMakeLists.txt" @ONLY)
   endif()
+  set(_cmakepresets_in "${_srcdir}/CMakePresets.json.in")
+  if(EXISTS "${_cmakepresets_in}")
+    configure_file("${_cmakepresets_in}" "${_workdir}/src/CMakePresets.json" @ONLY)
+  endif()
   if(_rcgt_DO_CONFIGURE)
     if(NOT _rcgt_GENERATOR)
       set(_rcgt_GENERATOR "${CMakeGUITest_GENERATOR}")
@@ -118,3 +122,37 @@ 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)
+
+run_cmake_gui_test(presetArg:preset
+  ARGS
+    -S "${CMakeGUITest_BINARY_DIR}/presetArg-preset/src"
+    "--preset=ninja"
+  )
+run_cmake_gui_test(presetArg:presetBinary
+  ARGS
+    -S "${CMakeGUITest_BINARY_DIR}/presetArg-presetBinary/src"
+    -B "${CMakeGUITest_BINARY_DIR}/presetArg-presetBinary/build"
+    "--preset=ninja"
+  )
+run_cmake_gui_test(presetArg:presetBinaryChange
+  ARGS
+    -S "${CMakeGUITest_BINARY_DIR}/presetArg-presetBinaryChange/src"
+    -B "${CMakeGUITest_BINARY_DIR}/presetArg-presetBinaryChange/build"
+    "--preset=ninja"
+  )
+run_cmake_gui_test(presetArg:noPresetBinaryChange
+  ARGS
+    -S "${CMakeGUITest_BINARY_DIR}/presetArg-noPresetBinaryChange/src"
+    -B "${CMakeGUITest_BINARY_DIR}/presetArg-noPresetBinaryChange/build"
+  )
+run_cmake_gui_test(presetArg:presetConfigExists
+  ARGS
+    -S "${CMakeGUITest_BINARY_DIR}/presetArg-presetConfigExists/src"
+    "--preset=ninja"
+  )
+run_cmake_gui_test(presetArg:noExist
+  ARGS
+    -S "${CMakeGUITest_BINARY_DIR}/presetArg-noExist/src"
+    "--preset=noExist"
+  )
+run_cmake_gui_test(changingPresets)

+ 265 - 0
Tests/CMakeGUI/CMakeGUITest.cxx

@@ -5,6 +5,10 @@
 #include "QCMake.h"
 #include <QApplication>
 #include <QEventLoop>
+#include <QFile>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
 #include <QMessageBox>
 #include <QSettings>
 #include <QString>
@@ -18,6 +22,9 @@
 #include "CatchShow.h"
 #include "FirstConfigure.h"
 
+using WindowSetupHelper = std::function<void(CMakeSetupDialog*)>;
+Q_DECLARE_METATYPE(WindowSetupHelper)
+
 namespace {
 void loopSleep(int msecs = 500)
 {
@@ -172,6 +179,264 @@ void CMakeGUITest::environment()
   QCOMPARE(penv.value("REMOVED_VARIABLE"), "Removed variable");
 }
 
+void CMakeGUITest::presetArg()
+{
+  QFETCH(WindowSetupHelper, setupFunction);
+  QFETCH(QString, presetName);
+  QFETCH(QString, sourceDir);
+  QFETCH(QString, binaryDir);
+  QFETCH(QCMakePropertyList, properties);
+
+  if (setupFunction) {
+    setupFunction(this->m_window);
+  }
+
+  // Wait a bit for everything to update
+  loopSleep();
+
+  QCOMPARE(this->m_window->Preset->presetName(), presetName);
+  QCOMPARE(this->m_window->SourceDirectory->text(), sourceDir);
+  QCOMPARE(this->m_window->BinaryDirectory->currentText(), binaryDir);
+
+  auto actualProperties =
+    this->m_window->CacheValues->cacheModel()->properties();
+  QCOMPARE(actualProperties.size(), properties.size());
+  for (int i = 0; i < actualProperties.size(); ++i) {
+    // operator==() only compares Key, we need to compare Value and Type too
+    QCOMPARE(actualProperties[i].Key, properties[i].Key);
+    QCOMPARE(actualProperties[i].Value, properties[i].Value);
+    QCOMPARE(actualProperties[i].Type, properties[i].Type);
+  }
+}
+
+namespace {
+QCMakePropertyList makePresetProperties(const QString& name)
+{
+  return QCMakePropertyList{
+    QCMakeProperty{
+      /*Key=*/"FALSE_VARIABLE",
+      /*Value=*/false,
+      /*Strings=*/{},
+      /*Help=*/"",
+      /*Type=*/QCMakeProperty::BOOL,
+      /*Advanced=*/false,
+    },
+    QCMakeProperty{
+      /*Key=*/"FILEPATH_VARIABLE",
+      /*Value=*/
+      QString::fromLocal8Bit(CMakeGUITest_BINARY_DIR "/%1/src/CMakeLists.txt")
+        .arg(name),
+      /*Strings=*/{},
+      /*Help=*/"",
+      /*Type=*/QCMakeProperty::FILEPATH,
+      /*Advanced=*/false,
+    },
+    QCMakeProperty{
+      /*Key=*/"ON_VARIABLE",
+      /*Value=*/true,
+      /*Strings=*/{},
+      /*Help=*/"",
+      /*Type=*/QCMakeProperty::BOOL,
+      /*Advanced=*/false,
+    },
+    QCMakeProperty{
+      /*Key=*/"PATH_VARIABLE",
+      /*Value=*/
+      QString::fromLocal8Bit(CMakeGUITest_BINARY_DIR "/%1/src").arg(name),
+      /*Strings=*/{},
+      /*Help=*/"",
+      /*Type=*/QCMakeProperty::PATH,
+      /*Advanced=*/false,
+    },
+    QCMakeProperty{
+      /*Key=*/"STRING_VARIABLE",
+      /*Value=*/"String value",
+      /*Strings=*/{},
+      /*Help=*/"",
+      /*Type=*/QCMakeProperty::STRING,
+      /*Advanced=*/false,
+    },
+    QCMakeProperty{
+      /*Key=*/"UNINITIALIZED_VARIABLE",
+      /*Value=*/"Uninitialized value",
+      /*Strings=*/{},
+      /*Help=*/"",
+      /*Type=*/QCMakeProperty::STRING,
+      /*Advanced=*/false,
+    },
+  };
+}
+}
+
+void CMakeGUITest::presetArg_data()
+{
+  QTest::addColumn<WindowSetupHelper>("setupFunction");
+  QTest::addColumn<QString>("presetName");
+  QTest::addColumn<QString>("sourceDir");
+  QTest::addColumn<QString>("binaryDir");
+  QTest::addColumn<QCMakePropertyList>("properties");
+
+  QTest::newRow("preset") << WindowSetupHelper{} << "ninja"
+                          << CMakeGUITest_BINARY_DIR "/presetArg-preset/src"
+                          << CMakeGUITest_BINARY_DIR
+    "/presetArg-preset/src/build"
+                          << makePresetProperties("presetArg-preset");
+  QTest::newRow("presetBinary")
+    << WindowSetupHelper{} << "ninja"
+    << CMakeGUITest_BINARY_DIR "/presetArg-presetBinary/src"
+    << CMakeGUITest_BINARY_DIR "/presetArg-presetBinary/build"
+    << makePresetProperties("presetArg-presetBinary");
+  QTest::newRow("presetBinaryChange")
+    << WindowSetupHelper{ [](CMakeSetupDialog* window) {
+         loopSleep();
+         window->Preset->setPresetName("ninja2");
+       } }
+    << "ninja2" << CMakeGUITest_BINARY_DIR "/presetArg-presetBinaryChange/src"
+    << CMakeGUITest_BINARY_DIR "/presetArg-presetBinaryChange/src/build"
+    << makePresetProperties("presetArg-presetBinaryChange");
+  QTest::newRow("noPresetBinaryChange")
+    << WindowSetupHelper{ [](CMakeSetupDialog* window) {
+         loopSleep();
+         window->Preset->setPresetName("ninja");
+       } }
+    << "ninja" << CMakeGUITest_BINARY_DIR "/presetArg-noPresetBinaryChange/src"
+    << CMakeGUITest_BINARY_DIR "/presetArg-noPresetBinaryChange/src/build"
+    << makePresetProperties("presetArg-noPresetBinaryChange");
+  QTest::newRow("presetConfigExists")
+    << WindowSetupHelper{} << "ninja"
+    << CMakeGUITest_BINARY_DIR "/presetArg-presetConfigExists/src"
+    << CMakeGUITest_BINARY_DIR "/presetArg-presetConfigExists/src/build"
+    << makePresetProperties("presetArg-presetConfigExists");
+  QTest::newRow("noExist") << WindowSetupHelper{} << QString{}
+                           << CMakeGUITest_BINARY_DIR "/presetArg-noExist/src"
+                           << "" << QCMakePropertyList{};
+}
+
+namespace {
+void writePresets(const QString& buildDir, const QStringList& names)
+{
+  QJsonArray presets{
+    QJsonObject{
+      { "name", "base" },
+      { "generator", "Ninja" },
+      { "binaryDir",
+        QString::fromLocal8Bit("${sourceDir}/%1/${presetName}")
+          .arg(buildDir) },
+      { "hidden", true },
+    },
+  };
+
+  for (auto const& name : names) {
+    presets.append(QJsonObject{
+      { "name", name },
+      { "inherits", QJsonArray{ "base" } },
+    });
+  }
+
+  QJsonDocument doc{ QJsonObject{
+    { "version", 1 },
+    { "configurePresets", presets },
+  } };
+
+  QFile presetsFile(CMakeGUITest_BINARY_DIR
+                    "/changingPresets/src/CMakePresets.json");
+  bool open = presetsFile.open(QIODevice::WriteOnly);
+  Q_ASSERT(open);
+  presetsFile.write(doc.toJson());
+}
+}
+
+void CMakeGUITest::changingPresets()
+{
+  QDir::root().mkpath(CMakeGUITest_BINARY_DIR "/changingPresets/src");
+
+  this->m_window->SourceDirectory->setText(CMakeGUITest_BINARY_DIR
+                                           "/changingPresets/src");
+  loopSleep();
+  QCOMPARE(this->m_window->Preset->presetName(), QString{});
+  QCOMPARE(this->m_window->Preset->presets().size(), 0);
+  QCOMPARE(this->m_window->BinaryDirectory->currentText(), "");
+  QCOMPARE(this->m_window->Preset->isHidden(), true);
+  QCOMPARE(this->m_window->PresetLabel->isHidden(), true);
+
+  writePresets("build1", { "preset" });
+  loopSleep(1500);
+  QCOMPARE(this->m_window->Preset->presetName(), QString{});
+  QCOMPARE(this->m_window->Preset->presets().size(), 1);
+  QCOMPARE(this->m_window->BinaryDirectory->currentText(), "");
+  QCOMPARE(this->m_window->Preset->isHidden(), false);
+  QCOMPARE(this->m_window->PresetLabel->isHidden(), false);
+
+  this->m_window->Preset->setPresetName("preset");
+  loopSleep();
+  QCOMPARE(this->m_window->Preset->presetName(), "preset");
+  QCOMPARE(this->m_window->Preset->presets().size(), 1);
+  QCOMPARE(this->m_window->BinaryDirectory->currentText(),
+           CMakeGUITest_BINARY_DIR "/changingPresets/src/build1/preset");
+  QCOMPARE(this->m_window->Preset->isHidden(), false);
+  QCOMPARE(this->m_window->PresetLabel->isHidden(), false);
+
+  writePresets("build2", { "preset2", "preset" });
+  loopSleep(1500);
+  QCOMPARE(this->m_window->Preset->presetName(), "preset");
+  QCOMPARE(this->m_window->Preset->presets().size(), 2);
+  QCOMPARE(this->m_window->BinaryDirectory->currentText(),
+           CMakeGUITest_BINARY_DIR "/changingPresets/src/build1/preset");
+  QCOMPARE(this->m_window->Preset->isHidden(), false);
+  QCOMPARE(this->m_window->PresetLabel->isHidden(), false);
+
+  writePresets("build3", { "preset2" });
+  loopSleep(1500);
+  QCOMPARE(this->m_window->Preset->presetName(), QString{});
+  QCOMPARE(this->m_window->Preset->presets().size(), 1);
+  QCOMPARE(this->m_window->BinaryDirectory->currentText(),
+           CMakeGUITest_BINARY_DIR "/changingPresets/src/build1/preset");
+  QCOMPARE(this->m_window->Preset->isHidden(), false);
+  QCOMPARE(this->m_window->PresetLabel->isHidden(), false);
+
+  this->m_window->Preset->setPresetName("preset2");
+  loopSleep();
+  QCOMPARE(this->m_window->Preset->presetName(), "preset2");
+  QCOMPARE(this->m_window->Preset->presets().size(), 1);
+  QCOMPARE(this->m_window->BinaryDirectory->currentText(),
+           CMakeGUITest_BINARY_DIR "/changingPresets/src/build3/preset2");
+  QCOMPARE(this->m_window->Preset->isHidden(), false);
+  QCOMPARE(this->m_window->PresetLabel->isHidden(), false);
+
+  QDir::root().mkpath(CMakeGUITest_BINARY_DIR "/changingPresets/src2");
+  QFile::copy(CMakeGUITest_BINARY_DIR "/changingPresets/src/CMakePresets.json",
+              CMakeGUITest_BINARY_DIR
+              "/changingPresets/src2/CMakePresets.json");
+  this->m_window->SourceDirectory->setText(CMakeGUITest_BINARY_DIR
+                                           "/changingPresets/src2");
+  loopSleep();
+  QCOMPARE(this->m_window->Preset->presetName(), QString{});
+  QCOMPARE(this->m_window->Preset->presets().size(), 1);
+  QCOMPARE(this->m_window->BinaryDirectory->currentText(),
+           CMakeGUITest_BINARY_DIR "/changingPresets/src/build3/preset2");
+  QCOMPARE(this->m_window->Preset->isHidden(), false);
+  QCOMPARE(this->m_window->PresetLabel->isHidden(), false);
+
+  this->m_window->Preset->setPresetName("preset2");
+  loopSleep();
+  QCOMPARE(this->m_window->Preset->presetName(), "preset2");
+  QCOMPARE(this->m_window->Preset->presets().size(), 1);
+  QCOMPARE(this->m_window->BinaryDirectory->currentText(),
+           CMakeGUITest_BINARY_DIR "/changingPresets/src2/build3/preset2");
+  QCOMPARE(this->m_window->Preset->isHidden(), false);
+  QCOMPARE(this->m_window->PresetLabel->isHidden(), false);
+
+  QFile(CMakeGUITest_BINARY_DIR "/changingPresets/src2/CMakePresets.json")
+    .remove();
+  loopSleep(1500);
+  QCOMPARE(this->m_window->Preset->presetName(), QString{});
+  QCOMPARE(this->m_window->Preset->presets().size(), 0);
+  QCOMPARE(this->m_window->BinaryDirectory->currentText(),
+           CMakeGUITest_BINARY_DIR "/changingPresets/src2/build3/preset2");
+  QCOMPARE(this->m_window->Preset->isHidden(), true);
+  QCOMPARE(this->m_window->PresetLabel->isHidden(), true);
+}
+
 void SetupDefaultQSettings()
 {
   QSettings::setDefaultFormat(QSettings::IniFormat);

+ 3 - 0
Tests/CMakeGUI/CMakeGUITest.h

@@ -23,4 +23,7 @@ private slots:
   void simpleConfigure();
   void simpleConfigure_data();
   void environment();
+  void presetArg();
+  void presetArg_data();
+  void changingPresets();
 };

+ 21 - 0
Tests/CMakeGUI/CMakeLists.txt

@@ -72,3 +72,24 @@ add_cmake_gui_lib_test(QCMakeCacheModel
   MOC_SOURCES
     QCMakeCacheModelTest.h
   )
+add_cmake_gui_lib_test(QCMakePreset
+  SOURCES
+    QCMakePresetTest.cxx
+    QCMakePresetTest.h
+  MOC_SOURCES
+    QCMakePresetTest.h
+  )
+add_cmake_gui_lib_test(QCMakePresetItemModel
+  SOURCES
+    QCMakePresetItemModelTest.cxx
+    QCMakePresetItemModelTest.h
+  MOC_SOURCES
+    QCMakePresetItemModelTest.h
+  )
+add_cmake_gui_lib_test(QCMakePresetComboBox
+  SOURCES
+    QCMakePresetComboBoxTest.cxx
+    QCMakePresetComboBoxTest.h
+  MOC_SOURCES
+    QCMakePresetComboBoxTest.h
+  )

+ 80 - 0
Tests/CMakeGUI/QCMakePresetComboBoxTest.cxx

@@ -0,0 +1,80 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "QCMakePresetComboBoxTest.h"
+
+#include <QtTest>
+
+void QCMakePresetComboBoxTest::changePresets()
+{
+  QCMakePresetComboBox box;
+  QSignalSpy presetChanged(&box, &QCMakePresetComboBox::presetChanged);
+
+  QCOMPARE(presetChanged.size(), 0);
+
+  box.setPresets({});
+  QCOMPARE(presetChanged.size(), 0);
+
+  box.setPresetName(QString{});
+  QCOMPARE(presetChanged.size(), 0);
+
+  box.setPresets({
+    {
+      /*name=*/"preset",
+      /*description=*/"",
+      /*description=*/"",
+      /*generator=*/"Ninja",
+      /*architecture=*/"",
+      /*toolset=*/"",
+      /*setGenConfig=*/true,
+      /*enabled=*/true,
+    },
+  });
+  QCOMPARE(presetChanged.size(), 0);
+
+  box.setPresetName(QString{});
+  QCOMPARE(presetChanged.size(), 0);
+
+  box.setPresetName("noexist");
+  QCOMPARE(presetChanged.size(), 0);
+
+  box.setPresetName("preset");
+  QCOMPARE(presetChanged.size(), 1);
+  QCOMPARE(presetChanged.last(), QList<QVariant>{ "preset" });
+
+  box.setPresets({
+    {
+      /*name=*/"preset",
+      /*description=*/"",
+      /*description=*/"",
+      /*generator=*/"Ninja Multi-Config",
+      /*architecture=*/"",
+      /*toolset=*/"",
+      /*setGenConfig=*/true,
+      /*enabled=*/true,
+    },
+  });
+  QCOMPARE(presetChanged.size(), 1);
+
+  box.setPresetName("noexist");
+  QCOMPARE(presetChanged.size(), 2);
+  QCOMPARE(presetChanged.last(), QList<QVariant>{ QString{} });
+
+  box.setPresetName("preset");
+  QCOMPARE(presetChanged.size(), 3);
+  QCOMPARE(presetChanged.last(), QList<QVariant>{ "preset" });
+
+  box.blockSignals(true);
+  box.setPresetName(QString{});
+  box.blockSignals(false);
+  QCOMPARE(presetChanged.size(), 3);
+
+  box.setPresetName("preset");
+  QCOMPARE(presetChanged.size(), 4);
+  QCOMPARE(presetChanged.last(), QList<QVariant>{ "preset" });
+
+  box.setPresets({});
+  QCOMPARE(presetChanged.size(), 5);
+  QCOMPARE(presetChanged.last(), QList<QVariant>{ QString{} });
+}
+
+QTEST_MAIN(QCMakePresetComboBoxTest)

+ 13 - 0
Tests/CMakeGUI/QCMakePresetComboBoxTest.h

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

+ 162 - 0
Tests/CMakeGUI/QCMakePresetItemModelTest.cxx

@@ -0,0 +1,162 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "QCMakePresetItemModelTest.h"
+
+#include <utility>
+
+#include "QCMakePreset.h"
+#include "QCMakePresetItemModel.h"
+#include <QHash>
+#include <QMetaType>
+#include <QSignalSpy>
+#include <QVariant>
+#include <QVector>
+#include <QtTest>
+
+using QItemDataHash = QHash<Qt::ItemDataRole, QVariant>;
+
+void QCMakePresetItemModelTest::initTestCase()
+{
+  QMetaType::registerComparators<QCMakePreset>();
+}
+
+void QCMakePresetItemModelTest::initTestCase_data()
+{
+  QTest::addColumn<QVector<QCMakePreset>>("presets");
+  QTest::addColumn<QVector<QItemDataHash>>("data");
+
+  QVector<QCMakePreset> presets{
+    QCMakePreset{
+      /*name=*/"no-description",
+      /*description=*/"",
+      /*description=*/"",
+      /*generator=*/"",
+      /*architecture=*/"",
+      /*toolset=*/"",
+      /*setGenConfig=*/true,
+      /*enabled=*/true,
+    },
+    QCMakePreset{
+      /*name=*/"short-description",
+      /*description=*/"Short Description",
+      /*description=*/"",
+      /*generator=*/"",
+      /*architecture=*/"",
+      /*toolset=*/"",
+      /*setGenConfig=*/true,
+      /*enabled=*/true,
+    },
+    QCMakePreset{
+      /*name=*/"long-description",
+      /*description=*/"",
+      /*description=*/"Long Description",
+      /*generator=*/"",
+      /*architecture=*/"",
+      /*toolset=*/"",
+      /*setGenConfig=*/true,
+      /*enabled=*/true,
+    },
+    QCMakePreset{
+      /*name=*/"disabled",
+      /*description=*/"",
+      /*description=*/"",
+      /*generator=*/"",
+      /*architecture=*/"",
+      /*toolset=*/"",
+      /*setGenConfig=*/true,
+      /*enabled=*/false,
+    },
+  };
+  QVector<QItemDataHash> data{
+    QItemDataHash{
+      { Qt::AccessibleDescriptionRole, "" },
+      { Qt::DisplayRole, "no-description" },
+      { Qt::ToolTipRole, "" },
+      { Qt::UserRole, QVariant::fromValue(presets[0]) },
+      { Qt::FontRole, QFont{} },
+    },
+    QItemDataHash{
+      { Qt::AccessibleDescriptionRole, "" },
+      { Qt::DisplayRole, "Short Description" },
+      { Qt::ToolTipRole, "" },
+      { Qt::UserRole, QVariant::fromValue(presets[1]) },
+      { Qt::FontRole, QFont{} },
+    },
+    QItemDataHash{
+      { Qt::AccessibleDescriptionRole, "" },
+      { Qt::DisplayRole, "long-description" },
+      { Qt::ToolTipRole, "Long Description" },
+      { Qt::UserRole, QVariant::fromValue(presets[2]) },
+      { Qt::FontRole, QFont{} },
+    },
+    QItemDataHash{
+      { Qt::AccessibleDescriptionRole, "" },
+      { Qt::DisplayRole, "disabled" },
+      { Qt::ToolTipRole, "" },
+      { Qt::UserRole, QVariant::fromValue(presets[3]) },
+      { Qt::FontRole, QFont{} },
+    },
+    QItemDataHash{
+      { Qt::AccessibleDescriptionRole, "separator" },
+      { Qt::DisplayRole, QVariant{} },
+      { Qt::ToolTipRole, QVariant{} },
+      { Qt::UserRole, QVariant{} },
+      { Qt::FontRole, QFont{} },
+    },
+    QItemDataHash{
+      { Qt::AccessibleDescriptionRole, "" },
+      { Qt::DisplayRole, "<custom>" },
+      { Qt::ToolTipRole, "Specify all settings manually" },
+      { Qt::UserRole, QVariant{} },
+      { Qt::FontRole,
+        []() {
+          QFont f;
+          f.setItalic(true);
+          return f;
+        }() },
+    },
+  };
+  QTest::newRow("many") << presets << data;
+  QTest::newRow("none") << QVector<QCMakePreset>{}
+                        << QVector<QItemDataHash>{ data.last() };
+}
+
+void QCMakePresetItemModelTest::data()
+{
+  QFETCH_GLOBAL(QVector<QCMakePreset>, presets);
+  QFETCH_GLOBAL(QVector<QItemDataHash>, data);
+  QFETCH(Qt::ItemDataRole, role);
+
+  QCMakePresetItemModel model;
+  QSignalSpy spy1(&model, &QCMakePresetItemModel::modelAboutToBeReset);
+  QSignalSpy spy2(&model, &QCMakePresetItemModel::modelReset);
+  model.setPresets(presets);
+  QCOMPARE(spy1.size(), 1);
+  QCOMPARE(spy2.size(), 1);
+
+  QVector<QVariant> expectedData(data.size());
+  for (int i = 0; i < data.size(); ++i) {
+    expectedData[i] = data[i][role];
+  }
+
+  auto rows = model.rowCount();
+  QVector<QVariant> actualData(rows);
+  for (int i = 0; i < rows; ++i) {
+    actualData[i] = model.data(model.index(i, 0), role);
+  }
+
+  QCOMPARE(actualData, expectedData);
+}
+
+void QCMakePresetItemModelTest::data_data()
+{
+  QTest::addColumn<Qt::ItemDataRole>("role");
+
+  QTest::newRow("accessible") << Qt::AccessibleDescriptionRole;
+  QTest::newRow("display") << Qt::DisplayRole;
+  QTest::newRow("tooltip") << Qt::ToolTipRole;
+  QTest::newRow("user") << Qt::UserRole;
+  QTest::newRow("font") << Qt::FontRole;
+}
+
+QTEST_MAIN(QCMakePresetItemModelTest)

+ 17 - 0
Tests/CMakeGUI/QCMakePresetItemModelTest.h

@@ -0,0 +1,17 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "QCMakePresetItemModel.h"
+#include <QObject>
+
+class QCMakePresetItemModelTest : public QObject
+{
+  Q_OBJECT
+private slots:
+  void initTestCase();
+  void initTestCase_data();
+
+  void data();
+  void data_data();
+};

+ 82 - 0
Tests/CMakeGUI/QCMakePresetTest.cxx

@@ -0,0 +1,82 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "QCMakePresetTest.h"
+
+#include <utility>
+
+#include "QCMakePreset.h"
+#include <QtTest>
+
+namespace {
+QCMakePreset makePreset()
+{
+  return QCMakePreset{
+    /*name=*/"name",
+    /*displayName=*/"displayName",
+    /*description=*/"description",
+    /*generator=*/"generator",
+    /*architecture=*/"architecture",
+    /*toolset=*/"toolset",
+    /*setGenConfig=*/true,
+    /*enabled=*/true,
+  };
+}
+
+template <typename T, typename U>
+QCMakePreset makePreset(T QCMakePreset::*field, U&& value)
+{
+  auto preset = makePreset();
+  preset.*field = std::forward<U>(value);
+  return preset;
+}
+}
+
+void QCMakePresetTest::equality()
+{
+  QFETCH(QCMakePreset, rhs);
+  QFETCH(bool, equal);
+  QFETCH(bool, lt);
+  QFETCH(bool, gt);
+
+  auto lhs = makePreset();
+  QVERIFY((lhs == rhs) == equal);
+  QVERIFY((lhs != rhs) == !equal);
+  QVERIFY((lhs < rhs) == lt);
+  QVERIFY((lhs >= rhs) == !lt);
+  QVERIFY((lhs > rhs) == gt);
+  QVERIFY((lhs <= rhs) == !gt);
+}
+
+void QCMakePresetTest::equality_data()
+{
+  QTest::addColumn<QCMakePreset>("rhs");
+  QTest::addColumn<bool>("equal");
+  QTest::addColumn<bool>("lt");
+  QTest::addColumn<bool>("gt");
+
+  QTest::newRow("equal") << makePreset() << true << false << false;
+  QTest::newRow("name") << makePreset(&QCMakePreset::name, "other-name")
+                        << false << true << false;
+  QTest::newRow("displayName")
+    << makePreset(&QCMakePreset::displayName, "other-displayName") << false
+    << true << false;
+  QTest::newRow("description")
+    << makePreset(&QCMakePreset::description, "other-description") << false
+    << true << false;
+  QTest::newRow("generator")
+    << makePreset(&QCMakePreset::generator, "other-generator") << false << true
+    << false;
+  QTest::newRow("architecture")
+    << makePreset(&QCMakePreset::architecture, "other-architecture") << false
+    << true << false;
+  QTest::newRow("toolset") << makePreset(&QCMakePreset::toolset,
+                                         "other-toolset")
+                           << false << false << true;
+  QTest::newRow("setGenConfig")
+    << makePreset(&QCMakePreset::setGenConfig, false) << false << false
+    << true;
+  QTest::newRow("enabled") << makePreset(&QCMakePreset::enabled, false)
+                           << false << false << true;
+}
+
+QTEST_MAIN(QCMakePresetTest)

+ 14 - 0
Tests/CMakeGUI/QCMakePresetTest.h

@@ -0,0 +1,14 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "QCMakePreset.h"
+#include <QObject>
+
+class QCMakePresetTest : public QObject
+{
+  Q_OBJECT
+private slots:
+  void equality();
+  void equality_data();
+};

+ 33 - 0
Tests/CMakeGUI/presetArg-noPresetBinaryChange/CMakePresets.json.in

@@ -0,0 +1,33 @@
+{
+  "version": 1,
+  "configurePresets": [
+    {
+      "name": "ninja",
+      "generator": "Ninja",
+      "binaryDir": "${sourceDir}/build",
+      "cacheVariables": {
+        "STRING_VARIABLE": {
+          "type": "STRING",
+          "value": "String value"
+        },
+        "PATH_VARIABLE": {
+          "type": "PATH",
+          "value": "${sourceDir}"
+        },
+        "FILEPATH_VARIABLE": {
+          "type": "FILEPATH",
+          "value": "${sourceDir}/CMakeLists.txt"
+        },
+        "ON_VARIABLE": {
+          "type": "BOOL",
+          "value": "ON"
+        },
+        "FALSE_VARIABLE": {
+          "type": "BOOL",
+          "value": "FALSE"
+        },
+        "UNINITIALIZED_VARIABLE": "Uninitialized value"
+      }
+    }
+  ]
+}

+ 33 - 0
Tests/CMakeGUI/presetArg-preset/CMakePresets.json.in

@@ -0,0 +1,33 @@
+{
+  "version": 1,
+  "configurePresets": [
+    {
+      "name": "ninja",
+      "generator": "Ninja",
+      "binaryDir": "${sourceDir}/build",
+      "cacheVariables": {
+        "STRING_VARIABLE": {
+          "type": "STRING",
+          "value": "String value"
+        },
+        "PATH_VARIABLE": {
+          "type": "PATH",
+          "value": "${sourceDir}"
+        },
+        "FILEPATH_VARIABLE": {
+          "type": "FILEPATH",
+          "value": "${sourceDir}/CMakeLists.txt"
+        },
+        "ON_VARIABLE": {
+          "type": "BOOL",
+          "value": "ON"
+        },
+        "FALSE_VARIABLE": {
+          "type": "BOOL",
+          "value": "FALSE"
+        },
+        "UNINITIALIZED_VARIABLE": "Uninitialized value"
+      }
+    }
+  ]
+}

+ 33 - 0
Tests/CMakeGUI/presetArg-presetBinary/CMakePresets.json.in

@@ -0,0 +1,33 @@
+{
+  "version": 1,
+  "configurePresets": [
+    {
+      "name": "ninja",
+      "generator": "Ninja",
+      "binaryDir": "${sourceDir}/build",
+      "cacheVariables": {
+        "STRING_VARIABLE": {
+          "type": "STRING",
+          "value": "String value"
+        },
+        "PATH_VARIABLE": {
+          "type": "PATH",
+          "value": "${sourceDir}"
+        },
+        "FILEPATH_VARIABLE": {
+          "type": "FILEPATH",
+          "value": "${sourceDir}/CMakeLists.txt"
+        },
+        "ON_VARIABLE": {
+          "type": "BOOL",
+          "value": "ON"
+        },
+        "FALSE_VARIABLE": {
+          "type": "BOOL",
+          "value": "FALSE"
+        },
+        "UNINITIALIZED_VARIABLE": "Uninitialized value"
+      }
+    }
+  ]
+}

+ 39 - 0
Tests/CMakeGUI/presetArg-presetBinaryChange/CMakePresets.json.in

@@ -0,0 +1,39 @@
+{
+  "version": 1,
+  "configurePresets": [
+    {
+      "name": "ninja",
+      "generator": "Ninja",
+      "binaryDir": "${sourceDir}/build",
+      "cacheVariables": {
+        "STRING_VARIABLE": {
+          "type": "STRING",
+          "value": "String value"
+        },
+        "PATH_VARIABLE": {
+          "type": "PATH",
+          "value": "${sourceDir}"
+        },
+        "FILEPATH_VARIABLE": {
+          "type": "FILEPATH",
+          "value": "${sourceDir}/CMakeLists.txt"
+        },
+        "ON_VARIABLE": {
+          "type": "BOOL",
+          "value": "ON"
+        },
+        "FALSE_VARIABLE": {
+          "type": "BOOL",
+          "value": "FALSE"
+        },
+        "UNINITIALIZED_VARIABLE": "Uninitialized value"
+      }
+    },
+    {
+      "name": "ninja2",
+      "inherits": [
+        "ninja"
+      ]
+    }
+  ]
+}

+ 2 - 0
Tests/CMakeGUI/presetArg-presetConfigExists/CMakeLists.txt.in

@@ -0,0 +1,2 @@
+cmake_minimum_required(VERSION 3.18)
+project(sourceBinaryArgs-sourceDir NONE)

+ 33 - 0
Tests/CMakeGUI/presetArg-presetConfigExists/CMakePresets.json.in

@@ -0,0 +1,33 @@
+{
+  "version": 1,
+  "configurePresets": [
+    {
+      "name": "ninja",
+      "generator": "Ninja",
+      "binaryDir": "${sourceDir}/build",
+      "cacheVariables": {
+        "STRING_VARIABLE": {
+          "type": "STRING",
+          "value": "String value"
+        },
+        "PATH_VARIABLE": {
+          "type": "PATH",
+          "value": "${sourceDir}"
+        },
+        "FILEPATH_VARIABLE": {
+          "type": "FILEPATH",
+          "value": "${sourceDir}/CMakeLists.txt"
+        },
+        "ON_VARIABLE": {
+          "type": "BOOL",
+          "value": "ON"
+        },
+        "FALSE_VARIABLE": {
+          "type": "BOOL",
+          "value": "FALSE"
+        },
+        "UNINITIALIZED_VARIABLE": "Uninitialized value"
+      }
+    }
+  ]
+}

+ 2 - 0
Tests/CMakeGUI/presetArg-presetConfigExists/CMakeSetup.ini.in

@@ -0,0 +1,2 @@
+[Settings]
+StartPath\WhereBuild0=@CMake_BINARY_DIR@