diff --git a/README.md b/README.md index 7e67249..6e91f36 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,17 @@ Needs Qt ≥ 6.8 (Quick, WaylandClient, DBus, Test), KF6 WindowSystem, KF6 Confi ## 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 | |---|---| | 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, 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) | +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` Dev hooks: `SLOPOSK_AUTOTYPE=text` commits on focus; `SLOPOSK_SCREENSHOT=file.png` saves the window and quits. diff --git a/data/org.sloposk.keyboard.desktop b/data/org.sloposk.keyboard.desktop.in similarity index 56% rename from data/org.sloposk.keyboard.desktop rename to data/org.sloposk.keyboard.desktop.in index e12d0d4..be15074 100644 --- a/data/org.sloposk.keyboard.desktop +++ b/data/org.sloposk.keyboard.desktop.in @@ -1,10 +1,11 @@ [Desktop Entry] Name=sloposk Comment=iPad-style floating on-screen keyboard -Exec=sloposk +Exec=@SLOPOSK_EXEC@ Type=Application Icon=input-keyboard-virtual NoDisplay=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 diff --git a/qml/Main.qml b/qml/Main.qml index a7c0c32..c271548 100644 --- a/qml/Main.qml +++ b/qml/Main.qml @@ -7,6 +7,7 @@ Window { required property LayoutModel layoutModel required property KeyboardController controller required property Settings settings + required property Diagnostics diagnostics property var placement: null // FloatingWindow, set from C++ property bool settingsOpen: false // Height of the key area, for C++ keeping key sizes when rows change. @@ -58,6 +59,7 @@ Window { visible: root.settingsOpen settings: root.settings layoutModel: root.layoutModel + diagnostics: root.diagnostics } DragHandle { @@ -69,6 +71,7 @@ Window { SettingsButton { anchors { left: parent.left; leftMargin: 8; bottom: parent.bottom } checked: root.settingsOpen + alert: root.diagnostics.problems.length > 0 onClicked: root.settingsOpen = !root.settingsOpen } diff --git a/qml/ProblemList.qml b/qml/ProblemList.qml new file mode 100644 index 0000000..99e833f --- /dev/null +++ b/qml/ProblemList.qml @@ -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 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 + } + } +} diff --git a/qml/SettingsButton.qml b/qml/SettingsButton.qml index f0443ed..ece49d4 100644 --- a/qml/SettingsButton.qml +++ b/qml/SettingsButton.qml @@ -5,6 +5,8 @@ Item { id: button property bool checked: false + // Something doesn't work (see Diagnostics): a dot invites a look in settings. + property bool alert: false signal clicked() implicitWidth: 36 @@ -17,6 +19,16 @@ Item { 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 { id: tap onTapped: button.clicked() diff --git a/qml/SettingsPanel.qml b/qml/SettingsPanel.qml index dc22cd7..b3bab0f 100644 --- a/qml/SettingsPanel.qml +++ b/qml/SettingsPanel.qml @@ -8,101 +8,113 @@ Flickable { required property Settings settings required property LayoutModel layoutModel + required property Diagnostics diagnostics clip: true - contentHeight: columns.height + 2 * columns.y + contentHeight: content.height + 2 * content.y boundsBehavior: Flickable.StopAtBounds - Row { - id: columns - readonly property real columnWidth: (width - spacing) / 2 - + Column { + id: content x: 16 y: 16 width: panel.width - 2 * x - spacing: 32 + spacing: 16 - Column { - width: columns.columnWidth - spacing: 4 - - SectionHeader { text: qsTr("Typing") } - - ToggleRow { - width: parent.width - text: qsTr("Auto-Capitalization") - detail: qsTr("Shift at the start of a sentence") - checked: panel.settings.autoCapitalize - onToggled: checked => panel.settings.autoCapitalize = checked - } - - ToggleRow { - width: parent.width - text: qsTr("Hold Keys") - detail: qsTr("Keys stay down while touched, like a hardware keyboard; no accents or flicks") - checked: panel.settings.holdKeys - onToggled: checked => panel.settings.holdKeys = checked - } - - ToggleRow { - width: parent.width - text: qsTr("Always Show") - detail: qsTr("Type into any window, not just text fields") - checked: panel.settings.alwaysVisible - onToggled: checked => panel.settings.alwaysVisible = checked - } - - ToggleRow { - width: parent.width - text: qsTr("“.” Shortcut") - detail: qsTr("Double-tap space for a period and a space") - checked: panel.settings.doubleSpacePeriod - onToggled: checked => panel.settings.doubleSpacePeriod = checked - } - - SectionHeader { text: qsTr("Appearance"); topPadding: 12 } - - SliderRow { - width: parent.width - text: qsTr("Background") - from: panel.settings.minBackgroundOpacity - value: panel.settings.backgroundOpacity - onMoved: value => panel.settings.backgroundOpacity = value - } - - ToggleRow { - width: parent.width - text: qsTr("Blur Behind") - detail: qsTr("Frosted glass under a see-through background, if the desktop supports it") - checked: panel.settings.blurBackground - onToggled: checked => panel.settings.blurBackground = checked - } + ProblemList { + width: parent.width + problems: panel.diagnostics.problems } - Column { - width: columns.columnWidth - spacing: 4 + Row { + id: columns + readonly property real columnWidth: (width - spacing) / 2 - SectionHeader { text: qsTr("Keys") } + width: parent.width + spacing: 32 - ToggleRow { - width: parent.width - text: qsTr("Full Keyboard") - detail: qsTr("Number row, tab, ctrl, alt — like iPad Pro") - checked: panel.settings.fullKeyboard - onToggled: checked => panel.settings.fullKeyboard = checked - } + Column { + width: columns.columnWidth + spacing: 4 - Repeater { - // "main" is the typing keys; the rest come from the template. - model: [{ id: "main", name: qsTr("Typing keys") }].concat(panel.layoutModel.sections) + SectionHeader { text: qsTr("Typing") } ToggleRow { - required property var modelData width: parent.width - text: modelData.name - checked: panel.settings.visibleSections.includes(modelData.id) - onToggled: checked => panel.settings.setSectionVisible(modelData.id, checked) + text: qsTr("Auto-Capitalization") + detail: qsTr("Shift at the start of a sentence") + checked: panel.settings.autoCapitalize + onToggled: checked => panel.settings.autoCapitalize = checked + } + + ToggleRow { + width: parent.width + text: qsTr("Hold Keys") + detail: qsTr("Keys stay down while touched, like a hardware keyboard; no accents or flicks") + checked: panel.settings.holdKeys + onToggled: checked => panel.settings.holdKeys = checked + } + + ToggleRow { + width: parent.width + text: qsTr("Always Show") + detail: qsTr("Type into any window, not just text fields") + checked: panel.settings.alwaysVisible + onToggled: checked => panel.settings.alwaysVisible = checked + } + + ToggleRow { + width: parent.width + text: qsTr("“.” Shortcut") + detail: qsTr("Double-tap space for a period and a space") + checked: panel.settings.doubleSpacePeriod + onToggled: checked => panel.settings.doubleSpacePeriod = checked + } + + SectionHeader { text: qsTr("Appearance"); topPadding: 12 } + + SliderRow { + width: parent.width + text: qsTr("Background") + from: panel.settings.minBackgroundOpacity + value: panel.settings.backgroundOpacity + onMoved: value => panel.settings.backgroundOpacity = value + } + + ToggleRow { + width: parent.width + text: qsTr("Blur Behind") + detail: qsTr("Frosted glass under a see-through background, if the desktop supports it") + checked: panel.settings.blurBackground + onToggled: checked => panel.settings.blurBackground = checked + } + } + + Column { + width: columns.columnWidth + spacing: 4 + + SectionHeader { text: qsTr("Keys") } + + ToggleRow { + width: parent.width + text: qsTr("Full Keyboard") + detail: qsTr("Number row, tab, ctrl, alt — like iPad Pro") + checked: panel.settings.fullKeyboard + onToggled: checked => panel.settings.fullKeyboard = checked + } + + Repeater { + // "main" is the typing keys; the rest come from the template. + model: [{ id: "main", name: qsTr("Typing keys") }].concat(panel.layoutModel.sections) + + ToggleRow { + required property var modelData + width: parent.width + text: modelData.name + checked: panel.settings.visibleSections.includes(modelData.id) + onToggled: checked => panel.settings.setSectionVisible(modelData.id, checked) + } } } } diff --git a/qml/Theme.qml b/qml/Theme.qml index 39af7e9..0220c56 100644 --- a/qml/Theme.qml +++ b/qml/Theme.qml @@ -16,6 +16,7 @@ QtObject { readonly property color accent: dark ? "#0a84ff" : "#007aff" readonly property color accentText: "#ffffff" readonly property color toggleOn: dark ? "#30d158" : "#34c759" + readonly property color warning: dark ? "#ff9f0a" : "#ff9500" readonly property real windowRadius: 14 readonly property real keyRadius: 6 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4bef203..7006eae 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -26,6 +26,7 @@ qt_add_library(sloposk_core STATIC window/WindowSettings.h window/WindowSettings.cpp settings/DebouncedStore.h settings/DebouncedStore.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. @@ -39,6 +40,7 @@ set(SLOPOSK_QML_FILES SectionHeader.qml SettingsButton.qml SettingsPanel.qml + ProblemList.qml ToggleRow.qml SliderRow.qml Theme.qml @@ -68,6 +70,7 @@ target_include_directories(sloposk_core PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/input ${CMAKE_CURRENT_SOURCE_DIR}/layout ${CMAKE_CURRENT_SOURCE_DIR}/settings + ${CMAKE_CURRENT_SOURCE_DIR}/diagnostics ) target_link_libraries(sloposk_core PUBLIC Qt6::Quick PkgConfig::XKB) @@ -77,6 +80,7 @@ qt_add_executable(sloposk main.cpp app/DevHooks.h app/DevHooks.cpp app/DBusControl.h app/DBusControl.cpp + app/Checks.h app/Checks.cpp app/QuitOnSignal.h app/QuitOnSignal.cpp app/SystemLayouts.h app/SystemLayouts.cpp backend/InputMethodProtocol.h @@ -107,4 +111,13 @@ target_link_libraries(sloposk PRIVATE ) 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) diff --git a/src/app/Checks.cpp b/src/app/Checks.cpp new file mode 100644 index 0000000..c8fcbd6 --- /dev/null +++ b/src/app/Checks.cpp @@ -0,0 +1,62 @@ +#include "Checks.h" + +#include "SystemLayouts.h" +#include "backend/KWinInputMethod.h" +#include "diagnostics/Diagnostics.h" + +#include + +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.")); +} diff --git a/src/app/Checks.h b/src/app/Checks.h new file mode 100644 index 0000000..50619a2 --- /dev/null +++ b/src/app/Checks.h @@ -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); diff --git a/src/app/SystemLayouts.cpp b/src/app/SystemLayouts.cpp index f656671..44f1f44 100644 --- a/src/app/SystemLayouts.cpp +++ b/src/app/SystemLayouts.cpp @@ -27,7 +27,7 @@ SystemLayouts::SystemLayouts(QObject *parent) bus.connect(Service, Path, Interface, QStringLiteral("layoutChanged"), this, SLOT(setCurrent(uint))); auto *watcher = new QDBusServiceWatcher(Service, bus, QDBusServiceWatcher::WatchForOwnerChange, this); 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(); } @@ -38,10 +38,9 @@ void SystemLayouts::fetchCurrent() connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { watcher->deleteLater(); const QDBusPendingReply reply = *watcher; - if (reply.isError()) - return; - m_system = true; - setCurrent(reply.value()); + setSystem(!reply.isError()); + if (m_system) + setCurrent(reply.value()); }); } @@ -56,6 +55,12 @@ void SystemLayouts::next(quint32 count) setCurrent(index); } +void SystemLayouts::setSystem(bool system) +{ + m_system = system; + Q_EMIT serviceChecked(system); +} + void SystemLayouts::setCurrent(uint index) { if (index == m_current) diff --git a/src/app/SystemLayouts.h b/src/app/SystemLayouts.h index 857b7e2..89176bd 100644 --- a/src/app/SystemLayouts.h +++ b/src/app/SystemLayouts.h @@ -19,12 +19,15 @@ public: Q_SIGNALS: void currentChanged(); + // After each attempt to reach the service, and when it goes away. + void serviceChecked(bool available); private Q_SLOTS: void setCurrent(uint index); private: void fetchCurrent(); + void setSystem(bool system); bool m_system = false; quint32 m_current = 0; diff --git a/src/backend/KWinFakeInput.cpp b/src/backend/KWinFakeInput.cpp index d65d8c7..f97ec81 100644 --- a/src/backend/KWinFakeInput.cpp +++ b/src/backend/KWinFakeInput.cpp @@ -1,11 +1,9 @@ #include "KWinFakeInput.h" -#include #include namespace { -constexpr uint32_t KeysymVersion = 6; // keyboard_keysym; keyboard_key is 4 constexpr uint32_t DestroyVersion = 5; constexpr uint32_t Released = 0; @@ -26,15 +24,11 @@ constexpr ModifierKey ModifierKeys[] = { } // namespace KWinFakeInput::KWinFakeInput() - : QWaylandClientExtensionTemplate(KeysymVersion) + : QWaylandClientExtensionTemplate(RequiredVersion) { connect(this, &QWaylandClientExtension::activeChanged, this, [this] { - 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")); + if (isActive()) + authenticate(QStringLiteral("sloposk"), QStringLiteral("On-screen keyboard: type into any window")); }); initialize(); } @@ -52,7 +46,7 @@ uint32_t KWinFakeInput::boundVersion() const bool KWinFakeInput::canSendKeysyms() const { - return isActive() && boundVersion() >= KeysymVersion; + return isActive() && boundVersion() >= RequiredVersion; } void KWinFakeInput::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) diff --git a/src/backend/KWinFakeInput.h b/src/backend/KWinFakeInput.h index 639d3c5..d402ee4 100644 --- a/src/backend/KWinFakeInput.h +++ b/src/backend/KWinFakeInput.h @@ -24,10 +24,12 @@ public: void setKeyPressed(quint32 keysym, bool pressed); // Types each character as its keysym. void typeText(const QString &text); - -private: + // KWin offers the protocol only to clients whose desktop file lists it + // (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: // sending a request it lacks makes libwayland abort. uint32_t boundVersion() const; - bool canSendKeysyms() const; }; diff --git a/src/backend/KWinInputMethod.cpp b/src/backend/KWinInputMethod.cpp index 61ca99c..89bb3e4 100644 --- a/src/backend/KWinInputMethod.cpp +++ b/src/backend/KWinInputMethod.cpp @@ -70,9 +70,11 @@ void KWinInputMethod::setPreedit(const QString &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) { - if (m_context) + if (m_context && (modifiers == Qt::NoModifier || !m_fakeInput.canSendKeysyms())) m_context->sendKeysym(keysym, modifiers); else m_fakeInput.sendKeysym(keysym, modifiers); diff --git a/src/backend/KWinInputMethod.h b/src/backend/KWinInputMethod.h index 5687cf8..1cc2eb5 100644 --- a/src/backend/KWinInputMethod.h +++ b/src/backend/KWinInputMethod.h @@ -42,6 +42,8 @@ public: // False when the compositor didn't hand us the input-method global. bool isAvailable() const; + // Key events outside text fields, held keys and modifier combos. + const KWinFakeInput &fakeInput() const { return m_fakeInput; } bool isActive() const override; TextContext context() const override; diff --git a/src/diagnostics/Diagnostics.cpp b/src/diagnostics/Diagnostics.cpp new file mode 100644 index 0000000..1523821 --- /dev/null +++ b/src/diagnostics/Diagnostics.cpp @@ -0,0 +1,18 @@ +#include "Diagnostics.h" + +#include + +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(); +} diff --git a/src/diagnostics/Diagnostics.h b/src/diagnostics/Diagnostics.h new file mode 100644 index 0000000..061e066 --- /dev/null +++ b/src/diagnostics/Diagnostics.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include +#include + +// 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 m_problems; // id → message, ordered by id +}; diff --git a/src/main.cpp b/src/main.cpp index ea00996..c3054e4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,9 +1,11 @@ +#include "app/Checks.h" #include "app/DBusControl.h" #include "app/DevHooks.h" #include "app/QuitOnSignal.h" #include "app/SystemLayouts.h" #include "backend/KWinInputMethod.h" #include "backend/WaylandKeymap.h" +#include "diagnostics/Diagnostics.h" #include "engine/DirectEngine.h" #include "input/KeyboardController.h" #include "layout/LayoutBuilder.h" @@ -118,6 +120,8 @@ void connectTextContext(KWinInputMethod &backend, KeyboardController &controller int main(int argc, char *argv[]) { blockQuitSignals(); + // libwayland unsets it once connected. + const bool launchedByKWin = qEnvironmentVariableIsSet("WAYLAND_SOCKET"); QGuiApplication app(argc, argv); quitOnSignal(); QGuiApplication::setApplicationName(QStringLiteral("sloposk")); @@ -130,7 +134,9 @@ int main(int argc, char *argv[]) return 1; } + Diagnostics diagnostics; KWinInputMethod backend; + watchInputMethod(diagnostics, backend, launchedByKWin); DirectEngine engine(backend); LayoutModel model; KeyboardController controller(engine, model); @@ -142,6 +148,7 @@ int main(int argc, char *argv[]) WaylandKeymap keymap; SystemLayouts systemLayouts; + watchSystemLayouts(diagnostics, systemLayouts); const auto rebuildLayout = connectLayouts(*layoutTemplate, keymap, systemLayouts, settings, controller, model); QObject::connect(&settings, &Settings::typingOptionsChanged, &controller, [&] { controller.setOptions(settings.typingOptions()); }); @@ -151,6 +158,7 @@ int main(int argc, char *argv[]) {QStringLiteral("layoutModel"), QVariant::fromValue(&model)}, {QStringLiteral("controller"), QVariant::fromValue(&controller)}, {QStringLiteral("settings"), QVariant::fromValue(&settings)}, + {QStringLiteral("diagnostics"), QVariant::fromValue(&diagnostics)}, }); qml.loadFromModule("Sloposk", "Main"); auto *window = qml.rootObjects().isEmpty() ? nullptr : qobject_cast(qml.rootObjects().first()); @@ -170,8 +178,7 @@ int main(int argc, char *argv[]) connectVisibility(visibility, backend, settings, controller, placement); DBusControl dbus(visibility); - if (!dbus.registerOnSessionBus()) - qWarning("D-Bus control unavailable (is another instance running?)"); + reportDBusControl(diagnostics, dbus.registerOnSessionBus()); installDevHooks(backend, *window); return app.exec(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2335bdc..d173d55 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -16,6 +16,7 @@ sloposk_add_test(tst_textrules) sloposk_add_test(tst_deadkeys) sloposk_add_test(tst_keyrepeater) sloposk_add_test(tst_visibility) +sloposk_add_test(tst_diagnostics) # Needs the QML module itself, not just the C++ backing library. sloposk_add_test(tst_keyboardsurface) diff --git a/tests/tst_diagnostics.cpp b/tests/tst_diagnostics.cpp new file mode 100644 index 0000000..127dbae --- /dev/null +++ b/tests/tst_diagnostics.cpp @@ -0,0 +1,46 @@ +#include "diagnostics/Diagnostics.h" + +#include +#include + +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"