From 3dd12e5da1a89ef67fb6da33108ba3a8c5c72337 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Tue, 2 Sep 2025 18:59:46 -0500 Subject: [PATCH 1/6] Gui: Improve version migration UX --- src/App/ApplicationDirectories.cpp | 3 + src/Gui/CMakeLists.txt | 2 + src/Gui/StartupProcess.cpp | 150 +------------ src/Gui/StartupProcess.h | 3 +- src/Gui/VersionMigrator.cpp | 326 +++++++++++++++++++++++++++++ src/Gui/VersionMigrator.h | 58 +++++ 6 files changed, 397 insertions(+), 145 deletions(-) create mode 100644 src/Gui/VersionMigrator.cpp create mode 100644 src/Gui/VersionMigrator.h diff --git a/src/App/ApplicationDirectories.cpp b/src/App/ApplicationDirectories.cpp index 2102bd49bc..08685a29c5 100644 --- a/src/App/ApplicationDirectories.cpp +++ b/src/App/ApplicationDirectories.cpp @@ -41,6 +41,8 @@ #include #include +#include "Base/Console.h" + using namespace App; namespace fs = std::filesystem; @@ -567,6 +569,7 @@ void ApplicationDirectories::migrateAllPaths(const std::vector &paths) } else { newPath = path / versionStringForPath(major, minor); } + Base::Console().message("Migrating config from %s to %s\n", Base::FileInfo::pathToString(path), Base::FileInfo::pathToString(newPath)); if (fs::exists(newPath)) { throw Base::RuntimeError("Cannot migrate config - path already exists: " + Base::FileInfo::pathToString(newPath)); } diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index fbd4fe2f98..447056d4c5 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -1386,6 +1386,7 @@ SET(FreeCADGui_CPP_SRCS StartupProcess.cpp TransactionObject.cpp ToolHandler.cpp + VersionMigrator.cpp StyleParameters/Parser.cpp StyleParameters/ParameterManager.cpp ) @@ -1429,6 +1430,7 @@ SET(FreeCADGui_SRCS StartupProcess.h TransactionObject.h ToolHandler.h + VersionMigrator.h StyleParameters/Parser.h StyleParameters/ParameterManager.h ) diff --git a/src/Gui/StartupProcess.cpp b/src/Gui/StartupProcess.cpp index 820017cb29..5d00a0348b 100644 --- a/src/Gui/StartupProcess.cpp +++ b/src/Gui/StartupProcess.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -52,12 +53,13 @@ #include "GuiApplication.h" #include "MainWindow.h" #include "Language/Translator.h" +#include "VersionMigrator.h" #include -#include #include using namespace Gui; +namespace fs = std::filesystem; StartupProcess::StartupProcess() = default; @@ -231,7 +233,7 @@ void StartupPostProcess::execute() showMainWindow(); activateWorkbench(); checkParameters(); - runWelcomeScreen(); + checkVersionMigration(); } void StartupPostProcess::setWindowTitle() @@ -557,145 +559,7 @@ void StartupPostProcess::checkParameters() } } -void StartupPostProcess::runWelcomeScreen() -{ - // If the user is running a custom directory set, there is no migration to versioned directories - if (App::Application::directories()->usingCustomDirectories()) { - return; - } - - auto prefGroup = App::GetApplication().GetParameterGroupByPath( - "User parameter:BaseApp/Preferences/Migration"); - - // Split our comma-separated list of already-migrated-to version directories into a set for easy - // searching - auto splitCommas = [](const std::string &input) { - std::set result; - std::stringstream ss(input); - std::string token; - - while (std::getline(ss, token, ',')) { - result.insert(token); - } - - return result; - }; - - std::string offeredToMigrateToVersionedConfig = - prefGroup->GetASCII("OfferedToMigrateToVersionedConfig", ""); - std::set knownVersions; - if (!offeredToMigrateToVersionedConfig.empty()) { - knownVersions = splitCommas(offeredToMigrateToVersionedConfig); - } - - auto joinCommas = [](const std::set& s) { - std::ostringstream oss; - for (auto it = s.begin(); it != s.end(); ++it) { - if (it != s.begin()) { - oss << ','; - } - oss << *it; - } - return oss.str(); - }; - - int major = std::stoi(App::Application::Config()["BuildVersionMajor"]); - int minor = std::stoi(App::Application::Config()["BuildVersionMinor"]); - std::string currentVersionedDirName = App::ApplicationDirectories::versionStringForPath(major, minor); - if (!knownVersions.contains(currentVersionedDirName) - && !App::Application::directories()->usingCurrentVersionConfig( - App::Application::directories()->getUserAppDataDir())) { - auto programName = QString::fromStdString(App::Application::getExecutableName()); - auto result = QMessageBox::question( - mainWindow, - QObject::tr("Welcome to %1 v%2.%3").arg(programName, QString::number(major), QString::number(minor)), - QObject::tr("Welcome to %1 v%2.%3\n\n").arg(programName, QString::number(major), QString::number(minor)) - + QObject::tr("Configuration data and addons from previous program version found. " - "Migrate the old configuration to this version?"), - QMessageBox::Yes | QMessageBox::No); - knownVersions.insert(currentVersionedDirName); - prefGroup->SetASCII("OfferedToMigrateToVersionedConfig", joinCommas(knownVersions)); - if (result == QMessageBox::Yes) { - migrateToCurrentVersion(); - } - } +void StartupPostProcess::checkVersionMigration() const { + VersionMigrator migrator(mainWindow); + migrator.execute(); } - -class PathMigrationWorker : public QObject -{ - Q_OBJECT - -public: - void run () { - try { - App::GetApplication().GetUserParameter().SaveDocument(); - App::Application::directories()->migrateAllPaths( - {App::Application::getUserAppDataDir(), App::Application::getUserConfigPath()}); - Q_EMIT(complete()); - } catch (const Base::Exception& e) { - Base::Console().error("Error migrating configuration data: %s\n", e.what()); - Q_EMIT(failed()); - } catch (const std::exception& e) { - Base::Console().error("Unrecognized error migrating configuration data: %s\n", e.what()); - Q_EMIT(failed()); - } catch (...) { - Base::Console().error("Error migrating configuration data\n"); - Q_EMIT(failed()); - } - } - -Q_SIGNALS: - void complete(); - void failed(); -}; - -void StartupPostProcess::migrateToCurrentVersion() -{ - auto *workerThread = new QThread(mainWindow); - auto *worker = new PathMigrationWorker(); - worker->moveToThread(workerThread); - QObject::connect(workerThread, &QThread::started, worker, &PathMigrationWorker::run); - - auto migrationRunning = new QMessageBox(mainWindow); - migrationRunning->setWindowTitle(QObject::tr("Migrating")); - migrationRunning->setText(QObject::tr("Migrating configuration data and addons...")); - migrationRunning->setStandardButtons(QMessageBox::NoButton); - QObject::connect(worker, &PathMigrationWorker::complete, migrationRunning, &QMessageBox::accept); - QObject::connect(worker, &PathMigrationWorker::failed, migrationRunning, &QMessageBox::reject); - - workerThread->start(); - migrationRunning->exec(); - - if (migrationRunning->result() == QDialog::Accepted) { - auto* restarting = new QMessageBox(mainWindow); - restarting->setText( - QObject::tr("Migration complete. Restarting...")); - restarting->setWindowTitle(QObject::tr("Restarting")); - restarting->setStandardButtons(QMessageBox::NoButton); - auto closeNotice = [restarting]() { - restarting->reject(); - }; - - // Insert a short delay before restart so the user can see the success message, and - // knows it's a restart and not a crash... - const int delayRestartMillis {2000}; - QTimer::singleShot(delayRestartMillis, closeNotice); - restarting->exec(); - - QObject::connect(qApp, &QCoreApplication::aboutToQuit, [=] { - if (getMainWindow()->close()) { - auto args = QApplication::arguments(); - args.removeFirst(); - QProcess::startDetached(QApplication::applicationFilePath(), - args, - QApplication::applicationDirPath()); - } - }); - QCoreApplication::exit(0); - _exit(0); // No really. Die. - } else { - QMessageBox::critical(mainWindow, QObject::tr("Migration failed"),QObject::tr("Migration failed. See the Report View for details.")); - } -} - -#include "StartupProcess.moc" diff --git a/src/Gui/StartupProcess.h b/src/Gui/StartupProcess.h index f3d045d098..82408c725b 100644 --- a/src/Gui/StartupProcess.h +++ b/src/Gui/StartupProcess.h @@ -76,8 +76,7 @@ private: void showMainWindow(); void activateWorkbench(); void checkParameters(); - void runWelcomeScreen(); - void migrateToCurrentVersion(); + void checkVersionMigration() const; private: bool loadFromPythonModule = false; diff --git a/src/Gui/VersionMigrator.cpp b/src/Gui/VersionMigrator.cpp new file mode 100644 index 0000000000..7a9be8b3aa --- /dev/null +++ b/src/Gui/VersionMigrator.cpp @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +/*************************************************************************** + * Copyright (c) 2025 The FreeCAD project association AISBL * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + **************************************************************************/ + +#include "PreCompiled.h" +#ifndef _PreComp_ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#endif + +#include "VersionMigrator.h" + +#include "MainWindow.h" +#include +#include + + +using namespace Gui; +namespace fs = std::filesystem; + + +uintmax_t calculateDirectorySize(const fs::path &dir) { + uintmax_t size = 0; + for (auto &entry: fs::recursive_directory_iterator(dir)) { + if (fs::is_regular_file(entry.status())) { + size += fs::file_size(entry.path()); + } + } + return size; +} + +std::set getKnownVersions() { + auto splitCommas = [](const std::string &input) { + std::set result; + std::stringstream ss(input); + std::string token; + while (std::getline(ss, token, ',')) { + result.insert(token); + } + return result; + }; + + auto prefGroup = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Migration"); + + // Split our comma-separated list of already-migrated-to version directories into a set for easy + // searching + std::string offeredToMigrateToVersionedConfig = + prefGroup->GetASCII("OfferedToMigrateToVersionedConfig", ""); + std::set knownVersions; + if (!offeredToMigrateToVersionedConfig.empty()) { + knownVersions = splitCommas(offeredToMigrateToVersionedConfig); + } + return knownVersions; +} + +void setKnownVersions(const std::set &knownVersions) { + auto joinCommas = [](const std::set &s) { + std::ostringstream oss; + for (auto it = s.begin(); it != s.end(); ++it) { + if (it != s.begin()) { + oss << ','; + } + oss << *it; + } + return oss.str(); + }; + + auto prefGroup = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Migration"); + prefGroup->SetASCII("OfferedToMigrateToVersionedConfig", joinCommas(knownVersions)); +} + +void markCurrentVersionAsKnown() { + int major = std::stoi(App::Application::Config()["BuildVersionMajor"]); + int minor = std::stoi(App::Application::Config()["BuildVersionMinor"]); + std::string currentVersionedDirName = App::ApplicationDirectories::versionStringForPath(major, minor); + std::set knownVersions = getKnownVersions(); + knownVersions.insert(currentVersionedDirName); + setKnownVersions(knownVersions); +} + +VersionMigrator::VersionMigrator(MainWindow *mw) : QObject(mw), mainWindow(mw) { +} + +void VersionMigrator::execute() { + // If the user is running a custom directory set, there is no migration to versioned directories + if (App::Application::directories()->usingCustomDirectories()) { + return; + } + + auto prefGroup = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Migration"); + + // Split our comma-separated list of already-migrated-to version directories into a set for easy + // searching + std::string offeredToMigrateToVersionedConfig = + prefGroup->GetASCII("OfferedToMigrateToVersionedConfig", ""); + std::set knownVersions = getKnownVersions(); + + int major = std::stoi(App::Application::Config()["BuildVersionMajor"]); + int minor = std::stoi(App::Application::Config()["BuildVersionMinor"]); + std::string currentVersionedDirName = App::ApplicationDirectories::versionStringForPath(major, minor); + if (!knownVersions.contains(currentVersionedDirName) + && !App::Application::directories()->usingCurrentVersionConfig( + App::Application::directories()->getUserAppDataDir())) { + auto programName = QString::fromStdString(App::Application::getExecutableName()); + auto result = QMessageBox::question( + mainWindow, + QObject::tr("Welcome to %1 v%2.%3").arg(programName, QString::number(major), QString::number(minor)), + QObject::tr("Welcome to %1 v%2.%3\n\n").arg(programName, QString::number(major), QString::number(minor)) + + QObject::tr("Configuration data and addons from previous program version found. " + "Migrate the configuration to a new directory for this version? Answering 'No' will " + "continue to use the old directory. 'Yes' will copy it."), + QMessageBox::Yes | QMessageBox::No); + if (result == QMessageBox::Yes) { + confirmMigration(); + } else { + // Don't ask again for this version + markCurrentVersionAsKnown(); + } + } +} + +class DirectorySizeCalculationWorker : public QObject { + Q_OBJECT + +public: + void run() { + auto dir = App::Application::directories()->getUserAppDataDir(); + uintmax_t size = 0; + auto thisThread = QThread::currentThread(); + for (auto &entry: fs::recursive_directory_iterator(dir)) { + if (thisThread->isInterruptionRequested()) { + Q_EMIT(cancelled()); + Q_EMIT(finished()); + return; + } + if (fs::is_regular_file(entry.status())) { + size += fs::file_size(entry.path()); + } + } + Q_EMIT(sizeFound(size)); + Q_EMIT(finished()); + } + +Q_SIGNALS: + void finished(); + + void sizeFound(uintmax_t _t1); + + void cancelled(); +}; + +class PathMigrationWorker : public QObject { + Q_OBJECT + +public: + void run() { + try { + App::GetApplication().GetUserParameter().SaveDocument(); + App::Application::directories()->migrateAllPaths( + { + App::Application::directories()->getUserAppDataDir(), + App::Application::directories()->getUserConfigPath() + }); + Q_EMIT(complete()); + } catch (const Base::Exception &e) { + Base::Console().error("Error migrating configuration data: %s\n", e.what()); + Q_EMIT(failed()); + } catch (const std::exception &e) { + Base::Console().error("Unrecognized error migrating configuration data: %s\n", e.what()); + Q_EMIT(failed()); + } catch (...) { + Base::Console().error("Error migrating configuration data\n"); + Q_EMIT(failed()); + } + Q_EMIT(finished()); + } + +Q_SIGNALS: + void finished(); + + void complete(); + + void failed(); +}; + +void VersionMigrator::confirmMigration() { + auto *workerThread = new QThread(mainWindow); + auto *worker = new DirectorySizeCalculationWorker(); + worker->moveToThread(workerThread); + connect(workerThread, &QThread::started, worker, &DirectorySizeCalculationWorker::run); + + connect(worker, &DirectorySizeCalculationWorker::sizeFound, this, &VersionMigrator::showSizeOfMigration); + connect(worker, &DirectorySizeCalculationWorker::finished, workerThread, &QThread::quit); + connect(worker, &DirectorySizeCalculationWorker::finished, worker, &QObject::deleteLater); + connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater); + + auto calculatingSize = new QMessageBox(mainWindow); + calculatingSize->setWindowTitle(QObject::tr("Calculating size")); + calculatingSize->setText(QObject::tr("Calculating directory size…")); + calculatingSize->setStandardButtons(QMessageBox::Cancel); + connect(worker, &DirectorySizeCalculationWorker::sizeFound, calculatingSize, &QMessageBox::accept); + connect(calculatingSize, &QMessageBox::rejected, workerThread, &QThread::requestInterruption); + connect(calculatingSize, &QMessageBox::rejected, this, &VersionMigrator::migrationCancelled); + + workerThread->start(); + calculatingSize->exec(); +} + +void VersionMigrator::migrationCancelled() const { + auto message = QObject::tr( + "Migration cancelled. Ask again on next restart?"); + auto result = QMessageBox::question( + mainWindow, + QObject::tr("Migration cancelled"), + message, + QMessageBox::Yes | QMessageBox::No); + if (result == QMessageBox::No) { + markCurrentVersionAsKnown(); + } +} + +void VersionMigrator::showSizeOfMigration(uintmax_t size) { + auto sizeString = QLocale().formattedDataSize(static_cast(size)); + auto result = QMessageBox::question( + mainWindow, + QObject::tr("Migration Size"), + QObject::tr("Migrating will copy %1 into a versioned subdirectory. Continue?").arg(sizeString), + QMessageBox::Yes | QMessageBox::No + ); + if (result == QMessageBox::Yes) { + migrateToCurrentVersion(); + } else { + migrationCancelled(); + } +} + +void VersionMigrator::migrateToCurrentVersion() { + auto oldKnownVersions = getKnownVersions(); + markCurrentVersionAsKnown(); // This MUST be done before the migration, or it won't get remembered + auto *workerThread = new QThread(mainWindow); + auto *worker = new PathMigrationWorker(); + worker->moveToThread(workerThread); + connect(workerThread, &QThread::started, worker, &PathMigrationWorker::run); + connect(worker, &PathMigrationWorker::finished, workerThread, &QThread::quit); + connect(worker, &PathMigrationWorker::finished, worker, &QObject::deleteLater); + connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater); + + auto migrationRunning = new QMessageBox(mainWindow); + migrationRunning->setWindowTitle(QObject::tr("Migrating")); + migrationRunning->setText(QObject::tr("Migrating configuration data and addons...")); + migrationRunning->setStandardButtons(QMessageBox::NoButton); + connect(worker, &PathMigrationWorker::complete, migrationRunning, &QMessageBox::accept); + connect(worker, &PathMigrationWorker::failed, migrationRunning, &QMessageBox::reject); + + workerThread->start(); + migrationRunning->exec(); + + if (migrationRunning->result() == QDialog::Accepted) { + App::GetApplication().GetUserParameter().SaveDocument(); // Flush to disk before restarting + auto *restarting = new QMessageBox(mainWindow); + restarting->setText( + QObject::tr("Migration complete. Restarting...")); + restarting->setWindowTitle(QObject::tr("Restarting")); + restarting->setStandardButtons(QMessageBox::NoButton); + auto closeNotice = [restarting]() { + restarting->reject(); + }; + + // Insert a short delay before restart so the user can see the success message, and + // knows it's a restart and not a crash... + constexpr int delayRestartMillis{2000}; + QTimer::singleShot(delayRestartMillis, closeNotice); + restarting->exec(); + + connect(qApp, &QCoreApplication::aboutToQuit, [=] { + if (getMainWindow()->close()) { + auto args = QApplication::arguments(); + args.removeFirst(); + QProcess::startDetached(QApplication::applicationFilePath(), + args, + QApplication::applicationDirPath()); + } + }); + QCoreApplication::exit(0); + _exit(0); // No really. Die. + } else { + setKnownVersions(oldKnownVersions); // Reset, we didn't migrate after all + QMessageBox::critical(mainWindow, QObject::tr("Migration failed"), + QObject::tr("Migration failed. See the Report View for details.")); + } +} + +#include "VersionMigrator.moc" diff --git a/src/Gui/VersionMigrator.h b/src/Gui/VersionMigrator.h new file mode 100644 index 0000000000..36d90e560b --- /dev/null +++ b/src/Gui/VersionMigrator.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +/*************************************************************************** + * Copyright (c) 2025 The FreeCAD project association AISBL * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + **************************************************************************/ + +#ifndef GUI_VERSIONMIGRATOR_H +#define GUI_VERSIONMIGRATOR_H + +#include +#include +#include + +namespace Gui { + + class MainWindow; + + +class GuiExport VersionMigrator : public QObject +{ + Q_OBJECT + +public: + explicit VersionMigrator(MainWindow *mw); + void execute(); + +protected Q_SLOTS: + + void confirmMigration(); + void migrationCancelled() const; + void showSizeOfMigration(uintmax_t size); + void migrateToCurrentVersion(); + +private: + MainWindow* mainWindow; +}; + + +} + +#endif // GUI_VERSIONMIGRATOR_H From 26ba6969f6ca33e01f0e06d618390f2f332c5f26 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Tue, 9 Sep 2025 14:56:53 -0500 Subject: [PATCH 2/6] Gui: Migration UI take 2 --- src/Gui/CMakeLists.txt | 6 +- .../DlgVersionMigrator.cpp} | 173 +++++++++--------- .../DlgVersionMigrator.h} | 44 +++-- src/Gui/Dialogs/DlgVersionMigrator.ui | 109 +++++++++++ src/Gui/StartupProcess.cpp | 10 +- 5 files changed, 226 insertions(+), 116 deletions(-) rename src/Gui/{VersionMigrator.cpp => Dialogs/DlgVersionMigrator.cpp} (69%) rename src/Gui/{VersionMigrator.h => Dialogs/DlgVersionMigrator.h} (64%) create mode 100644 src/Gui/Dialogs/DlgVersionMigrator.ui diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index 447056d4c5..6f0bfbbe0c 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -448,6 +448,7 @@ SET(Gui_UIC_SRCS Dialogs/DlgPropertyLink.ui Dialogs/DlgRevertToBackupConfig.ui Dialogs/DlgThemeEditor.ui + Dialogs/DlgVersionMigrator.ui PreferencePages/DlgSettings3DView.ui PreferencePages/DlgSettingsCacheDirectory.ui Dialogs/DlgSettingsColorGradient.ui @@ -564,6 +565,7 @@ SET(Dialog_CPP_SRCS Dialogs/DlgRevertToBackupConfigImp.cpp Dialogs/DlgExpressionInput.cpp Dialogs/DlgThemeEditor.cpp + Dialogs/DlgVersionMigrator.cpp TaskDlgRelocation.cpp Dialogs/DlgCheckableMessageBox.cpp TaskTransform.cpp @@ -606,6 +608,7 @@ SET(Dialog_HPP_SRCS Dialogs/DlgCheckableMessageBox.h Dialogs/DlgExpressionInput.h Dialogs/DlgThemeEditor.h + Dialogs/DlgVersionMigrator.h TaskDlgRelocation.h TaskTransform.h Dialogs/DlgUndoRedo.h @@ -652,6 +655,7 @@ SET(Dialog_SRCS Dialogs/DlgTreeWidget.ui Dialogs/DlgExpressionInput.ui Dialogs/DlgCreateNewPreferencePack.ui + Dialogs/DlgVersionMigrator.ui DownloadManager.ui DownloadItem.ui DocumentRecovery.ui @@ -1386,7 +1390,6 @@ SET(FreeCADGui_CPP_SRCS StartupProcess.cpp TransactionObject.cpp ToolHandler.cpp - VersionMigrator.cpp StyleParameters/Parser.cpp StyleParameters/ParameterManager.cpp ) @@ -1430,7 +1433,6 @@ SET(FreeCADGui_SRCS StartupProcess.h TransactionObject.h ToolHandler.h - VersionMigrator.h StyleParameters/Parser.h StyleParameters/ParameterManager.h ) diff --git a/src/Gui/VersionMigrator.cpp b/src/Gui/Dialogs/DlgVersionMigrator.cpp similarity index 69% rename from src/Gui/VersionMigrator.cpp rename to src/Gui/Dialogs/DlgVersionMigrator.cpp index 7a9be8b3aa..3ec92021ec 100644 --- a/src/Gui/VersionMigrator.cpp +++ b/src/Gui/Dialogs/DlgVersionMigrator.cpp @@ -23,8 +23,7 @@ #include "PreCompiled.h" #ifndef _PreComp_ -#include -#include +#include #include #include #include @@ -33,32 +32,26 @@ #include #include +#include #include #include #include #endif -#include "VersionMigrator.h" +#include "DlgVersionMigrator.h" +#include "ui_DlgVersionMigrator.h" -#include "MainWindow.h" +#include "../MainWindow.h" #include #include +#include "QTextBrowser" -using namespace Gui; + +using namespace Gui::Dialog; namespace fs = std::filesystem; -uintmax_t calculateDirectorySize(const fs::path &dir) { - uintmax_t size = 0; - for (auto &entry: fs::recursive_directory_iterator(dir)) { - if (fs::is_regular_file(entry.status())) { - size += fs::file_size(entry.path()); - } - } - return size; -} - std::set getKnownVersions() { auto splitCommas = [](const std::string &input) { std::set result; @@ -110,46 +103,63 @@ void markCurrentVersionAsKnown() { setKnownVersions(knownVersions); } -VersionMigrator::VersionMigrator(MainWindow *mw) : QObject(mw), mainWindow(mw) { -} -void VersionMigrator::execute() { - // If the user is running a custom directory set, there is no migration to versioned directories - if (App::Application::directories()->usingCustomDirectories()) { - return; - } +DlgVersionMigrator::DlgVersionMigrator(MainWindow *mw) : + QDialog(mw) + , mainWindow(mw) + , sizeCalculationWorkerThread(nullptr) + , ui(std::make_unique()) +{ + ui->setupUi(this); auto prefGroup = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Migration"); - // Split our comma-separated list of already-migrated-to version directories into a set for easy - // searching - std::string offeredToMigrateToVersionedConfig = - prefGroup->GetASCII("OfferedToMigrateToVersionedConfig", ""); - std::set knownVersions = getKnownVersions(); + int major = std::stoi(App::Application::Config()["BuildVersionMajor"]); + int minor = std::stoi(App::Application::Config()["BuildVersionMinor"]); + auto programName = QString::fromStdString(App::Application::getExecutableName()); + + // NOTE: All rich-text strings are generated programmatically so that translators don't have to deal with the + // markup. The two strings in the middle of the dialog are set in the UI file. + + auto welcomeString = QStringLiteral("") + QObject::tr("Welcome to %1 %2.%3\n\n").arg( + programName, QString::number(major), QString::number(minor)) + QStringLiteral(""); + + auto calculatingSizeString = QStringLiteral("") + QObject::tr("Calculating size…") + QStringLiteral(""); + + auto shareConfigurationString = QStringLiteral("") + + QObject::tr("Share configuration between versions") + QStringLiteral(""); + + ui->welcomeLabel->setText(welcomeString); + ui->sizeLabel->setText(calculatingSizeString); + ui->shareLinkLabel->setText(shareConfigurationString); + ui->shareLinkLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); + + connect(ui->copyButton, &QPushButton::clicked, this, &DlgVersionMigrator::migrateToCurrentVersion); + connect(ui->shareLinkLabel, &QLabel::linkActivated, this, &DlgVersionMigrator::doNotMigrate); +} + +DlgVersionMigrator::~DlgVersionMigrator() = default; + +int DlgVersionMigrator::exec() { + // If the user is running a custom directory set, there is no migration to versioned directories + if (App::Application::directories()->usingCustomDirectories()) { + return 0; + } + std::set knownVersions = getKnownVersions(); int major = std::stoi(App::Application::Config()["BuildVersionMajor"]); int minor = std::stoi(App::Application::Config()["BuildVersionMinor"]); std::string currentVersionedDirName = App::ApplicationDirectories::versionStringForPath(major, minor); - if (!knownVersions.contains(currentVersionedDirName) - && !App::Application::directories()->usingCurrentVersionConfig( - App::Application::directories()->getUserAppDataDir())) { - auto programName = QString::fromStdString(App::Application::getExecutableName()); - auto result = QMessageBox::question( - mainWindow, - QObject::tr("Welcome to %1 v%2.%3").arg(programName, QString::number(major), QString::number(minor)), - QObject::tr("Welcome to %1 v%2.%3\n\n").arg(programName, QString::number(major), QString::number(minor)) - + QObject::tr("Configuration data and addons from previous program version found. " - "Migrate the configuration to a new directory for this version? Answering 'No' will " - "continue to use the old directory. 'Yes' will copy it."), - QMessageBox::Yes | QMessageBox::No); - if (result == QMessageBox::Yes) { - confirmMigration(); - } else { - // Don't ask again for this version - markCurrentVersionAsKnown(); + if (!knownVersions.contains(currentVersionedDirName) && !App::Application::directories()->usingCurrentVersionConfig( + App::Application::directories()->getUserAppDataDir())) { + calculateMigrationSize(); + auto result = QDialog::exec(); + if (sizeCalculationWorkerThread && sizeCalculationWorkerThread->isRunning()) { + sizeCalculationWorkerThread->quit(); } } + return 0; } class DirectorySizeCalculationWorker : public QObject { @@ -216,58 +226,41 @@ Q_SIGNALS: void failed(); }; -void VersionMigrator::confirmMigration() { - auto *workerThread = new QThread(mainWindow); +void DlgVersionMigrator::calculateMigrationSize() { + sizeCalculationWorkerThread = new QThread(mainWindow); auto *worker = new DirectorySizeCalculationWorker(); - worker->moveToThread(workerThread); - connect(workerThread, &QThread::started, worker, &DirectorySizeCalculationWorker::run); + worker->moveToThread(sizeCalculationWorkerThread); + connect(sizeCalculationWorkerThread, &QThread::started, worker, &DirectorySizeCalculationWorker::run); - connect(worker, &DirectorySizeCalculationWorker::sizeFound, this, &VersionMigrator::showSizeOfMigration); - connect(worker, &DirectorySizeCalculationWorker::finished, workerThread, &QThread::quit); + connect(worker, &DirectorySizeCalculationWorker::sizeFound, this, &DlgVersionMigrator::showSizeOfMigration); + connect(worker, &DirectorySizeCalculationWorker::finished, sizeCalculationWorkerThread, &QThread::quit); connect(worker, &DirectorySizeCalculationWorker::finished, worker, &QObject::deleteLater); - connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater); + connect(sizeCalculationWorkerThread, &QThread::finished, sizeCalculationWorkerThread, &QObject::deleteLater); - auto calculatingSize = new QMessageBox(mainWindow); - calculatingSize->setWindowTitle(QObject::tr("Calculating size")); - calculatingSize->setText(QObject::tr("Calculating directory size…")); - calculatingSize->setStandardButtons(QMessageBox::Cancel); - connect(worker, &DirectorySizeCalculationWorker::sizeFound, calculatingSize, &QMessageBox::accept); - connect(calculatingSize, &QMessageBox::rejected, workerThread, &QThread::requestInterruption); - connect(calculatingSize, &QMessageBox::rejected, this, &VersionMigrator::migrationCancelled); - - workerThread->start(); - calculatingSize->exec(); + sizeCalculationWorkerThread->start(); } -void VersionMigrator::migrationCancelled() const { - auto message = QObject::tr( - "Migration cancelled. Ask again on next restart?"); - auto result = QMessageBox::question( - mainWindow, - QObject::tr("Migration cancelled"), - message, - QMessageBox::Yes | QMessageBox::No); - if (result == QMessageBox::No) { - markCurrentVersionAsKnown(); +void DlgVersionMigrator::doNotMigrate(const QString &linkText) +{ + Q_UNUSED(linkText); + markCurrentVersionAsKnown(); + if (sizeCalculationWorkerThread && sizeCalculationWorkerThread->isRunning()) { + sizeCalculationWorkerThread->quit(); } + close(); } -void VersionMigrator::showSizeOfMigration(uintmax_t size) { +void DlgVersionMigrator::showSizeOfMigration(uintmax_t size) { auto sizeString = QLocale().formattedDataSize(static_cast(size)); - auto result = QMessageBox::question( - mainWindow, - QObject::tr("Migration Size"), - QObject::tr("Migrating will copy %1 into a versioned subdirectory. Continue?").arg(sizeString), - QMessageBox::Yes | QMessageBox::No - ); - if (result == QMessageBox::Yes) { - migrateToCurrentVersion(); - } else { - migrationCancelled(); - } + auto sizeMessage = QStringLiteral("") + + QObject::tr("Estimated size of data to copy: %1").arg(sizeString) + + QStringLiteral(""); + ui->sizeLabel->setText(sizeMessage); + sizeCalculationWorkerThread = nullptr; // Deleted via a previously-configured deleteLater() } -void VersionMigrator::migrateToCurrentVersion() { +void DlgVersionMigrator::migrateToCurrentVersion() { + hide(); auto oldKnownVersions = getKnownVersions(); markCurrentVersionAsKnown(); // This MUST be done before the migration, or it won't get remembered auto *workerThread = new QThread(mainWindow); @@ -278,9 +271,9 @@ void VersionMigrator::migrateToCurrentVersion() { connect(worker, &PathMigrationWorker::finished, worker, &QObject::deleteLater); connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater); - auto migrationRunning = new QMessageBox(mainWindow); + auto migrationRunning = new QMessageBox(this); migrationRunning->setWindowTitle(QObject::tr("Migrating")); - migrationRunning->setText(QObject::tr("Migrating configuration data and addons...")); + migrationRunning->setText(QObject::tr("Migrating configuration data and addons…")); migrationRunning->setStandardButtons(QMessageBox::NoButton); connect(worker, &PathMigrationWorker::complete, migrationRunning, &QMessageBox::accept); connect(worker, &PathMigrationWorker::failed, migrationRunning, &QMessageBox::reject); @@ -290,16 +283,16 @@ void VersionMigrator::migrateToCurrentVersion() { if (migrationRunning->result() == QDialog::Accepted) { App::GetApplication().GetUserParameter().SaveDocument(); // Flush to disk before restarting - auto *restarting = new QMessageBox(mainWindow); + auto *restarting = new QMessageBox(this); restarting->setText( - QObject::tr("Migration complete. Restarting...")); + QObject::tr("Migration complete. Restarting…")); restarting->setWindowTitle(QObject::tr("Restarting")); restarting->setStandardButtons(QMessageBox::NoButton); auto closeNotice = [restarting]() { restarting->reject(); }; - // Insert a short delay before restart so the user can see the success message, and + // Insert a short delay before restart so the user can see the success message and // knows it's a restart and not a crash... constexpr int delayRestartMillis{2000}; QTimer::singleShot(delayRestartMillis, closeNotice); @@ -323,4 +316,4 @@ void VersionMigrator::migrateToCurrentVersion() { } } -#include "VersionMigrator.moc" +#include "DlgVersionMigrator.moc" diff --git a/src/Gui/VersionMigrator.h b/src/Gui/Dialogs/DlgVersionMigrator.h similarity index 64% rename from src/Gui/VersionMigrator.h rename to src/Gui/Dialogs/DlgVersionMigrator.h index 36d90e560b..1a0bdb1bc3 100644 --- a/src/Gui/VersionMigrator.h +++ b/src/Gui/Dialogs/DlgVersionMigrator.h @@ -21,38 +21,46 @@ * * **************************************************************************/ -#ifndef GUI_VERSIONMIGRATOR_H -#define GUI_VERSIONMIGRATOR_H +#ifndef GUI_DIALOG_VERSIONMIGRATOR_H +#define GUI_DIALOG_VERSIONMIGRATOR_H #include #include -#include +#include +#include + namespace Gui { class MainWindow; + namespace Dialog { -class GuiExport VersionMigrator : public QObject -{ - Q_OBJECT + class GuiExport DlgVersionMigrator final : public QDialog + { + Q_OBJECT -public: - explicit VersionMigrator(MainWindow *mw); - void execute(); + public: + explicit DlgVersionMigrator(MainWindow *mw); + ~DlgVersionMigrator() override; + Q_DISABLE_COPY_MOVE(DlgVersionMigrator) -protected Q_SLOTS: + int exec() override; - void confirmMigration(); - void migrationCancelled() const; - void showSizeOfMigration(uintmax_t size); - void migrateToCurrentVersion(); + protected Q_SLOTS: -private: - MainWindow* mainWindow; -}; + void calculateMigrationSize(); // Async -> this starts the process and immediately returns + void showSizeOfMigration(uintmax_t size); + void migrateToCurrentVersion(); + void doNotMigrate(const QString &linkText); + private: + MainWindow* mainWindow; + QThread* sizeCalculationWorkerThread; + std::unique_ptr ui; + }; + } } -#endif // GUI_VERSIONMIGRATOR_H +#endif // GUI_DIALOG_VERSIONMIGRATOR_H diff --git a/src/Gui/Dialogs/DlgVersionMigrator.ui b/src/Gui/Dialogs/DlgVersionMigrator.ui new file mode 100644 index 0000000000..59196516f2 --- /dev/null +++ b/src/Gui/Dialogs/DlgVersionMigrator.ui @@ -0,0 +1,109 @@ + + + + Gui::Dialog::DlgVersionMigrator + + + Qt::WindowModal + + + + 0 + 0 + 527 + 233 + + + + Dialog + + + true + + + + + + TextLabel + + + Qt::AutoText + + + + + + + Configuration data and addons from a previous program version were found. Migrate the configuration to a new directory for this version? + + + Qt::PlainText + + + true + + + + + + + Copying the configuration will ensure that any changes from the new version will not affect the previous installation. Sharing configuration between versions can cause problems and is not recommended. + + + Qt::PlainText + + + true + + + + + + + TextLabel + + + Qt::AutoText + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + Share configuration between versions + + + Qt::AutoText + + + + + + + Copy Configuration + + + + + + + + + + diff --git a/src/Gui/StartupProcess.cpp b/src/Gui/StartupProcess.cpp index 5d00a0348b..5d9974b0f0 100644 --- a/src/Gui/StartupProcess.cpp +++ b/src/Gui/StartupProcess.cpp @@ -30,8 +30,6 @@ #include #include #include -#include -#include #include #include #include @@ -53,13 +51,12 @@ #include "GuiApplication.h" #include "MainWindow.h" #include "Language/Translator.h" -#include "VersionMigrator.h" +#include "Dialogs/DlgVersionMigrator.h" #include #include using namespace Gui; -namespace fs = std::filesystem; StartupProcess::StartupProcess() = default; @@ -560,6 +557,7 @@ void StartupPostProcess::checkParameters() } void StartupPostProcess::checkVersionMigration() const { - VersionMigrator migrator(mainWindow); - migrator.execute(); + auto migrator = new Dialog::DlgVersionMigrator (mainWindow); + migrator->exec(); + migrator->deleteLater(); } From 909464c17be7825e01b2d7e5d4a1b1e2a6a2f888 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Thu, 11 Sep 2025 16:58:18 -0500 Subject: [PATCH 3/6] Gui: Add window title --- src/Gui/Dialogs/DlgVersionMigrator.cpp | 244 +++++++++++++++---------- src/Gui/Dialogs/DlgVersionMigrator.h | 8 +- src/Gui/Dialogs/DlgVersionMigrator.ui | 29 ++- 3 files changed, 171 insertions(+), 110 deletions(-) diff --git a/src/Gui/Dialogs/DlgVersionMigrator.cpp b/src/Gui/Dialogs/DlgVersionMigrator.cpp index 3ec92021ec..e055807269 100644 --- a/src/Gui/Dialogs/DlgVersionMigrator.cpp +++ b/src/Gui/Dialogs/DlgVersionMigrator.cpp @@ -21,22 +21,22 @@ * * **************************************************************************/ -#include "PreCompiled.h" -#ifndef _PreComp_ +#include #include -#include +#include #include #include #include #include #include +#include #include +#include #include #include #include #include -#endif #include "DlgVersionMigrator.h" #include "ui_DlgVersionMigrator.h" @@ -51,56 +51,58 @@ using namespace Gui::Dialog; namespace fs = std::filesystem; - -std::set getKnownVersions() { - auto splitCommas = [](const std::string &input) { - std::set result; - std::stringstream ss(input); - std::string token; - while (std::getline(ss, token, ',')) { - result.insert(token); - } - return result; +bool isCurrentVersionKnown() +{ + std::set paths = { + App::Application::directories()->getUserAppDataDir(), + App::Application::directories()->getUserConfigPath() }; - - auto prefGroup = App::GetApplication().GetParameterGroupByPath( - "User parameter:BaseApp/Preferences/Migration"); - - // Split our comma-separated list of already-migrated-to version directories into a set for easy - // searching - std::string offeredToMigrateToVersionedConfig = - prefGroup->GetASCII("OfferedToMigrateToVersionedConfig", ""); - std::set knownVersions; - if (!offeredToMigrateToVersionedConfig.empty()) { - knownVersions = splitCommas(offeredToMigrateToVersionedConfig); - } - return knownVersions; -} - -void setKnownVersions(const std::set &knownVersions) { - auto joinCommas = [](const std::set &s) { - std::ostringstream oss; - for (auto it = s.begin(); it != s.end(); ++it) { - if (it != s.begin()) { - oss << ','; - } - oss << *it; - } - return oss.str(); - }; - - auto prefGroup = App::GetApplication().GetParameterGroupByPath( - "User parameter:BaseApp/Preferences/Migration"); - prefGroup->SetASCII("OfferedToMigrateToVersionedConfig", joinCommas(knownVersions)); -} - -void markCurrentVersionAsKnown() { int major = std::stoi(App::Application::Config()["BuildVersionMajor"]); int minor = std::stoi(App::Application::Config()["BuildVersionMinor"]); std::string currentVersionedDirName = App::ApplicationDirectories::versionStringForPath(major, minor); - std::set knownVersions = getKnownVersions(); - knownVersions.insert(currentVersionedDirName); - setKnownVersions(knownVersions); + for (auto &path : paths) { + if (App::Application::directories()->usingCurrentVersionConfig(path)) { + return true; + } + fs::path markerPath = path; + if (App::Application::directories()->isVersionedPath(path)) { + markerPath = path.parent_path(); + } + markerPath /= currentVersionedDirName + ".do_not_migrate"; + if (fs::exists(markerPath)) { + return true; + } + } + return false; +} + +void markCurrentVersionAsDoNotMigrate() { + std::set paths = { + App::Application::directories()->getUserAppDataDir(), + App::Application::directories()->getUserConfigPath() + }; + int major = std::stoi(App::Application::Config()["BuildVersionMajor"]); + int minor = std::stoi(App::Application::Config()["BuildVersionMinor"]); + std::string currentVersionedDirName = App::ApplicationDirectories::versionStringForPath(major, minor); + for (auto &path : paths) { + if (App::Application::directories()->usingCurrentVersionConfig(path)) { + // No action to take: the migration is done, so this call doesn't need to do anything + continue; + } + fs::path markerPath = path; + if (App::Application::directories()->isVersionedPath(path)) { + markerPath = path.parent_path(); + } + markerPath /= currentVersionedDirName + ".do_not_migrate"; + std::ofstream markerFile(markerPath); + if (!markerFile.is_open()) { + Base::Console().error("Unable to open marker file %s\n", Base::FileInfo::pathToString(markerPath).c_str()); + continue; + } + markerFile << "Migration to version " << currentVersionedDirName << " was declined. " + << "To request migration again, delete this file."; + markerFile.close(); + } } @@ -123,21 +125,31 @@ DlgVersionMigrator::DlgVersionMigrator(MainWindow *mw) : // NOTE: All rich-text strings are generated programmatically so that translators don't have to deal with the // markup. The two strings in the middle of the dialog are set in the UI file. - auto welcomeString = QStringLiteral("") + QObject::tr("Welcome to %1 %2.%3\n\n").arg( - programName, QString::number(major), QString::number(minor)) + QStringLiteral(""); + auto programNameString = tr("Welcome to %1 %2.%3\n\n").arg( + programName, QString::number(major), QString::number(minor)); + auto welcomeString = QStringLiteral("") + programNameString + QStringLiteral(""); - auto calculatingSizeString = QStringLiteral("") + QObject::tr("Calculating size…") + QStringLiteral(""); + auto calculatingSizeString = QStringLiteral("") + tr("Calculating size…") + QStringLiteral(""); auto shareConfigurationString = QStringLiteral("") + - QObject::tr("Share configuration between versions") + QStringLiteral(""); + tr("Share configuration between versions") + QStringLiteral(""); + setWindowTitle(programNameString); ui->welcomeLabel->setText(welcomeString); ui->sizeLabel->setText(calculatingSizeString); - ui->shareLinkLabel->setText(shareConfigurationString); - ui->shareLinkLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); - connect(ui->copyButton, &QPushButton::clicked, this, &DlgVersionMigrator::migrateToCurrentVersion); - connect(ui->shareLinkLabel, &QLabel::linkActivated, this, &DlgVersionMigrator::doNotMigrate); + connect(ui->copyButton, &QPushButton::clicked, this, &DlgVersionMigrator::migrate); + connect(ui->helpButton, &QPushButton::clicked, this, &DlgVersionMigrator::help); + + // Set up the menu actions for the two hidden options + auto *menu = new QMenu(ui->menuButton); + QAction *share = menu->addAction(tr("Share configuration with previous version")); + QAction *reset = menu->addAction(tr("Use a new default configuration")); + ui->menuButton->setMenu(menu); + ui->menuButton->setPopupMode(QToolButton::InstantPopup); + ui->menuButton->setStyleSheet(QStringLiteral("QToolButton::menu-indicator { image: none; width: 0px; }")); + connect(share, &QAction::triggered, this, &DlgVersionMigrator::share); + connect(reset, &QAction::triggered, this, &DlgVersionMigrator::freshStart); } DlgVersionMigrator::~DlgVersionMigrator() = default; @@ -147,14 +159,9 @@ int DlgVersionMigrator::exec() { if (App::Application::directories()->usingCustomDirectories()) { return 0; } - std::set knownVersions = getKnownVersions(); - int major = std::stoi(App::Application::Config()["BuildVersionMajor"]); - int minor = std::stoi(App::Application::Config()["BuildVersionMinor"]); - std::string currentVersionedDirName = App::ApplicationDirectories::versionStringForPath(major, minor); - if (!knownVersions.contains(currentVersionedDirName) && !App::Application::directories()->usingCurrentVersionConfig( - App::Application::directories()->getUserAppDataDir())) { + if (!isCurrentVersionKnown()) { calculateMigrationSize(); - auto result = QDialog::exec(); + QDialog::exec(); if (sizeCalculationWorkerThread && sizeCalculationWorkerThread->isRunning()) { sizeCalculationWorkerThread->quit(); } @@ -162,6 +169,7 @@ int DlgVersionMigrator::exec() { return 0; } + class DirectorySizeCalculationWorker : public QObject { Q_OBJECT @@ -240,10 +248,9 @@ void DlgVersionMigrator::calculateMigrationSize() { sizeCalculationWorkerThread->start(); } -void DlgVersionMigrator::doNotMigrate(const QString &linkText) +void DlgVersionMigrator::share() { - Q_UNUSED(linkText); - markCurrentVersionAsKnown(); + markCurrentVersionAsDoNotMigrate(); if (sizeCalculationWorkerThread && sizeCalculationWorkerThread->isRunning()) { sizeCalculationWorkerThread->quit(); } @@ -259,10 +266,8 @@ void DlgVersionMigrator::showSizeOfMigration(uintmax_t size) { sizeCalculationWorkerThread = nullptr; // Deleted via a previously-configured deleteLater() } -void DlgVersionMigrator::migrateToCurrentVersion() { +void DlgVersionMigrator::migrate() { hide(); - auto oldKnownVersions = getKnownVersions(); - markCurrentVersionAsKnown(); // This MUST be done before the migration, or it won't get remembered auto *workerThread = new QThread(mainWindow); auto *worker = new PathMigrationWorker(); worker->moveToThread(workerThread); @@ -282,38 +287,79 @@ void DlgVersionMigrator::migrateToCurrentVersion() { migrationRunning->exec(); if (migrationRunning->result() == QDialog::Accepted) { - App::GetApplication().GetUserParameter().SaveDocument(); // Flush to disk before restarting - auto *restarting = new QMessageBox(this); - restarting->setText( - QObject::tr("Migration complete. Restarting…")); - restarting->setWindowTitle(QObject::tr("Restarting")); - restarting->setStandardButtons(QMessageBox::NoButton); - auto closeNotice = [restarting]() { - restarting->reject(); - }; - - // Insert a short delay before restart so the user can see the success message and - // knows it's a restart and not a crash... - constexpr int delayRestartMillis{2000}; - QTimer::singleShot(delayRestartMillis, closeNotice); - restarting->exec(); - - connect(qApp, &QCoreApplication::aboutToQuit, [=] { - if (getMainWindow()->close()) { - auto args = QApplication::arguments(); - args.removeFirst(); - QProcess::startDetached(QApplication::applicationFilePath(), - args, - QApplication::applicationDirPath()); - } - }); - QCoreApplication::exit(0); - _exit(0); // No really. Die. + restart(); } else { - setKnownVersions(oldKnownVersions); // Reset, we didn't migrate after all QMessageBox::critical(mainWindow, QObject::tr("Migration failed"), QObject::tr("Migration failed. See the Report View for details.")); } } + + +void DlgVersionMigrator::freshStart() +{ + // Create the versioned directories, but don't put anything in them + std::set paths = { + App::Application::directories()->getUserAppDataDir(), + App::Application::directories()->getUserConfigPath() + }; + int major = std::stoi(App::Application::Config()["BuildVersionMajor"]); + int minor = std::stoi(App::Application::Config()["BuildVersionMinor"]); + std::string currentVersionedDirName = App::ApplicationDirectories::versionStringForPath(major, minor); + for (auto &path : paths) { + if (App::Application::directories()->usingCurrentVersionConfig(path)) { + continue; + } + fs::path versionDir = path; + if (App::Application::directories()->isVersionedPath(path)) { + versionDir = path.parent_path(); + } + versionDir /= currentVersionedDirName; + if (fs::exists(versionDir)) { + continue; + } + fs::create_directory(versionDir); + } + restart(); +} + +void DlgVersionMigrator::help() +{ + auto helpPage = QStringLiteral("https://wiki.freecad.org/Version_migration"); + QDesktopServices::openUrl(QUrl(helpPage)); +} + +void DlgVersionMigrator::restart() +{ + App::GetApplication().GetUserParameter().SaveDocument(); // Flush to disk before restarting + auto *restarting = new QMessageBox(this); + restarting->setText( + QObject::tr("Migration complete. Restarting…")); + restarting->setWindowTitle(QObject::tr("Restarting")); + restarting->setStandardButtons(QMessageBox::NoButton); + auto closeNotice = [restarting]() { + restarting->reject(); + }; + + // Insert a short delay before restart so the user can see the success message and + // knows it's a restart and not a crash... + constexpr int delayRestartMillis{2000}; + QTimer::singleShot(delayRestartMillis, closeNotice); + restarting->exec(); + + connect(qApp, &QCoreApplication::aboutToQuit, [=] { + if (getMainWindow()->close()) { + auto args = QApplication::arguments(); + args.removeFirst(); + QProcess::startDetached(QApplication::applicationFilePath(), + args, + QApplication::applicationDirPath()); + } + }); + QCoreApplication::exit(0); + _exit(0); // No really. Die. +} + + + #include "DlgVersionMigrator.moc" diff --git a/src/Gui/Dialogs/DlgVersionMigrator.h b/src/Gui/Dialogs/DlgVersionMigrator.h index 1a0bdb1bc3..4de31692e5 100644 --- a/src/Gui/Dialogs/DlgVersionMigrator.h +++ b/src/Gui/Dialogs/DlgVersionMigrator.h @@ -51,13 +51,17 @@ namespace Gui { void calculateMigrationSize(); // Async -> this starts the process and immediately returns void showSizeOfMigration(uintmax_t size); - void migrateToCurrentVersion(); - void doNotMigrate(const QString &linkText); + void migrate(); + void share(); + void freshStart(); + void help(); private: MainWindow* mainWindow; QThread* sizeCalculationWorkerThread; std::unique_ptr ui; + + void restart(); }; } diff --git a/src/Gui/Dialogs/DlgVersionMigrator.ui b/src/Gui/Dialogs/DlgVersionMigrator.ui index 59196516f2..3b0006c614 100644 --- a/src/Gui/Dialogs/DlgVersionMigrator.ui +++ b/src/Gui/Dialogs/DlgVersionMigrator.ui @@ -1,6 +1,5 @@ - Gui::Dialog::DlgVersionMigrator @@ -10,7 +9,7 @@ 0 0 - 527 + 556 233 @@ -69,6 +68,13 @@ + + + + Help + + + @@ -83,20 +89,25 @@ - + - - Share configuration between versions + Copy Configuration (Recommended) - - Qt::AutoText + + true + + + false - + - Copy Configuration + + + + Qt::ToolButtonTextOnly From bc01dc17a47c1c54415f3f234182ae99e05aecee Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Wed, 17 Sep 2025 16:15:39 +0200 Subject: [PATCH 4/6] Gui: Support for default and flat buttons in QSS --- src/Gui/Stylesheets/FreeCAD.qss | 30 ++++++++++++++----- .../Stylesheets/parameters/FreeCAD Dark.yaml | 3 ++ .../Stylesheets/parameters/FreeCAD Light.yaml | 5 +++- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/Gui/Stylesheets/FreeCAD.qss b/src/Gui/Stylesheets/FreeCAD.qss index 05aab36e2c..38ff3d0cca 100644 --- a/src/Gui/Stylesheets/FreeCAD.qss +++ b/src/Gui/Stylesheets/FreeCAD.qss @@ -115,7 +115,9 @@ Main window ==================================================================================================*/ QMainWindow, QDockWidget { - background-color: @PrimaryColor;/* main background color */} + background-color: @PrimaryColor;/* main background color */ +} + QDialog { background-color: @DialogBackgroundColor; } @@ -1174,13 +1176,23 @@ https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qpushbutton --------------------------------------------------------------------------- */ QPushButton { - background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 @ButtonTopBackgroundColor,stop:1 @ButtonBottomBackgroundColor); + background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 @ButtonTopBackgroundColor, stop:1 @ButtonBottomBackgroundColor); border: 1px solid @ButtonBorderColor; border-radius: @InputFieldBorderRadius; padding: 3px 12px; min-width: 64px; } +QPushButton:default { + background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 @DefaultButtonTopBackgroundColor, stop:1 @DefaultButtonBottomBackgroundColor); + border: 1px solid @DefaultButtonBorderColor; +} + +QAbstractButton[flat="true"] { + background-color: transparent; + border: 1px solid transparent; +} + #CreateNewRow > QPushButton { border-radius: @InputFieldBorderRadius; } @@ -1199,12 +1211,12 @@ QPushButton:checked { background-color: qlineargradient(x1:0, y1:1, x2:0, y2:0, stop:0 @ButtonTopBackgroundColor, stop:0.6 @ButtonBottomBackgroundColor); } -QPushButton:focus { - border: 1px solid @AccentColor; +QPushButton:focus, QAbstractButton[flat="true"]:focus { + border: 1px solid @AccentColor; } QPushButton:checked:disabled { - border: 1px solid @AccentColor; + border: 1px solid @AccentColor; color: @TextDisabledColor; outline: none; } @@ -1218,11 +1230,15 @@ QPushButton:hover { background-color: @GeneralBackgroundHoverColor; } -QPushButton:pressed { +QAbstractButton[flat="true"]:hover { + border: 1px solid @ButtonBorderColor; +} + +QPushButton:pressed, QAbstractButton[flat="true"]:pressed { border: 1px solid @GeneralBorderHoverColor; } -QPushButton:selected { +QPushButton:selected, QAbstractButton[flat="true"]:selected { border: 1px solid @GeneralBorderHoverColor; } diff --git a/src/Gui/Stylesheets/parameters/FreeCAD Dark.yaml b/src/Gui/Stylesheets/parameters/FreeCAD Dark.yaml index 82ab75dab1..a5413ffec7 100644 --- a/src/Gui/Stylesheets/parameters/FreeCAD Dark.yaml +++ b/src/Gui/Stylesheets/parameters/FreeCAD Dark.yaml @@ -4,6 +4,9 @@ ButtonBorderColor: "@GeneralBorderColor" ButtonBorderHooverColor: "@GeneralBorderColor" ButtonBottomBackgroundColor: "@PrimaryColorLighten2" ButtonTopBackgroundColor: "@PrimaryColorLighten3" +DefaultButtonBottomBackgroundColor: "blend(@ButtonBottomBackgroundColor, @AccentColor, 10)" +DefaultButtonTopBackgroundColor: "blend(@ButtonTopBackgroundColor, @AccentColor, 10)" +DefaultButtonBorderColor: "blend(@ButtonBorderColor, @AccentColor, 20)" CheckBoxBackgroundColor: "@TextEditFieldBackgroundColor" CheckBoxBorderColor: "@GeneralBorderColor" DialogBackgroundColor: "@PrimaryColorLighten3" diff --git a/src/Gui/Stylesheets/parameters/FreeCAD Light.yaml b/src/Gui/Stylesheets/parameters/FreeCAD Light.yaml index 31621e39cb..667f158e0c 100644 --- a/src/Gui/Stylesheets/parameters/FreeCAD Light.yaml +++ b/src/Gui/Stylesheets/parameters/FreeCAD Light.yaml @@ -4,9 +4,12 @@ ButtonBorderColor: "@GeneralBorderColor" ButtonBorderHooverColor: "@GeneralBorderColor" ButtonBottomBackgroundColor: "@PrimaryColorDarken1" ButtonTopBackgroundColor: "@PrimaryColorLighten3" +DefaultButtonBottomBackgroundColor: "blend(@ButtonBottomBackgroundColor, @AccentColor, 5)" +DefaultButtonTopBackgroundColor: "blend(@ButtonTopBackgroundColor, @AccentColor, 5)" +DefaultButtonBorderColor: "blend(@ButtonBorderColor, @AccentColor, 40)" CheckBoxBackgroundColor: "@TextEditFieldBackgroundColor" CheckBoxBorderColor: "@GeneralBorderColor" -DialogBackgroundColor: "@PrimaryColorLighten4" +DialogBackgroundColor: "@PrimaryColorLighten3" GeneralAlternateBackgroundColor: "@PrimaryColor" GeneralBackgroundColor: "@PrimaryColor" GeneralBackgroundHoverColor: "@PrimaryColorLighten5" From eed3e09792206f8ea8e0b935e07e3c8a68195fc6 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Wed, 17 Sep 2025 12:16:24 -0500 Subject: [PATCH 5/6] Gui: Migration complete wording tweak --- src/Gui/Dialogs/DlgVersionMigrator.cpp | 8 ++++---- src/Gui/Dialogs/DlgVersionMigrator.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Gui/Dialogs/DlgVersionMigrator.cpp b/src/Gui/Dialogs/DlgVersionMigrator.cpp index e055807269..fdfdc7ec93 100644 --- a/src/Gui/Dialogs/DlgVersionMigrator.cpp +++ b/src/Gui/Dialogs/DlgVersionMigrator.cpp @@ -287,7 +287,7 @@ void DlgVersionMigrator::migrate() { migrationRunning->exec(); if (migrationRunning->result() == QDialog::Accepted) { - restart(); + restart(tr("Migration complete")); } else { QMessageBox::critical(mainWindow, QObject::tr("Migration failed"), QObject::tr("Migration failed. See the Report View for details.")); @@ -320,7 +320,7 @@ void DlgVersionMigrator::freshStart() } fs::create_directory(versionDir); } - restart(); + restart(tr("New default configuration created")); } void DlgVersionMigrator::help() @@ -329,12 +329,12 @@ void DlgVersionMigrator::help() QDesktopServices::openUrl(QUrl(helpPage)); } -void DlgVersionMigrator::restart() +void DlgVersionMigrator::restart(const QString &message) { App::GetApplication().GetUserParameter().SaveDocument(); // Flush to disk before restarting auto *restarting = new QMessageBox(this); restarting->setText( - QObject::tr("Migration complete. Restarting…")); + message + QObject::tr(" → Restarting…")); restarting->setWindowTitle(QObject::tr("Restarting")); restarting->setStandardButtons(QMessageBox::NoButton); auto closeNotice = [restarting]() { diff --git a/src/Gui/Dialogs/DlgVersionMigrator.h b/src/Gui/Dialogs/DlgVersionMigrator.h index 4de31692e5..90b76e7853 100644 --- a/src/Gui/Dialogs/DlgVersionMigrator.h +++ b/src/Gui/Dialogs/DlgVersionMigrator.h @@ -61,7 +61,7 @@ namespace Gui { QThread* sizeCalculationWorkerThread; std::unique_ptr ui; - void restart(); + void restart(const QString &message); }; } From cdb4da3cc74c21251bb5319d65f70e686c381db9 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Wed, 17 Sep 2025 15:45:07 -0500 Subject: [PATCH 6/6] Gui: Address review comments --- src/Gui/Dialogs/DlgVersionMigrator.cpp | 6 ++++++ src/Gui/Dialogs/DlgVersionMigrator.ui | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/Gui/Dialogs/DlgVersionMigrator.cpp b/src/Gui/Dialogs/DlgVersionMigrator.cpp index fdfdc7ec93..068631310c 100644 --- a/src/Gui/Dialogs/DlgVersionMigrator.cpp +++ b/src/Gui/Dialogs/DlgVersionMigrator.cpp @@ -135,7 +135,12 @@ DlgVersionMigrator::DlgVersionMigrator(MainWindow *mw) : tr("Share configuration between versions") + QStringLiteral(""); setWindowTitle(programNameString); +#ifdef Q_OS_MACOS + // macOS does not show the window title on modals, so add the extra label ui->welcomeLabel->setText(welcomeString); +#else + ui->welcomeLabel->hide(); +#endif ui->sizeLabel->setText(calculatingSizeString); connect(ui->copyButton, &QPushButton::clicked, this, &DlgVersionMigrator::migrate); @@ -148,6 +153,7 @@ DlgVersionMigrator::DlgVersionMigrator(MainWindow *mw) : ui->menuButton->setMenu(menu); ui->menuButton->setPopupMode(QToolButton::InstantPopup); ui->menuButton->setStyleSheet(QStringLiteral("QToolButton::menu-indicator { image: none; width: 0px; }")); + ui->menuButton->setProperty("flat", true); connect(share, &QAction::triggered, this, &DlgVersionMigrator::share); connect(reset, &QAction::triggered, this, &DlgVersionMigrator::freshStart); } diff --git a/src/Gui/Dialogs/DlgVersionMigrator.ui b/src/Gui/Dialogs/DlgVersionMigrator.ui index 3b0006c614..10e63e5776 100644 --- a/src/Gui/Dialogs/DlgVersionMigrator.ui +++ b/src/Gui/Dialogs/DlgVersionMigrator.ui @@ -66,6 +66,19 @@ + + + + Qt::Vertical + + + + 20 + 40 + + + +