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:
@@ -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
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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 {
|
||||||
@@ -107,4 +118,5 @@ Flickable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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."));
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user