Debounce settings writes and save them on SIGTERM

- DebouncedStore, taken out of WindowSettings, now also saves the opacity
  slider once a drag ends instead of on every step
- SIGTERM/SIGINT (how KWin stops the input method) quit normally, so
  pending geometry and settings are written
- The slider's minimum opacity comes from Settings instead of a copy in QML

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 10:13:56 +02:00
co-authored by Claude Opus 5.5
parent cb7cf1e04d
commit 990d62ffcf
11 changed files with 127 additions and 31 deletions
+1 -1
View File
@@ -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
}
+2
View File
@@ -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
+44
View File
@@ -0,0 +1,44 @@
#include "QuitOnSignal.h"
#include <QCoreApplication>
#include <QSocketNotifier>
#include <csignal>
#include <sys/signalfd.h>
#include <unistd.h>
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();
});
}
+8
View File
@@ -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();
+3
View File
@@ -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"));
+37
View File
@@ -0,0 +1,37 @@
#include "DebouncedStore.h"
#include <QSettings>
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();
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <QObject>
#include <QTimer>
#include <QVariantMap>
// 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
};
+1 -1
View File
@@ -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();
}
+4
View File
@@ -1,5 +1,6 @@
#pragma once
#include "DebouncedStore.h"
#include "input/TypingOptions.h"
#include <QObject>
@@ -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;
+1 -22
View File
@@ -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<QRect> WindowSettings::load(const QString &screen) const
@@ -38,15 +27,5 @@ std::optional<QRect> 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);
}
+3 -7
View File
@@ -1,8 +1,9 @@
#pragma once
#include "settings/DebouncedStore.h"
#include <QObject>
#include <QRect>
#include <QTimer>
#include <optional>
// 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<QRect> 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;
};