Shrink to a floating button instead of hiding
With "Shrink to Button" on, the dismiss key shrinks the window to just itself: a see-through button that can be dragged anywhere. Tapping it grows the keyboard back around it, kept on screen. KWin moves and resizes the window through a one-off script, as Wayland clients can't place themselves. Also moves the window back on screen whenever it appears (KWin remembers where the button was), and releases the mouse after a window manager move or resize, which used to eat the next press on the grab bar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
- 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 (⚙)
|
- 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
|
||||||
|
- "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
|
- 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)
|
- See-through background (opacity slider in ⚙) with optional blur behind it (KWin's blur effect)
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,18 @@ import Sloposk
|
|||||||
KeyboardSurface {
|
KeyboardSurface {
|
||||||
id: surface
|
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 {
|
Repeater {
|
||||||
|
id: keys
|
||||||
model: surface.model
|
model: surface.model
|
||||||
delegate: Key {
|
delegate: Key {
|
||||||
shiftMode: surface.controller ? surface.controller.shiftMode : KeyboardController.Off
|
shiftMode: surface.controller ? surface.controller.shiftMode : KeyboardController.Off
|
||||||
|
|||||||
+30
-6
@@ -9,6 +9,8 @@ Window {
|
|||||||
required property Settings settings
|
required property Settings settings
|
||||||
required property Diagnostics diagnostics
|
required property Diagnostics diagnostics
|
||||||
property var placement: null // FloatingWindow, set from C++
|
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
|
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.
|
||||||
readonly property real keysHeight: keyboard.height
|
readonly property real keysHeight: keyboard.height
|
||||||
@@ -16,23 +18,31 @@ Window {
|
|||||||
// Transparent room above the keyboard for long-press popups on the top row.
|
// Transparent room above the keyboard for long-press popups on the top row.
|
||||||
readonly property real popupMargin: 90
|
readonly property real popupMargin: 90
|
||||||
// Outside this, input passes through to whatever is below the window.
|
// 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(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.
|
// 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(panel.x, panel.y, panel.width, panel.height)
|
||||||
: Qt.rect(0, 0, 0, 0)
|
: Qt.rect(0, 0, 0, 0)
|
||||||
|
|
||||||
width: 1000
|
width: 1000
|
||||||
height: 340 + popupMargin
|
height: 340 + popupMargin
|
||||||
minimumWidth: 480
|
// None while shrunk to the floating button.
|
||||||
minimumHeight: 180 + popupMargin
|
minimumWidth: collapsed ? 0 : 480
|
||||||
|
minimumHeight: collapsed ? 0 : 180 + popupMargin
|
||||||
visible: false
|
visible: false
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
onInputRegionChanged: placement?.setInputRegion(inputRegion)
|
onInputRegionChanged: placement?.setInputRegion(inputRegion)
|
||||||
onBlurRegionChanged: placement?.setBlurRegion(blurRegion, Theme.windowRadius)
|
onBlurRegionChanged: placement?.setBlurRegion(blurRegion, Theme.windowRadius)
|
||||||
|
// For CollapseController.
|
||||||
|
function dismissKeyRect() {
|
||||||
|
return keyboard.keyRect("dismiss");
|
||||||
|
}
|
||||||
|
|
||||||
onPlacementChanged: {
|
onPlacementChanged: {
|
||||||
placement?.setInputRegion(inputRegion);
|
placement?.setInputRegion(inputRegion);
|
||||||
placement?.setBlurRegion(blurRegion, Theme.windowRadius);
|
placement?.setBlurRegion(blurRegion, Theme.windowRadius);
|
||||||
@@ -41,6 +51,7 @@ Window {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
id: panel
|
id: panel
|
||||||
anchors { fill: parent; topMargin: root.popupMargin }
|
anchors { fill: parent; topMargin: root.popupMargin }
|
||||||
|
visible: !root.collapsed
|
||||||
radius: Theme.windowRadius
|
radius: Theme.windowRadius
|
||||||
color: Qt.rgba(Theme.background.r, Theme.background.g, Theme.background.b, root.settings.backgroundOpacity)
|
color: Qt.rgba(Theme.background.r, Theme.background.g, Theme.background.b, root.settings.backgroundOpacity)
|
||||||
|
|
||||||
@@ -66,7 +77,7 @@ Window {
|
|||||||
id: handle
|
id: handle
|
||||||
anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
|
anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
|
||||||
pillWidth: Math.min(120, width - 2 * (quickToggles.x + quickToggles.width + 12))
|
pillWidth: Math.min(120, width - 2 * (quickToggles.x + quickToggles.width + 12))
|
||||||
onDragStarted: root.startSystemMove()
|
onDragStarted: root.placement.startMove()
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsButton {
|
SettingsButton {
|
||||||
@@ -86,7 +97,20 @@ Window {
|
|||||||
|
|
||||||
ResizeHandle {
|
ResizeHandle {
|
||||||
anchors { right: parent.right; bottom: parent.bottom }
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,14 @@ Flickable {
|
|||||||
onToggled: checked => panel.settings.alwaysVisible = checked
|
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 {
|
ToggleRow {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: qsTr("“.” Shortcut")
|
text: qsTr("“.” Shortcut")
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ set(SLOPOSK_QML_FILES
|
|||||||
Key.qml
|
Key.qml
|
||||||
AlternatesPopup.qml
|
AlternatesPopup.qml
|
||||||
DragHandle.qml
|
DragHandle.qml
|
||||||
|
FloatingButton.qml
|
||||||
ResizeHandle.qml
|
ResizeHandle.qml
|
||||||
SectionHeader.qml
|
SectionHeader.qml
|
||||||
SettingsButton.qml
|
SettingsButton.qml
|
||||||
@@ -91,7 +92,9 @@ qt_add_executable(sloposk
|
|||||||
backend/KWinInputContext.h backend/KWinInputContext.cpp
|
backend/KWinInputContext.h backend/KWinInputContext.cpp
|
||||||
backend/KWinFakeInput.h backend/KWinFakeInput.cpp
|
backend/KWinFakeInput.h backend/KWinFakeInput.cpp
|
||||||
backend/WaylandKeymap.h backend/WaylandKeymap.cpp
|
backend/WaylandKeymap.h backend/WaylandKeymap.cpp
|
||||||
|
window/CollapseController.h window/CollapseController.cpp
|
||||||
window/FloatingWindow.h window/FloatingWindow.cpp
|
window/FloatingWindow.h window/FloatingWindow.cpp
|
||||||
|
window/KWinScript.h window/KWinScript.cpp
|
||||||
window/KWinRule.h window/KWinRule.cpp
|
window/KWinRule.h window/KWinRule.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+23
-7
@@ -13,6 +13,7 @@
|
|||||||
#include "layout/LayoutLoader.h"
|
#include "layout/LayoutLoader.h"
|
||||||
#include "layout/LayoutModel.h"
|
#include "layout/LayoutModel.h"
|
||||||
#include "settings/Settings.h"
|
#include "settings/Settings.h"
|
||||||
|
#include "window/CollapseController.h"
|
||||||
#include "window/FloatingWindow.h"
|
#include "window/FloatingWindow.h"
|
||||||
#include "window/KWinRule.h"
|
#include "window/KWinRule.h"
|
||||||
#include "window/VisibilityController.h"
|
#include "window/VisibilityController.h"
|
||||||
@@ -82,12 +83,15 @@ void connectKeySettings(Settings &settings, LayoutModel &model, FloatingWindow &
|
|||||||
[keepingKeySize, rebuild] { keepingKeySize(rebuild); });
|
[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()))
|
if (const auto geometry = settings.load(placement.screenName()))
|
||||||
placement.setGeometry(*geometry);
|
placement.setGeometry(*geometry);
|
||||||
QObject::connect(&placement, &FloatingWindow::geometryChanged, &settings,
|
QObject::connect(&placement, &FloatingWindow::geometryChanged, &settings, [&] {
|
||||||
[&] { settings.save(placement.screenName(), placement.geometry()); });
|
if (!collapse.isCollapsed())
|
||||||
|
settings.save(placement.screenName(), placement.geometry());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// A focused field shows the keyboard only if KWin's policy wants one now;
|
// 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,
|
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.
|
// First, so the initial state below reaches the window too.
|
||||||
QObject::connect(&visibility, &VisibilityController::visibleChanged, &placement,
|
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.
|
// work) there is no focus signal: stay visible so it can be tried out.
|
||||||
const auto updateStandalone = [&] { visibility.setStandalone(!backend.isAvailable()); };
|
const auto updateStandalone = [&] { visibility.setStandalone(!backend.isAvailable()); };
|
||||||
QObject::connect(&backend, &KWinInputMethod::availableChanged, &visibility, updateStandalone);
|
QObject::connect(&backend, &KWinInputMethod::availableChanged, &visibility, updateStandalone);
|
||||||
QObject::connect(&controller, &KeyboardController::dismissRequested, &visibility, &VisibilityController::hide);
|
|
||||||
updateStandalone();
|
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)
|
void connectTextContext(KWinInputMethod &backend, KeyboardController &controller)
|
||||||
{
|
{
|
||||||
QObject::connect(&backend, &TextBackend::activeChanged, &controller,
|
QObject::connect(&backend, &TextBackend::activeChanged, &controller,
|
||||||
@@ -191,14 +204,17 @@ int main(int argc, char *argv[])
|
|||||||
installKWinRule(QGuiApplication::desktopFileName());
|
installKWinRule(QGuiApplication::desktopFileName());
|
||||||
FloatingWindow placement(window);
|
FloatingWindow placement(window);
|
||||||
window->setProperty("placement", QVariant::fromValue(&placement));
|
window->setProperty("placement", QVariant::fromValue(&placement));
|
||||||
|
CollapseController collapse(*window, placement);
|
||||||
|
window->setProperty("collapse", QVariant::fromValue(&collapse));
|
||||||
WindowSettings windowSettings;
|
WindowSettings windowSettings;
|
||||||
restoreAndPersistGeometry(placement, windowSettings);
|
restoreAndPersistGeometry(placement, windowSettings, collapse);
|
||||||
connectKeySettings(settings, model, placement, *window, rebuildLayout);
|
connectKeySettings(settings, model, placement, *window, rebuildLayout);
|
||||||
|
|
||||||
VisibilityController visibility;
|
VisibilityController visibility;
|
||||||
KWinVirtualKeyboard virtualKeyboardPolicy;
|
KWinVirtualKeyboard virtualKeyboardPolicy;
|
||||||
connectFieldFocus(visibility, backend, virtualKeyboardPolicy);
|
connectFieldFocus(visibility, backend, virtualKeyboardPolicy);
|
||||||
connectVisibility(visibility, backend, settings, controller, placement);
|
connectVisibility(visibility, backend, settings, placement);
|
||||||
|
connectDismiss(controller, settings, collapse, visibility);
|
||||||
|
|
||||||
DBusControl dbus(visibility);
|
DBusControl dbus(visibility);
|
||||||
reportDBusControl(diagnostics, dbus.registerOnSessionBus());
|
reportDBusControl(diagnostics, dbus.registerOnSessionBus());
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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 CollapseToButtonKey = "window/collapseToButton";
|
||||||
constexpr auto BackgroundOpacityKey = "appearance/backgroundOpacity";
|
constexpr auto BackgroundOpacityKey = "appearance/backgroundOpacity";
|
||||||
constexpr auto BlurBackgroundKey = "appearance/blurBackground";
|
constexpr auto BlurBackgroundKey = "appearance/blurBackground";
|
||||||
}
|
}
|
||||||
@@ -28,6 +29,7 @@ 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_collapseToButton = store.value(CollapseToButtonKey, false).toBool();
|
||||||
m_backgroundOpacity = qBound(MinBackgroundOpacity, store.value(BackgroundOpacityKey, 1.0).toReal(), 1.0);
|
m_backgroundOpacity = qBound(MinBackgroundOpacity, store.value(BackgroundOpacityKey, 1.0).toReal(), 1.0);
|
||||||
m_blurBackground = store.value(BlurBackgroundKey, true).toBool();
|
m_blurBackground = store.value(BlurBackgroundKey, true).toBool();
|
||||||
}
|
}
|
||||||
@@ -60,6 +62,15 @@ void Settings::setAlwaysVisible(bool always)
|
|||||||
Q_EMIT alwaysVisibleChanged();
|
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)
|
void Settings::setFullKeyboard(bool full)
|
||||||
{
|
{
|
||||||
if (full == m_fullKeyboard)
|
if (full == m_fullKeyboard)
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ 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)
|
||||||
|
// 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.
|
// 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 backgroundOpacity READ backgroundOpacity WRITE setBackgroundOpacity NOTIFY appearanceChanged)
|
||||||
Q_PROPERTY(qreal minBackgroundOpacity READ minBackgroundOpacity CONSTANT)
|
Q_PROPERTY(qreal minBackgroundOpacity READ minBackgroundOpacity CONSTANT)
|
||||||
@@ -49,6 +51,8 @@ public:
|
|||||||
void setFullKeyboard(bool full);
|
void setFullKeyboard(bool full);
|
||||||
bool alwaysVisible() const { return m_alwaysVisible; }
|
bool alwaysVisible() const { return m_alwaysVisible; }
|
||||||
void setAlwaysVisible(bool always);
|
void setAlwaysVisible(bool always);
|
||||||
|
bool collapseToButton() const { return m_collapseToButton; }
|
||||||
|
void setCollapseToButton(bool collapse);
|
||||||
|
|
||||||
static constexpr qreal MinBackgroundOpacity = 0.1;
|
static constexpr qreal MinBackgroundOpacity = 0.1;
|
||||||
static qreal minBackgroundOpacity() { return MinBackgroundOpacity; }
|
static qreal minBackgroundOpacity() { return MinBackgroundOpacity; }
|
||||||
@@ -62,6 +66,7 @@ Q_SIGNALS:
|
|||||||
void visibleSectionsChanged();
|
void visibleSectionsChanged();
|
||||||
void fullKeyboardChanged();
|
void fullKeyboardChanged();
|
||||||
void alwaysVisibleChanged();
|
void alwaysVisibleChanged();
|
||||||
|
void collapseToButtonChanged();
|
||||||
void appearanceChanged();
|
void appearanceChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -72,6 +77,7 @@ private:
|
|||||||
QStringList m_visibleSections;
|
QStringList m_visibleSections;
|
||||||
bool m_fullKeyboard = false;
|
bool m_fullKeyboard = false;
|
||||||
bool m_alwaysVisible = false;
|
bool m_alwaysVisible = false;
|
||||||
|
bool m_collapseToButton = false;
|
||||||
qreal m_backgroundOpacity = 1.0;
|
qreal m_backgroundOpacity = 1.0;
|
||||||
bool m_blurBackground = true;
|
bool m_blurBackground = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#include "CollapseController.h"
|
||||||
|
|
||||||
|
#include "FloatingWindow.h"
|
||||||
|
|
||||||
|
#include <QQuickWindow>
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QRectF>
|
||||||
|
#include <QSize>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
#include "FloatingWindow.h"
|
#include "FloatingWindow.h"
|
||||||
|
|
||||||
|
#include "KWinScript.h"
|
||||||
|
|
||||||
#include <KWindowEffects>
|
#include <KWindowEffects>
|
||||||
|
#include <QCursor>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QMouseEvent>
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
#include <QWindow>
|
#include <QWindow>
|
||||||
@@ -12,6 +17,18 @@ FloatingWindow::FloatingWindow(QWindow *window, QObject *parent)
|
|||||||
window->setFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus);
|
window->setFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus);
|
||||||
for (const auto changed : {&QWindow::xChanged, &QWindow::yChanged, &QWindow::widthChanged, &QWindow::heightChanged})
|
for (const auto changed : {&QWindow::xChanged, &QWindow::yChanged, &QWindow::widthChanged, &QWindow::heightChanged})
|
||||||
connect(window, changed, this, &FloatingWindow::geometryChanged);
|
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
|
QRect FloatingWindow::geometry() const
|
||||||
@@ -38,6 +55,30 @@ void FloatingWindow::resizeBy(qreal dw, qreal dh)
|
|||||||
m_window->resize(size);
|
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)
|
void FloatingWindow::setInputRegion(const QRectF ®ion)
|
||||||
{
|
{
|
||||||
m_window->setMask(QRegion(region.toAlignedRect()));
|
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()));
|
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()
|
void FloatingWindow::show()
|
||||||
{
|
{
|
||||||
m_window->setVisible(true);
|
m_window->setVisible(true);
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ class QWindow;
|
|||||||
// Makes a keyboard window a frameless, always-on-top window that never takes
|
// 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);
|
// focus. The window manager places it and moves it (Super+drag included);
|
||||||
// on KWin Wayland that relies on the rule from installKWinRule().
|
// 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
|
class FloatingWindow : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -25,18 +27,30 @@ public:
|
|||||||
|
|
||||||
// Grows from the top-left corner; respects the window's minimum size.
|
// Grows from the top-left corner; respects the window's minimum size.
|
||||||
Q_INVOKABLE void resizeBy(qreal dw, qreal dh);
|
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.
|
// 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.
|
// 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 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 show();
|
||||||
Q_INVOKABLE void hide();
|
Q_INVOKABLE void hide();
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void geometryChanged();
|
void geometryChanged();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QRect screenArea() const;
|
QRect screenArea() const;
|
||||||
|
void releaseMouse();
|
||||||
|
|
||||||
QWindow *m_window;
|
QWindow *m_window;
|
||||||
|
bool m_exposed = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#include "KWinScript.h"
|
||||||
|
|
||||||
|
#include <QDBusConnection>
|
||||||
|
#include <QDBusMessage>
|
||||||
|
#include <QDBusPendingCallWatcher>
|
||||||
|
#include <QDBusPendingReply>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QTemporaryFile>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
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<QTemporaryFile>(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<int> 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] {});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
// 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);
|
||||||
Reference in New Issue
Block a user