diff --git a/README.md b/README.md index 450c5ab..aaf0d99 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ iPad-style floating on-screen keyboard for Plasma Wayland. Qt 6, C++ core, QML U - Alt Gr (right of space in full mode) types the keymap's AltGr symbols (sk: Alt Gr+J = '), sticky like the others - Optional PC key blocks: function row, navigation (ins/home/pg up, del/end/pg dn, arrows), numeric keypad; any mix, typing keys included (⚙) - Floating: drag the bottom bar, resize from the corner; geometry is remembered per screen +- "Shrink to Button" (⚙): ⌨ leaves just itself as a see-through floating button (drag it anywhere); tap it for the keyboard back, around the button and kept on screen - Follows Plasma's virtual keyboard setting (tray toggle): off never pops up on focus, the default only after touch or a pen; "Always Show" and D-Bus `Show`/`Toggle` still show it - See-through background (opacity slider in ⚙) with optional blur behind it (KWin's blur effect) diff --git a/qml/FloatingButton.qml b/qml/FloatingButton.qml new file mode 100644 index 0000000..a44373c --- /dev/null +++ b/qml/FloatingButton.qml @@ -0,0 +1,46 @@ +import QtQuick + +// The dismiss key left floating while the keyboard is shrunk; see-through so +// it hides less. Tap to bring the keyboard back; dragging is up to the owner +// (the window manager's move). +Item { + id: button + + signal tapped() + signal dragStarted() + + opacity: 0.6 + Accessible.role: Accessible.Button + Accessible.name: qsTr("Show Keyboard") + + Rectangle { + anchors.fill: parent + radius: Theme.keyRadius + color: tap.pressed ? Theme.characterKey : Theme.functionKey + } + + Text { + anchors.centerIn: parent + text: "⌨" + font.pixelSize: button.height * 0.4 + color: Theme.text + } + + TapHandler { + id: tap + // The move ends with a release that would otherwise count as a tap. + property bool moved: false + onPressedChanged: if (pressed) moved = false + onTapped: if (!moved) button.tapped() + } + + DragHandler { + target: null + onActiveChanged: { + if (!active) + return; + tap.moved = true; + button.dragStarted(); + } + } +} diff --git a/qml/Keyboard.qml b/qml/Keyboard.qml index 6dd46ed..4273a95 100644 --- a/qml/Keyboard.qml +++ b/qml/Keyboard.qml @@ -4,7 +4,18 @@ import Sloposk KeyboardSurface { id: surface + // Window-relative rect of the first key of `type`; empty if there is none. + function keyRect(type) { + for (let i = 0; i < keys.count; ++i) { + const key = keys.itemAt(i); + if (key?.keyType === type) + return key.mapToItem(null, 0, 0, key.width, key.height); + } + return Qt.rect(0, 0, 0, 0); + } + Repeater { + id: keys model: surface.model delegate: Key { shiftMode: surface.controller ? surface.controller.shiftMode : KeyboardController.Off diff --git a/qml/Main.qml b/qml/Main.qml index 7f3eda3..8803474 100644 --- a/qml/Main.qml +++ b/qml/Main.qml @@ -9,6 +9,8 @@ Window { required property Settings settings required property Diagnostics diagnostics property var placement: null // FloatingWindow, set from C++ + property var collapse: null // CollapseController, set from C++ + readonly property bool collapsed: collapse?.collapsed ?? false property bool settingsOpen: false // Height of the key area, for C++ keeping key sizes when rows change. readonly property real keysHeight: keyboard.height @@ -16,23 +18,31 @@ Window { // Transparent room above the keyboard for long-press popups on the top row. readonly property real popupMargin: 90 // Outside this, input passes through to whatever is below the window. - readonly property rect inputRegion: keyboard.alternates.open + readonly property rect inputRegion: collapsed + ? Qt.rect(floatingButton.x, floatingButton.y, floatingButton.width, floatingButton.height) + : keyboard.alternates.open ? Qt.rect(0, 0, width, height) : Qt.rect(panel.x, panel.y, panel.width, panel.height) // Empty when nothing shows through, so the compositor skips the blur. - readonly property rect blurRegion: settings.blurBackground && settings.backgroundOpacity < 1 + readonly property rect blurRegion: !collapsed && settings.blurBackground && settings.backgroundOpacity < 1 ? Qt.rect(panel.x, panel.y, panel.width, panel.height) : Qt.rect(0, 0, 0, 0) width: 1000 height: 340 + popupMargin - minimumWidth: 480 - minimumHeight: 180 + popupMargin + // None while shrunk to the floating button. + minimumWidth: collapsed ? 0 : 480 + minimumHeight: collapsed ? 0 : 180 + popupMargin visible: false color: "transparent" onInputRegionChanged: placement?.setInputRegion(inputRegion) onBlurRegionChanged: placement?.setBlurRegion(blurRegion, Theme.windowRadius) + // For CollapseController. + function dismissKeyRect() { + return keyboard.keyRect("dismiss"); + } + onPlacementChanged: { placement?.setInputRegion(inputRegion); placement?.setBlurRegion(blurRegion, Theme.windowRadius); @@ -41,6 +51,7 @@ Window { Rectangle { id: panel anchors { fill: parent; topMargin: root.popupMargin } + visible: !root.collapsed radius: Theme.windowRadius color: Qt.rgba(Theme.background.r, Theme.background.g, Theme.background.b, root.settings.backgroundOpacity) @@ -66,7 +77,7 @@ Window { id: handle anchors { left: parent.left; right: parent.right; bottom: parent.bottom } pillWidth: Math.min(120, width - 2 * (quickToggles.x + quickToggles.width + 12)) - onDragStarted: root.startSystemMove() + onDragStarted: root.placement.startMove() } SettingsButton { @@ -86,7 +97,20 @@ Window { ResizeHandle { anchors { right: parent.right; bottom: parent.bottom } - onDragStarted: root.startSystemResize(Qt.RightEdge | Qt.BottomEdge) + onDragStarted: root.placement.startResize(Qt.RightEdge | Qt.BottomEdge) } } + + FloatingButton { + id: floatingButton + readonly property rect keyRect: root.collapse?.keyRect ?? Qt.rect(0, 0, 0, 0) + // Over the key while the window is full size, then in the shrunk window. + x: Math.max(0, Math.min(keyRect.x, root.width - width)) + y: Math.max(0, Math.min(keyRect.y, root.height - height)) + width: keyRect.width + height: keyRect.height + visible: root.collapsed + onTapped: root.collapse.expand() + onDragStarted: root.placement.startMove() + } } diff --git a/qml/SettingsPanel.qml b/qml/SettingsPanel.qml index b3bab0f..f34d658 100644 --- a/qml/SettingsPanel.qml +++ b/qml/SettingsPanel.qml @@ -63,6 +63,14 @@ Flickable { onToggled: checked => panel.settings.alwaysVisible = checked } + ToggleRow { + width: parent.width + text: qsTr("Shrink to Button") + detail: qsTr("⌨ leaves a floating button instead of hiding; tap it to bring the keyboard back") + checked: panel.settings.collapseToButton + onToggled: checked => panel.settings.collapseToButton = checked + } + ToggleRow { width: parent.width text: qsTr("“.” Shortcut") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a6e8f95..0097e80 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -36,6 +36,7 @@ set(SLOPOSK_QML_FILES Key.qml AlternatesPopup.qml DragHandle.qml + FloatingButton.qml ResizeHandle.qml SectionHeader.qml SettingsButton.qml @@ -91,7 +92,9 @@ qt_add_executable(sloposk backend/KWinInputContext.h backend/KWinInputContext.cpp backend/KWinFakeInput.h backend/KWinFakeInput.cpp backend/WaylandKeymap.h backend/WaylandKeymap.cpp + window/CollapseController.h window/CollapseController.cpp window/FloatingWindow.h window/FloatingWindow.cpp + window/KWinScript.h window/KWinScript.cpp window/KWinRule.h window/KWinRule.cpp ) diff --git a/src/main.cpp b/src/main.cpp index 4b441a1..c9ac594 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,6 +13,7 @@ #include "layout/LayoutLoader.h" #include "layout/LayoutModel.h" #include "settings/Settings.h" +#include "window/CollapseController.h" #include "window/FloatingWindow.h" #include "window/KWinRule.h" #include "window/VisibilityController.h" @@ -82,12 +83,15 @@ void connectKeySettings(Settings &settings, LayoutModel &model, FloatingWindow & [keepingKeySize, rebuild] { keepingKeySize(rebuild); }); } -void restoreAndPersistGeometry(FloatingWindow &placement, WindowSettings &settings) +// The shrunk button's geometry isn't the keyboard's: not saved. +void restoreAndPersistGeometry(FloatingWindow &placement, WindowSettings &settings, const CollapseController &collapse) { if (const auto geometry = settings.load(placement.screenName())) placement.setGeometry(*geometry); - QObject::connect(&placement, &FloatingWindow::geometryChanged, &settings, - [&] { settings.save(placement.screenName(), placement.geometry()); }); + QObject::connect(&placement, &FloatingWindow::geometryChanged, &settings, [&] { + if (!collapse.isCollapsed()) + settings.save(placement.screenName(), placement.geometry()); + }); } // A focused field shows the keyboard only if KWin's policy wants one now; @@ -112,7 +116,7 @@ void connectFieldFocus(VisibilityController &visibility, KWinInputMethod &backen } void connectVisibility(VisibilityController &visibility, KWinInputMethod &backend, const Settings &settings, - KeyboardController &controller, FloatingWindow &placement) + FloatingWindow &placement) { // First, so the initial state below reaches the window too. QObject::connect(&visibility, &VisibilityController::visibleChanged, &placement, @@ -124,10 +128,19 @@ void connectVisibility(VisibilityController &visibility, KWinInputMethod &backen // work) there is no focus signal: stay visible so it can be tried out. const auto updateStandalone = [&] { visibility.setStandalone(!backend.isAvailable()); }; QObject::connect(&backend, &KWinInputMethod::availableChanged, &visibility, updateStandalone); - QObject::connect(&controller, &KeyboardController::dismissRequested, &visibility, &VisibilityController::hide); updateStandalone(); } +// The dismiss key hides the keyboard, or shrinks it to a floating button. +void connectDismiss(KeyboardController &controller, const Settings &settings, CollapseController &collapse, + VisibilityController &visibility) +{ + QObject::connect(&controller, &KeyboardController::dismissRequested, &visibility, [&] { + if (!settings.collapseToButton() || !collapse.collapse()) + visibility.hide(); + }); +} + void connectTextContext(KWinInputMethod &backend, KeyboardController &controller) { QObject::connect(&backend, &TextBackend::activeChanged, &controller, @@ -191,14 +204,17 @@ int main(int argc, char *argv[]) installKWinRule(QGuiApplication::desktopFileName()); FloatingWindow placement(window); window->setProperty("placement", QVariant::fromValue(&placement)); + CollapseController collapse(*window, placement); + window->setProperty("collapse", QVariant::fromValue(&collapse)); WindowSettings windowSettings; - restoreAndPersistGeometry(placement, windowSettings); + restoreAndPersistGeometry(placement, windowSettings, collapse); connectKeySettings(settings, model, placement, *window, rebuildLayout); VisibilityController visibility; KWinVirtualKeyboard virtualKeyboardPolicy; connectFieldFocus(visibility, backend, virtualKeyboardPolicy); - connectVisibility(visibility, backend, settings, controller, placement); + connectVisibility(visibility, backend, settings, placement); + connectDismiss(controller, settings, collapse, visibility); DBusControl dbus(visibility); reportDBusControl(diagnostics, dbus.registerOnSessionBus()); diff --git a/src/settings/Settings.cpp b/src/settings/Settings.cpp index 5809e86..e60ad2a 100644 --- a/src/settings/Settings.cpp +++ b/src/settings/Settings.cpp @@ -11,6 +11,7 @@ constexpr auto HoldKeysKey = "typing/holdKeys"; constexpr auto VisibleSectionsKey = "layout/visibleSections"; constexpr auto FullKeyboardKey = "layout/fullKeyboard"; constexpr auto AlwaysVisibleKey = "window/alwaysVisible"; +constexpr auto CollapseToButtonKey = "window/collapseToButton"; constexpr auto BackgroundOpacityKey = "appearance/backgroundOpacity"; constexpr auto BlurBackgroundKey = "appearance/blurBackground"; } @@ -28,6 +29,7 @@ Settings::Settings(QObject *parent) m_visibleSections = {MainSectionId}; m_fullKeyboard = store.value(FullKeyboardKey, false).toBool(); m_alwaysVisible = store.value(AlwaysVisibleKey, false).toBool(); + m_collapseToButton = store.value(CollapseToButtonKey, false).toBool(); m_backgroundOpacity = qBound(MinBackgroundOpacity, store.value(BackgroundOpacityKey, 1.0).toReal(), 1.0); m_blurBackground = store.value(BlurBackgroundKey, true).toBool(); } @@ -60,6 +62,15 @@ void Settings::setAlwaysVisible(bool always) Q_EMIT alwaysVisibleChanged(); } +void Settings::setCollapseToButton(bool collapse) +{ + if (collapse == m_collapseToButton) + return; + m_collapseToButton = collapse; + QSettings().setValue(CollapseToButtonKey, collapse); + Q_EMIT collapseToButtonChanged(); +} + void Settings::setFullKeyboard(bool full) { if (full == m_fullKeyboard) diff --git a/src/settings/Settings.h b/src/settings/Settings.h index 357d8ee..4d7ba51 100644 --- a/src/settings/Settings.h +++ b/src/settings/Settings.h @@ -23,6 +23,8 @@ class Settings : public QObject Q_PROPERTY(bool fullKeyboard READ fullKeyboard WRITE setFullKeyboard NOTIFY fullKeyboardChanged) // Shown even without a focused text field, to type into any window. Q_PROPERTY(bool alwaysVisible READ alwaysVisible WRITE setAlwaysVisible NOTIFY alwaysVisibleChanged) + // The dismiss key shrinks the keyboard to a floating button instead of hiding it. + Q_PROPERTY(bool collapseToButton READ collapseToButton WRITE setCollapseToButton NOTIFY collapseToButtonChanged) // 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) @@ -49,6 +51,8 @@ public: void setFullKeyboard(bool full); bool alwaysVisible() const { return m_alwaysVisible; } void setAlwaysVisible(bool always); + bool collapseToButton() const { return m_collapseToButton; } + void setCollapseToButton(bool collapse); static constexpr qreal MinBackgroundOpacity = 0.1; static qreal minBackgroundOpacity() { return MinBackgroundOpacity; } @@ -62,6 +66,7 @@ Q_SIGNALS: void visibleSectionsChanged(); void fullKeyboardChanged(); void alwaysVisibleChanged(); + void collapseToButtonChanged(); void appearanceChanged(); private: @@ -72,6 +77,7 @@ private: QStringList m_visibleSections; bool m_fullKeyboard = false; bool m_alwaysVisible = false; + bool m_collapseToButton = false; qreal m_backgroundOpacity = 1.0; bool m_blurBackground = true; }; diff --git a/src/window/CollapseController.cpp b/src/window/CollapseController.cpp new file mode 100644 index 0000000..0bf6cb8 --- /dev/null +++ b/src/window/CollapseController.cpp @@ -0,0 +1,74 @@ +#include "CollapseController.h" + +#include "FloatingWindow.h" + +#include + +namespace { +constexpr int ExpandTimeoutMs = 500; + +bool roughlyEqual(QSize a, QSize b) +{ + return qAbs(a.width() - b.width()) <= 1 && qAbs(a.height() - b.height()) <= 1; +} +} // namespace + +CollapseController::CollapseController(QQuickWindow &window, FloatingWindow &placement, QObject *parent) + : QObject(parent) + , m_window(window) + , m_placement(placement) +{ + m_expanding.setSingleShot(true); + m_expanding.setInterval(ExpandTimeoutMs); + connect(&m_expanding, &QTimer::timeout, this, &CollapseController::finishExpanding); + connect(&window, &QWindow::widthChanged, this, &CollapseController::windowResized); + connect(&window, &QWindow::heightChanged, this, &CollapseController::windowResized); +} + +bool CollapseController::collapse() +{ + if (m_collapsed) + return true; + QVariant rect; + QMetaObject::invokeMethod(&m_window, "dismissKeyRect", Q_RETURN_ARG(QVariant, rect)); + if (rect.toRectF().isEmpty()) + return false; + m_keyRect = rect.toRectF(); + m_expandedSize = m_window.size(); + m_collapsed = true; + Q_EMIT collapsedChanged(); + // After the next frame, once the compositor knows the lower minimum size. + connect(&m_window, &QQuickWindow::frameSwapped, this, [this] { + if (m_collapsed) + m_placement.moveResize(m_keyRect, false); + }, Qt::SingleShotConnection); + return true; +} + +void CollapseController::expand() +{ + if (!m_collapsed || m_expanding.isActive()) + return; + if (roughlyEqual(m_window.size(), m_expandedSize)) { + finishExpanding(); // never shrank: nothing to move + return; + } + // Stays collapsed until the window has grown, so the keys don't flash squeezed. + m_placement.moveResize(QRectF(-m_keyRect.topLeft(), m_expandedSize), true); + m_expanding.start(); +} + +void CollapseController::windowResized() +{ + if (m_expanding.isActive() && roughlyEqual(m_window.size(), m_expandedSize)) + finishExpanding(); +} + +void CollapseController::finishExpanding() +{ + m_expanding.stop(); + if (!roughlyEqual(m_window.size(), m_expandedSize)) + m_window.resize(m_expandedSize); // no KWin to do it + m_collapsed = false; + Q_EMIT collapsedChanged(); +} diff --git a/src/window/CollapseController.h b/src/window/CollapseController.h new file mode 100644 index 0000000..59f941c --- /dev/null +++ b/src/window/CollapseController.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include + +class FloatingWindow; +class QQuickWindow; + +// Shrinks the keyboard window to just its dismiss key, floating on its own, +// and grows it back with the key under the button again (kept on screen). +// The window itself shrinks, so it can be dragged anywhere and covers +// nothing else. QML draws from `collapsed` and `keyRect`. +class CollapseController : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool collapsed READ isCollapsed NOTIFY collapsedChanged) + // The key in the full window; the button stays there until the window shrinks. + Q_PROPERTY(QRectF keyRect READ keyRect NOTIFY collapsedChanged) + +public: + CollapseController(QQuickWindow &window, FloatingWindow &placement, QObject *parent = nullptr); + + bool isCollapsed() const { return m_collapsed; } + QRectF keyRect() const { return m_keyRect; } + + // False if the keyboard has no dismiss key to shrink to. + bool collapse(); + Q_INVOKABLE void expand(); + +Q_SIGNALS: + void collapsedChanged(); + +private: + void windowResized(); + void finishExpanding(); + + QQuickWindow &m_window; + FloatingWindow &m_placement; + bool m_collapsed = false; + QRectF m_keyRect; + QSize m_expandedSize; + QTimer m_expanding; // waits for KWin; runs out without it +}; diff --git a/src/window/FloatingWindow.cpp b/src/window/FloatingWindow.cpp index 751047f..8664357 100644 --- a/src/window/FloatingWindow.cpp +++ b/src/window/FloatingWindow.cpp @@ -1,6 +1,11 @@ #include "FloatingWindow.h" +#include "KWinScript.h" + #include +#include +#include +#include #include #include #include @@ -12,6 +17,18 @@ FloatingWindow::FloatingWindow(QWindow *window, QObject *parent) window->setFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus); for (const auto changed : {&QWindow::xChanged, &QWindow::yChanged, &QWindow::widthChanged, &QWindow::heightChanged}) connect(window, changed, this, &FloatingWindow::geometryChanged); + window->installEventFilter(this); +} + +bool FloatingWindow::eventFilter(QObject *watched, QEvent *event) +{ + // Exposed means mapped, so KWin knows the window by now. + if (watched == m_window && event->type() == QEvent::Expose) { + if (m_window->isExposed() && !m_exposed) + moveResize(QRectF(QPointF(), m_window->size()), true); + m_exposed = m_window->isExposed(); + } + return false; } QRect FloatingWindow::geometry() const @@ -38,6 +55,30 @@ void FloatingWindow::resizeBy(qreal dw, qreal dh) m_window->resize(size); } +void FloatingWindow::moveResize(const QRectF &relative, bool keepOnScreen) +{ + runKWinScript(QStringLiteral(R"( +for (const w of workspace.windowList()) { + if (w.resourceClass !== "%1") + continue; + const width = %4, height = %5; + let x = w.frameGeometry.x + %2, y = w.frameGeometry.y + %3; + if (%6) { + const area = workspace.clientArea(KWin.MaximizeArea, w); + x = Math.max(area.x, Math.min(x, area.x + area.width - width)); + y = Math.max(area.y, Math.min(y, area.y + area.height - height)); + } + w.frameGeometry = {x: x, y: y, width: width, height: height}; +} +)") + .arg(QGuiApplication::desktopFileName()) + .arg(relative.x()) + .arg(relative.y()) + .arg(relative.width()) + .arg(relative.height()) + .arg(keepOnScreen ? QStringLiteral("true") : QStringLiteral("false"))); +} + void FloatingWindow::setInputRegion(const QRectF ®ion) { m_window->setMask(QRegion(region.toAlignedRect())); @@ -54,6 +95,30 @@ void FloatingWindow::setBlurRegion(const QRectF ®ion, qreal radius) KWindowEffects::enableBlurBehind(m_window, true, QRegion(path.toFillPolygon().toPolygon())); } +void FloatingWindow::startMove() +{ + if (m_window->startSystemMove()) + releaseMouse(); +} + +void FloatingWindow::startResize(Qt::Edges edges) +{ + if (m_window->startSystemResize(edges)) + releaseMouse(); +} + +// The window manager keeps the button's release: without one, the pressed +// item holds on to the mouse and eats the next press. +void FloatingWindow::releaseMouse() +{ + if (!(QGuiApplication::mouseButtons() & Qt::LeftButton)) + return; // touch + const QPointF global = QCursor::pos(m_window->screen()); + QMouseEvent release(QEvent::MouseButtonRelease, m_window->mapFromGlobal(global), global, Qt::LeftButton, + Qt::NoButton, QGuiApplication::keyboardModifiers()); + QCoreApplication::sendEvent(m_window, &release); +} + void FloatingWindow::show() { m_window->setVisible(true); diff --git a/src/window/FloatingWindow.h b/src/window/FloatingWindow.h index fa46fe8..00e8816 100644 --- a/src/window/FloatingWindow.h +++ b/src/window/FloatingWindow.h @@ -9,7 +9,9 @@ class QWindow; // Makes a keyboard window a frameless, always-on-top window that never takes // focus. The window manager places it and moves it (Super+drag included); // on KWin Wayland that relies on the rule from installKWinRule(). -// Geometry is relative to the window's screen. +// Each time it appears, it's moved back on screen: KWin remembers where it +// was, which may be somewhere it was smaller. Geometry is relative to the +// window's screen. class FloatingWindow : public QObject { Q_OBJECT @@ -25,18 +27,30 @@ public: // Grows from the top-left corner; respects the window's minimum size. Q_INVOKABLE void resizeBy(qreal dw, qreal dh); + // Moves the window by `relative`'s offset and resizes it to its size, + // optionally clamped to the screen's free area. Done by KWin (Wayland + // clients can't place themselves), so it applies later. + void moveResize(const QRectF &relative, bool keepOnScreen); // Only this part of the window takes input; the rest passes through. Q_INVOKABLE void setInputRegion(const QRectF ®ion); // Asks the compositor to blur behind this rounded rect; an empty rect turns blur off. Q_INVOKABLE void setBlurRegion(const QRectF ®ion, qreal radius); + // The window manager's move and resize, for mouse and touch alike. + Q_INVOKABLE void startMove(); + Q_INVOKABLE void startResize(Qt::Edges edges); Q_INVOKABLE void show(); Q_INVOKABLE void hide(); Q_SIGNALS: void geometryChanged(); +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + private: QRect screenArea() const; + void releaseMouse(); QWindow *m_window; + bool m_exposed = false; }; diff --git a/src/window/KWinScript.cpp b/src/window/KWinScript.cpp new file mode 100644 index 0000000..c1cd8cb --- /dev/null +++ b/src/window/KWinScript.cpp @@ -0,0 +1,56 @@ +#include "KWinScript.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +const QString Service = QStringLiteral("org.kde.KWin"); +const QString ScriptingPath = QStringLiteral("/Scripting"); +const QString ScriptingInterface = QStringLiteral("org.kde.kwin.Scripting"); + +// KWin reads the file on a worker thread some time after `run`. +constexpr int KeepFileMs = 10'000; + +QDBusMessage kwinCall(const QString &path, const QString &interface, const QString &method, const QVariantList &args = {}) +{ + QDBusMessage call = QDBusMessage::createMethodCall(Service, path, interface, method); + call.setArguments(args); + return call; +} + +} // namespace + +void runKWinScript(const QString &source) +{ + static int count = 0; + const QString plugin = QStringLiteral("sloposk-%1").arg(++count); + // Unloading from within: `stop` right after `run` could beat the script. + const QString unload = QStringLiteral("\ncallDBus(\"%1\", \"%2\", \"%3\", \"unloadScript\", \"%4\");\n") + .arg(Service, ScriptingPath, ScriptingInterface, plugin); + + auto file = std::make_shared(QDir::tempPath() + QStringLiteral("/sloposk-XXXXXX.js")); + if (!file->open() || file->write((source + unload).toUtf8()) < 0 || !file->flush()) { + qWarning("Can't write a KWin script: %s", qPrintable(file->errorString())); + return; + } + auto *watcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall( + kwinCall(ScriptingPath, ScriptingInterface, QStringLiteral("loadScript"), {file->fileName(), plugin}))); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, [file](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + const QDBusPendingReply reply = *watcher; + if (reply.isError() || reply.value() < 0) { + qWarning("KWin didn't load a script: %s", qPrintable(reply.error().message())); + return; + } + const QString path = ScriptingPath + QStringLiteral("/Script") + QString::number(reply.value()); + QDBusConnection::sessionBus().asyncCall(kwinCall(path, QStringLiteral("org.kde.kwin.Script"), QStringLiteral("run"))); + QTimer::singleShot(KeepFileMs, [file] {}); + }); +} diff --git a/src/window/KWinScript.h b/src/window/KWinScript.h new file mode 100644 index 0000000..9ebbd3b --- /dev/null +++ b/src/window/KWinScript.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +// Runs a one-off KWin script (JavaScript) over D-Bus; it unloads itself when +// done. Asynchronous; failures are only logged. +void runKWinScript(const QString &source);