// SPDX-License-Identifier: LGPL-2.1-or-later /*************************************************************************** * Copyright (c) 2024 Werner Mayer * * * * 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 #include #include #include #endif #include "StartupProcess.h" #include "Application.h" #include "AutoSaver.h" #include "Dialogs/DlgCheckableMessageBox.h" #include "FileDialog.h" #include "GuiApplication.h" #include "MainWindow.h" #include "Language/Translator.h" #include #include #include using namespace Gui; StartupProcess::StartupProcess() = default; void StartupProcess::setupApplication() { QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL); // Automatic scaling for legacy apps (disable once all parts of GUI are aware of HiDpi) ParameterGrp::handle hDPI = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/HighDPI"); bool disableDpiScaling = hDPI->GetBool("DisableDpiScaling", false); if (disableDpiScaling) { #ifdef FC_OS_WIN32 SetProcessDPIAware(); // call before the main event loop #endif #if QT_VERSION < QT_VERSION_CHECK(6,0,0) QApplication::setAttribute(Qt::AA_DisableHighDpiScaling); #endif } else { // Enable automatic scaling based on pixel density of display (added in Qt 5.6) #if QT_VERSION < QT_VERSION_CHECK(6,0,0) QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); #endif #if defined(Q_OS_WIN) QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); #endif } #if QT_VERSION < QT_VERSION_CHECK(6,0,0) //Enable support for highres images (added in Qt 5.1, but off by default) QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); #endif // Use software rendering for OpenGL ParameterGrp::handle hOpenGL = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/OpenGL"); bool useSoftwareOpenGL = hOpenGL->GetBool("UseSoftwareOpenGL", false); if (useSoftwareOpenGL) { QApplication::setAttribute(Qt::AA_UseSoftwareOpenGL); } // By default (on platforms that support it, see docs for // Qt::AA_CompressHighFrequencyEvents) QT applies compression // for high frequency events (mouse move, touch, window resizes) // to keep things smooth even when handling the event takes a // while (e.g. to calculate snapping). // However, tablet pen move events (and mouse move events // synthesised from those) are not compressed by default (to // allow maximum precision when e.g. hand-drawing curves), // leading to unacceptable slowdowns using a tablet pen. Enable // compression for tablet events here to solve that. QCoreApplication::setAttribute(Qt::AA_CompressTabletEvents); } void StartupProcess::execute() { setLibraryPath(); setStyleSheetPaths(); setImagePaths(); registerEventType(); setThemePaths(); setupFileDialog(); } void StartupProcess::setLibraryPath() { QString plugin; plugin = QString::fromStdString(App::Application::getHomePath()); plugin += QLatin1String("/plugins"); QCoreApplication::addLibraryPath(plugin); } void StartupProcess::setStyleSheetPaths() { // setup the search paths for Qt style sheets QStringList qssPaths; qssPaths << QString::fromUtf8( (App::Application::getUserAppDataDir() + "Gui/Stylesheets/").c_str()) << QString::fromUtf8((App::Application::getResourceDir() + "Gui/Stylesheets/").c_str()) << QLatin1String(":/stylesheets"); QDir::setSearchPaths(QStringLiteral("qss"), qssPaths); // setup the search paths for Qt overlay style sheets QStringList qssOverlayPaths; qssOverlayPaths << QString::fromUtf8((App::Application::getUserAppDataDir() + "Gui/Stylesheets/overlay").c_str()) << QString::fromUtf8((App::Application::getResourceDir() + "Gui/Stylesheets/overlay").c_str()); QDir::setSearchPaths(QStringLiteral("overlay"), qssOverlayPaths); } void StartupProcess::setImagePaths() { // set search paths for images QStringList imagePaths; imagePaths << QString::fromUtf8((App::Application::getUserAppDataDir() + "Gui/images").c_str()) << QString::fromUtf8((App::Application::getUserAppDataDir() + "pixmaps").c_str()) << QLatin1String(":/icons"); QDir::setSearchPaths(QStringLiteral("images"), imagePaths); } void StartupProcess::registerEventType() { // register action style event type ActionStyleEvent::EventType = QEvent::registerEventType(QEvent::User + 1); } void StartupProcess::setThemePaths() { #if !defined(Q_OS_LINUX) QIcon::setThemeSearchPaths(QIcon::themeSearchPaths() << QStringLiteral(":/icons/FreeCAD-default")); #endif ParameterGrp::handle hTheme = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Bitmaps/Theme"); std::string searchpath = hTheme->GetASCII("SearchPath"); if (!searchpath.empty()) { QStringList searchPaths = QIcon::themeSearchPaths(); searchPaths.prepend(QString::fromUtf8(searchpath.c_str())); QIcon::setThemeSearchPaths(searchPaths); } std::string name = hTheme->GetASCII("Name"); if (!name.empty()) { QIcon::setThemeName(QString::fromLatin1(name.c_str())); } } void StartupProcess::setupFileDialog() { #if defined(FC_OS_LINUX) // See #0001588 QString path = FileDialog::restoreLocation(); FileDialog::setWorkingDirectory(QDir::currentPath()); FileDialog::saveLocation(path); #else FileDialog::setWorkingDirectory(FileDialog::restoreLocation()); #endif } // ------------------------------------------------------------------------------------------------ StartupPostProcess::StartupPostProcess(MainWindow* mw, Application& guiApp, QApplication* app) : mainWindow{mw} , guiApp{guiApp} , qtApp(app) { } void StartupPostProcess::setLoadFromPythonModule(bool value) { loadFromPythonModule = value; } void StartupPostProcess::execute() { setWindowTitle(); setProcessMessages(); setAutoSaving(); setToolBarIconSize(); setWheelEventFilter(); setLocale(); setCursorFlashing(); setQtStyle(); checkOpenGL(); loadOpenInventor(); setBranding(); showMainWindow(); activateWorkbench(); checkParameters(); runWelcomeScreen(); } void StartupPostProcess::setWindowTitle() { // empty window title QString sets default title (app + version) mainWindow->setWindowTitle(QString()); } void StartupPostProcess::setProcessMessages() { if (!loadFromPythonModule) { QObject::connect(qtApp, SIGNAL(messageReceived(const QList &)), mainWindow, SLOT(processMessages(const QList &))); } } void StartupPostProcess::setAutoSaving() { ParameterGrp::handle hDocGrp = WindowParameter::getDefaultParameter()->GetGroup("Document"); int timeout = int(hDocGrp->GetInt("AutoSaveTimeout", 15L)); // 15 min if (!hDocGrp->GetBool("AutoSaveEnabled", true)) { timeout = 0; } AutoSaver::instance()->setTimeout(timeout * 60000); // NOLINT AutoSaver::instance()->setCompressed(hDocGrp->GetBool("AutoSaveCompressed", true)); } void StartupPostProcess::setToolBarIconSize() { // set toolbar icon size ParameterGrp::handle hGrp = WindowParameter::getDefaultParameter()->GetGroup("General"); int size = int(hGrp->GetInt("ToolbarIconSize", 0)); // must not be lower than this if (size >= 16) { // NOLINT mainWindow->setIconSize(QSize(size,size)); } } void StartupPostProcess::setWheelEventFilter() { // filter wheel events for combo boxes ParameterGrp::handle hGrp = WindowParameter::getDefaultParameter()->GetGroup("General"); if (hGrp->GetBool("ComboBoxWheelEventFilter", false)) { auto filter = new WheelEventFilter(qtApp); qtApp->installEventFilter(filter); } } void StartupPostProcess::setLocale() { // For values different to 1 and 2 use the OS locale settings ParameterGrp::handle hGrp = WindowParameter::getDefaultParameter()->GetGroup("General"); auto localeFormat = hGrp->GetInt("UseLocaleFormatting", 0); if (localeFormat == 1) { Translator::instance()->setLocale( hGrp->GetASCII("Language", Translator::instance()->activeLanguage().c_str())); } else if (localeFormat == 2) { Translator::instance()->setLocale("C.UTF-8"); } } void StartupPostProcess::setCursorFlashing() { // set text cursor blinking state ParameterGrp::handle hGrp = WindowParameter::getDefaultParameter()->GetGroup("General"); int blinkTime = hGrp->GetBool("EnableCursorBlinking", true) ? -1 : 0; QApplication::setCursorFlashTime(blinkTime); } void StartupPostProcess::setQtStyle() { ParameterGrp::handle hGrp = WindowParameter::getDefaultParameter()->GetGroup("MainWindow"); auto qtStyle = hGrp->GetASCII("QtStyle"); if (qtStyle.empty()) { qtStyle = "Fusion"; hGrp->SetASCII("QtStyle", qtStyle); } else if (qtStyle == "System") { // Special value to not set a QtStyle explicitly return; } QApplication::setStyle(QString::fromStdString(qtStyle)); } void StartupPostProcess::checkOpenGL() { QWindow window; window.setSurfaceType(QWindow::OpenGLSurface); window.create(); QOpenGLContext context; if (context.create()) { context.makeCurrent(&window); if (!context.functions()->hasOpenGLFeature(QOpenGLFunctions::Framebuffers)) { Base::Console().log("This system does not support framebuffer objects\n"); } if (!context.functions()->hasOpenGLFeature(QOpenGLFunctions::NPOTTextures)) { Base::Console().log("This system does not support NPOT textures\n"); } int major = context.format().majorVersion(); int minor = context.format().minorVersion(); #ifdef NDEBUG // In release mode, issue a warning to users that their version of OpenGL is // potentially going to cause problems if (major < 2) { auto message = QObject::tr("This system is running OpenGL %1.%2. " "FreeCAD requires OpenGL 2.0 or above. " "Upgrade the graphics driver and/or card as required.") .arg(major) .arg(minor) + QStringLiteral("\n"); Base::Console().warning(message.toStdString().c_str()); Dialog::DlgCheckableMessageBox::showMessage( QCoreApplication::applicationName() + QStringLiteral(" - ") + QObject::tr("Invalid OpenGL Version"), message); } #endif const char* glVersion = reinterpret_cast(glGetString(GL_VERSION)); Base::Console().log("OpenGL version is: %d.%d (%s)\n", major, minor, glVersion); } } void StartupPostProcess::loadOpenInventor() { bool loadedInventor = false; if (loadFromPythonModule) { loadedInventor = SoDB::isInitialized(); } if (!loadedInventor) { // init the Inventor subsystem Application::initOpenInventor(); } } void StartupPostProcess::setBranding() { QString home = QString::fromStdString(App::Application::getHomePath()); const std::map& cfg = App::Application::Config(); std::map::const_iterator it; it = cfg.find("WindowTitle"); if (it != cfg.end()) { QString title = QString::fromUtf8(it->second.c_str()); mainWindow->setWindowTitle(title); } it = cfg.find("WindowIcon"); if (it != cfg.end()) { QString path = QString::fromUtf8(it->second.c_str()); if (QDir(path).isRelative()) { path = QFileInfo(QDir(home), path).absoluteFilePath(); } QApplication::setWindowIcon(QIcon(path)); } it = cfg.find("ProgramLogo"); if (it != cfg.end()) { QString path = QString::fromUtf8(it->second.c_str()); if (QDir(path).isRelative()) { path = QFileInfo(QDir(home), path).absoluteFilePath(); } QPixmap px(path); if (!px.isNull()) { auto logo = new QLabel(); logo->setPixmap(px.scaledToHeight(32)); mainWindow->statusBar()->addPermanentWidget(logo, 0); logo->setFrameShape(QFrame::NoFrame); } } } void StartupPostProcess::setImportImageFormats() { QList supportedFormats = QImageReader::supportedImageFormats(); std::stringstream str; str << "Image formats ("; for (const auto& ext : supportedFormats) { str << "*." << ext.constData() << " *." << ext.toUpper().constData() << " "; } str << ")"; std::string filter = str.str(); App::GetApplication().addImportType(filter.c_str(), "FreeCADGui"); } void StartupPostProcess::showMainWindow() { // show splasher while initializing the GUI if (!Application::hiddenMainWindow() && !loadFromPythonModule) { mainWindow->startSplasher(); } // running the GUI init script try { Base::Console().log("Run Gui init script\n"); Application::runInitGuiScript(); setImportImageFormats(); } catch (const Base::Exception& e) { Base::Console().error("Error in FreeCADGuiInit.py: %s\n", e.what()); mainWindow->stopSplasher(); throw; } // stop splash screen and set immediately the active window that may be of interest // for scripts using Python binding for Qt mainWindow->stopSplasher(); mainWindow->activateWindow(); } void StartupPostProcess::activateWorkbench() { // Activate the correct workbench std::string start = App::Application::Config()["StartWorkbench"]; Base::Console().log("Init: Activating default workbench %s\n", start.c_str()); std::string autoload = App::GetApplication() .GetParameterGroupByPath("User parameter:BaseApp/Preferences/General") ->GetASCII("AutoloadModule", start.c_str()); if ("$LastModule" == autoload) { start = App::GetApplication() .GetParameterGroupByPath("User parameter:BaseApp/Preferences/General") ->GetASCII("LastModule", start.c_str()); } else { start = autoload; } // if the auto workbench is not visible then force to use the default workbech // and replace the wrong entry in the parameters QStringList wb = guiApp.workbenches(); if (!wb.contains(QString::fromLatin1(start.c_str()))) { start = App::Application::Config()["StartWorkbench"]; if ("$LastModule" == autoload) { App::GetApplication() .GetParameterGroupByPath("User parameter:BaseApp/Preferences/General") ->SetASCII("LastModule", start.c_str()); } else { App::GetApplication() .GetParameterGroupByPath("User parameter:BaseApp/Preferences/General") ->SetASCII("AutoloadModule", start.c_str()); } } // Call this before showing the main window because otherwise: // 1. it shows a white window for a few seconds which doesn't look nice // 2. the layout of the toolbars is completely broken guiApp.activateWorkbench(start.c_str()); // show the main window if (!Application::hiddenMainWindow()) { Base::Console().log("Init: Showing main window\n"); mainWindow->loadWindowSettings(); } //initialize spaceball. if (auto fcApp = qobject_cast(qtApp)) { fcApp->initSpaceball(mainWindow); } setStyleSheet(); // Now run the background autoload, for workbenches that should be loaded at startup, but not // displayed to the user immediately autoloadModules(wb); // Reactivate the startup workbench guiApp.activateWorkbench(start.c_str()); } void StartupPostProcess::setStyleSheet() { ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/MainWindow"); std::string style = hGrp->GetASCII("StyleSheet"); if (style.empty()) { // check the branding settings const auto& config = App::Application::Config(); auto it = config.find("StyleSheet"); if (it != config.end()) { style = it->second; } } guiApp.setStyleSheet(QLatin1String(style.c_str()), hGrp->GetBool("TiledBackground", false)); } void StartupPostProcess::autoloadModules(const QStringList& wb) { // Now run the background autoload, for workbenches that should be loaded at startup, but not // displayed to the user immediately std::string autoloadCSV = App::GetApplication() .GetParameterGroupByPath("User parameter:BaseApp/Preferences/General") ->GetASCII("BackgroundAutoloadModules", ""); // Tokenize the comma-separated list and load the requested workbenches if they exist in this // installation std::stringstream stream(autoloadCSV); std::string workbench; while (std::getline(stream, workbench, ',')) { if (wb.contains(QString::fromLatin1(workbench.c_str()))) { guiApp.activateWorkbench(workbench.c_str()); } } } void StartupPostProcess::checkParameters() { if (App::GetApplication().GetSystemParameter().IgnoreSave()) { Base::Console().warning("System parameter file couldn't be opened.\n" "Continue with an empty configuration that won't be saved.\n"); } if (App::GetApplication().GetUserParameter().IgnoreSave()) { Base::Console().warning("User parameter file couldn't be opened.\n" "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 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(); } } } 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"