From d0cea474a6cc45b28114ba158061d42c0cfed484 Mon Sep 17 00:00:00 2001 From: Gerhard Schwanzer Date: Sun, 5 Jul 2026 11:10:44 +0200 Subject: [PATCH 1/4] Fix Qt-only build without KF5 The BUILD_WITH_KF5 option was checked with DEFINED, so passing -DBUILD_WITH_KF5=OFF still entered the KF5 setup path. Skip the KF5 setup when disabled and provide small Qt-only replacements for the KDE color widgets used by .ui files in that build mode. Assisted-by: pi coding agent / Mika (OpenAI GPT-5.5) --- CMakeLists.txt | 8 ++++ cmake/fetch_kdeaddons.cmake | 2 +- cmake/qet_compilation_vars.cmake | 9 ++++ sources/ui/nokde/kcolorbutton.cpp | 70 +++++++++++++++++++++++++++++++ sources/ui/nokde/kcolorbutton.h | 48 +++++++++++++++++++++ sources/ui/nokde/kcolorcombo.cpp | 49 ++++++++++++++++++++++ sources/ui/nokde/kcolorcombo.h | 38 +++++++++++++++++ 7 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 sources/ui/nokde/kcolorbutton.cpp create mode 100644 sources/ui/nokde/kcolorbutton.h create mode 100644 sources/ui/nokde/kcolorcombo.cpp create mode 100644 sources/ui/nokde/kcolorcombo.h diff --git a/CMakeLists.txt b/CMakeLists.txt index edec55380..ef2853be1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -148,6 +148,14 @@ target_include_directories( ${QET_DIR}/sources/svg ) +if(NOT BUILD_WITH_KF5) + target_include_directories( + ${PROJECT_NAME} + PRIVATE + ${QET_DIR}/sources/ui/nokde + ) +endif() + install(TARGETS ${PROJECT_NAME}) if (NOT MINGW) diff --git a/cmake/fetch_kdeaddons.cmake b/cmake/fetch_kdeaddons.cmake index ac5a340f9..0867afec4 100644 --- a/cmake/fetch_kdeaddons.cmake +++ b/cmake/fetch_kdeaddons.cmake @@ -16,7 +16,7 @@ message(" - fetch_kdeaddons") -if(DEFINED BUILD_WITH_KF5) +if(BUILD_WITH_KF5) Include(FetchContent) option(BUILD_KF5 "Build KF5 libraries, use system ones otherwise" YES) diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 7e150a6e7..7c905d87b 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -724,6 +724,15 @@ set(QET_SRC_FILES ${QET_DIR}/sources/xml/terminalstriplayoutpatternxml.h ) +if(NOT BUILD_WITH_KF5) + list(APPEND QET_SRC_FILES + ${QET_DIR}/sources/ui/nokde/kcolorbutton.cpp + ${QET_DIR}/sources/ui/nokde/kcolorbutton.h + ${QET_DIR}/sources/ui/nokde/kcolorcombo.cpp + ${QET_DIR}/sources/ui/nokde/kcolorcombo.h + ) +endif() + set(TS_FILES ${QET_DIR}/lang/qet_ar.ts ${QET_DIR}/lang/qet_ca.ts diff --git a/sources/ui/nokde/kcolorbutton.cpp b/sources/ui/nokde/kcolorbutton.cpp new file mode 100644 index 000000000..3436d06f1 --- /dev/null +++ b/sources/ui/nokde/kcolorbutton.cpp @@ -0,0 +1,70 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include "kcolorbutton.h" + +#include +#include + +namespace { +QColor fallbackColor() +{ + return QPalette{}.color(QPalette::Button); +} +} + +KColorButton::KColorButton(QWidget *parent) : + QPushButton{parent}, + m_color{fallbackColor()} +{ + connect(this, &QPushButton::clicked, this, &KColorButton::chooseColor); + updateButton(); +} + +QColor KColorButton::color() const +{ + return m_color; +} + +void KColorButton::setColor(const QColor &color) +{ + m_color = color.isValid() ? color : fallbackColor(); + updateButton(); +} + +void KColorButton::chooseColor() +{ + const auto selected = QColorDialog::getColor(m_color, this); + if (!selected.isValid() || selected == m_color) { + return; + } + + m_color = selected; + updateButton(); + emit changed(m_color); +} + +void KColorButton::updateButton() +{ + setText(m_color.name()); + + auto pal = palette(); + pal.setColor(QPalette::Button, m_color); + setAutoFillBackground(true); + setPalette(pal); + update(); +} diff --git a/sources/ui/nokde/kcolorbutton.h b/sources/ui/nokde/kcolorbutton.h new file mode 100644 index 000000000..453ff9530 --- /dev/null +++ b/sources/ui/nokde/kcolorbutton.h @@ -0,0 +1,48 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#ifndef QET_KCOLORBUTTON_H +#define QET_KCOLORBUTTON_H + +#include +#include + +class KColorButton : public QPushButton +{ + Q_OBJECT + + public: + explicit KColorButton(QWidget *parent = nullptr); + + QColor color() const; + + public slots: + void setColor(const QColor &color); + + signals: + void changed(const QColor &color); + + private slots: + void chooseColor(); + + private: + void updateButton(); + + QColor m_color; +}; + +#endif // QET_KCOLORBUTTON_H diff --git a/sources/ui/nokde/kcolorcombo.cpp b/sources/ui/nokde/kcolorcombo.cpp new file mode 100644 index 000000000..e90e25d13 --- /dev/null +++ b/sources/ui/nokde/kcolorcombo.cpp @@ -0,0 +1,49 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include "kcolorcombo.h" + +#include + +KColorCombo::KColorCombo(QWidget *parent) : + QComboBox{parent} +{ + connect( + this, + QOverload::of(&QComboBox::activated), + this, + [this](int index) { + emit activated(itemData(index).value()); + }); +} + +void KColorCombo::setColors(const QList &colors) +{ + clear(); + for (const auto &color : colors) { + addItem(color.name(), color); + } +} + +QColor KColorCombo::color(int index) const +{ + if (index < 0 || index >= count()) { + return {}; + } + + return itemData(index).value(); +} diff --git a/sources/ui/nokde/kcolorcombo.h b/sources/ui/nokde/kcolorcombo.h new file mode 100644 index 000000000..94d70948f --- /dev/null +++ b/sources/ui/nokde/kcolorcombo.h @@ -0,0 +1,38 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#ifndef QET_KCOLORCOMBO_H +#define QET_KCOLORCOMBO_H + +#include +#include + +class KColorCombo : public QComboBox +{ + Q_OBJECT + + public: + explicit KColorCombo(QWidget *parent = nullptr); + + void setColors(const QList &colors); + QColor color(int index) const; + + signals: + void activated(const QColor &color); +}; + +#endif // QET_KCOLORCOMBO_H From e6124e941ade86bc9e9af659a18c708e82d2d5bf Mon Sep 17 00:00:00 2001 From: Gerhard Schwanzer Date: Sun, 5 Jul 2026 13:16:19 +0200 Subject: [PATCH 2/4] Add Qt-only autosave recovery fallback Provide a small KAutoSaveFile-compatible implementation for the no-KF5 build path and use it to keep the existing crash-recovery code active when BUILD_WITH_KF5=OFF. The normal KF5 build still uses the KDE KAutoSaveFile implementation. Assisted-by: pi coding agent / Mika (OpenAI GPT-5.5) --- cmake/qet_compilation_vars.cmake | 2 + sources/qetapp.cpp | 5 +- sources/qetdiagrameditor.cpp | 4 +- sources/qetdiagrameditor.h | 6 - sources/qetproject.cpp | 9 - sources/qetproject.h | 11 +- sources/ui/nokde/kautosavefile.cpp | 284 +++++++++++++++++++++++++++++ sources/ui/nokde/kautosavefile.h | 61 +++++++ 8 files changed, 350 insertions(+), 32 deletions(-) create mode 100644 sources/ui/nokde/kautosavefile.cpp create mode 100644 sources/ui/nokde/kautosavefile.h diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 7c905d87b..3bb29d113 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -726,6 +726,8 @@ set(QET_SRC_FILES if(NOT BUILD_WITH_KF5) list(APPEND QET_SRC_FILES + ${QET_DIR}/sources/ui/nokde/kautosavefile.cpp + ${QET_DIR}/sources/ui/nokde/kautosavefile.h ${QET_DIR}/sources/ui/nokde/kcolorbutton.cpp ${QET_DIR}/sources/ui/nokde/kcolorbutton.h ${QET_DIR}/sources/ui/nokde/kcolorcombo.cpp diff --git a/sources/qetapp.cpp b/sources/qetapp.cpp index 88f6b2642..a19461e33 100644 --- a/sources/qetapp.cpp +++ b/sources/qetapp.cpp @@ -47,6 +47,7 @@ #include #include #ifdef BUILD_WITHOUT_KF5 +# include "ui/nokde/kautosavefile.h" #else # include #endif @@ -2500,9 +2501,6 @@ void QETApp::buildSystemTrayMenu() */ void QETApp::checkBackupFiles() { -#ifdef BUILD_WITHOUT_KF5 - return; -#else QList stale_files = KAutoSaveFile::allStaleFiles(); //Remove from the list @stale_files, the stales file of opened project @@ -2577,7 +2575,6 @@ void QETApp::checkBackupFiles() delete stale; } } -#endif } /** diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index 2b1d9607c..55fc73319 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -49,6 +49,7 @@ #include "ui/terminalnumberingdialog.h" #include #ifdef BUILD_WITHOUT_KF5 +# include "ui/nokde/kautosavefile.h" #else # include #endif @@ -1948,8 +1949,6 @@ bool QETDiagramEditor::drawGrid() const return m_draw_grid->isChecked(); } -#ifdef BUILD_WITHOUT_KF5 -#else /** @brief QETDiagramEditor::openBackupFiles @param backup_files @@ -1980,7 +1979,6 @@ void QETDiagramEditor::openBackupFiles(QList backup_files) DialogWaiting::dropInstance(); } } -#endif /** met a jour le menu "Fenetres" */ diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h index 5a26b5f06..309e108e5 100644 --- a/sources/qetdiagrameditor.h +++ b/sources/qetdiagrameditor.h @@ -44,10 +44,7 @@ class ElementsCollectionWidget; class AutoNumberingDockWidget; class TerminalNumberingDialog; -#ifdef BUILD_WITHOUT_KF5 -#else class KAutoSaveFile; -#endif /** This class represents the main window of the QElectroTech diagram editor and, ipso facto, the most important part of the QElectroTech user interface. @@ -72,10 +69,7 @@ class QETDiagramEditor : public QETMainWindow ProjectView *currentProjectView() const; QETProject *currentProject() const; bool drawGrid() const; -#ifdef BUILD_WITHOUT_KF5 -#else void openBackupFiles (QList backup_files); -#endif protected: bool event(QEvent *) override; diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index 64bead38f..86d884aad 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -93,8 +93,6 @@ QETProject::QETProject(const QString &path, QObject *parent) : init(); } -#ifdef BUILD_WITHOUT_KF5 -#else /** @brief QETProject::QETProject @param backup : backup file to open, QETProject take ownership of backup. @@ -129,7 +127,6 @@ QETProject::QETProject(KAutoSaveFile *backup, QObject *parent) : init(); } -#endif /** @brief QETProject::~QETProject @@ -342,15 +339,12 @@ void QETProject::setFilePath(const QString &filepath) if (filepath == m_file_path) { return; } -#ifdef BUILD_WITHOUT_KF5 -#else //Don't close/re-point the backup file while a backup is still writing it. m_backup_future.waitForFinished(); if (m_backup_file.isOpen()) { m_backup_file.close(); } m_backup_file.setManagedFile(QUrl::fromLocalFile(filepath)); -#endif m_file_path = filepath; QFileInfo fi(m_file_path); @@ -1813,8 +1807,6 @@ void QETProject::writeBackup() { if (!m_backup_enabled) return; -#ifdef BUILD_WITHOUT_KF5 -#else # if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove //Don't launch a new backup while the previous one is still writing: //both would write through &m_backup_file on different threads. @@ -1830,7 +1822,6 @@ void QETProject::writeBackup() qDebug() << "Help code for QT 6 or later" << "QtConcurrent::run its backwards now...function, object, args"; # endif -#endif } /** diff --git a/sources/qetproject.h b/sources/qetproject.h index 0ed828953..5f13cf4a8 100644 --- a/sources/qetproject.h +++ b/sources/qetproject.h @@ -30,6 +30,7 @@ #include "titleblockproperties.h" #ifdef BUILD_WITHOUT_KF5 +# include "ui/nokde/kautosavefile.h" #else # include #endif @@ -48,10 +49,6 @@ class XmlElementCollection; class QTimer; class TerminalStrip; -#ifdef BUILD_WITHOUT_KF5 -#else -class KAutoSaveFile; -#endif /** This class represents a QET project. Typically saved as a .qet file, it @@ -79,10 +76,7 @@ class QETProject : public QObject public: QETProject (QObject *parent = nullptr); QETProject (const QString &path, QObject * = nullptr); -#ifdef BUILD_WITHOUT_KF5 -#else QETProject (KAutoSaveFile *backup, QObject *parent=nullptr); -#endif ~QETProject() override; private: @@ -297,10 +291,7 @@ class QETProject : public QObject QTimer m_save_backup_timer, m_autosave_timer; QFuture m_backup_future; -#ifdef BUILD_WITHOUT_KF5 -#else KAutoSaveFile m_backup_file; -#endif QUuid m_uuid = QUuid::createUuid(); projectDataBase m_data_base; QVector m_terminal_strip_vector; diff --git a/sources/ui/nokde/kautosavefile.cpp b/sources/ui/nokde/kautosavefile.cpp new file mode 100644 index 000000000..1e896e38f --- /dev/null +++ b/sources/ui/nokde/kautosavefile.cpp @@ -0,0 +1,284 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include "kautosavefile.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +const auto autosaveSuffix = QStringLiteral(".qetautosave"); + +QString staleFilesDir() +{ + auto data_dir = QStandardPaths::writableLocation( + QStandardPaths::AppDataLocation); + while (data_dir.endsWith(QLatin1Char('/'))) { + data_dir.chop(1); + } + if (data_dir.isEmpty()) { + return {}; + } + + return data_dir + QDir::separator() + QStringLiteral("autosave"); +} + +QString metadataFileName(const QString &autosave_file_name) +{ + return autosave_file_name + QStringLiteral(".path"); +} + +QUrl normalizeManagedFile(const QUrl &url) +{ + if (url.isEmpty()) { + return {}; + } + + if (url.isLocalFile() || url.scheme().isEmpty()) { + const auto path = url.isLocalFile() ? url.toLocalFile() : url.path(); + QUrl normalized; + normalized.setPath(QDir::cleanPath(QFileInfo(path).absoluteFilePath())); + return normalized; + } + + return url; +} + +QString storedManagedFile(const QUrl &url) +{ + if (url.isLocalFile() || url.scheme().isEmpty()) { + return url.path(); + } + + return url.toString(QUrl::FullyEncoded); +} + +QUrl managedFileFromStorage(const QString &stored_path) +{ + if (!stored_path.startsWith(QLatin1Char('/'))) { + return QUrl(stored_path); + } + + QUrl url; + url.setPath(QDir::cleanPath(stored_path)); + return url; +} + +bool writeManagedFileMetadata(const QString &autosave_file_name, const QUrl &managed_file) +{ + QFile metadata_file(metadataFileName(autosave_file_name)); + if (!metadata_file.open(QIODevice::WriteOnly + | QIODevice::Truncate + | QIODevice::Text)) { + return false; + } + + QTextStream stream(&metadata_file); + stream << storedManagedFile(managed_file) << '\n'; + stream.flush(); + return metadata_file.error() == QFile::NoError; +} + +QUrl readManagedFileMetadata(const QString &autosave_file_name) +{ + QFile metadata_file(metadataFileName(autosave_file_name)); + if (!metadata_file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return {}; + } + + QTextStream stream(&metadata_file); + const auto stored_path = stream.readLine().trimmed(); + if (stored_path.isEmpty()) { + return {}; + } + + return normalizeManagedFile(managedFileFromStorage(stored_path)); +} + +QString autosaveFileName(const QUrl &managed_file) +{ + const auto stored_path = storedManagedFile(managed_file); + const auto digest = QCryptographicHash::hash( + stored_path.toUtf8(), + QCryptographicHash::Sha256).toHex().left(16); + + auto basename = QFileInfo(managed_file.path()).fileName(); + if (basename.isEmpty()) { + basename = QStringLiteral("autosave"); + } + + auto encoded_basename = QString::fromLatin1( + QUrl::toPercentEncoding(basename)); + if (encoded_basename.size() > 80) { + encoded_basename.truncate(80); + } + + const auto random = QString::number( + QRandomGenerator::global()->generate64(), 16); + + return QStringLiteral("%1_%2_%3%4").arg( + encoded_basename, + QString::fromLatin1(digest), + random, + autosaveSuffix); +} + +QStringList findAllStaleFiles(const QString &application_name) +{ + Q_UNUSED(application_name) + + const auto dir_path = staleFilesDir(); + if (dir_path.isEmpty()) { + return {}; + } + + QDir dir(dir_path); + const auto entries = dir.entryList( + {QStringLiteral("*") + autosaveSuffix}, + QDir::Files); + + QStringList files; + for (const auto &entry : entries) { + files << dir.absoluteFilePath(entry); + } + + return files; +} + +} // namespace + +KAutoSaveFile::KAutoSaveFile(const QUrl &filename, QObject *parent) : + QFile{parent} +{ + setManagedFile(filename); +} + +KAutoSaveFile::KAutoSaveFile(QObject *parent) : + QFile{parent} +{ +} + +KAutoSaveFile::~KAutoSaveFile() +{ + releaseLock(); +} + +QUrl KAutoSaveFile::managedFile() const +{ + return m_managed_file; +} + +void KAutoSaveFile::setManagedFile(const QUrl &filename) +{ + releaseLock(); + + m_managed_file = normalizeManagedFile(filename); + m_managed_file_name_changed = true; + setFileName({}); +} + +void KAutoSaveFile::releaseLock() +{ + if (m_lock && m_lock->isLocked()) { + const auto autosave_file_name = fileName(); + m_lock.reset(); + + if (!autosave_file_name.isEmpty()) { + QFile::remove(metadataFileName(autosave_file_name)); + remove(); + } + } else { + m_lock.reset(); + } +} + +bool KAutoSaveFile::open(OpenMode openmode) +{ + if (m_managed_file.isEmpty()) { + return false; + } + + if (m_managed_file_name_changed) { + const auto stale_dir = staleFilesDir(); + if (stale_dir.isEmpty() || !QDir().mkpath(stale_dir)) { + return false; + } + + setFileName(QDir(stale_dir).absoluteFilePath( + autosaveFileName(m_managed_file))); + if (!writeManagedFileMetadata(fileName(), m_managed_file)) { + setFileName({}); + return false; + } + + m_managed_file_name_changed = false; + } + + if (!QFile::open(openmode)) { + return false; + } + + if (!m_lock) { + m_lock = std::make_unique( + fileName() + QStringLiteral(".lock")); + m_lock->setStaleLockTime(60 * 1000); + } + + if (m_lock->isLocked() || m_lock->tryLock()) { + return true; + } + + close(); + return false; +} + +QList KAutoSaveFile::staleFiles( + const QUrl &url, + const QString &applicationName) +{ + const auto managed_file_filter = normalizeManagedFile(url); + QList stale_files; + + for (const auto &file : findAllStaleFiles(applicationName)) { + const auto managed_file = readManagedFileMetadata(file); + if (managed_file.isEmpty()) { + continue; + } + if (!managed_file_filter.isEmpty() + && managed_file != managed_file_filter) { + continue; + } + + auto *stale_file = new KAutoSaveFile(managed_file); + stale_file->setFileName(file); + stale_file->m_managed_file_name_changed = false; + stale_files << stale_file; + } + + return stale_files; +} + +QList KAutoSaveFile::allStaleFiles(const QString &applicationName) +{ + return staleFiles({}, applicationName); +} diff --git a/sources/ui/nokde/kautosavefile.h b/sources/ui/nokde/kautosavefile.h new file mode 100644 index 000000000..0389b730c --- /dev/null +++ b/sources/ui/nokde/kautosavefile.h @@ -0,0 +1,61 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#ifndef QET_KAUTOSAVEFILE_H +#define QET_KAUTOSAVEFILE_H + +#include +#include +#include + +#include + +class QLockFile; + +/** + Small Qt-only replacement for the KAutoSaveFile API used by QET. + + It stores crash-recovery files below the application data autosave folder + and protects each recovery file with QLockFile. The original managed file + path is kept in a sidecar file so allStaleFiles() can present recoverable + projects on the next startup. +*/ +class KAutoSaveFile : public QFile +{ + public: + explicit KAutoSaveFile(const QUrl &filename, QObject *parent = nullptr); + explicit KAutoSaveFile(QObject *parent = nullptr); + ~KAutoSaveFile() override; + + QUrl managedFile() const; + void setManagedFile(const QUrl &filename); + virtual void releaseLock(); + bool open(OpenMode openmode) override; + + static QList staleFiles( + const QUrl &url, + const QString &applicationName = QString()); + static QList allStaleFiles( + const QString &applicationName = QString()); + + private: + QUrl m_managed_file; + std::unique_ptr m_lock; + bool m_managed_file_name_changed = false; +}; + +#endif // QET_KAUTOSAVEFILE_H From 7a97e873d98d07686160c91698ddb2f9767a8d29 Mon Sep 17 00:00:00 2001 From: Gerhard Schwanzer Date: Sun, 5 Jul 2026 15:52:25 +0200 Subject: [PATCH 3/4] Test Qt-only autosave recovery fallback Add a no-KF5 Catch regression test that leaves a KAutoSaveFile-compatible backup behind from a child process, then verifies stale-file discovery, stale-lock recovery, reading, and cleanup. Assisted-by: pi coding agent / Mika (OpenAI GPT-5.5) --- tests/catch/CMakeLists.txt | 14 +++++ tests/catch/src/kautosavefile_test.cpp | 81 ++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 tests/catch/src/kautosavefile_test.cpp diff --git a/tests/catch/CMakeLists.txt b/tests/catch/CMakeLists.txt index 5d39537fe..fb4ead5a3 100644 --- a/tests/catch/CMakeLists.txt +++ b/tests/catch/CMakeLists.txt @@ -83,6 +83,20 @@ add_executable( ${QET_DIR}/sources/borderproperties.h ) +if(NOT BUILD_WITH_KF5) + target_sources( + ${PROJECT_NAME} + PRIVATE + src/kautosavefile_test.cpp + ${QET_DIR}/sources/ui/nokde/kautosavefile.cpp + ) + target_include_directories( + ${PROJECT_NAME} + PRIVATE + ${QET_DIR}/sources/ui/nokde + ) +endif() + target_link_libraries( ${PROJECT_NAME} PUBLIC diff --git a/tests/catch/src/kautosavefile_test.cpp b/tests/catch/src/kautosavefile_test.cpp new file mode 100644 index 000000000..c2690bb9c --- /dev/null +++ b/tests/catch/src/kautosavefile_test.cpp @@ -0,0 +1,81 @@ +#include "../../../sources/ui/nokde/kautosavefile.h" + +#include + +#include +#include +#include +#include +#include + +#include + +#ifdef Q_OS_UNIX +#include +#include +#include +#endif + +TEST_CASE("Qt-only KAutoSaveFile recovers stale files", "[nokde][autosave]") +{ +#ifndef Q_OS_UNIX + SUCCEED("crash-style stale lock test is Unix-only"); +#else + QTemporaryDir data_home; + REQUIRE(data_home.isValid()); + + qputenv("XDG_DATA_HOME", QFile::encodeName(data_home.path())); + QCoreApplication::setOrganizationName(QStringLiteral("QElectroTech")); + QCoreApplication::setApplicationName(QStringLiteral("KAutoSaveFileTest")); + + const auto managed_path = data_home.filePath(QStringLiteral("project.qet")); + QFile managed_file(managed_path); + REQUIRE(managed_file.open(QIODevice::WriteOnly | QIODevice::Text)); + REQUIRE(managed_file.write("\n") > 0); + managed_file.close(); + + const QByteArray payload("\n"); + const auto child_pid = fork(); + REQUIRE(child_pid >= 0); + + if (child_pid == 0) { + KAutoSaveFile backup(QUrl::fromLocalFile(managed_path)); + if (!backup.open(QIODevice::WriteOnly + | QIODevice::Truncate + | QIODevice::Text)) { + _exit(2); + } + if (backup.write(payload) != payload.size()) { + _exit(3); + } + if (!backup.flush()) { + _exit(4); + } + + _exit(0); + } + + int status = 0; + REQUIRE(waitpid(child_pid, &status, 0) == child_pid); + REQUIRE(WIFEXITED(status)); + REQUIRE(WEXITSTATUS(status) == 0); + + auto stale_files = KAutoSaveFile::allStaleFiles(); + REQUIRE(stale_files.size() == 1); + + std::unique_ptr stale_file(stale_files.takeFirst()); + CHECK(stale_file->managedFile().path() + == QFileInfo(managed_path).absoluteFilePath()); + REQUIRE(stale_file->open(QIODevice::ReadOnly | QIODevice::Text)); + CHECK(stale_file->readAll() == payload); + + const auto autosave_file_name = stale_file->fileName(); + const auto metadata_file_name = autosave_file_name + QStringLiteral(".path"); + const auto lock_file_name = autosave_file_name + QStringLiteral(".lock"); + stale_file.reset(); + + CHECK_FALSE(QFile::exists(autosave_file_name)); + CHECK_FALSE(QFile::exists(metadata_file_name)); + CHECK_FALSE(QFile::exists(lock_file_name)); +#endif +} From dcec0bf7ffd0970702646645d0de7429985fa271 Mon Sep 17 00:00:00 2001 From: Gerhard Schwanzer Date: Sun, 5 Jul 2026 17:39:38 +0200 Subject: [PATCH 4/4] Skip actively locked autosave files Check the QLockFile in staleFiles() before returning a no-KF5 recovery candidate, matching the KAutoSaveFile contract that actively owned autosave files are not stale. Extend the no-KF5 Catch test so a child process keeps the autosave lock alive while allStaleFiles() runs, then verify recovery after the child is killed. Assisted-by: pi coding agent / Mika (OpenAI GPT-5.5) --- sources/ui/nokde/kautosavefile.cpp | 23 ++++++++++++++++-- tests/catch/src/kautosavefile_test.cpp | 33 +++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/sources/ui/nokde/kautosavefile.cpp b/sources/ui/nokde/kautosavefile.cpp index 1e896e38f..d9a38450f 100644 --- a/sources/ui/nokde/kautosavefile.cpp +++ b/sources/ui/nokde/kautosavefile.cpp @@ -48,6 +48,11 @@ QString metadataFileName(const QString &autosave_file_name) return autosave_file_name + QStringLiteral(".path"); } +QString lockFileName(const QString &autosave_file_name) +{ + return autosave_file_name + QStringLiteral(".lock"); +} + QUrl normalizeManagedFile(const QUrl &url) { if (url.isEmpty()) { @@ -165,6 +170,18 @@ QStringList findAllStaleFiles(const QString &application_name) return files; } +bool autosaveFileIsRecoverable(const QString &autosave_file_name) +{ + QLockFile lock(lockFileName(autosave_file_name)); + lock.setStaleLockTime(60 * 1000); + if (!lock.tryLock()) { + return false; + } + + lock.unlock(); + return true; +} + } // namespace KAutoSaveFile::KAutoSaveFile(const QUrl &filename, QObject *parent) : @@ -239,8 +256,7 @@ bool KAutoSaveFile::open(OpenMode openmode) } if (!m_lock) { - m_lock = std::make_unique( - fileName() + QStringLiteral(".lock")); + m_lock = std::make_unique(lockFileName(fileName())); m_lock->setStaleLockTime(60 * 1000); } @@ -268,6 +284,9 @@ QList KAutoSaveFile::staleFiles( && managed_file != managed_file_filter) { continue; } + if (!autosaveFileIsRecoverable(file)) { + continue; + } auto *stale_file = new KAutoSaveFile(managed_file); stale_file->setFileName(file); diff --git a/tests/catch/src/kautosavefile_test.cpp b/tests/catch/src/kautosavefile_test.cpp index c2690bb9c..64258c117 100644 --- a/tests/catch/src/kautosavefile_test.cpp +++ b/tests/catch/src/kautosavefile_test.cpp @@ -11,6 +11,7 @@ #include #ifdef Q_OS_UNIX +#include #include #include #include @@ -34,11 +35,16 @@ TEST_CASE("Qt-only KAutoSaveFile recovers stale files", "[nokde][autosave]") REQUIRE(managed_file.write("\n") > 0); managed_file.close(); + int ready_pipe[2] = {-1, -1}; + REQUIRE(pipe(ready_pipe) == 0); + const QByteArray payload("\n"); const auto child_pid = fork(); REQUIRE(child_pid >= 0); if (child_pid == 0) { + close(ready_pipe[0]); + KAutoSaveFile backup(QUrl::fromLocalFile(managed_path)); if (!backup.open(QIODevice::WriteOnly | QIODevice::Truncate @@ -52,13 +58,34 @@ TEST_CASE("Qt-only KAutoSaveFile recovers stale files", "[nokde][autosave]") _exit(4); } - _exit(0); + const char ready = '1'; + if (write(ready_pipe[1], &ready, 1) != 1) { + _exit(5); + } + close(ready_pipe[1]); + + for (;;) { + pause(); + } } + close(ready_pipe[1]); + char ready = 0; + REQUIRE(read(ready_pipe[0], &ready, 1) == 1); + close(ready_pipe[0]); + REQUIRE(ready == '1'); + + auto active_files = KAutoSaveFile::allStaleFiles(); + CHECK(active_files.isEmpty()); + for (auto *file : active_files) { + delete file; + } + + REQUIRE(kill(child_pid, SIGKILL) == 0); int status = 0; REQUIRE(waitpid(child_pid, &status, 0) == child_pid); - REQUIRE(WIFEXITED(status)); - REQUIRE(WEXITSTATUS(status) == 0); + REQUIRE(WIFSIGNALED(status)); + REQUIRE(WTERMSIG(status) == SIGKILL); auto stale_files = KAutoSaveFile::allStaleFiles(); REQUIRE(stale_files.size() == 1);