diff options
-rw-r--r-- | autotests/CMakeLists.txt | 5 | ||||
-rw-r--r-- | autotests/kwindowstatesavertest.cpp | 90 | ||||
-rw-r--r-- | src/gui/CMakeLists.txt | 2 | ||||
-rw-r--r-- | src/gui/kwindowstatesaver.cpp | 136 | ||||
-rw-r--r-- | src/gui/kwindowstatesaver.h | 144 |
5 files changed, 377 insertions, 0 deletions
diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 7b5a73b1..458f8f97 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -82,3 +82,8 @@ if (NOT CMAKE_CROSSCOMPILING) add_subdirectory(kconfig_compiler) endif() endif() + +find_package(Qt${QT_MAJOR_VERSION} OPTIONAL_COMPONENTS Widgets) +if (TARGET Qt${QT_MAJOR_VERSION}::Widgets) + ecm_add_test(kwindowstatesavertest.cpp LINK_LIBRARIES KF5::ConfigGui Qt${QT_MAJOR_VERSION}::Test Qt${QT_MAJOR_VERSION}::Widgets) +endif() diff --git a/autotests/kwindowstatesavertest.cpp b/autotests/kwindowstatesavertest.cpp new file mode 100644 index 00000000..a8b67218 --- /dev/null +++ b/autotests/kwindowstatesavertest.cpp @@ -0,0 +1,90 @@ +/* + SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org> + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "kwindowstatesaver.h" +#include "kconfiggroup.h" +#include "ksharedconfig.h" + +#include <QSignalSpy> +#include <QStandardPaths> +#include <QTest> + +#include <QFontDialog> + +class KWindowStateSaverTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testTopLevelDialog(); + void testSubDialog(); +}; + +void KWindowStateSaverTest::initTestCase() +{ + QStandardPaths::setTestModeEnabled(true); +} + +void KWindowStateSaverTest::testTopLevelDialog() +{ + auto cfg = KSharedConfig::openStateConfig(); + cfg->deleteGroup("topLevelDialogTest"); + QSize dlgSize(720, 720); + + { + QFontDialog dlg; + new KWindowStateSaver(&dlg, "topLevelDialogTest"); + dlg.show(); + QTest::qWait(10); // give the window time to show up, so we simulate a user-triggered resize + dlg.resize(dlgSize); + QTest::qWait(500); // give the state saver time to trigger + QCOMPARE(dlg.size(), dlgSize); + } + + QVERIFY(cfg->hasGroup("topLevelDialogTest")); + + { + QFontDialog dlg; + new KWindowStateSaver(&dlg, "topLevelDialogTest"); + dlg.show(); + QTest::qWait(100); // give the window time to show up properly + QCOMPARE(dlg.size(), dlgSize); + } +} + +void KWindowStateSaverTest::testSubDialog() +{ + QWidget mainWindow; + mainWindow.show(); + QTest::qWait(10); + + auto cfg = KSharedConfig::openStateConfig(); + cfg->deleteGroup("subDialogTest"); + QSize dlgSize(700, 500); + + { + auto dlg = new QFontDialog(&mainWindow); + new KWindowStateSaver(dlg, "subDialogTest"); + dlg->show(); + QTest::qWait(10); // give the window time to show up, so we simulate a user-triggered resize + dlg->resize(dlgSize); + QTest::qWait(500); // give the state saver time to trigger + QCOMPARE(dlg->size(), dlgSize); + delete dlg; + } + + QVERIFY(cfg->hasGroup("subDialogTest")); + + { + auto dlg = new QFontDialog(&mainWindow); + new KWindowStateSaver(dlg, "subDialogTest"); + dlg->show(); + QTest::qWait(100); // give the window time to show up properly + QCOMPARE(dlg->size(), dlgSize); + } +} + +QTEST_MAIN(KWindowStateSaverTest) +#include "kwindowstatesavertest.moc" diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index c7474893..e717c1c1 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -11,6 +11,7 @@ target_sources(KF5ConfigGui PRIVATE kstandardshortcut.cpp kstandardshortcutwatcher.cpp kwindowconfig.cpp + kwindowstatesaver.cpp ) ecm_qt_declare_logging_category(KF5ConfigGui @@ -48,6 +49,7 @@ ecm_generate_headers(KConfigGui_HEADERS KStandardShortcut KStandardShortcutWatcher KWindowConfig + KWindowStateSaver REQUIRED_HEADERS KConfigGui_HEADERS ) diff --git a/src/gui/kwindowstatesaver.cpp b/src/gui/kwindowstatesaver.cpp new file mode 100644 index 00000000..d3eaa99e --- /dev/null +++ b/src/gui/kwindowstatesaver.cpp @@ -0,0 +1,136 @@ +/* + SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org> + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "kwindowstatesaver.h" +#include "ksharedconfig.h" +#include "kwindowconfig.h" + +#include <QWindow> + +class KWindowStateSaverPrivate +{ +public: + QWindow *window = nullptr; + KConfigGroup configGroup; + std::function<QWindow *()> windowHandleCallback; + int timerId = 0; + + void init(KWindowStateSaver *q); + void initWidget(QObject *widget, KWindowStateSaver *q); +}; + +void KWindowStateSaverPrivate::init(KWindowStateSaver *q) +{ + if (!window) { + return; + } + + KWindowConfig::restoreWindowSize(window, configGroup); + KWindowConfig::restoreWindowPosition(window, configGroup); + + const auto deferredSave = [q, this]() { + if (!timerId) { + timerId = q->startTimer(250); + } + }; + QObject::connect(window, &QWindow::widthChanged, q, deferredSave); + QObject::connect(window, &QWindow::heightChanged, q, deferredSave); + QObject::connect(window, &QWindow::xChanged, q, deferredSave); + QObject::connect(window, &QWindow::yChanged, q, deferredSave); +} + +void KWindowStateSaverPrivate::initWidget(QObject *widget, KWindowStateSaver *q) +{ + if (!window && windowHandleCallback) { + window = windowHandleCallback(); + } + if (window) { + init(q); + } else { + widget->installEventFilter(q); + } +} + +KWindowStateSaver::KWindowStateSaver(QWindow *window, const KConfigGroup &configGroup) + : QObject(window) + , d(new KWindowStateSaverPrivate) +{ + Q_ASSERT(window); + d->window = window; + d->configGroup = configGroup; + d->init(this); +} + +KWindowStateSaver::KWindowStateSaver(QWindow *window, const QString &configGroupName) + : QObject(window) + , d(new KWindowStateSaverPrivate) +{ + Q_ASSERT(window); + d->window = window; + d->configGroup = KConfigGroup(KSharedConfig::openStateConfig(), configGroupName); + d->init(this); +} + +KWindowStateSaver::KWindowStateSaver(QWindow *window, const char *configGroupName) + : QObject(window) + , d(new KWindowStateSaverPrivate) +{ + Q_ASSERT(window); + d->window = window; + d->configGroup = KConfigGroup(KSharedConfig::openStateConfig(), configGroupName); + d->init(this); +} + +KWindowStateSaver::~KWindowStateSaver() +{ + delete d; +} + +void KWindowStateSaver::timerEvent(QTimerEvent *event) +{ + killTimer(event->timerId()); + KWindowConfig::saveWindowPosition(d->window, d->configGroup); + KWindowConfig::saveWindowSize(d->window, d->configGroup); + d->timerId = 0; +} + +bool KWindowStateSaver::eventFilter(QObject *watched, QEvent *event) +{ + // QEvent::PlatformSurface would give us a valid window, but if there are + // intial resizings (explicitly or via layout constraints) those would then + // already overwrite our restored values. So wait until all that is done + // and only restore afterwards. + if (event->type() == QEvent::ShowToParent && !d->window) { + watched->removeEventFilter(this); + d->window = d->windowHandleCallback(); + d->init(this); + } + + return QObject::eventFilter(watched, event); +} + +void KWindowStateSaver::initWidget(QObject *widget, const std::function<QWindow *()> &windowHandleCallback, const KConfigGroup &configGroup) +{ + d = new KWindowStateSaverPrivate; + d->windowHandleCallback = windowHandleCallback; + d->configGroup = configGroup; + d->initWidget(widget, this); +} + +void KWindowStateSaver::initWidget(QObject *widget, const std::function<QWindow *()> &windowHandleCallback, const QString &configGroupName) +{ + d = new KWindowStateSaverPrivate; + d->windowHandleCallback = windowHandleCallback; + d->configGroup = KConfigGroup(KSharedConfig::openStateConfig(), configGroupName); + d->initWidget(widget, this); +} + +void KWindowStateSaver::initWidget(QObject *widget, const std::function<QWindow *()> &windowHandleCallback, const char *configGroupName) +{ + d = new KWindowStateSaverPrivate; + d->windowHandleCallback = windowHandleCallback; + d->configGroup = KConfigGroup(KSharedConfig::openStateConfig(), configGroupName); + d->initWidget(widget, this); +} diff --git a/src/gui/kwindowstatesaver.h b/src/gui/kwindowstatesaver.h new file mode 100644 index 00000000..47b6ae95 --- /dev/null +++ b/src/gui/kwindowstatesaver.h @@ -0,0 +1,144 @@ +/* + SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org> + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KWINDOWSTATESAVER_H +#define KWINDOWSTATESAVER_H + +#include <kconfiggroup.h> +#include <kconfiggui_export.h> + +#include <QObject> + +class QWindow; +class KWindowStateSaverPrivate; + +/** + * Saves and restores a window size and (when possible) position. + * + * This is useful for retrofitting persisting window geometry on existing windows or dialogs, + * without having to modify those classes themselves, or having to inherit from them. + * For this, create a new instance of KWindowStateSaver for every window that should have it's + * state persisted, and pass it the window or widget as well as the config group the state + * should be stored in. The KWindowStateSaver will restore an existing state and then monitor + * the window for subsequent changes to persist. It will delete itself once the window is + * deleted. + * + * @code + * QPrintPreviewDialog dlg = ... + * new KWindowStateSaver(&dlg, "printPreviewDialogState"); + * ... + * dlg.exec(); + * @endcode + * + * Note that freshly created top-level QWidgets (such as the dialog in the above example) + * do not have an associated QWindow yet (ie. windowHandle() return @c nullptr). KWindowStateSaver + * supports this with its QWidget constructors which will monitor the widget for having + * its associated QWindow created before continuing with that. + * + * When implementing your own windows/dialogs, using KWindowConfig directly can be an + * alternative. + * + * @see KWindowConfig + * @since 5.92 + */ +class KCONFIGGUI_EXPORT KWindowStateSaver : public QObject +{ + Q_OBJECT +public: + /** + * Create a new window state saver for @p window. + * @param configGroup A KConfigGroup that holds the window state. + */ + explicit KWindowStateSaver(QWindow *window, const KConfigGroup &configGroup); + /** + * Create a new window state saver for @p window. + * @param configGroupName The name of a KConfigGroup in the default state + * configuration (see KSharedConfig::openStateConfig) that holds the window state. + */ + explicit KWindowStateSaver(QWindow *window, const QString &configGroupName); + /** + * Create a new window state saver for @p window. + * @param configGroupName The name of a KConfigGroup in the default state + * configuration (see KSharedConfig::openStateConfig) that holds the window state. + */ + explicit KWindowStateSaver(QWindow *window, const char *configGroupName); + + /** + * Create a new window state saver for @p widget. + * Use this for widgets that aren't shown yet and would still return @c nullptr from windowHandle(). + * @param configGroup A KConfigGroup that holds the window state. + */ + template<typename Widget> + explicit inline KWindowStateSaver(Widget *widget, const KConfigGroup &configGroup); + /** + * Create a new window state saver for @p widget. + * Use this for widgets that aren't shown yet and would still return @c nullptr from windowHandle(). + * @param configGroupName The name of a KConfigGroup in the default state + * configuration (see KSharedConfig::openStateConfig) that holds the window state. + */ + template<typename Widget> + explicit inline KWindowStateSaver(Widget *widget, const QString &configGroupName); + /** + * Create a new window state saver for @p widget. + * Use this for widgets that aren't shown yet and would still return @c nullptr from windowHandle(). + * @param configGroupName The name of a KConfigGroup in the default state + * configuration (see KSharedConfig::openStateConfig) that holds the window state. + */ + template<typename Widget> + explicit inline KWindowStateSaver(Widget *widget, const char *configGroupName); + + ~KWindowStateSaver(); + +private: + void timerEvent(QTimerEvent *event) override; + bool eventFilter(QObject *watched, QEvent *event) override; + + // API used by template code, so technically part of the ABI + void initWidget(QObject *widget, const std::function<QWindow *()> &windowHandleCallback, const KConfigGroup &configGroup); + void initWidget(QObject *widget, const std::function<QWindow *()> &windowHandleCallback, const QString &configGroupName); + void initWidget(QObject *widget, const std::function<QWindow *()> &windowHandleCallback, const char *configGroupName); + + // cannot use std::unique_ptr due to the template ctors + // not seeing the full private class + KWindowStateSaverPrivate *d = nullptr; +}; + +template<typename Widget> +KWindowStateSaver::KWindowStateSaver(Widget *widget, const KConfigGroup &configGroup) + : QObject(widget) +{ + initWidget( + widget, + [widget]() { + return widget->windowHandle(); + }, + configGroup); +} + +template<typename Widget> +KWindowStateSaver::KWindowStateSaver(Widget *widget, const QString &configGroupName) + : QObject(widget) +{ + initWidget( + widget, + [widget]() { + return widget->windowHandle(); + }, + configGroupName); +} + +template<typename Widget> +KWindowStateSaver::KWindowStateSaver(Widget *widget, const char *configGroupName) + : QObject(widget) +{ + initWidget( + widget, + [widget]() { + return widget->windowHandle(); + }, + configGroupName); +} + +#endif // KWINDOWSTATESAVER_H |