diff --git a/CMakeLists.txt b/CMakeLists.txt index 5ac306c..1c18dde 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) find_package(Qt6 6.8 REQUIRED COMPONENTS Gui Quick Widgets Test DBus WaylandClient) find_package(LayerShellQt REQUIRED) +find_package(KF6WindowSystem REQUIRED) find_package(PkgConfig REQUIRED) pkg_check_modules(XKB REQUIRED IMPORTED_TARGET xkbcommon) pkg_check_modules(WAYLAND REQUIRED IMPORTED_TARGET wayland-client) diff --git a/README.md b/README.md index 9b4cd64..81e1e02 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ iPad-style floating on-screen keyboard for Plasma Wayland. Qt 6, C++ core, QML U - Ctrl/Alt/Super stick like shift (tap, double-tap, hold) and turn keys into shortcuts; a tapped or held shift joins them (Shift+F5, Shift+Tab, Shift+arrows), auto-shift and caps lock don't - 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 +- See-through background (opacity slider in ⚙) with optional blur behind it (KWin's blur effect) ## Build & test @@ -21,7 +22,7 @@ cmake -B build -G Ninja && cmake --build build ctest --test-dir build ``` -Needs Qt ≥ 6.8 (Quick, WaylandClient, DBus, Test), LayerShellQt, xkbcommon, wayland-protocols. +Needs Qt ≥ 6.8 (Quick, WaylandClient, DBus, Test), LayerShellQt, KF6 WindowSystem, xkbcommon, wayland-protocols. ## Run @@ -62,7 +63,7 @@ Everything else is `layouts/template.json`; `~/.local/share/sloposk/template.jso ## Settings -`~/.config/sloposk/sloposk.conf`, also editable from the ⚙ panel: `typing/autoCapitalize`, `typing/doubleSpacePeriod`, `typing/holdKeys`, `layout/visibleSections` (`main` plus section ids), `layout/fullKeyboard`, `window/alwaysVisible`. +`~/.config/sloposk/sloposk.conf`, also editable from the ⚙ panel: `typing/autoCapitalize`, `typing/doubleSpacePeriod`, `typing/holdKeys`, `layout/visibleSections` (`main` plus section ids), `layout/fullKeyboard`, `window/alwaysVisible`, `appearance/backgroundOpacity` (0.1–1), `appearance/blurBackground`. ## Architecture diff --git a/qml/Main.qml b/qml/Main.qml index b0a6dbf..1a5b26a 100644 --- a/qml/Main.qml +++ b/qml/Main.qml @@ -18,6 +18,10 @@ Window { readonly property rect inputRegion: 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 + ? Qt.rect(panel.x, panel.y, panel.width, panel.height) + : Qt.rect(0, 0, 0, 0) width: 1000 height: 340 + popupMargin @@ -27,13 +31,17 @@ Window { color: "transparent" onInputRegionChanged: placement?.setInputRegion(inputRegion) - onPlacementChanged: placement?.setInputRegion(inputRegion) + onBlurRegionChanged: placement?.setBlurRegion(blurRegion, Theme.windowRadius) + onPlacementChanged: { + placement?.setInputRegion(inputRegion); + placement?.setBlurRegion(blurRegion, Theme.windowRadius); + } Rectangle { id: panel anchors { fill: parent; topMargin: root.popupMargin } radius: Theme.windowRadius - color: Theme.background + color: Qt.rgba(Theme.background.r, Theme.background.g, Theme.background.b, root.settings.backgroundOpacity) Keyboard { id: keyboard diff --git a/qml/SettingsPanel.qml b/qml/SettingsPanel.qml index a6e686a..2e9a6b3 100644 --- a/qml/SettingsPanel.qml +++ b/qml/SettingsPanel.qml @@ -1,21 +1,25 @@ import QtQuick import Sloposk -// Replaces the keys while open. Binds straight to Settings, which persists. -Rectangle { +// Replaces the keys while open, over the window's own background so +// opacity changes show live. Binds straight to Settings, which persists. +Flickable { id: panel required property Settings settings required property LayoutModel layoutModel - color: Theme.background - radius: Theme.windowRadius + clip: true + contentHeight: columns.height + 2 * columns.y + boundsBehavior: Flickable.StopAtBounds Row { id: columns readonly property real columnWidth: (width - spacing) / 2 - anchors { fill: parent; margins: 16 } + x: 16 + y: 16 + width: panel.width - 2 * x spacing: 32 Column { @@ -55,6 +59,24 @@ Rectangle { checked: panel.settings.doubleSpacePeriod onToggled: checked => panel.settings.doubleSpacePeriod = checked } + + SectionHeader { text: qsTr("Appearance"); topPadding: 12 } + + SliderRow { + width: parent.width + text: qsTr("Background") + from: 0.1 // 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 { diff --git a/qml/SliderRow.qml b/qml/SliderRow.qml new file mode 100644 index 0000000..c66360b --- /dev/null +++ b/qml/SliderRow.qml @@ -0,0 +1,81 @@ +import QtQuick + +// A labelled slider in the iOS style; value runs from `from` to 1. +Item { + id: row + + required property string text + property real from: 0 + property real value: 1 + signal moved(real value) + + implicitHeight: 52 + + function moveTo(x) { + const fraction = Math.max(0, Math.min(1, (x - knob.width / 2) / (track.width - knob.width))); + row.moved(row.from + fraction * (1 - row.from)); + } + + Text { + id: label + anchors { left: parent.left; verticalCenter: parent.verticalCenter } + width: Math.min(implicitWidth, parent.width / 3) + text: row.text + color: Theme.text + font.pixelSize: 16 + elide: Text.ElideRight + } + + Text { + id: percent + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + width: 44 + horizontalAlignment: Text.AlignRight + text: Math.round(row.value * 100) + "%" + color: Theme.secondaryText + font.pixelSize: 14 + } + + Item { + id: track + anchors { left: label.right; leftMargin: 12; right: percent.left; rightMargin: 8 } + height: parent.height + + readonly property real fraction: (row.value - row.from) / (1 - row.from) + + Rectangle { + anchors { left: parent.left; right: parent.right; verticalCenter: parent.verticalCenter } + height: 4 + radius: 2 + color: Theme.functionKey + + Rectangle { + width: knob.x + knob.width / 2 + height: parent.height + radius: parent.radius + color: Theme.accent + } + } + + Rectangle { + id: knob + x: track.fraction * (track.width - width) + anchors.verticalCenter: parent.verticalCenter + width: 26 + height: width + radius: width / 2 + color: "#ffffff" + border { color: Theme.keyShadow; width: 0.5 } + } + + TapHandler { + onTapped: eventPoint => row.moveTo(eventPoint.position.x) + } + DragHandler { + target: null + xAxis.enabled: true + yAxis.enabled: false + onCentroidChanged: if (active) row.moveTo(centroid.position.x) + } + } +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 64a0d14..fa9de7b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -39,6 +39,7 @@ set(SLOPOSK_QML_FILES SettingsButton.qml SettingsPanel.qml ToggleRow.qml + SliderRow.qml Theme.qml ) list(TRANSFORM SLOPOSK_QML_FILES PREPEND ${PROJECT_SOURCE_DIR}/qml/ OUTPUT_VARIABLE sloposk_qml_paths) @@ -98,6 +99,7 @@ target_link_libraries(sloposk PRIVATE Qt6::DBus Qt6::WaylandClient LayerShellQt::Interface + KF6::WindowSystem PkgConfig::WAYLAND ) diff --git a/src/settings/Settings.cpp b/src/settings/Settings.cpp index 68723d2..50d63e1 100644 --- a/src/settings/Settings.cpp +++ b/src/settings/Settings.cpp @@ -11,6 +11,8 @@ constexpr auto HoldKeysKey = "typing/holdKeys"; constexpr auto VisibleSectionsKey = "layout/visibleSections"; constexpr auto FullKeyboardKey = "layout/fullKeyboard"; constexpr auto AlwaysVisibleKey = "window/alwaysVisible"; +constexpr auto BackgroundOpacityKey = "appearance/backgroundOpacity"; +constexpr auto BlurBackgroundKey = "appearance/blurBackground"; } Settings::Settings(QObject *parent) @@ -26,6 +28,27 @@ Settings::Settings(QObject *parent) m_visibleSections = {MainSectionId}; m_fullKeyboard = store.value(FullKeyboardKey, false).toBool(); m_alwaysVisible = store.value(AlwaysVisibleKey, false).toBool(); + m_backgroundOpacity = qBound(MinBackgroundOpacity, store.value(BackgroundOpacityKey, 1.0).toReal(), 1.0); + m_blurBackground = store.value(BlurBackgroundKey, true).toBool(); +} + +void Settings::setBackgroundOpacity(qreal opacity) +{ + opacity = qBound(MinBackgroundOpacity, opacity, 1.0); + if (qFuzzyCompare(opacity, m_backgroundOpacity)) + return; + m_backgroundOpacity = opacity; + QSettings().setValue(BackgroundOpacityKey, opacity); + Q_EMIT appearanceChanged(); +} + +void Settings::setBlurBackground(bool blur) +{ + if (blur == m_blurBackground) + return; + m_blurBackground = blur; + QSettings().setValue(BlurBackgroundKey, blur); + Q_EMIT appearanceChanged(); } void Settings::setAlwaysVisible(bool always) diff --git a/src/settings/Settings.h b/src/settings/Settings.h index 0bacb5f..906d926 100644 --- a/src/settings/Settings.h +++ b/src/settings/Settings.h @@ -22,6 +22,10 @@ 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) + // Opacity of the panel behind the keys (MinBackgroundOpacity–1); keys stay opaque. + Q_PROPERTY(qreal backgroundOpacity READ backgroundOpacity WRITE setBackgroundOpacity NOTIFY appearanceChanged) + // Blurs what shows through a translucent background (needs compositor support, e.g. KWin). + Q_PROPERTY(bool blurBackground READ blurBackground WRITE setBlurBackground NOTIFY appearanceChanged) public: explicit Settings(QObject *parent = nullptr); @@ -44,11 +48,18 @@ public: bool alwaysVisible() const { return m_alwaysVisible; } void setAlwaysVisible(bool always); + static constexpr qreal MinBackgroundOpacity = 0.1; + qreal backgroundOpacity() const { return m_backgroundOpacity; } + void setBackgroundOpacity(qreal opacity); + bool blurBackground() const { return m_blurBackground; } + void setBlurBackground(bool blur); + Q_SIGNALS: void typingOptionsChanged(); void visibleSectionsChanged(); void fullKeyboardChanged(); void alwaysVisibleChanged(); + void appearanceChanged(); private: void setTypingOptions(const TypingOptions &options); @@ -57,4 +68,6 @@ private: QStringList m_visibleSections; bool m_fullKeyboard = false; bool m_alwaysVisible = false; + qreal m_backgroundOpacity = 1.0; + bool m_blurBackground = true; }; diff --git a/src/window/FloatingWindow.cpp b/src/window/FloatingWindow.cpp index cae9839..95ca263 100644 --- a/src/window/FloatingWindow.cpp +++ b/src/window/FloatingWindow.cpp @@ -1,6 +1,8 @@ #include "FloatingWindow.h" +#include #include +#include #include #include #include @@ -81,6 +83,17 @@ void FloatingWindow::setInputRegion(const QRectF ®ion) m_window->setMask(QRegion(region.toAlignedRect())); } +void FloatingWindow::setBlurRegion(const QRectF ®ion, qreal radius) +{ + if (region.isEmpty()) { + KWindowEffects::enableBlurBehind(m_window, false); + return; + } + QPainterPath path; + path.addRoundedRect(region, radius, radius); + KWindowEffects::enableBlurBehind(m_window, true, QRegion(path.toFillPolygon().toPolygon())); +} + void FloatingWindow::show() { m_window->setVisible(true); diff --git a/src/window/FloatingWindow.h b/src/window/FloatingWindow.h index 9bbf639..79b8e25 100644 --- a/src/window/FloatingWindow.h +++ b/src/window/FloatingWindow.h @@ -28,6 +28,8 @@ public: Q_INVOKABLE void resizeBy(qreal dw, qreal dh); // 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); Q_INVOKABLE void show(); Q_INVOKABLE void hide();