See-through keyboard background with optional blur
- Opacity slider for the panel behind the keys (10–100%); keys stay opaque - Blur Behind: asks the compositor (KWin) to blur under the rounded panel via KF6 WindowSystem; skipped at full opacity - Settings panel draws over the window background so changes show live, and scrolls when it doesn't fit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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(Qt6 6.8 REQUIRED COMPONENTS Gui Quick Widgets Test DBus WaylandClient)
|
||||||
find_package(LayerShellQt REQUIRED)
|
find_package(LayerShellQt REQUIRED)
|
||||||
|
find_package(KF6WindowSystem REQUIRED)
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
pkg_check_modules(XKB REQUIRED IMPORTED_TARGET xkbcommon)
|
pkg_check_modules(XKB REQUIRED IMPORTED_TARGET xkbcommon)
|
||||||
pkg_check_modules(WAYLAND REQUIRED IMPORTED_TARGET wayland-client)
|
pkg_check_modules(WAYLAND REQUIRED IMPORTED_TARGET wayland-client)
|
||||||
|
|||||||
@@ -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
|
- 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 (⚙)
|
- 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
|
- 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
|
## Build & test
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ cmake -B build -G Ninja && cmake --build build
|
|||||||
ctest --test-dir 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
|
## Run
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ Everything else is `layouts/template.json`; `~/.local/share/sloposk/template.jso
|
|||||||
|
|
||||||
## Settings
|
## 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
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -18,6 +18,10 @@ Window {
|
|||||||
readonly property rect inputRegion: keyboard.alternates.open
|
readonly property rect inputRegion: keyboard.alternates.open
|
||||||
? Qt.rect(0, 0, width, height)
|
? Qt.rect(0, 0, width, height)
|
||||||
: Qt.rect(panel.x, panel.y, panel.width, panel.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
|
width: 1000
|
||||||
height: 340 + popupMargin
|
height: 340 + popupMargin
|
||||||
@@ -27,13 +31,17 @@ Window {
|
|||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
onInputRegionChanged: placement?.setInputRegion(inputRegion)
|
onInputRegionChanged: placement?.setInputRegion(inputRegion)
|
||||||
onPlacementChanged: placement?.setInputRegion(inputRegion)
|
onBlurRegionChanged: placement?.setBlurRegion(blurRegion, Theme.windowRadius)
|
||||||
|
onPlacementChanged: {
|
||||||
|
placement?.setInputRegion(inputRegion);
|
||||||
|
placement?.setBlurRegion(blurRegion, Theme.windowRadius);
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: panel
|
id: panel
|
||||||
anchors { fill: parent; topMargin: root.popupMargin }
|
anchors { fill: parent; topMargin: root.popupMargin }
|
||||||
radius: Theme.windowRadius
|
radius: Theme.windowRadius
|
||||||
color: Theme.background
|
color: Qt.rgba(Theme.background.r, Theme.background.g, Theme.background.b, root.settings.backgroundOpacity)
|
||||||
|
|
||||||
Keyboard {
|
Keyboard {
|
||||||
id: keyboard
|
id: keyboard
|
||||||
|
|||||||
+27
-5
@@ -1,21 +1,25 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Sloposk
|
import Sloposk
|
||||||
|
|
||||||
// Replaces the keys while open. Binds straight to Settings, which persists.
|
// Replaces the keys while open, over the window's own background so
|
||||||
Rectangle {
|
// opacity changes show live. Binds straight to Settings, which persists.
|
||||||
|
Flickable {
|
||||||
id: panel
|
id: panel
|
||||||
|
|
||||||
required property Settings settings
|
required property Settings settings
|
||||||
required property LayoutModel layoutModel
|
required property LayoutModel layoutModel
|
||||||
|
|
||||||
color: Theme.background
|
clip: true
|
||||||
radius: Theme.windowRadius
|
contentHeight: columns.height + 2 * columns.y
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: columns
|
id: columns
|
||||||
readonly property real columnWidth: (width - spacing) / 2
|
readonly property real columnWidth: (width - spacing) / 2
|
||||||
|
|
||||||
anchors { fill: parent; margins: 16 }
|
x: 16
|
||||||
|
y: 16
|
||||||
|
width: panel.width - 2 * x
|
||||||
spacing: 32
|
spacing: 32
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -55,6 +59,24 @@ Rectangle {
|
|||||||
checked: panel.settings.doubleSpacePeriod
|
checked: panel.settings.doubleSpacePeriod
|
||||||
onToggled: checked => panel.settings.doubleSpacePeriod = checked
|
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 {
|
Column {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@ set(SLOPOSK_QML_FILES
|
|||||||
SettingsButton.qml
|
SettingsButton.qml
|
||||||
SettingsPanel.qml
|
SettingsPanel.qml
|
||||||
ToggleRow.qml
|
ToggleRow.qml
|
||||||
|
SliderRow.qml
|
||||||
Theme.qml
|
Theme.qml
|
||||||
)
|
)
|
||||||
list(TRANSFORM SLOPOSK_QML_FILES PREPEND ${PROJECT_SOURCE_DIR}/qml/ OUTPUT_VARIABLE sloposk_qml_paths)
|
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::DBus
|
||||||
Qt6::WaylandClient
|
Qt6::WaylandClient
|
||||||
LayerShellQt::Interface
|
LayerShellQt::Interface
|
||||||
|
KF6::WindowSystem
|
||||||
PkgConfig::WAYLAND
|
PkgConfig::WAYLAND
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ constexpr auto HoldKeysKey = "typing/holdKeys";
|
|||||||
constexpr auto VisibleSectionsKey = "layout/visibleSections";
|
constexpr auto VisibleSectionsKey = "layout/visibleSections";
|
||||||
constexpr auto FullKeyboardKey = "layout/fullKeyboard";
|
constexpr auto FullKeyboardKey = "layout/fullKeyboard";
|
||||||
constexpr auto AlwaysVisibleKey = "window/alwaysVisible";
|
constexpr auto AlwaysVisibleKey = "window/alwaysVisible";
|
||||||
|
constexpr auto BackgroundOpacityKey = "appearance/backgroundOpacity";
|
||||||
|
constexpr auto BlurBackgroundKey = "appearance/blurBackground";
|
||||||
}
|
}
|
||||||
|
|
||||||
Settings::Settings(QObject *parent)
|
Settings::Settings(QObject *parent)
|
||||||
@@ -26,6 +28,27 @@ Settings::Settings(QObject *parent)
|
|||||||
m_visibleSections = {MainSectionId};
|
m_visibleSections = {MainSectionId};
|
||||||
m_fullKeyboard = store.value(FullKeyboardKey, false).toBool();
|
m_fullKeyboard = store.value(FullKeyboardKey, false).toBool();
|
||||||
m_alwaysVisible = store.value(AlwaysVisibleKey, 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)
|
void Settings::setAlwaysVisible(bool always)
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ class Settings : public QObject
|
|||||||
Q_PROPERTY(bool fullKeyboard READ fullKeyboard WRITE setFullKeyboard NOTIFY fullKeyboardChanged)
|
Q_PROPERTY(bool fullKeyboard READ fullKeyboard WRITE setFullKeyboard NOTIFY fullKeyboardChanged)
|
||||||
// Shown even without a focused text field, to type into any window.
|
// Shown even without a focused text field, to type into any window.
|
||||||
Q_PROPERTY(bool alwaysVisible READ alwaysVisible WRITE setAlwaysVisible NOTIFY alwaysVisibleChanged)
|
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:
|
public:
|
||||||
explicit Settings(QObject *parent = nullptr);
|
explicit Settings(QObject *parent = nullptr);
|
||||||
@@ -44,11 +48,18 @@ public:
|
|||||||
bool alwaysVisible() const { return m_alwaysVisible; }
|
bool alwaysVisible() const { return m_alwaysVisible; }
|
||||||
void setAlwaysVisible(bool always);
|
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:
|
Q_SIGNALS:
|
||||||
void typingOptionsChanged();
|
void typingOptionsChanged();
|
||||||
void visibleSectionsChanged();
|
void visibleSectionsChanged();
|
||||||
void fullKeyboardChanged();
|
void fullKeyboardChanged();
|
||||||
void alwaysVisibleChanged();
|
void alwaysVisibleChanged();
|
||||||
|
void appearanceChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setTypingOptions(const TypingOptions &options);
|
void setTypingOptions(const TypingOptions &options);
|
||||||
@@ -57,4 +68,6 @@ private:
|
|||||||
QStringList m_visibleSections;
|
QStringList m_visibleSections;
|
||||||
bool m_fullKeyboard = false;
|
bool m_fullKeyboard = false;
|
||||||
bool m_alwaysVisible = false;
|
bool m_alwaysVisible = false;
|
||||||
|
qreal m_backgroundOpacity = 1.0;
|
||||||
|
bool m_blurBackground = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#include "FloatingWindow.h"
|
#include "FloatingWindow.h"
|
||||||
|
|
||||||
|
#include <KWindowEffects>
|
||||||
#include <LayerShellQt/Window>
|
#include <LayerShellQt/Window>
|
||||||
|
#include <QPainterPath>
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
#include <QWindow>
|
#include <QWindow>
|
||||||
@@ -81,6 +83,17 @@ void FloatingWindow::setInputRegion(const QRectF ®ion)
|
|||||||
m_window->setMask(QRegion(region.toAlignedRect()));
|
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()
|
void FloatingWindow::show()
|
||||||
{
|
{
|
||||||
m_window->setVisible(true);
|
m_window->setVisible(true);
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ public:
|
|||||||
Q_INVOKABLE void resizeBy(qreal dw, qreal dh);
|
Q_INVOKABLE void resizeBy(qreal dw, qreal dh);
|
||||||
// Only this part of the window takes input; the rest passes through.
|
// Only this part of the window takes input; the rest passes through.
|
||||||
Q_INVOKABLE void setInputRegion(const QRectF ®ion);
|
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 show();
|
||||||
Q_INVOKABLE void hide();
|
Q_INVOKABLE void hide();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user