diff --git a/src/Gui/PreCompiled.h b/src/Gui/PreCompiled.h index f1b3ce8192..a5e5327814 100644 --- a/src/Gui/PreCompiled.h +++ b/src/Gui/PreCompiled.h @@ -77,6 +77,7 @@ #include #include #include +#include #include #include #include diff --git a/src/Gui/StartupProcess.cpp b/src/Gui/StartupProcess.cpp index c26bf72a24..e7613e2073 100644 --- a/src/Gui/StartupProcess.cpp +++ b/src/Gui/StartupProcess.cpp @@ -24,14 +24,21 @@ #include "PreCompiled.h" #ifndef _PreComp_ #include -#include #include #include +#include #include #include +#include #include +#include +#include #include #include + +#include +#include +#include #endif #include "StartupProcess.h" @@ -43,6 +50,7 @@ #include "MainWindow.h" #include "Language/Translator.h" #include +#include #include @@ -220,6 +228,7 @@ void StartupPostProcess::execute() showMainWindow(); activateWorkbench(); checkParameters(); + runWelcomeScreen(); } void StartupPostProcess::setWindowTitle() @@ -544,3 +553,136 @@ void StartupPostProcess::checkParameters() "Continue with an empty configuration that won't be saved.\n"); } } + +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 + std::string offeredToMigrateToVersionedConfig = + prefGroup->GetASCII("OfferedToMigrateToVersionedConfig", ""); + std::set knownVersions; + if (!offeredToMigrateToVersionedConfig.empty()) { + for (auto&& part : offeredToMigrateToVersionedConfig | std::views::split(',')) { + knownVersions.emplace(part.begin(), part.end()); + } + } + + 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(); + } + } +} + +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 0ec4ad0e03..f3d045d098 100644 --- a/src/Gui/StartupProcess.h +++ b/src/Gui/StartupProcess.h @@ -28,6 +28,7 @@ #include class QApplication; +class QMessageBox; namespace Gui { @@ -75,6 +76,8 @@ private: void showMainWindow(); void activateWorkbench(); void checkParameters(); + void runWelcomeScreen(); + void migrateToCurrentVersion(); private: bool loadFromPythonModule = false;