Report unavailable protocols and services; send combos as real keys

Diagnostics collects what this session doesn't allow, logs it, and lists
it in the settings panel (with a dot on the gear): KWin not launching
sloposk as its keyboard (detected by WAYLAND_SOCKET, since KWin offers the
input-method global to every client), fake input missing or too old,
org.kde.keyboard unreachable, and the D-Bus control name taken.

Modifier combos go through fake input even in text fields: KWin drops
the input method's modifier mask for most clients, so Ctrl+A typed "a".

The desktop file is generated with an absolute Exec, which KWin needs to
match it: one for the installed binary, one for the build's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 11:36:56 +02:00
co-authored by Claude Opus 5.5
parent 6164839886
commit 499e5ee76e
21 changed files with 362 additions and 104 deletions
+4 -1
View File
@@ -26,14 +26,17 @@ Needs Qt ≥ 6.8 (Quick, WaylandClient, DBus, Test), KF6 WindowSystem, KF6 Confi
## Run ## Run
KWin only offers the input-method protocol to the process it launches itself. KWin offers the input-method protocol and fake input (held keys, shortcuts, typing outside text fields) to the keyboard it launches itself, so launch sloposk through KWin.
| Where | How | | Where | How |
|---|---| |---|---|
| Nested session (safe) | `kwin_wayland --no-lockscreen --socket sloposk-dev --width 1400 --height 900 --inputmethod $PWD/build/src/sloposk kwrite` | | Nested session (safe) | `kwin_wayland --no-lockscreen --socket sloposk-dev --width 1400 --height 900 --inputmethod $PWD/build/src/sloposk kwrite` |
| Desktop | `cmake --install build --prefix ~/.local`, then System Settings → Keyboard → Virtual Keyboard → **sloposk** | | Desktop | `cmake --install build --prefix ~/.local`, then System Settings → Keyboard → Virtual Keyboard → **sloposk** |
| Desktop, from the build | `ln -s $PWD/build/src/org.sloposk.keyboard.desktop ~/.local/share/applications/`, then pick **sloposk** as above |
| UI only | `build/src/sloposk` (stays visible, types nowhere) | | UI only | `build/src/sloposk` (stays visible, types nowhere) |
Started any other way (e.g. `kioclient exec`), KWin grants neither protocol; the ⚙ panel lists what's missing.
Show/hide, e.g. from a global shortcut: `qdbus org.sloposk.Keyboard /Keyboard org.sloposk.Keyboard.Toggle` Show/hide, e.g. from a global shortcut: `qdbus org.sloposk.Keyboard /Keyboard org.sloposk.Keyboard.Toggle`
Dev hooks: `SLOPOSK_AUTOTYPE=text` commits on focus; `SLOPOSK_SCREENSHOT=file.png` saves the window and quits. Dev hooks: `SLOPOSK_AUTOTYPE=text` commits on focus; `SLOPOSK_SCREENSHOT=file.png` saves the window and quits.
@@ -1,10 +1,11 @@
[Desktop Entry] [Desktop Entry]
Name=sloposk Name=sloposk
Comment=iPad-style floating on-screen keyboard Comment=iPad-style floating on-screen keyboard
Exec=sloposk Exec=@SLOPOSK_EXEC@
Type=Application Type=Application
Icon=input-keyboard-virtual Icon=input-keyboard-virtual
NoDisplay=true NoDisplay=true
X-KDE-Wayland-VirtualKeyboard=true X-KDE-Wayland-VirtualKeyboard=true
# Key events for windows without a text field (fake input is restricted). # Key events for windows without a text field and held keys (fake input is
# restricted). KWin matches Exec by absolute path, so it must not be bare.
X-KDE-Wayland-Interfaces=org_kde_kwin_fake_input X-KDE-Wayland-Interfaces=org_kde_kwin_fake_input
+3
View File
@@ -7,6 +7,7 @@ Window {
required property LayoutModel layoutModel required property LayoutModel layoutModel
required property KeyboardController controller required property KeyboardController controller
required property Settings settings required property Settings settings
required property Diagnostics diagnostics
property var placement: null // FloatingWindow, set from C++ property var placement: null // FloatingWindow, set from C++
property bool settingsOpen: false property bool settingsOpen: false
// Height of the key area, for C++ keeping key sizes when rows change. // Height of the key area, for C++ keeping key sizes when rows change.
@@ -58,6 +59,7 @@ Window {
visible: root.settingsOpen visible: root.settingsOpen
settings: root.settings settings: root.settings
layoutModel: root.layoutModel layoutModel: root.layoutModel
diagnostics: root.diagnostics
} }
DragHandle { DragHandle {
@@ -69,6 +71,7 @@ Window {
SettingsButton { SettingsButton {
anchors { left: parent.left; leftMargin: 8; bottom: parent.bottom } anchors { left: parent.left; leftMargin: 8; bottom: parent.bottom }
checked: root.settingsOpen checked: root.settingsOpen
alert: root.diagnostics.problems.length > 0
onClicked: root.settingsOpen = !root.settingsOpen onClicked: root.settingsOpen = !root.settingsOpen
} }
+26
View File
@@ -0,0 +1,26 @@
import QtQuick
// What doesn't work in this session and why (see Diagnostics); hidden when all is well.
Column {
id: list
property list<string> problems
visible: problems.length > 0
spacing: 4
SectionHeader { text: qsTr("Not Available"); color: Theme.warning }
Repeater {
model: list.problems
Text {
required property string modelData
width: list.width
text: "⚠ " + modelData
color: Theme.text
font.pixelSize: 13
wrapMode: Text.Wrap
}
}
}
+12
View File
@@ -5,6 +5,8 @@ Item {
id: button id: button
property bool checked: false property bool checked: false
// Something doesn't work (see Diagnostics): a dot invites a look in settings.
property bool alert: false
signal clicked() signal clicked()
implicitWidth: 36 implicitWidth: 36
@@ -17,6 +19,16 @@ Item {
color: tap.pressed ? Theme.text : Theme.handle color: tap.pressed ? Theme.text : Theme.handle
} }
Rectangle {
visible: button.alert && !button.checked
width: 6
height: 6
radius: 3
color: Theme.warning
x: parent.width / 2 + 6
y: 3
}
TapHandler { TapHandler {
id: tap id: tap
onTapped: button.clicked() onTapped: button.clicked()
+16 -4
View File
@@ -8,18 +8,29 @@ Flickable {
required property Settings settings required property Settings settings
required property LayoutModel layoutModel required property LayoutModel layoutModel
required property Diagnostics diagnostics
clip: true clip: true
contentHeight: columns.height + 2 * columns.y contentHeight: content.height + 2 * content.y
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
Column {
id: content
x: 16
y: 16
width: panel.width - 2 * x
spacing: 16
ProblemList {
width: parent.width
problems: panel.diagnostics.problems
}
Row { Row {
id: columns id: columns
readonly property real columnWidth: (width - spacing) / 2 readonly property real columnWidth: (width - spacing) / 2
x: 16 width: parent.width
y: 16
width: panel.width - 2 * x
spacing: 32 spacing: 32
Column { Column {
@@ -108,3 +119,4 @@ Flickable {
} }
} }
} }
}
+1
View File
@@ -16,6 +16,7 @@ QtObject {
readonly property color accent: dark ? "#0a84ff" : "#007aff" readonly property color accent: dark ? "#0a84ff" : "#007aff"
readonly property color accentText: "#ffffff" readonly property color accentText: "#ffffff"
readonly property color toggleOn: dark ? "#30d158" : "#34c759" readonly property color toggleOn: dark ? "#30d158" : "#34c759"
readonly property color warning: dark ? "#ff9f0a" : "#ff9500"
readonly property real windowRadius: 14 readonly property real windowRadius: 14
readonly property real keyRadius: 6 readonly property real keyRadius: 6
+14 -1
View File
@@ -26,6 +26,7 @@ qt_add_library(sloposk_core STATIC
window/WindowSettings.h window/WindowSettings.cpp window/WindowSettings.h window/WindowSettings.cpp
settings/DebouncedStore.h settings/DebouncedStore.cpp settings/DebouncedStore.h settings/DebouncedStore.cpp
settings/Settings.h settings/Settings.cpp settings/Settings.h settings/Settings.cpp
diagnostics/Diagnostics.h diagnostics/Diagnostics.cpp
) )
# QML lives in the top-level qml/ dir; alias each file so the module sees it at its root. # QML lives in the top-level qml/ dir; alias each file so the module sees it at its root.
@@ -39,6 +40,7 @@ set(SLOPOSK_QML_FILES
SectionHeader.qml SectionHeader.qml
SettingsButton.qml SettingsButton.qml
SettingsPanel.qml SettingsPanel.qml
ProblemList.qml
ToggleRow.qml ToggleRow.qml
SliderRow.qml SliderRow.qml
Theme.qml Theme.qml
@@ -68,6 +70,7 @@ target_include_directories(sloposk_core PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/input ${CMAKE_CURRENT_SOURCE_DIR}/input
${CMAKE_CURRENT_SOURCE_DIR}/layout ${CMAKE_CURRENT_SOURCE_DIR}/layout
${CMAKE_CURRENT_SOURCE_DIR}/settings ${CMAKE_CURRENT_SOURCE_DIR}/settings
${CMAKE_CURRENT_SOURCE_DIR}/diagnostics
) )
target_link_libraries(sloposk_core PUBLIC Qt6::Quick PkgConfig::XKB) target_link_libraries(sloposk_core PUBLIC Qt6::Quick PkgConfig::XKB)
@@ -77,6 +80,7 @@ qt_add_executable(sloposk
main.cpp main.cpp
app/DevHooks.h app/DevHooks.cpp app/DevHooks.h app/DevHooks.cpp
app/DBusControl.h app/DBusControl.cpp app/DBusControl.h app/DBusControl.cpp
app/Checks.h app/Checks.cpp
app/QuitOnSignal.h app/QuitOnSignal.cpp app/QuitOnSignal.h app/QuitOnSignal.cpp
app/SystemLayouts.h app/SystemLayouts.cpp app/SystemLayouts.h app/SystemLayouts.cpp
backend/InputMethodProtocol.h backend/InputMethodProtocol.h
@@ -107,4 +111,13 @@ target_link_libraries(sloposk PRIVATE
) )
install(TARGETS sloposk) install(TARGETS sloposk)
install(FILES ${PROJECT_SOURCE_DIR}/data/org.sloposk.keyboard.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) # KWin grants restricted protocols by matching Exec to the absolute executable
# path. One file runs the installed binary; the other the build's, for development.
set(SLOPOSK_EXEC ${CMAKE_INSTALL_FULL_BINDIR}/sloposk)
configure_file(${PROJECT_SOURCE_DIR}/data/org.sloposk.keyboard.desktop.in
${CMAKE_CURRENT_BINARY_DIR}/install/org.sloposk.keyboard.desktop @ONLY)
set(SLOPOSK_EXEC ${CMAKE_CURRENT_BINARY_DIR}/sloposk)
configure_file(${PROJECT_SOURCE_DIR}/data/org.sloposk.keyboard.desktop.in
${CMAKE_CURRENT_BINARY_DIR}/org.sloposk.keyboard.desktop @ONLY)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/install/org.sloposk.keyboard.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
+62
View File
@@ -0,0 +1,62 @@
#include "Checks.h"
#include "SystemLayouts.h"
#include "backend/KWinInputMethod.h"
#include "diagnostics/Diagnostics.h"
#include <QCoreApplication>
namespace {
QString tr(const char *text)
{
return QCoreApplication::translate("Checks", text);
}
QString fakeInputProblem(const KWinFakeInput &fakeInput)
{
const QString lost = tr("Hold Keys, shortcuts like Ctrl+A and typing outside text fields don't work.");
if (!fakeInput.isActive())
return tr("KWin doesn't let sloposk send key events (it allows the virtual keyboard it launched). ") + lost;
return tr("KWin's fake input is too old (version %1, needs %2). ")
.arg(fakeInput.boundVersion())
.arg(KWinFakeInput::RequiredVersion)
+ lost;
}
} // namespace
void watchInputMethod(Diagnostics &diagnostics, const KWinInputMethod &backend, bool launchedByKWin)
{
// Qt binds globals KWin already announced on construction, so the first
// check is final unless KWin restarts.
const auto checkInputMethod = [&] {
diagnostics.check(QStringLiteral("input-method"), launchedByKWin && backend.isAvailable(),
tr("Not running as KWin's virtual keyboard, so it can't type into text fields: "
"choose sloposk in System Settings → Keyboard → Virtual Keyboard."));
};
const KWinFakeInput &fakeInput = backend.fakeInput();
const auto checkFakeInput = [&] {
diagnostics.check(QStringLiteral("fake-input"), fakeInput.canSendKeysyms(), fakeInputProblem(fakeInput));
};
QObject::connect(&backend, &KWinInputMethod::availableChanged, &diagnostics, checkInputMethod);
QObject::connect(&fakeInput, &QWaylandClientExtension::activeChanged, &diagnostics, checkFakeInput);
checkInputMethod();
checkFakeInput();
}
void watchSystemLayouts(Diagnostics &diagnostics, const SystemLayouts &layouts)
{
QObject::connect(&layouts, &SystemLayouts::serviceChecked, &diagnostics, [&](bool available) {
diagnostics.check(QStringLiteral("system-layouts"), available,
tr("KWin's keyboard layout service (org.kde.keyboard) isn't reachable: "
"the globe key switches only the on-screen layout."));
});
}
void reportDBusControl(Diagnostics &diagnostics, bool registered)
{
diagnostics.check(QStringLiteral("dbus-control"), registered,
tr("Couldn't claim org.sloposk.Keyboard on D-Bus (is another sloposk running?): "
"show/hide commands won't reach this one."));
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
class Diagnostics;
class KWinInputMethod;
class SystemLayouts;
// Reports to `diagnostics` what this session doesn't allow, and keeps the
// reports current as protocols and services come and go.
// KWin offers the input-method global to every client but only activates
// the process it launched, which it hands a connection: `launchedByKWin` is
// whether WAYLAND_SOCKET was set at startup.
void watchInputMethod(Diagnostics &diagnostics, const KWinInputMethod &backend, bool launchedByKWin);
void watchSystemLayouts(Diagnostics &diagnostics, const SystemLayouts &layouts);
void reportDBusControl(Diagnostics &diagnostics, bool registered);
+9 -4
View File
@@ -27,7 +27,7 @@ SystemLayouts::SystemLayouts(QObject *parent)
bus.connect(Service, Path, Interface, QStringLiteral("layoutChanged"), this, SLOT(setCurrent(uint))); bus.connect(Service, Path, Interface, QStringLiteral("layoutChanged"), this, SLOT(setCurrent(uint)));
auto *watcher = new QDBusServiceWatcher(Service, bus, QDBusServiceWatcher::WatchForOwnerChange, this); auto *watcher = new QDBusServiceWatcher(Service, bus, QDBusServiceWatcher::WatchForOwnerChange, this);
connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, &SystemLayouts::fetchCurrent); connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, &SystemLayouts::fetchCurrent);
connect(watcher, &QDBusServiceWatcher::serviceUnregistered, this, [this] { m_system = false; }); connect(watcher, &QDBusServiceWatcher::serviceUnregistered, this, [this] { setSystem(false); });
fetchCurrent(); fetchCurrent();
} }
@@ -38,9 +38,8 @@ void SystemLayouts::fetchCurrent()
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
watcher->deleteLater(); watcher->deleteLater();
const QDBusPendingReply<uint> reply = *watcher; const QDBusPendingReply<uint> reply = *watcher;
if (reply.isError()) setSystem(!reply.isError());
return; if (m_system)
m_system = true;
setCurrent(reply.value()); setCurrent(reply.value());
}); });
} }
@@ -56,6 +55,12 @@ void SystemLayouts::next(quint32 count)
setCurrent(index); setCurrent(index);
} }
void SystemLayouts::setSystem(bool system)
{
m_system = system;
Q_EMIT serviceChecked(system);
}
void SystemLayouts::setCurrent(uint index) void SystemLayouts::setCurrent(uint index)
{ {
if (index == m_current) if (index == m_current)
+3
View File
@@ -19,12 +19,15 @@ public:
Q_SIGNALS: Q_SIGNALS:
void currentChanged(); void currentChanged();
// After each attempt to reach the service, and when it goes away.
void serviceChecked(bool available);
private Q_SLOTS: private Q_SLOTS:
void setCurrent(uint index); void setCurrent(uint index);
private: private:
void fetchCurrent(); void fetchCurrent();
void setSystem(bool system);
bool m_system = false; bool m_system = false;
quint32 m_current = 0; quint32 m_current = 0;
+3 -9
View File
@@ -1,11 +1,9 @@
#include "KWinFakeInput.h" #include "KWinFakeInput.h"
#include <QtLogging>
#include <xkbcommon/xkbcommon.h> #include <xkbcommon/xkbcommon.h>
namespace { namespace {
constexpr uint32_t KeysymVersion = 6; // keyboard_keysym; keyboard_key is 4
constexpr uint32_t DestroyVersion = 5; constexpr uint32_t DestroyVersion = 5;
constexpr uint32_t Released = 0; constexpr uint32_t Released = 0;
@@ -26,14 +24,10 @@ constexpr ModifierKey ModifierKeys[] = {
} // namespace } // namespace
KWinFakeInput::KWinFakeInput() KWinFakeInput::KWinFakeInput()
: QWaylandClientExtensionTemplate(KeysymVersion) : QWaylandClientExtensionTemplate(RequiredVersion)
{ {
connect(this, &QWaylandClientExtension::activeChanged, this, [this] { connect(this, &QWaylandClientExtension::activeChanged, this, [this] {
if (!isActive()) if (isActive())
return;
if (!canSendKeysyms())
qWarning("KWin's fake input is too old (version %u, need %u): typing outside text fields is off",
boundVersion(), KeysymVersion);
authenticate(QStringLiteral("sloposk"), QStringLiteral("On-screen keyboard: type into any window")); authenticate(QStringLiteral("sloposk"), QStringLiteral("On-screen keyboard: type into any window"));
}); });
initialize(); initialize();
@@ -52,7 +46,7 @@ uint32_t KWinFakeInput::boundVersion() const
bool KWinFakeInput::canSendKeysyms() const bool KWinFakeInput::canSendKeysyms() const
{ {
return isActive() && boundVersion() >= KeysymVersion; return isActive() && boundVersion() >= RequiredVersion;
} }
void KWinFakeInput::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) void KWinFakeInput::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers)
+5 -3
View File
@@ -24,10 +24,12 @@ public:
void setKeyPressed(quint32 keysym, bool pressed); void setKeyPressed(quint32 keysym, bool pressed);
// Types each character as its keysym. // Types each character as its keysym.
void typeText(const QString &text); void typeText(const QString &text);
// KWin offers the protocol only to clients whose desktop file lists it
private: // (see the .desktop.in); isActive() says whether it did.
// keyboard_keysym, which everything here uses, needs this version.
static constexpr uint32_t RequiredVersion = 6;
bool canSendKeysyms() const;
// The version KWin bound, which may be older than the one asked for: // The version KWin bound, which may be older than the one asked for:
// sending a request it lacks makes libwayland abort. // sending a request it lacks makes libwayland abort.
uint32_t boundVersion() const; uint32_t boundVersion() const;
bool canSendKeysyms() const;
}; };
+3 -1
View File
@@ -70,9 +70,11 @@ void KWinInputMethod::setPreedit(const QString &text)
m_context->preedit(text); m_context->preedit(text);
} }
// KWin drops the input method's modifier mask for most clients (it forwards
// only the key), so Ctrl+A would type "a": combos hold real modifier keys.
void KWinInputMethod::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) void KWinInputMethod::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers)
{ {
if (m_context) if (m_context && (modifiers == Qt::NoModifier || !m_fakeInput.canSendKeysyms()))
m_context->sendKeysym(keysym, modifiers); m_context->sendKeysym(keysym, modifiers);
else else
m_fakeInput.sendKeysym(keysym, modifiers); m_fakeInput.sendKeysym(keysym, modifiers);
+2
View File
@@ -42,6 +42,8 @@ public:
// False when the compositor didn't hand us the input-method global. // False when the compositor didn't hand us the input-method global.
bool isAvailable() const; bool isAvailable() const;
// Key events outside text fields, held keys and modifier combos.
const KWinFakeInput &fakeInput() const { return m_fakeInput; }
bool isActive() const override; bool isActive() const override;
TextContext context() const override; TextContext context() const override;
+18
View File
@@ -0,0 +1,18 @@
#include "Diagnostics.h"
#include <QtLogging>
void Diagnostics::check(const QString &id, bool ok, const QString &message)
{
if (ok) {
if (m_problems.remove(id) == 0)
return;
qInfo("Resolved: %s", qPrintable(id));
} else {
if (m_problems.value(id) == message)
return;
m_problems.insert(id, message);
qWarning("%s", qPrintable(message));
}
Q_EMIT problemsChanged();
}
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <QMap>
#include <QObject>
#include <QStringList>
#include <QtQml/qqmlregistration.h>
// Features that don't work in this session (a protocol KWin doesn't offer, a
// missing D-Bus service...), each with a sentence for the user. Logged when
// they appear or clear, and listed in the settings panel.
class Diagnostics : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("Created in C++")
Q_PROPERTY(QStringList problems READ problems NOTIFY problemsChanged)
public:
using QObject::QObject;
// Records the problem `id` with `message` unless `ok`; clears it if `ok`.
void check(const QString &id, bool ok, const QString &message);
QStringList problems() const { return m_problems.values(); }
Q_SIGNALS:
void problemsChanged();
private:
QMap<QString, QString> m_problems; // id → message, ordered by id
};
+9 -2
View File
@@ -1,9 +1,11 @@
#include "app/Checks.h"
#include "app/DBusControl.h" #include "app/DBusControl.h"
#include "app/DevHooks.h" #include "app/DevHooks.h"
#include "app/QuitOnSignal.h" #include "app/QuitOnSignal.h"
#include "app/SystemLayouts.h" #include "app/SystemLayouts.h"
#include "backend/KWinInputMethod.h" #include "backend/KWinInputMethod.h"
#include "backend/WaylandKeymap.h" #include "backend/WaylandKeymap.h"
#include "diagnostics/Diagnostics.h"
#include "engine/DirectEngine.h" #include "engine/DirectEngine.h"
#include "input/KeyboardController.h" #include "input/KeyboardController.h"
#include "layout/LayoutBuilder.h" #include "layout/LayoutBuilder.h"
@@ -118,6 +120,8 @@ void connectTextContext(KWinInputMethod &backend, KeyboardController &controller
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
blockQuitSignals(); blockQuitSignals();
// libwayland unsets it once connected.
const bool launchedByKWin = qEnvironmentVariableIsSet("WAYLAND_SOCKET");
QGuiApplication app(argc, argv); QGuiApplication app(argc, argv);
quitOnSignal(); quitOnSignal();
QGuiApplication::setApplicationName(QStringLiteral("sloposk")); QGuiApplication::setApplicationName(QStringLiteral("sloposk"));
@@ -130,7 +134,9 @@ int main(int argc, char *argv[])
return 1; return 1;
} }
Diagnostics diagnostics;
KWinInputMethod backend; KWinInputMethod backend;
watchInputMethod(diagnostics, backend, launchedByKWin);
DirectEngine engine(backend); DirectEngine engine(backend);
LayoutModel model; LayoutModel model;
KeyboardController controller(engine, model); KeyboardController controller(engine, model);
@@ -142,6 +148,7 @@ int main(int argc, char *argv[])
WaylandKeymap keymap; WaylandKeymap keymap;
SystemLayouts systemLayouts; SystemLayouts systemLayouts;
watchSystemLayouts(diagnostics, systemLayouts);
const auto rebuildLayout = connectLayouts(*layoutTemplate, keymap, systemLayouts, settings, controller, model); const auto rebuildLayout = connectLayouts(*layoutTemplate, keymap, systemLayouts, settings, controller, model);
QObject::connect(&settings, &Settings::typingOptionsChanged, &controller, QObject::connect(&settings, &Settings::typingOptionsChanged, &controller,
[&] { controller.setOptions(settings.typingOptions()); }); [&] { controller.setOptions(settings.typingOptions()); });
@@ -151,6 +158,7 @@ int main(int argc, char *argv[])
{QStringLiteral("layoutModel"), QVariant::fromValue(&model)}, {QStringLiteral("layoutModel"), QVariant::fromValue(&model)},
{QStringLiteral("controller"), QVariant::fromValue(&controller)}, {QStringLiteral("controller"), QVariant::fromValue(&controller)},
{QStringLiteral("settings"), QVariant::fromValue(&settings)}, {QStringLiteral("settings"), QVariant::fromValue(&settings)},
{QStringLiteral("diagnostics"), QVariant::fromValue(&diagnostics)},
}); });
qml.loadFromModule("Sloposk", "Main"); qml.loadFromModule("Sloposk", "Main");
auto *window = qml.rootObjects().isEmpty() ? nullptr : qobject_cast<QQuickWindow *>(qml.rootObjects().first()); auto *window = qml.rootObjects().isEmpty() ? nullptr : qobject_cast<QQuickWindow *>(qml.rootObjects().first());
@@ -170,8 +178,7 @@ int main(int argc, char *argv[])
connectVisibility(visibility, backend, settings, controller, placement); connectVisibility(visibility, backend, settings, controller, placement);
DBusControl dbus(visibility); DBusControl dbus(visibility);
if (!dbus.registerOnSessionBus()) reportDBusControl(diagnostics, dbus.registerOnSessionBus());
qWarning("D-Bus control unavailable (is another instance running?)");
installDevHooks(backend, *window); installDevHooks(backend, *window);
return app.exec(); return app.exec();
+1
View File
@@ -16,6 +16,7 @@ sloposk_add_test(tst_textrules)
sloposk_add_test(tst_deadkeys) sloposk_add_test(tst_deadkeys)
sloposk_add_test(tst_keyrepeater) sloposk_add_test(tst_keyrepeater)
sloposk_add_test(tst_visibility) sloposk_add_test(tst_visibility)
sloposk_add_test(tst_diagnostics)
# Needs the QML module itself, not just the C++ backing library. # Needs the QML module itself, not just the C++ backing library.
sloposk_add_test(tst_keyboardsurface) sloposk_add_test(tst_keyboardsurface)
+46
View File
@@ -0,0 +1,46 @@
#include "diagnostics/Diagnostics.h"
#include <QSignalSpy>
#include <QTest>
class TestDiagnostics : public QObject
{
Q_OBJECT
private Q_SLOTS:
void reportsAndClears()
{
Diagnostics diagnostics;
QSignalSpy changed(&diagnostics, &Diagnostics::problemsChanged);
QTest::ignoreMessage(QtWarningMsg, "broken");
diagnostics.check(QStringLiteral("a"), false, QStringLiteral("broken"));
QCOMPARE(diagnostics.problems(), QStringList{QStringLiteral("broken")});
diagnostics.check(QStringLiteral("a"), true, QStringLiteral("broken"));
QVERIFY(diagnostics.problems().isEmpty());
QCOMPARE(changed.count(), 2);
}
void repeatsAreQuiet()
{
Diagnostics diagnostics;
QSignalSpy changed(&diagnostics, &Diagnostics::problemsChanged);
diagnostics.check(QStringLiteral("a"), true, QStringLiteral("fine"));
QTest::ignoreMessage(QtWarningMsg, "broken");
diagnostics.check(QStringLiteral("a"), false, QStringLiteral("broken"));
diagnostics.check(QStringLiteral("a"), false, QStringLiteral("broken")); // no second warning
QCOMPARE(changed.count(), 1);
}
void listsByIdNotArrival()
{
Diagnostics diagnostics;
QTest::ignoreMessage(QtWarningMsg, "second");
QTest::ignoreMessage(QtWarningMsg, "first");
diagnostics.check(QStringLiteral("b"), false, QStringLiteral("second"));
diagnostics.check(QStringLiteral("a"), false, QStringLiteral("first"));
QCOMPARE(diagnostics.problems(), (QStringList{QStringLiteral("first"), QStringLiteral("second")}));
}
};
QTEST_GUILESS_MAIN(TestDiagnostics)
#include "tst_diagnostics.moc"