Files
create/src/Gui/StartupProcess.cpp
Kacper Donat 8995f43a15 Gui: Fix stylesheet loading order
For some platforms due to event loop and timer triggering being not
deterministic stylesheets could be loaded before style parameters were
fully loaded. This caused warnings about style parameters to appear
potentially confusing users.

This commit changes the stylesheet loading to happen much earlier in the
process so the stylesheet is applied before theme preselection has
chance to happen.
2025-11-19 13:00:14 -06:00

574 lines
20 KiB
C++

// SPDX-License-Identifier: LGPL-2.1-or-later
/***************************************************************************
* Copyright (c) 2024 Werner Mayer <wmayer[at]users.sourceforge.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
**************************************************************************/
#include <FCConfig.h>
#ifdef FC_OS_WIN32
# include <windows.h>
#endif
#include <QApplication>
#include <QImageReader>
#include <QLabel>
#include <QOpenGLContext>
#include <QOpenGLFunctions>
#include <QProcess>
#include <QStatusBar>
#include <QThread>
#include <QTimer>
#include <QWindow>
#include <Inventor/SoDB.h>
#include <set>
#include <string>
#include <ranges>
#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 "Dialogs/DlgVersionMigrator.h"
#include <App/Application.h>
#include <Base/Console.h>
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();
setStyleSheet();
checkOpenGL();
loadOpenInventor();
setBranding();
showMainWindow();
activateWorkbench();
checkParameters();
checkVersionMigration();
}
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<QString>&)),
mainWindow,
SLOT(processMessages(const QList<QString>&))
);
}
}
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<const char*>(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<std::string, std::string>& cfg = App::Application::Config();
std::map<std::string, std::string>::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<QByteArray> 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<GUIApplicationNativeEventAware*>(qtApp)) {
fcApp->initSpaceball(mainWindow);
}
// 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::checkVersionMigration() const
{
auto migrator = new Dialog::DlgVersionMigrator(mainWindow);
migrator->exec();
migrator->deleteLater();
}