diff --git a/src/Gui/Application.cpp b/src/Gui/Application.cpp index 3f8810b652..a6a2fadd58 100644 --- a/src/Gui/Application.cpp +++ b/src/Gui/Application.cpp @@ -29,7 +29,6 @@ # include # include # include -# include # include # include # include @@ -60,7 +59,6 @@ #include #include "Application.h" -#include "AutoSaver.h" #include "AxisOriginPy.h" #include "BitmapFactory.h" #include "Command.h" @@ -68,7 +66,6 @@ #include "CommandPy.h" #include "Control.h" #include "PreferencePages/DlgSettingsCacheDirectory.h" -#include "DlgCheckableMessageBox.h" #include "DocumentPy.h" #include "DocumentRecovery.h" #include "EditorView.h" @@ -89,6 +86,7 @@ #include "SelectionFilterPy.h" #include "SoQtOffscreenRendererPy.h" #include "SplitView3DInventor.h" +#include "StartupProcess.h" #include "TaskView/TaskView.h" #include "TaskView/TaskDialogPython.h" #include "TransactionObject.h" @@ -140,357 +138,8 @@ namespace sp = std::placeholders; Application* Application::Instance = nullptr; -namespace { - void setImportImageFormats(); -} - -#ifdef FC_DEBUG // redirect Coin messages to FreeCAD -void messageHandlerCoin(const SoError * error, void * /*userdata*/); -#endif - - namespace Gui { - - -void initGuiAppPreMainWindow(bool calledByGuiPy) -{ - QString plugin; - plugin = QString::fromStdString(App::Application::getHomePath()); - plugin += QLatin1String("/plugins"); - QCoreApplication::addLibraryPath(plugin); - - // 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(QString::fromLatin1("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); - - // 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(QString::fromLatin1("images"), imagePaths); - - // register action style event type - ActionStyleEvent::EventType = QEvent::registerEventType(QEvent::User + 1); - - ParameterGrp::handle hTheme = App::GetApplication().GetParameterGroupByPath( - "User parameter:BaseApp/Preferences/Bitmaps/Theme"); -#if !defined(Q_OS_LINUX) - QIcon::setThemeSearchPaths(QIcon::themeSearchPaths() - << QString::fromLatin1(":/icons/FreeCAD-default")); - QIcon::setThemeName(QLatin1String("FreeCAD-default")); -#else - // Option to opt-out from using a Linux desktop icon theme. - // https://forum.freecad.org/viewtopic.php?f=4&t=35624 - bool themePaths = hTheme->GetBool("ThemeSearchPaths",true); - if (!themePaths) { - QStringList searchPaths; - searchPaths.prepend(QString::fromUtf8(":/icons")); - QIcon::setThemeSearchPaths(searchPaths); - QIcon::setThemeName(QLatin1String("FreeCAD-default")); - } -#endif - - 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())); - } - -#if defined(FC_OS_LINUX) - // See #0001588 - QString path = FileDialog::restoreLocation(); - FileDialog::setWorkingDirectory(QDir::currentPath()); - FileDialog::saveLocation(path); -#else - FileDialog::setWorkingDirectory(FileDialog::restoreLocation()); -#endif -} - - -void initGuiAppPostMainWindow(bool calledByGuiPy, QApplication &mApp, MainWindow &mw, GUIApplicationNativeEventAware *pmAppNativeEventAware) -{ - Application &app = *Gui::Application::Instance; - - // allow to disable version number - ParameterGrp::handle hGen = - App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/General"); - bool showVersion = hGen->GetBool("ShowVersionInTitle", true); - - if (showVersion) { - // set main window title with FreeCAD Version - std::map& config = App::Application::Config(); - QString major = QString::fromLatin1(config["BuildVersionMajor"].c_str()); - QString minor = QString::fromLatin1(config["BuildVersionMinor"].c_str()); - QString point = QString::fromLatin1(config["BuildVersionPoint"].c_str()); - QString suffix = QString::fromLatin1(config["BuildVersionSuffix"].c_str()); - QString title = - QString::fromLatin1("%1 %2.%3.%4%5").arg(mApp.applicationName(), major, minor, point, suffix); - mw.setWindowTitle(title); - } - else { - QString title = mApp.applicationName(); - if (title.isEmpty()) { - title = QString::fromLatin1(App::Application::Config()["ExeName"].c_str()); - } - mw.setWindowTitle(title); - } - - if (pmAppNativeEventAware != nullptr) { - QObject::connect(&mApp, SIGNAL(messageReceived(const QList &)), - &mw, SLOT(processMessages(const QList &))); - } - - ParameterGrp::handle hDocGrp = WindowParameter::getDefaultParameter()->GetGroup("Document"); - int timeout = hDocGrp->GetInt("AutoSaveTimeout", 15); // 15 min - if (!hDocGrp->GetBool("AutoSaveEnabled", true)) - timeout = 0; - AutoSaver::instance()->setTimeout(timeout * 60000); - AutoSaver::instance()->setCompressed(hDocGrp->GetBool("AutoSaveCompressed", true)); - - // set toolbar icon size - ParameterGrp::handle hGrp = WindowParameter::getDefaultParameter()->GetGroup("General"); - int size = hGrp->GetInt("ToolbarIconSize", 0); - if (size >= 16) // must not be lower than this - mw.setIconSize(QSize(size,size)); - - // filter wheel events for combo boxes - if (hGrp->GetBool("ComboBoxWheelEventFilter", false)) { - auto filter = new WheelEventFilter(&mApp); - mApp.installEventFilter(filter); - } - - // For values different to 1 and 2 use the OS locale settings - 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"); - } - - // set text cursor blinking state - int blinkTime = hGrp->GetBool("EnableCursorBlinking", true) ? -1 : 0; - qApp->setCursorFlashTime(blinkTime); - - { - 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. " - "Please upgrade your graphics driver and/or card as required.") - .arg(major) - .arg(minor) - + QStringLiteral("\n"); - Base::Console().Warning(message.toStdString().c_str()); - Dialog::DlgCheckableMessageBox::showMessage( - Gui::GUISingleApplication::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); - } - } - - if (!calledByGuiPy) { - assert(!SoDB::isInitialized()); - } - if (!SoDB::isInitialized()) { - // init the Inventor subsystem - Application::initOpenInventor(); - } - - 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()); - mw.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)); - mw.statusBar()->addPermanentWidget(logo, 0); - logo->setFrameShape(QFrame::NoFrame); - } - } - bool hidden = false; - it = cfg.find("StartHidden"); - if (it != cfg.end()) { - hidden = true; - } - - if (calledByGuiPy) { - assert(!hidden); - } - - // show splasher while initializing the GUI - if (!hidden) - mw.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()); - mw.stopSplasher(); - throw; - - } - - // stop splash screen and set immediately the active window that may be of interest - // for scripts using Python binding for Qt - mw.stopSplasher(); - mApp.setActiveWindow(&mw); - - // 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 = app.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 - app.activateWorkbench(start.c_str()); - - // show the main window - if (!hidden) { - Base::Console().Log("Init: Showing main window\n"); - mw.loadWindowSettings(); - } - - //initialize spaceball. - if (pmAppNativeEventAware != nullptr) { - pmAppNativeEventAware->initSpaceball(&mw); - } - -#ifdef FC_DEBUG // redirect Coin messages to FreeCAD - SoDebugError::setHandlerCallback( messageHandlerCoin, 0 ); -#endif - - 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; - } - - app.setStyleSheet(QLatin1String(style.c_str()), hGrp->GetBool("TiledBackground", false)); - - // 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::vector backgroundAutoloadedModules; - std::stringstream stream(autoloadCSV); - std::string workbench; - while (std::getline(stream, workbench, ',')) { - if (wb.contains(QString::fromLatin1(workbench.c_str()))) - app.activateWorkbench(workbench.c_str()); - } - - // Reactivate the startup workbench - app.activateWorkbench(start.c_str()); -} - - class ViewProviderMap { std::unordered_map map; @@ -689,22 +338,6 @@ struct PyMethodDef FreeCADGui_methods[] = { } // namespace Gui -namespace { - void 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"); - } -} - Application::Application(bool GUIenabled) { //App::GetApplication().Attach(this); @@ -2312,83 +1945,11 @@ void Application::runInitGuiScript() Base::Interpreter().runString(Base::ScriptFactory().ProduceScript("FreeCADGuiInit")); } -void Application::runApplication() +namespace { +bool onlySingleInstance(GUISingleApplication& mainApp) { const std::map& cfg = App::Application::Config(); - std::map::const_iterator it; - - QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) - QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL); -#endif - - // 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 QT_VERSION >= QT_VERSION_CHECK(5,14,0) && 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); - } - - #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) - // 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); - #endif - - // A new QApplication - Base::Console().Log("Init: Creating Gui::Application and QApplication\n"); - - // if application not yet created by the splasher - int argc = App::Application::GetARGC(); - GUISingleApplication mainApp(argc, App::Application::GetARGV()); - // https://forum.freecad.org/viewtopic.php?f=3&t=15540 - mainApp.setAttribute(Qt::AA_DontShowIconsInMenus, false); - - // Make sure that we use '.' as decimal point. See also - // http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=559846 - // and issue #0002891 - // http://doc.qt.io/qt-5/qcoreapplication.html#locale-settings - setlocale(LC_NUMERIC, "C"); - - // check if a single or multiple instances can run - it = cfg.find("SingleInstance"); + auto it = cfg.find("SingleInstance"); if (it != cfg.end() && mainApp.isRunning()) { // send the file names to be opened to the server application so that this // opens them @@ -2411,81 +1972,79 @@ void Application::runApplication() break; } } - return; + + return true; } + return false; +} + +void setAppNameAndIcon() +{ + const std::map& cfg = App::Application::Config(); + // set application icon and window title - it = cfg.find("Application"); + auto it = cfg.find("Application"); if (it != cfg.end()) { - mainApp.setApplicationName(QString::fromUtf8(it->second.c_str())); + QApplication::setApplicationName(QString::fromUtf8(it->second.c_str())); } else { - mainApp.setApplicationName(QString::fromStdString(App::Application::getExecutableName())); + QApplication::setApplicationName(QString::fromStdString(App::Application::getExecutableName())); } #ifndef Q_OS_MACX - mainApp.setWindowIcon( + QApplication::setWindowIcon( Gui::BitmapFactory().pixmap(App::Application::Config()["AppIcon"].c_str())); #endif +} - initGuiAppPreMainWindow(false); - - Application app(true); - MainWindow mw; - mw.setProperty("QuitOnClosed", true); +void tryRunEventLoop(GUISingleApplication& mainApp) +{ + std::stringstream s; + s << App::Application::getUserCachePath() << App::Application::getExecutableName() + << "_" << QCoreApplication::applicationPid() << ".lock"; + // open a lock file with the PID + Base::FileInfo fi(s.str()); + Base::ofstream lock(fi); - initGuiAppPostMainWindow(false, mainApp, mw, &mainApp); - - Instance->d->startingUp = false; - - // gets called once we start the event loop - QTimer::singleShot(0, &mw, SLOT(delayedStartup())); - - // run the Application event loop - Base::Console().Log("Init: Entering event loop\n"); - - // boot phase reference point - // https://forum.freecad.org/viewtopic.php?f=10&t=21665 - Gui::getMainWindow()->setProperty("eventLoop", true); - - try { - std::stringstream s; - s << App::Application::getUserCachePath() << App::Application::getExecutableName() - << "_" << QCoreApplication::applicationPid() << ".lock"; - // open a lock file with the PID - Base::FileInfo fi(s.str()); - Base::ofstream lock(fi); - - // In case the file_lock cannot be created start FreeCAD without IPC support. + // In case the file_lock cannot be created start FreeCAD without IPC support. #if !defined(FC_OS_WIN32) || (BOOST_VERSION < 107600) - std::string filename = s.str(); + std::string filename = s.str(); #else - std::wstring filename = fi.toStdWString(); + std::wstring filename = fi.toStdWString(); #endif - std::unique_ptr flock; - try { - flock = std::make_unique(filename.c_str()); - flock->lock(); - } - catch (const boost::interprocess::interprocess_exception& e) { - QString msg = QString::fromLocal8Bit(e.what()); - Base::Console().Warning("Failed to create a file lock for the IPC: %s\n", - msg.toUtf8().constData()); - } + std::unique_ptr flock; + try { + flock = std::make_unique(filename.c_str()); + flock->lock(); + } + catch (const boost::interprocess::interprocess_exception& e) { + QString msg = QString::fromLocal8Bit(e.what()); + Base::Console().Warning("Failed to create a file lock for the IPC: %s\n", + msg.toUtf8().constData()); + } - Base::Console().Log("Init: Executing event loop...\n"); - mainApp.exec(); + Base::Console().Log("Init: Executing event loop...\n"); + QApplication::exec(); - // Qt can't handle exceptions thrown from event handlers, so we need - // to manually rethrow SystemExitExceptions. - if (mainApp.caughtException.get()) - throw Base::SystemExitException(*mainApp.caughtException.get()); + // Qt can't handle exceptions thrown from event handlers, so we need + // to manually rethrow SystemExitExceptions. + if (mainApp.caughtException) { + throw Base::SystemExitException(*mainApp.caughtException.get()); + } - // close the lock file, in case of a crash we can see the existing lock file - // on the next restart and try to repair the documents, if needed. - if (flock.get()) - flock->unlock(); - lock.close(); - fi.deleteFile(); + // close the lock file, in case of a crash we can see the existing lock file + // on the next restart and try to repair the documents, if needed. + if (flock) { + flock->unlock(); + } + lock.close(); + fi.deleteFile(); +} + +void runEventLoop(GUISingleApplication& mainApp) +{ + try { + tryRunEventLoop(mainApp); } catch (const Base::SystemExitException&) { Base::Console().Message("System exit\n"); @@ -2503,6 +2062,62 @@ void Application::runApplication() App::Application::destructObserver(); throw; } +} +} + +void Application::runApplication() +{ + StartupProcess::setupApplication(); + + // A new QApplication + Base::Console().Log("Init: Creating Gui::Application and QApplication\n"); + + // if application not yet created by the splasher + int argc = App::Application::GetARGC(); + GUISingleApplication mainApp(argc, App::Application::GetARGV()); + // https://forum.freecad.org/viewtopic.php?f=3&t=15540 + QApplication::setAttribute(Qt::AA_DontShowIconsInMenus, false); + + // Make sure that we use '.' as decimal point. See also + // http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=559846 + // and issue #0002891 + // http://doc.qt.io/qt-5/qcoreapplication.html#locale-settings + setlocale(LC_NUMERIC, "C"); + + // check if a single or multiple instances can run + if (onlySingleInstance(mainApp)) { + return; + } + + setAppNameAndIcon(); + + StartupProcess process; + process.execute(); + + Application app(true); + MainWindow mw; + mw.setProperty("QuitOnClosed", true); + +#ifdef FC_DEBUG // redirect Coin messages to FreeCAD + SoDebugError::setHandlerCallback( messageHandlerCoin, 0 ); +#endif + + StartupPostProcess postProcess(&mw, app, &mainApp); + postProcess.execute(); + + Instance->d->startingUp = false; + + // gets called once we start the event loop + QTimer::singleShot(0, &mw, SLOT(delayedStartup())); + + // run the Application event loop + Base::Console().Log("Init: Entering event loop\n"); + + // boot phase reference point + // https://forum.freecad.org/viewtopic.php?f=10&t=21665 + Gui::getMainWindow()->setProperty("eventLoop", true); + + runEventLoop(mainApp); Base::Console().Log("Finish: Event loop left\n"); } diff --git a/src/Gui/Application.h b/src/Gui/Application.h index a7c1b628cf..e9462b15c1 100644 --- a/src/Gui/Application.h +++ b/src/Gui/Application.h @@ -31,7 +31,6 @@ #define putpix() #include -#include "GuiApplication.h" class QCloseEvent; class SoNode; @@ -48,11 +47,6 @@ class PreferencePackManager; class ViewProvider; class ViewProviderDocumentObject; - -GuiExport void initGuiAppPreMainWindow(bool calledByGuiPy); -GuiExport void initGuiAppPostMainWindow(bool calledByGuiPy, QApplication &mApp, MainWindow &mw, GUIApplicationNativeEventAware *pmAppNativeEventAware); - - /** The Application main class * This is the central class of the GUI * @author Jürgen Riegel, Werner Mayer diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index f22cb8ffd0..c280270158 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -1191,6 +1191,7 @@ SET(FreeCADGui_CPP_SRCS Utilities.cpp WaitCursor.cpp ManualAlignment.cpp + StartupProcess.cpp TransactionObject.cpp UserSettings.cpp ) @@ -1228,6 +1229,7 @@ SET(FreeCADGui_SRCS Utilities.h WaitCursor.h ManualAlignment.h + StartupProcess.h TransactionObject.h UserSettings.h ${FreeCADGui_SDK_MOC_HDRS} diff --git a/src/Gui/StartupProcess.cpp b/src/Gui/StartupProcess.cpp new file mode 100644 index 0000000000..e294f7ce27 --- /dev/null +++ b/src/Gui/StartupProcess.cpp @@ -0,0 +1,569 @@ +// 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 +#endif + +#include "StartupProcess.h" +#include "Application.h" +#include "AutoSaver.h" +#include "DlgCheckableMessageBox.h" +#include "FileDialog.h" +#include "GuiApplication.h" +#include "MainWindow.h" +#include "Language/Translator.h" +#include + + +using namespace Gui; + + +StartupProcess::StartupProcess() = default; + +void StartupProcess::setupApplication() +{ + QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) + QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL); +#endif + + // 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 QT_VERSION >= QT_VERSION_CHECK(5,14,0) && 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); + } + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) + // 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); +#endif +} + +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(QString::fromLatin1("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(QString::fromLatin1("images"), imagePaths); +} + +void StartupProcess::registerEventType() +{ + // register action style event type + ActionStyleEvent::EventType = QEvent::registerEventType(QEvent::User + 1); +} + +void StartupProcess::setThemePaths() +{ + ParameterGrp::handle hTheme = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Bitmaps/Theme"); +#if !defined(Q_OS_LINUX) + QIcon::setThemeSearchPaths(QIcon::themeSearchPaths() + << QString::fromLatin1(":/icons/FreeCAD-default")); + QIcon::setThemeName(QLatin1String("FreeCAD-default")); +#else + // Option to opt-out from using a Linux desktop icon theme. + // https://forum.freecad.org/viewtopic.php?f=4&t=35624 + bool themePaths = hTheme->GetBool("ThemeSearchPaths",true); + if (!themePaths) { + QStringList searchPaths; + searchPaths.prepend(QString::fromUtf8(":/icons")); + QIcon::setThemeSearchPaths(searchPaths); + QIcon::setThemeName(QLatin1String("FreeCAD-default")); + } +#endif + + 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(); + checkOpenGL(); + loadOpenInventor(); + setBranding(); + showMainWindow(); + activateWorkbench(); +} + +void StartupPostProcess::setWindowTitle() +{ + // allow to disable version number + ParameterGrp::handle hGen = + App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/General"); + bool showVersion = hGen->GetBool("ShowVersionInTitle", true); + + QString appName = QCoreApplication::applicationName(); + if (appName.isEmpty()) { + appName = QString::fromLatin1(App::Application::Config()["ExeName"].c_str()); + } + if (showVersion) { + // set main window title with FreeCAD Version + std::map& config = App::Application::Config(); + QString major = QString::fromLatin1(config["BuildVersionMajor"].c_str()); + QString minor = QString::fromLatin1(config["BuildVersionMinor"].c_str()); + QString point = QString::fromLatin1(config["BuildVersionPoint"].c_str()); + QString suffix = QString::fromLatin1(config["BuildVersionSuffix"].c_str()); + QString title = QString::fromLatin1("%1 %2.%3.%4%5").arg( + appName, major, minor, point, suffix + ); + mainWindow->setWindowTitle(title); + } + else { + mainWindow->setWindowTitle(appName); + } +} + +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"); + } + +} + +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::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. " + "Please upgrade your 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"); +} + +bool StartupPostProcess::hiddenMainWindow() const +{ + const std::map& cfg = App::Application::Config(); + bool hidden = false; + auto it = cfg.find("StartHidden"); + if (it != cfg.end()) { + hidden = true; + } + + return hidden; +} + +void StartupPostProcess::showMainWindow() +{ + bool hidden = hiddenMainWindow(); + + // show splasher while initializing the GUI + if (!hidden && !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(); + qtApp->setActiveWindow(mainWindow); +} + +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 (!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()); + } + } +} diff --git a/src/Gui/StartupProcess.h b/src/Gui/StartupProcess.h new file mode 100644 index 0000000000..a6e169aeff --- /dev/null +++ b/src/Gui/StartupProcess.h @@ -0,0 +1,88 @@ +// 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 * + * . * + * * + **************************************************************************/ + +#ifndef GUI_STARTUPPROCESS_H +#define GUI_STARTUPPROCESS_H + +#include +#include + +class QApplication; + +namespace Gui { + +class Application; +class MainWindow; + +class GuiExport StartupProcess +{ +public: + StartupProcess(); + static void setupApplication(); + void execute(); + +private: + void setLibraryPath(); + void setStyleSheetPaths(); + void setImagePaths(); + void registerEventType(); + void setThemePaths(); + void setupFileDialog(); +}; + +class GuiExport StartupPostProcess +{ +public: + StartupPostProcess(MainWindow* mw, Application& guiApp, QApplication* app); + void setLoadFromPythonModule(bool value); + void execute(); + +private: + void setWindowTitle(); + void setProcessMessages(); + void setAutoSaving(); + void setToolBarIconSize(); + void setWheelEventFilter(); + void setLocale(); + void setCursorFlashing(); + void checkOpenGL(); + void loadOpenInventor(); + void setBranding(); + void setStyleSheet(); + void autoloadModules(const QStringList& wb); + void setImportImageFormats(); + bool hiddenMainWindow() const; + void showMainWindow(); + void activateWorkbench(); + +private: + bool loadFromPythonModule = false; + MainWindow* mainWindow; + Application& guiApp; + QApplication* qtApp; +}; + + +} + +#endif // GUI_STARTUPPROCESS_H diff --git a/src/Main/FreeCADGuiPy.cpp b/src/Main/FreeCADGuiPy.cpp index 0ecc0d8a74..3f5ee458c2 100644 --- a/src/Main/FreeCADGuiPy.cpp +++ b/src/Main/FreeCADGuiPy.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -286,7 +287,8 @@ QWidget* setupMainWindow() return nullptr; } - Gui::initGuiAppPreMainWindow(true); + Gui::StartupProcess process; + process.execute(); Base::PyGILStateLocker lock; // It's sufficient to create the config key @@ -301,7 +303,9 @@ QWidget* setupMainWindow() mw->setWindowIcon(qApp->windowIcon()); try { - Gui::initGuiAppPostMainWindow(true, *qApp, *mw, nullptr); + Gui::StartupPostProcess postProcess(mw, *Gui::Application::Instance, qApp); + postProcess.setLoadFromPythonModule(true); + postProcess.execute(); } catch (const Base::Exception&) { return nullptr;