diff --git a/qml/SettingsPanel.qml b/qml/SettingsPanel.qml index 2e9a6b3..dc22cd7 100644 --- a/qml/SettingsPanel.qml +++ b/qml/SettingsPanel.qml @@ -65,7 +65,7 @@ Flickable { SliderRow { width: parent.width text: qsTr("Background") - from: 0.1 // Settings::MinBackgroundOpacity + from: panel.settings.minBackgroundOpacity value: panel.settings.backgroundOpacity onMoved: value => panel.settings.backgroundOpacity = value } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fa9de7b..fc15cf8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -24,6 +24,7 @@ qt_add_library(sloposk_core STATIC layout/LayoutModel.h layout/LayoutModel.cpp window/VisibilityController.h window/VisibilityController.cpp window/WindowSettings.h window/WindowSettings.cpp + settings/DebouncedStore.h settings/DebouncedStore.cpp settings/Settings.h settings/Settings.cpp ) @@ -76,6 +77,7 @@ qt_add_executable(sloposk main.cpp app/DevHooks.h app/DevHooks.cpp app/DBusControl.h app/DBusControl.cpp + app/QuitOnSignal.h app/QuitOnSignal.cpp app/SystemLayouts.h app/SystemLayouts.cpp backend/InputMethodProtocol.h backend/KWinInputMethod.h backend/KWinInputMethod.cpp diff --git a/src/app/QuitOnSignal.cpp b/src/app/QuitOnSignal.cpp new file mode 100644 index 0000000..547437d --- /dev/null +++ b/src/app/QuitOnSignal.cpp @@ -0,0 +1,44 @@ +#include "QuitOnSignal.h" + +#include +#include + +#include +#include +#include + +namespace { + +sigset_t quitSignals() +{ + sigset_t set; + sigemptyset(&set); + sigaddset(&set, SIGTERM); + sigaddset(&set, SIGINT); + return set; +} + +} // namespace + +void blockQuitSignals() +{ + const sigset_t set = quitSignals(); + pthread_sigmask(SIG_BLOCK, &set, nullptr); +} + +void quitOnSignal() +{ + const sigset_t set = quitSignals(); + const int fd = signalfd(-1, &set, SFD_CLOEXEC | SFD_NONBLOCK); + if (fd < 0) { + qWarning("signalfd failed: settings may not be saved on SIGTERM"); + pthread_sigmask(SIG_UNBLOCK, &set, nullptr); + return; + } + auto *notifier = new QSocketNotifier(fd, QSocketNotifier::Read, QCoreApplication::instance()); + QObject::connect(notifier, &QSocketNotifier::activated, notifier, [fd] { + signalfd_siginfo info; + while (read(fd, &info, sizeof info) == sizeof info) { } + QCoreApplication::quit(); + }); +} diff --git a/src/app/QuitOnSignal.h b/src/app/QuitOnSignal.h new file mode 100644 index 0000000..fc262fe --- /dev/null +++ b/src/app/QuitOnSignal.h @@ -0,0 +1,8 @@ +#pragma once + +// KWin stops the input method with SIGTERM. Turns SIGTERM and SIGINT into a +// normal quit, so destructors run and pending settings are written. +// Call before QGuiApplication: threads Qt starts must inherit the blocked mask. +void blockQuitSignals(); +// Call once the application exists. +void quitOnSignal(); diff --git a/src/main.cpp b/src/main.cpp index e05f918..21a6db0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,6 @@ #include "app/DBusControl.h" #include "app/DevHooks.h" +#include "app/QuitOnSignal.h" #include "app/SystemLayouts.h" #include "backend/KWinInputMethod.h" #include "backend/WaylandKeymap.h" @@ -115,7 +116,9 @@ void connectTextContext(KWinInputMethod &backend, KeyboardController &controller int main(int argc, char *argv[]) { + blockQuitSignals(); QGuiApplication app(argc, argv); + quitOnSignal(); QGuiApplication::setApplicationName(QStringLiteral("sloposk")); QGuiApplication::setOrganizationName(QStringLiteral("sloposk")); // ~/.config/sloposk/sloposk.conf QGuiApplication::setDesktopFileName(QStringLiteral("org.sloposk.keyboard")); diff --git a/src/settings/DebouncedStore.cpp b/src/settings/DebouncedStore.cpp new file mode 100644 index 0000000..6a47e78 --- /dev/null +++ b/src/settings/DebouncedStore.cpp @@ -0,0 +1,37 @@ +#include "DebouncedStore.h" + +#include + +namespace { +constexpr int DebounceMs = 500; +} + +DebouncedStore::DebouncedStore(QObject *parent) + : QObject(parent) +{ + m_debounce.setSingleShot(true); + m_debounce.setInterval(DebounceMs); + connect(&m_debounce, &QTimer::timeout, this, &DebouncedStore::flush); +} + +DebouncedStore::~DebouncedStore() +{ + flush(); +} + +void DebouncedStore::setValue(const QString &key, const QVariant &value) +{ + m_pending.insert(key, value); + m_debounce.start(); +} + +void DebouncedStore::flush() +{ + m_debounce.stop(); + if (m_pending.isEmpty()) + return; + QSettings store; + for (auto it = m_pending.cbegin(); it != m_pending.cend(); ++it) + store.setValue(it.key(), it.value()); + m_pending.clear(); +} diff --git a/src/settings/DebouncedStore.h b/src/settings/DebouncedStore.h new file mode 100644 index 0000000..d0b4a37 --- /dev/null +++ b/src/settings/DebouncedStore.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +// QSettings writes that arrive in bursts (dragging a slider or the window), +// saved once the burst ends instead of on every step. Flushes on destruction. +class DebouncedStore : public QObject +{ + Q_OBJECT + +public: + explicit DebouncedStore(QObject *parent = nullptr); + ~DebouncedStore() override; + + void setValue(const QString &key, const QVariant &value); + void flush(); + +private: + QTimer m_debounce; + QVariantMap m_pending; // key → value +}; diff --git a/src/settings/Settings.cpp b/src/settings/Settings.cpp index 50d63e1..5809e86 100644 --- a/src/settings/Settings.cpp +++ b/src/settings/Settings.cpp @@ -38,7 +38,7 @@ void Settings::setBackgroundOpacity(qreal opacity) if (qFuzzyCompare(opacity, m_backgroundOpacity)) return; m_backgroundOpacity = opacity; - QSettings().setValue(BackgroundOpacityKey, opacity); + m_sliderStore.setValue(QString::fromLatin1(BackgroundOpacityKey), opacity); Q_EMIT appearanceChanged(); } diff --git a/src/settings/Settings.h b/src/settings/Settings.h index 906d926..357d8ee 100644 --- a/src/settings/Settings.h +++ b/src/settings/Settings.h @@ -1,5 +1,6 @@ #pragma once +#include "DebouncedStore.h" #include "input/TypingOptions.h" #include @@ -24,6 +25,7 @@ class Settings : public QObject Q_PROPERTY(bool alwaysVisible READ alwaysVisible WRITE setAlwaysVisible NOTIFY alwaysVisibleChanged) // Opacity of the panel behind the keys (MinBackgroundOpacity–1); keys stay opaque. Q_PROPERTY(qreal backgroundOpacity READ backgroundOpacity WRITE setBackgroundOpacity NOTIFY appearanceChanged) + Q_PROPERTY(qreal minBackgroundOpacity READ minBackgroundOpacity CONSTANT) // Blurs what shows through a translucent background (needs compositor support, e.g. KWin). Q_PROPERTY(bool blurBackground READ blurBackground WRITE setBlurBackground NOTIFY appearanceChanged) @@ -49,6 +51,7 @@ public: void setAlwaysVisible(bool always); static constexpr qreal MinBackgroundOpacity = 0.1; + static qreal minBackgroundOpacity() { return MinBackgroundOpacity; } qreal backgroundOpacity() const { return m_backgroundOpacity; } void setBackgroundOpacity(qreal opacity); bool blurBackground() const { return m_blurBackground; } @@ -64,6 +67,7 @@ Q_SIGNALS: private: void setTypingOptions(const TypingOptions &options); + DebouncedStore m_sliderStore; // slider drags change values many times a second TypingOptions m_typing; QStringList m_visibleSections; bool m_fullKeyboard = false; diff --git a/src/window/WindowSettings.cpp b/src/window/WindowSettings.cpp index 867b5a3..02a52bc 100644 --- a/src/window/WindowSettings.cpp +++ b/src/window/WindowSettings.cpp @@ -4,8 +4,6 @@ namespace { -constexpr int DebounceMs = 500; - QString key(const QString &screen) { // Screen names may contain '/', which QSettings treats as a group separator. @@ -17,15 +15,6 @@ QString key(const QString &screen) WindowSettings::WindowSettings(QObject *parent) : QObject(parent) { - m_debounce.setSingleShot(true); - m_debounce.setInterval(DebounceMs); - connect(&m_debounce, &QTimer::timeout, this, &WindowSettings::flush); -} - -WindowSettings::~WindowSettings() -{ - if (m_debounce.isActive()) - flush(); } std::optional WindowSettings::load(const QString &screen) const @@ -38,15 +27,5 @@ std::optional WindowSettings::load(const QString &screen) const void WindowSettings::save(const QString &screen, const QRect &geometry) { - if (m_debounce.isActive() && screen != m_pendingScreen) - flush(); - m_pendingScreen = screen; - m_pendingGeometry = geometry; - m_debounce.start(); -} - -void WindowSettings::flush() -{ - m_debounce.stop(); - QSettings().setValue(key(m_pendingScreen), m_pendingGeometry); + m_store.setValue(key(screen), geometry); } diff --git a/src/window/WindowSettings.h b/src/window/WindowSettings.h index 29987e0..a26a8b3 100644 --- a/src/window/WindowSettings.h +++ b/src/window/WindowSettings.h @@ -1,8 +1,9 @@ #pragma once +#include "settings/DebouncedStore.h" + #include #include -#include #include // Remembers the keyboard's geometry per screen (~/.config/sloposk.conf). @@ -13,15 +14,10 @@ class WindowSettings : public QObject public: explicit WindowSettings(QObject *parent = nullptr); - ~WindowSettings() override; std::optional load(const QString &screen) const; void save(const QString &screen, const QRect &geometry); private: - void flush(); - - QTimer m_debounce; - QString m_pendingScreen; - QRect m_pendingGeometry; + DebouncedStore m_store; };