Merge remote-tracking branch 'upstream/master' into review-635-fix

# Conflicts:
#	CMakeLists.txt
#	cmake/developer_options.cmake
#	cmake/qet_compilation_vars.cmake
#	sources/qetapp.cpp
This commit is contained in:
ispyisail
2026-09-22 23:10:34 +12:00
676 changed files with 150173 additions and 61935 deletions
+240 -34
View File
@@ -18,6 +18,7 @@
#include "qetapp.h"
#include "configdialog.h"
#include "qet.h"
#include "ui/configpage/configpages.h"
#include "editor/ui/qetelementeditor.h"
#include "elementscollectioncache.h"
@@ -26,6 +27,8 @@
#include "projectview.h"
#include "qetdiagrameditor.h"
#include "qeticons.h"
#include "qetpalette.h"
#include "qetstyle.h"
#include "utils/qetutils.h"
#include "qetmessagebox.h"
#include "qetproject.h"
@@ -44,6 +47,8 @@
# include "spacemouse/spacemouselistener.h"
# include "ui/configpage/spacemouseconfigpage.h"
#endif
#include "logging/qetlogger.h"
#include "logging/ui/diagnosticsreportdialog.h"
#include <cstdlib>
#include <iostream>
@@ -52,7 +57,9 @@
#include <QFontDatabase>
#include <QProcessEnvironment>
#include <QRegularExpression>
#ifdef BUILD_WITHOUT_KF5
#include <QStyleFactory>
#include <QStyleHints>
#ifdef BUILD_WITHOUT_KF
# include "ui/nokde/kautosavefile.h"
#else
# include <KAutoSaveFile>
@@ -120,14 +127,16 @@ QETApp::QETApp() :
}
initConfiguration();
initLanguage();
initIconTheme();
QET::Icons::initIcons();
initFonts();
initStyle();
QET::loadCustomColors();
initSplashScreen();
initSystemTray();
connect(&signal_map, SIGNAL(mapped(QWidget *)),
this, SLOT(invertMainWindowVisibility(QWidget *)));
connect(&signal_map, &QSignalMapper::mappedObject, this, [this](QObject *object) { invertMainWindowVisibility(qobject_cast<QWidget *>(object)); });
qApp->setQuitOnLastWindowClosed(false);
connect(qApp, &QApplication::lastWindowClosed,
this, &QETApp::checkRemainingWindows);
@@ -175,6 +184,7 @@ QETApp::QETApp() :
*/
QETApp::~QETApp()
{
QET::saveCustomColors();
m_elements_recent_files->save();
m_projects_recent_files->save();
@@ -207,6 +217,26 @@ QETApp *QETApp::instance()
return m_qetapp;
}
/**
@brief QETApp::loadedQetTranslationFile
@return path of the QET .qm file actually loaded, empty if none
(diagnostic helper for the startup log, see MachineInfo)
*/
QString QETApp::loadedQetTranslationFile()
{
return m_qetapp ? m_qetapp->qetTranslator.filePath() : QString();
}
/**
@brief QETApp::loadedQtTranslationFile
@return path of the Qt .qm file actually loaded, empty if none
(diagnostic helper for the startup log, see MachineInfo)
*/
QString QETApp::loadedQtTranslationFile()
{
return m_qetapp ? m_qetapp->qtTranslator.filePath() : QString();
}
/**
@brief QETApp::setLanguage
Change the language used by the application.
@@ -217,16 +247,11 @@ QETApp *QETApp::instance()
*/
void QETApp::setLanguage(const QString &desired_language) {
QString languages_path = languagesPath();
m_interface_language = desired_language;
// load Qt library translations
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
QString qt_l10n_path = QLibraryInfo::location(QLibraryInfo::TranslationsPath);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
QString qt_l10n_path = QLibraryInfo::path(QLibraryInfo::TranslationsPath);
#endif
if (!qtTranslator.load("qt_" + desired_language, qt_l10n_path))
{
qWarning() << "failed to load"
@@ -244,14 +269,21 @@ void QETApp::setLanguage(const QString &desired_language) {
// desired_language may be a full locale such as "pt_BR": try that exact
// translation, then the base language ("pt"), then fall back to English.
// French is the application's source language and needs no translation.
// A .qm compiled from an untranslated .ts (0% done) loads "successfully"
// but is empty: treat it as missing, so the user falls back to English
// instead of silently getting the French source strings.
const QString base_language = desired_language.section('_', 0, 0);
bool loaded = qetTranslator.load("qet_" + desired_language, languages_path);
auto loadQet = [this, &languages_path](const QString &name) {
return qetTranslator.load(name, languages_path)
&& !qetTranslator.isEmpty();
};
bool loaded = loadQet("qet_" + desired_language);
if (!loaded && base_language != desired_language)
loaded = qetTranslator.load("qet_" + base_language, languages_path);
loaded = loadQet("qet_" + base_language);
if (!loaded && base_language != "fr") {
// use of the English version by default
// utilisation de la version anglaise par defaut
if(!qetTranslator.load("qet_en", languages_path))
if(!loadQet("qet_en"))
qWarning() << "failed to load"
<< "qet_en" << languages_path << "(" << __FILE__
<< __LINE__ << __FUNCTION__ << ")";
@@ -277,7 +309,7 @@ QString QETApp::langFromSetting()
{
QSettings settings;
system_language = settings.value("lang", "system").toString();
if(system_language == "system") {
if ((system_language == "system") || (system_language == QString())) {
// Keep the full locale (e.g. "pt_BR"), not just the base language
// ("pt"): QET ships regional translations (pt_BR, nl_BE, nl_NL) and
// truncating here loaded the wrong one. setLanguage() falls back to
@@ -1433,7 +1465,7 @@ QFont QETApp::diagramTextsItemFont(qreal size)
@param size
@return dynamic text font with PointSizeF(size)
*/
QFont QETApp::dynamicTextsItemFont(qreal size)
QFont QETApp::dynamicTextsItemFont(qreal size)
{
QSettings settings;
//Font to use
@@ -1642,7 +1674,31 @@ void QETApp::receiveMessage(int instanceId, QByteArray message)
{
QString my_message(str.mid(20));
QStringList args_list = QET::splitWithSpaces(my_message);
openFiles(QETArguments(args_list));
// Deferred, not called directly.
//
// This slot runs inside SingleApplication's readyRead handling:
// SingleApplicationPrivate::slotDataAvailable() emits
// receivedMessage() synchronously from the socket's readyRead
// lambda. openFiles() then loads a project -- seconds of work on
// a large one -- and openAndAddProject() puts up a modal
// BackupDialog, whose exec() runs a nested event loop while the
// socket handler is still on the stack.
//
// During that nested loop the secondary instance exits, the
// connection closes and the QLocalSocket is deleted. When the
// dialog is dismissed and the stack unwinds, QMetaObject::
// activate() continues emitting on the freed sender and the
// process dies. Reported with a backtrace on PR #861;
// reproduced on Qt 6.10.2 by dismissing the dialog, which is the
// step that makes it fail -- leaving it open never unwinds.
//
// A zero-timer returns to the event loop first, so the socket
// stack is fully unwound before any of this runs.
const QETArguments deferred_args{args_list};
QTimer::singleShot(0, this, [this, deferred_args]() {
openFiles(deferred_args);
});
}
}
@@ -1745,8 +1801,10 @@ void QETApp::invertMainWindowVisibility(QWidget *window) {
false pour utiliser celles du theme en cours
*/
void QETApp::useSystemPalette(bool use) {
// The base palette is always initial_palette_ (see initStyle()); the
// setting only decides whether the user's style.css is layered on top.
qApp->setPalette(initial_palette_);
if (use) {
qApp->setPalette(initial_palette_);
// Drop any stylesheet previously loaded from style.css: with system
// colors requested, the palette set just above is what provides them.
//
@@ -1764,6 +1822,10 @@ void QETApp::useSystemPalette(bool use) {
file.close();
}
}
// Widgets with their own style sheet keep the palette they were
// polished with; after a live light/dark switch they would stay in
// the old colors (see QET::Palette::refreshStyleSheets).
QET::Palette::refreshStyleSheets();
}
/**
@@ -1803,7 +1865,7 @@ void QETApp::checkRemainingWindows()
*/
static bool sleep = true;
if (sleep) {
QTimer::singleShot(500, this, SLOT(checkRemainingWindows()));
QTimer::singleShot(500, this, &QETApp::checkRemainingWindows);
} else {
if (!diagramEditors().count() && !elementEditors().count()) {
qApp->quit();
@@ -1985,6 +2047,7 @@ void QETApp::openTitleBlockTemplate(const TitleBlockTemplateLocation &location,
qet_template_editor -> setOpenForDuplication(duplicate);
qet_template_editor -> edit(location);
qet_template_editor -> show();
qet_template_editor -> readSettingsState(); // must run after show() in Qt6
}
/**
@@ -1996,6 +2059,7 @@ void QETApp::openTitleBlockTemplate(const QString &filepath) {
QETTitleBlockTemplateEditor *qet_template_editor = new QETTitleBlockTemplateEditor();
qet_template_editor -> edit(filepath);
qet_template_editor -> show();
qet_template_editor -> readSettingsState(); // must run after show() in Qt6
}
/**
@@ -2293,17 +2357,88 @@ void QETApp::initFonts()
}
}
/**
@brief QETApp::initIconTheme
Register QET's two icon themes ("qet" and "qet-dark", see
misc/make_icon_themes.py and ico/icon-themes.qrc) and pick the one
matching the current palette. Must run before QET::Icons::initIcons(),
which looks icons up by name.
*/
void QETApp::initIconTheme()
{
QStringList paths = QIcon::themeSearchPaths();
paths.prepend(QStringLiteral(":/ico/themes"));
QIcon::setThemeSearchPaths(paths);
applyIconTheme(qApp->palette());
}
/**
@brief QETApp::applyIconTheme
Select "qet-dark" for a dark palette, "qet" otherwise. Icons created
with QIcon::fromTheme() re-resolve on their next paint, so this can be
called again whenever the palette changes.
*/
void QETApp::applyIconTheme(const QPalette &palette)
{
QIcon::setThemeName(QET::Palette::isDark(palette)
? QStringLiteral("qet-dark")
: QStringLiteral("qet"));
}
/**
@brief QETApp::initStyle
Setup the gui style
*/
void QETApp::initStyle()
{
// Wrap the running style so icons get a hover state (see qetstyle.h).
// The proxy keeps the base style's object name, so the Fusion checks
// below still see "fusion".
if (!qobject_cast<QETStyle *>(qApp->style()))
qApp->setStyle(new QETStyle(QStyleFactory::create(qApp->style()->objectName())));
initial_palette_ = qApp->palette();
#ifdef Q_OS_MACOS
// main.cpp forces the Fusion style on macOS, but the palette Qt hands
// us there is the one its platform theme builds for the native style:
// Window, Button and Base share one color, and in dark mode the
// Inactive ButtonText is black. Fusion draws its frames, gradients and
// combo box text from those roles, so controls lose their edges and
// combo text goes black once the window loses focus. Replace it with a
// palette laid out the way Fusion expects (see qetpalette.h).
//
// macOS only: on Linux, Fusion is Qt's default style on desktops
// without a platform theme, and the palette there carries the user's
// desktop colors, which must stay in effect. Making Fusion and this
// palette the default everywhere is discussed in #870.
if (QET::Palette::styleIsFusion(qApp->style()))
initial_palette_ = QET::Palette::forFusion(initial_palette_);
#endif
applyIconTheme(initial_palette_);
//Apply or not the system style
QSettings settings;
useSystemPalette(settings.value("usesystemcolors", true).toBool());
#if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
// Setting an application palette stops Qt from following the OS
// light/dark switch on its own, so follow it here. The platform accent
// color is not reachable any more at this point; the palette's own
// selection blue is used instead.
connect(qApp->styleHints(), &QStyleHints::colorSchemeChanged, this,
[this](Qt::ColorScheme scheme)
{
if (!QET::Palette::styleIsFusion(qApp->style()))
return;
initial_palette_ = scheme == Qt::ColorScheme::Dark
? QET::Palette::fusionDark()
: QET::Palette::fusionLight();
applyIconTheme(initial_palette_);
QSettings settings;
useSystemPalette(settings.value("usesystemcolors", true).toBool());
});
#endif
}
/**
@@ -2415,24 +2550,23 @@ void QETApp::initSystemTray()
reduce_appli -> setToolTip(tr("Réduire QElectroTech dans le systray"));
restore_appli -> setToolTip(tr("Restaurer QElectroTech"));
connect(quitter_qet, SIGNAL(triggered()), this, SLOT(quitQET()));
connect(reduce_appli, SIGNAL(triggered()), this, SLOT(reduceEveryEditor()));
connect(restore_appli, SIGNAL(triggered()), this, SLOT(restoreEveryEditor()));
connect(reduce_diagrams, SIGNAL(triggered()), this, SLOT(reduceDiagramEditors()));
connect(restore_diagrams, SIGNAL(triggered()), this, SLOT(restoreDiagramEditors()));
connect(reduce_elements, SIGNAL(triggered()), this, SLOT(reduceElementEditors()));
connect(restore_elements, SIGNAL(triggered()), this, SLOT(restoreElementEditors()));
connect(reduce_templates, SIGNAL(triggered()), this, SLOT(reduceTitleBlockTemplateEditors()));
connect(restore_templates,SIGNAL(triggered()), this, SLOT(restoreTitleBlockTemplateEditors()));
connect(new_diagram, SIGNAL(triggered()), this, SLOT(newDiagramEditor()));
connect(new_element, SIGNAL(triggered()), this, SLOT(newElementEditor()));
connect(quitter_qet, &QAction::triggered, this, &QETApp::quitQET);
connect(reduce_appli, &QAction::triggered, this, &QETApp::reduceEveryEditor);
connect(restore_appli, &QAction::triggered, this, &QETApp::restoreEveryEditor);
connect(reduce_diagrams, &QAction::triggered, this, &QETApp::reduceDiagramEditors);
connect(restore_diagrams, &QAction::triggered, this, &QETApp::restoreDiagramEditors);
connect(reduce_elements, &QAction::triggered, this, &QETApp::reduceElementEditors);
connect(restore_elements, &QAction::triggered, this, &QETApp::restoreElementEditors);
connect(reduce_templates, &QAction::triggered, this, &QETApp::reduceTitleBlockTemplateEditors);
connect(restore_templates, &QAction::triggered, this, &QETApp::restoreTitleBlockTemplateEditors);
connect(new_diagram, &QAction::triggered, this, &QETApp::newDiagramEditor);
connect(new_element, &QAction::triggered, this, &QETApp::newElementEditor);
// initialization of the systray icon
// initialisation de l'icone du systray
m_qsti = new QSystemTrayIcon(QET::Icons::QETLogo, this);
m_qsti -> setToolTip(tr("QElectroTech", "systray icon tooltip"));
connect(m_qsti, SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
this, SLOT(systray(QSystemTrayIcon::ActivationReason)));
connect(m_qsti, &QSystemTrayIcon::activated, this, &QETApp::systray);
m_qsti -> setContextMenu(menu_systray);
m_qsti -> show();
}
@@ -2452,7 +2586,7 @@ template <class T> void QETApp::addWindowsListToMenu(
QAction *current_menu = menu -> addAction(window -> windowTitle());
current_menu -> setCheckable(true);
current_menu -> setChecked(window -> isVisible());
connect(current_menu, SIGNAL(triggered()), &signal_map, SLOT(map()));
connect(current_menu, &QAction::triggered, &signal_map, qOverload<>(&QSignalMapper::map));
signal_map.setMapping(current_menu, window);
}
}
@@ -2589,10 +2723,28 @@ void QETApp::checkBackupFiles()
}
}
if (stale_files.isEmpty()) {
return;
if (!stale_files.isEmpty()) {
offerBackupFiles(stale_files);
}
// Discussion #644 step 5 asks that the recovery prompt and the crash
// report never show at the same time -- not that the report be dropped
// whenever there is something to recover. Offering it here, once the
// recovery prompt has been answered, keeps the two sequential without
// losing the report after the most common crash there is: one with a
// project open, which always leaves a stale file behind, so the report
// was unreachable in exactly the case it is most wanted (issue #901).
checkCrashDump();
}
/**
@brief QETApp::offerBackupFiles
Ask whether to reopen the recovery files left by a previous run, and
open or discard them accordingly.
@param stale_files : the recovery files to offer
*/
void QETApp::offerBackupFiles(const QList<KAutoSaveFile *> &stale_files)
{
QString text;
if(stale_files.size() == 1) {
text.append(tr("<b>Le fichier de restauration suivant a été trouvé,<br>"
@@ -2643,6 +2795,58 @@ void QETApp::checkBackupFiles()
}
}
/**
@brief QETApp::checkCrashDump
Discussion #644, step 5: if the crash handler (step 4) left an
unretrieved dump from a previous run, offer it to the user. Only
called from checkBackupFiles() when there was no stale project file
to recover this run, so the two prompts never both show at once.
*/
void QETApp::checkCrashDump()
{
QetLogger &logger = QetLogger::instance();
// Listed once, then used both to build the contents and to delete
// below. Re-listing after the dialog closes would delete a dump
// written while it was open, unseen -- see clearPendingCrashDump().
const QStringList offered = logger.pendingCrashDumpFiles();
if (offered.isEmpty()) {
return;
}
const QByteArray content = logger.pendingCrashDumpContents(offered);
DiagnosticsReportDialog dialog(
tr("Rapport de plantage"),
tr("QElectroTech ne s'est pas fermé correctement lors de sa dernière exécution.\n"
"Voici les derniers messages enregistrés avant l'arrêt -- vous pouvez les "
"enregistrer pour les joindre à un rapport de bug."),
content);
dialog.exec();
// Offered once, then marked retrieved -- regardless of whether the
// user chose to save it -- so it is never offered a second time.
logger.clearPendingCrashDump(offered);
}
/**
@brief QETApp::showDiagnosticsReport
Discussion #644, step 5: the manual "Help > Diagnostics > Save
report" action. Unlike checkCrashDump(), this is about the *current*,
still-running session, not a previous one.
*/
void QETApp::showDiagnosticsReport()
{
const QByteArray content = QetLogger::instance().buildDiagnosticsReport();
DiagnosticsReportDialog dialog(
tr("Rapport de diagnostic"),
tr("Ceci contient les derniers messages de journalisation de cette session. "
"Vérifiez le contenu avant de le joindre à un rapport de bug public."),
content);
dialog.exec();
}
/**
@brief QETApp::fetchWindowStats
Updates the booleans concerning the state of the windows
@@ -2856,3 +3060,5 @@ int QETApp::projectId(const QETProject *project) {
}
return(-1);
}
QString QETApp::m_interface_language;