Floating polish: visibility, resize, saved geometry, D-Bus, desktop entry
- VisibilityController: show on field focus, dismiss until next focus, manual show/hide/toggle overrides - FloatingWindow: resize from the corner grip, clamp to screen - WindowSettings: geometry per screen, debounced saves - DBusControl: org.sloposk.Keyboard Show/Hide/Toggle for global shortcuts - org.sloposk.keyboard.desktop so System Settings lists it as a virtual keyboard - README with build/run/layout docs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -5,7 +5,7 @@ set(CMAKE_CXX_STANDARD 20)
|
|||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
find_package(Qt6 6.8 REQUIRED COMPONENTS Gui Quick Widgets Test WaylandClient)
|
find_package(Qt6 6.8 REQUIRED COMPONENTS Gui Quick Widgets Test DBus WaylandClient)
|
||||||
find_package(LayerShellQt REQUIRED)
|
find_package(LayerShellQt 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)
|
||||||
@@ -13,6 +13,7 @@ pkg_check_modules(WAYLAND REQUIRED IMPORTED_TARGET wayland-client)
|
|||||||
pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)
|
pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)
|
||||||
|
|
||||||
qt_standard_project_setup(REQUIRES 6.8)
|
qt_standard_project_setup(REQUIRES 6.8)
|
||||||
|
include(GNUInstallDirs)
|
||||||
|
|
||||||
enable_testing()
|
enable_testing()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# sloposk
|
||||||
|
|
||||||
|
iPad-style floating on-screen keyboard for Plasma Wayland. Qt 6, C++ core, QML UI.
|
||||||
|
|
||||||
|
- Flick a key down for its secondary symbol; long-press for accented alternates
|
||||||
|
- iOS shift: tap = one letter, double-tap = caps lock, hold = shift while held
|
||||||
|
- Auto-capitalization, double-space → ". ", backspace repeat → word delete
|
||||||
|
- Long-press space = trackpad; slide from `.?123` onto a symbol and release
|
||||||
|
- Floating: drag the bottom bar, resize from the corner; geometry is remembered per screen
|
||||||
|
|
||||||
|
## Build & test
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -B build -G Ninja && cmake --build build
|
||||||
|
ctest --test-dir build
|
||||||
|
```
|
||||||
|
|
||||||
|
Needs Qt ≥ 6.8 (Quick, WaylandClient, DBus, Test), LayerShellQt, xkbcommon, wayland-protocols.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
KWin only offers the input-method protocol to the process it launches itself.
|
||||||
|
|
||||||
|
| Where | How |
|
||||||
|
|---|---|
|
||||||
|
| Nested session (safe) | `kwin_wayland --no-lockscreen --socket sloposk-dev --width 1400 --height 900 --inputmethod $PWD/build/src/sloposk kwrite` |
|
||||||
|
| Desktop | `cmake --install build --prefix ~/.local`, then System Settings → Keyboard → Virtual Keyboard → **sloposk** |
|
||||||
|
| UI only | `build/src/sloposk` (stays visible, types nowhere) |
|
||||||
|
|
||||||
|
Show/hide, e.g. from a global shortcut: `qdbus org.sloposk.Keyboard /Keyboard org.sloposk.Keyboard.Toggle`
|
||||||
|
|
||||||
|
Dev hooks: `SLOPOSK_AUTOTYPE=text` commits on focus; `SLOPOSK_SCREENSHOT=file.png` saves the window and quits.
|
||||||
|
|
||||||
|
## Layouts
|
||||||
|
|
||||||
|
JSON in `layouts/` (built in) or `~/.local/share/sloposk/layouts/` (overrides by `id`). The globe key cycles them; the system locale's comes first.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "sv-SE", "name": "Svenska", "locale": "sv_SE",
|
||||||
|
"base": "en-US",
|
||||||
|
"layers": [{"name": "letters", "rows": [[
|
||||||
|
"q",
|
||||||
|
{"label": "e", "flick": "3", "alternates": ["é", "è"]},
|
||||||
|
{"type": "backspace", "width": 1.5}
|
||||||
|
]]}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Key: a string, or an object with `type` (`char` `space` `backspace` `enter` `shift` `layer` `globe` `dismiss` `spacer`), `label`, `shifted`, `flick`, `alternates`, `width` (key units), `target` (for `layer`)
|
||||||
|
- `base`: take layers this layout doesn't define (e.g. numbers/symbols) from another layout
|
||||||
|
- Rows in a layer should add up to the same width; `tst_layoutloader` checks the built-ins
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
qml/ view only
|
||||||
|
input/ touch → PointerTracker gestures → KeyboardController rules
|
||||||
|
engine/ key output → text ops (DirectEngine; prediction/swipe later)
|
||||||
|
backend/ text ops → compositor (KWin input-method-v1)
|
||||||
|
layout/ JSON → Layout → LayoutModel (geometry, hit testing)
|
||||||
|
window/ floating layer-shell placement, visibility, saved geometry
|
||||||
|
app/ D-Bus control, dev hooks
|
||||||
|
```
|
||||||
|
|
||||||
|
`sloposk_core` (everything compositor-independent, plus the `Sloposk` QML module) is a static library shared by the app and the tests.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=sloposk
|
||||||
|
Comment=iPad-style floating on-screen keyboard
|
||||||
|
Exec=sloposk
|
||||||
|
Type=Application
|
||||||
|
Icon=input-keyboard-virtual
|
||||||
|
NoDisplay=true
|
||||||
|
X-KDE-Wayland-VirtualKeyboard=true
|
||||||
@@ -17,6 +17,8 @@ Window {
|
|||||||
|
|
||||||
width: 1000
|
width: 1000
|
||||||
height: 340 + popupMargin
|
height: 340 + popupMargin
|
||||||
|
minimumWidth: 480
|
||||||
|
minimumHeight: 180 + popupMargin
|
||||||
visible: false
|
visible: false
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
@@ -42,5 +44,10 @@ Window {
|
|||||||
anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
|
anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
|
||||||
onMoved: (dx, dy) => root.placement?.moveBy(dx, dy)
|
onMoved: (dx, dy) => root.placement?.moveBy(dx, dy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ResizeHandle {
|
||||||
|
anchors { right: parent.right; bottom: parent.bottom }
|
||||||
|
onResized: (dw, dh) => root.placement?.resizeBy(dw, dh)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
// Corner grip. The window grows from its top-left corner, so window
|
||||||
|
// coordinates stay put while resizing: report each step's delta.
|
||||||
|
Item {
|
||||||
|
id: grip
|
||||||
|
|
||||||
|
signal resized(real dw, real dh)
|
||||||
|
|
||||||
|
implicitWidth: 28
|
||||||
|
implicitHeight: 28
|
||||||
|
|
||||||
|
// Two strokes across the corner, each from (w - d, h) to (w, h - d).
|
||||||
|
Repeater {
|
||||||
|
model: [9, 16]
|
||||||
|
Rectangle {
|
||||||
|
required property real modelData
|
||||||
|
readonly property real inset: 4
|
||||||
|
|
||||||
|
width: modelData * Math.SQRT2
|
||||||
|
height: 2
|
||||||
|
radius: 1
|
||||||
|
color: Theme.handle
|
||||||
|
rotation: -45
|
||||||
|
x: grip.width - inset - modelData / 2 - width / 2
|
||||||
|
y: grip.height - inset - modelData / 2 - height / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DragHandler {
|
||||||
|
property point last
|
||||||
|
|
||||||
|
target: null
|
||||||
|
cursorShape: Qt.SizeFDiagCursor
|
||||||
|
onActiveChanged: last = centroid.scenePressPosition
|
||||||
|
onCentroidChanged: {
|
||||||
|
if (!active)
|
||||||
|
return
|
||||||
|
const p = centroid.scenePosition
|
||||||
|
grip.resized(p.x - last.x, p.y - last.y)
|
||||||
|
last = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-1
@@ -17,6 +17,8 @@ qt_add_library(sloposk_core STATIC
|
|||||||
layout/LayoutRepository.h layout/LayoutRepository.cpp
|
layout/LayoutRepository.h layout/LayoutRepository.cpp
|
||||||
layout/KeyGeometry.h layout/KeyGeometry.cpp
|
layout/KeyGeometry.h layout/KeyGeometry.cpp
|
||||||
layout/LayoutModel.h layout/LayoutModel.cpp
|
layout/LayoutModel.h layout/LayoutModel.cpp
|
||||||
|
window/VisibilityController.h window/VisibilityController.cpp
|
||||||
|
window/WindowSettings.h window/WindowSettings.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
# QML lives in the top-level qml/ dir; alias each file so the module sees it at its root.
|
# QML lives in the top-level qml/ dir; alias each file so the module sees it at its root.
|
||||||
@@ -26,6 +28,7 @@ set(SLOPOSK_QML_FILES
|
|||||||
Key.qml
|
Key.qml
|
||||||
AlternatesPopup.qml
|
AlternatesPopup.qml
|
||||||
DragHandle.qml
|
DragHandle.qml
|
||||||
|
ResizeHandle.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)
|
||||||
@@ -59,7 +62,8 @@ target_link_libraries(sloposk_core PUBLIC Qt6::Quick PkgConfig::XKB)
|
|||||||
|
|
||||||
qt_add_executable(sloposk
|
qt_add_executable(sloposk
|
||||||
main.cpp
|
main.cpp
|
||||||
DevHooks.h DevHooks.cpp
|
app/DevHooks.h app/DevHooks.cpp
|
||||||
|
app/DBusControl.h app/DBusControl.cpp
|
||||||
backend/InputMethodProtocol.h
|
backend/InputMethodProtocol.h
|
||||||
backend/KWinInputMethod.h backend/KWinInputMethod.cpp
|
backend/KWinInputMethod.h backend/KWinInputMethod.cpp
|
||||||
backend/KWinInputContext.h backend/KWinInputContext.cpp
|
backend/KWinInputContext.h backend/KWinInputContext.cpp
|
||||||
@@ -76,9 +80,11 @@ set_source_files_properties(${CMAKE_CURRENT_BINARY_DIR}/qwayland-input-method-un
|
|||||||
target_link_libraries(sloposk PRIVATE
|
target_link_libraries(sloposk PRIVATE
|
||||||
sloposk_core
|
sloposk_core
|
||||||
sloposk_coreplugin
|
sloposk_coreplugin
|
||||||
|
Qt6::DBus
|
||||||
Qt6::WaylandClient
|
Qt6::WaylandClient
|
||||||
LayerShellQt::Interface
|
LayerShellQt::Interface
|
||||||
PkgConfig::WAYLAND
|
PkgConfig::WAYLAND
|
||||||
)
|
)
|
||||||
|
|
||||||
install(TARGETS sloposk)
|
install(TARGETS sloposk)
|
||||||
|
install(FILES ${PROJECT_SOURCE_DIR}/data/org.sloposk.keyboard.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#include "DBusControl.h"
|
||||||
|
|
||||||
|
#include "window/VisibilityController.h"
|
||||||
|
|
||||||
|
#include <QDBusConnection>
|
||||||
|
|
||||||
|
DBusControl::DBusControl(VisibilityController &visibility, QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_visibility(visibility)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DBusControl::registerOnSessionBus()
|
||||||
|
{
|
||||||
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
return bus.registerService(QStringLiteral("org.sloposk.Keyboard"))
|
||||||
|
&& bus.registerObject(QStringLiteral("/Keyboard"), this, QDBusConnection::ExportScriptableSlots);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DBusControl::Show() { m_visibility.show(); }
|
||||||
|
void DBusControl::Hide() { m_visibility.hide(); }
|
||||||
|
void DBusControl::Toggle() { m_visibility.toggle(); }
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
class VisibilityController;
|
||||||
|
|
||||||
|
// Session-bus remote control, e.g. for a global shortcut:
|
||||||
|
// qdbus org.sloposk.Keyboard /Keyboard org.sloposk.Keyboard.Toggle
|
||||||
|
class DBusControl : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_CLASSINFO("D-Bus Interface", "org.sloposk.Keyboard")
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DBusControl(VisibilityController &visibility, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
// False if the bus is unavailable or another instance owns the name.
|
||||||
|
bool registerOnSessionBus();
|
||||||
|
|
||||||
|
public Q_SLOTS:
|
||||||
|
Q_SCRIPTABLE void Show();
|
||||||
|
Q_SCRIPTABLE void Hide();
|
||||||
|
Q_SCRIPTABLE void Toggle();
|
||||||
|
|
||||||
|
private:
|
||||||
|
VisibilityController &m_visibility;
|
||||||
|
};
|
||||||
+43
-17
@@ -1,10 +1,13 @@
|
|||||||
#include "DevHooks.h"
|
#include "app/DBusControl.h"
|
||||||
|
#include "app/DevHooks.h"
|
||||||
#include "backend/KWinInputMethod.h"
|
#include "backend/KWinInputMethod.h"
|
||||||
#include "engine/DirectEngine.h"
|
#include "engine/DirectEngine.h"
|
||||||
#include "input/KeyboardController.h"
|
#include "input/KeyboardController.h"
|
||||||
#include "layout/LayoutModel.h"
|
#include "layout/LayoutModel.h"
|
||||||
#include "layout/LayoutRepository.h"
|
#include "layout/LayoutRepository.h"
|
||||||
#include "window/FloatingWindow.h"
|
#include "window/FloatingWindow.h"
|
||||||
|
#include "window/VisibilityController.h"
|
||||||
|
#include "window/WindowSettings.h"
|
||||||
|
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QLocale>
|
#include <QLocale>
|
||||||
@@ -26,6 +29,36 @@ void loadLayouts(LayoutRepository &layouts)
|
|||||||
layouts.finalize(QLocale::system().name());
|
layouts.finalize(QLocale::system().name());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void restoreAndPersistGeometry(FloatingWindow &placement, WindowSettings &settings)
|
||||||
|
{
|
||||||
|
if (const auto geometry = settings.load(placement.screenName()))
|
||||||
|
placement.setGeometry(*geometry);
|
||||||
|
QObject::connect(&placement, &FloatingWindow::geometryChanged, &settings,
|
||||||
|
[&] { settings.save(placement.screenName(), placement.geometry()); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void connectVisibility(VisibilityController &visibility, KWinInputMethod &backend,
|
||||||
|
KeyboardController &controller, FloatingWindow &placement)
|
||||||
|
{
|
||||||
|
// Without the input-method global (run outside KWin's control, e.g. for UI
|
||||||
|
// 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(&backend, &TextBackend::activeChanged, &visibility,
|
||||||
|
[&] { visibility.setFieldActive(backend.isActive()); });
|
||||||
|
QObject::connect(&controller, &KeyboardController::dismissRequested, &visibility, &VisibilityController::hide);
|
||||||
|
QObject::connect(&visibility, &VisibilityController::visibleChanged, &placement,
|
||||||
|
[&](bool visible) { visible ? placement.show() : placement.hide(); });
|
||||||
|
updateStandalone();
|
||||||
|
}
|
||||||
|
|
||||||
|
void connectTextContext(KWinInputMethod &backend, KeyboardController &controller)
|
||||||
|
{
|
||||||
|
QObject::connect(&backend, &TextBackend::activeChanged, &controller, &KeyboardController::resetContext);
|
||||||
|
QObject::connect(&backend, &TextBackend::contextChanged, &controller,
|
||||||
|
[&] { controller.setContext(backend.context()); });
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
@@ -45,6 +78,7 @@ int main(int argc, char *argv[])
|
|||||||
DirectEngine engine(backend);
|
DirectEngine engine(backend);
|
||||||
LayoutModel model;
|
LayoutModel model;
|
||||||
KeyboardController controller(engine, model, layouts);
|
KeyboardController controller(engine, model, layouts);
|
||||||
|
connectTextContext(backend, controller);
|
||||||
|
|
||||||
QQmlApplicationEngine qml;
|
QQmlApplicationEngine qml;
|
||||||
qml.setInitialProperties({
|
qml.setInitialProperties({
|
||||||
@@ -60,24 +94,16 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
FloatingWindow placement(window);
|
FloatingWindow placement(window);
|
||||||
window->setProperty("placement", QVariant::fromValue(&placement));
|
window->setProperty("placement", QVariant::fromValue(&placement));
|
||||||
|
WindowSettings settings;
|
||||||
|
restoreAndPersistGeometry(placement, settings);
|
||||||
|
|
||||||
// Show while a text field is focused; with no input-method global (e.g. run
|
VisibilityController visibility;
|
||||||
// standalone for UI work) stay visible so the keyboard can still be tried out.
|
connectVisibility(visibility, backend, controller, placement);
|
||||||
auto updateVisibility = [&] {
|
|
||||||
if (backend.isActive() || !backend.isAvailable())
|
DBusControl dbus(visibility);
|
||||||
placement.show();
|
if (!dbus.registerOnSessionBus())
|
||||||
else
|
qWarning("D-Bus control unavailable (is another instance running?)");
|
||||||
placement.hide();
|
|
||||||
};
|
|
||||||
QObject::connect(&backend, &TextBackend::activeChanged, &app, updateVisibility);
|
|
||||||
QObject::connect(&backend, &KWinInputMethod::availableChanged, &app, updateVisibility);
|
|
||||||
QObject::connect(&controller, &KeyboardController::dismissRequested, &placement, &FloatingWindow::hide);
|
|
||||||
QObject::connect(&backend, &TextBackend::activeChanged, &controller, &KeyboardController::resetContext);
|
|
||||||
QObject::connect(&backend, &TextBackend::contextChanged, &controller,
|
|
||||||
[&] { controller.setContext(backend.context()); });
|
|
||||||
updateVisibility();
|
|
||||||
|
|
||||||
installDevHooks(backend, *window);
|
installDevHooks(backend, *window);
|
||||||
|
|
||||||
return app.exec();
|
return app.exec();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,19 +36,44 @@ FloatingWindow::FloatingWindow(QWindow *window, QObject *parent)
|
|||||||
apply();
|
apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
void FloatingWindow::setPosition(QPoint position)
|
QRect FloatingWindow::geometry() const
|
||||||
{
|
{
|
||||||
position = clamped(position);
|
return {m_position, m_window->size()};
|
||||||
if (position == m_position)
|
}
|
||||||
return;
|
|
||||||
m_position = position;
|
void FloatingWindow::setGeometry(const QRect &geometry)
|
||||||
|
{
|
||||||
|
const QSize size = geometry.size().expandedTo(m_window->minimumSize()).boundedTo(screenArea().size());
|
||||||
|
m_window->resize(size);
|
||||||
|
m_position = clamped(geometry.topLeft(), size);
|
||||||
apply();
|
apply();
|
||||||
Q_EMIT positionChanged();
|
}
|
||||||
|
|
||||||
|
QString FloatingWindow::screenName() const
|
||||||
|
{
|
||||||
|
return m_window->screen()->name();
|
||||||
}
|
}
|
||||||
|
|
||||||
void FloatingWindow::moveBy(qreal dx, qreal dy)
|
void FloatingWindow::moveBy(qreal dx, qreal dy)
|
||||||
{
|
{
|
||||||
setPosition(m_position + QPoint(qRound(dx), qRound(dy)));
|
const QPoint position = clamped(m_position + QPoint(qRound(dx), qRound(dy)), m_window->size());
|
||||||
|
if (position == m_position)
|
||||||
|
return;
|
||||||
|
m_position = position;
|
||||||
|
apply();
|
||||||
|
Q_EMIT geometryChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FloatingWindow::resizeBy(qreal dw, qreal dh)
|
||||||
|
{
|
||||||
|
const QSize maximum = screenArea().size() - QSize(m_position.x(), m_position.y());
|
||||||
|
const QSize size = (m_window->size() + QSize(qRound(dw), qRound(dh)))
|
||||||
|
.expandedTo(m_window->minimumSize())
|
||||||
|
.boundedTo(maximum);
|
||||||
|
if (size == m_window->size())
|
||||||
|
return;
|
||||||
|
m_window->resize(size);
|
||||||
|
Q_EMIT geometryChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void FloatingWindow::setInputRegion(const QRectF ®ion)
|
void FloatingWindow::setInputRegion(const QRectF ®ion)
|
||||||
@@ -66,19 +91,24 @@ void FloatingWindow::hide()
|
|||||||
m_window->setVisible(false);
|
m_window->setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
QPoint FloatingWindow::clamped(QPoint position) const
|
QRect FloatingWindow::screenArea() const
|
||||||
{
|
{
|
||||||
const QRect area = m_window->screen()->geometry();
|
return m_window->screen()->geometry();
|
||||||
const int maxX = qMax(0, area.width() - m_window->width());
|
}
|
||||||
const int maxY = qMax(0, area.height() - m_window->height());
|
|
||||||
|
QPoint FloatingWindow::clamped(QPoint position, QSize size) const
|
||||||
|
{
|
||||||
|
const QRect area = screenArea();
|
||||||
|
const int maxX = qMax(0, area.width() - size.width());
|
||||||
|
const int maxY = qMax(0, area.height() - size.height());
|
||||||
return {qBound(0, position.x(), maxX), qBound(0, position.y(), maxY)};
|
return {qBound(0, position.x(), maxX), qBound(0, position.y(), maxY)};
|
||||||
}
|
}
|
||||||
|
|
||||||
QPoint FloatingWindow::defaultPosition() const
|
QPoint FloatingWindow::defaultPosition() const
|
||||||
{
|
{
|
||||||
const QRect area = m_window->screen()->geometry();
|
const QRect area = screenArea();
|
||||||
return clamped({(area.width() - m_window->width()) / 2,
|
return clamped({(area.width() - m_window->width()) / 2, area.height() - m_window->height() - BottomGap},
|
||||||
area.height() - m_window->height() - BottomGap});
|
m_window->size());
|
||||||
}
|
}
|
||||||
|
|
||||||
void FloatingWindow::apply()
|
void FloatingWindow::apply()
|
||||||
@@ -86,5 +116,5 @@ void FloatingWindow::apply()
|
|||||||
if (m_layerSurface)
|
if (m_layerSurface)
|
||||||
m_layerSurface->setMargins({m_position.x(), m_position.y(), 0, 0});
|
m_layerSurface->setMargins({m_position.x(), m_position.y(), 0, 0});
|
||||||
else
|
else
|
||||||
m_window->setPosition(m_window->screen()->geometry().topLeft() + m_position);
|
m_window->setPosition(screenArea().topLeft() + m_position);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QPoint>
|
#include <QRect>
|
||||||
#include <QRectF>
|
#include <QRectF>
|
||||||
|
|
||||||
class QWindow;
|
class QWindow;
|
||||||
@@ -10,29 +10,34 @@ namespace LayerShellQt { class Window; }
|
|||||||
// Places a keyboard window as a free-floating overlay that never takes focus.
|
// Places a keyboard window as a free-floating overlay that never takes focus.
|
||||||
// On Wayland: a layer-shell surface anchored top-left, positioned via margins.
|
// On Wayland: a layer-shell surface anchored top-left, positioned via margins.
|
||||||
// On X11: a frameless, non-focusable, always-on-top tool window.
|
// On X11: a frameless, non-focusable, always-on-top tool window.
|
||||||
|
// Geometry is relative to the window's screen and always kept on it.
|
||||||
class FloatingWindow : public QObject
|
class FloatingWindow : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(QPoint position READ position WRITE setPosition NOTIFY positionChanged)
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// Must be called before the window is shown for the first time.
|
// Must be called before the window is shown for the first time.
|
||||||
explicit FloatingWindow(QWindow *window, QObject *parent = nullptr);
|
explicit FloatingWindow(QWindow *window, QObject *parent = nullptr);
|
||||||
|
|
||||||
QPoint position() const { return m_position; }
|
QRect geometry() const;
|
||||||
void setPosition(QPoint position);
|
void setGeometry(const QRect &geometry);
|
||||||
|
QString screenName() const;
|
||||||
|
|
||||||
Q_INVOKABLE void moveBy(qreal dx, qreal dy);
|
Q_INVOKABLE void moveBy(qreal dx, qreal dy);
|
||||||
|
// Grows from the top-left corner; respects the window's minimum size.
|
||||||
|
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);
|
||||||
Q_INVOKABLE void show();
|
Q_INVOKABLE void show();
|
||||||
Q_INVOKABLE void hide();
|
Q_INVOKABLE void hide();
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void positionChanged();
|
// Only for user-initiated changes (move/resize), not for restores.
|
||||||
|
void geometryChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QPoint clamped(QPoint position) const;
|
QRect screenArea() const;
|
||||||
|
QPoint clamped(QPoint position, QSize size) const;
|
||||||
QPoint defaultPosition() const;
|
QPoint defaultPosition() const;
|
||||||
void apply();
|
void apply();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#include "VisibilityController.h"
|
||||||
|
|
||||||
|
void VisibilityController::setFieldActive(bool active)
|
||||||
|
{
|
||||||
|
if (active == m_fieldActive)
|
||||||
|
return;
|
||||||
|
m_fieldActive = active;
|
||||||
|
m_override.reset();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VisibilityController::setStandalone(bool standalone)
|
||||||
|
{
|
||||||
|
m_standalone = standalone;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VisibilityController::show()
|
||||||
|
{
|
||||||
|
m_override = true;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VisibilityController::hide()
|
||||||
|
{
|
||||||
|
m_override = false;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VisibilityController::toggle()
|
||||||
|
{
|
||||||
|
m_override = !m_visible;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VisibilityController::update()
|
||||||
|
{
|
||||||
|
const bool visible = m_override.value_or(m_standalone || m_fieldActive);
|
||||||
|
if (visible == m_visible)
|
||||||
|
return;
|
||||||
|
m_visible = visible;
|
||||||
|
Q_EMIT visibleChanged(visible);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
// Decides whether the keyboard is shown:
|
||||||
|
// - while a text field is focused
|
||||||
|
// - always, when running standalone (no input-method connection)
|
||||||
|
// - show()/hide()/toggle() override that until the focus changes
|
||||||
|
class VisibilityController : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
using QObject::QObject;
|
||||||
|
|
||||||
|
bool isVisible() const { return m_visible; }
|
||||||
|
|
||||||
|
void setFieldActive(bool active);
|
||||||
|
void setStandalone(bool standalone);
|
||||||
|
|
||||||
|
void show();
|
||||||
|
void hide();
|
||||||
|
void toggle();
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void visibleChanged(bool visible);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void update();
|
||||||
|
|
||||||
|
bool m_fieldActive = false;
|
||||||
|
bool m_standalone = false;
|
||||||
|
std::optional<bool> m_override;
|
||||||
|
bool m_visible = false;
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#include "WindowSettings.h"
|
||||||
|
|
||||||
|
#include <QSettings>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int DebounceMs = 500;
|
||||||
|
|
||||||
|
QString key(const QString &screen)
|
||||||
|
{
|
||||||
|
// Screen names may contain '/', which QSettings treats as a group separator.
|
||||||
|
return QStringLiteral("geometry/") + QString(screen).replace(u'/', u'_');
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
WindowSettings::WindowSettings(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
m_debounce.setSingleShot(true);
|
||||||
|
m_debounce.setInterval(DebounceMs);
|
||||||
|
connect(&m_debounce, &QTimer::timeout, this, &WindowSettings::flush);
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowSettings::~WindowSettings()
|
||||||
|
{
|
||||||
|
if (m_debounce.isActive())
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<QRect> WindowSettings::load(const QString &screen) const
|
||||||
|
{
|
||||||
|
const QRect geometry = QSettings().value(key(screen)).toRect();
|
||||||
|
if (!geometry.isValid())
|
||||||
|
return std::nullopt;
|
||||||
|
return geometry;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowSettings::save(const QString &screen, const QRect &geometry)
|
||||||
|
{
|
||||||
|
if (m_debounce.isActive() && screen != m_pendingScreen)
|
||||||
|
flush();
|
||||||
|
m_pendingScreen = screen;
|
||||||
|
m_pendingGeometry = geometry;
|
||||||
|
m_debounce.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowSettings::flush()
|
||||||
|
{
|
||||||
|
m_debounce.stop();
|
||||||
|
QSettings().setValue(key(m_pendingScreen), m_pendingGeometry);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QRect>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
// Remembers the keyboard's geometry per screen (~/.config/sloposk.conf).
|
||||||
|
// Saving is debounced so dragging doesn't hammer the disk.
|
||||||
|
class WindowSettings : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit WindowSettings(QObject *parent = nullptr);
|
||||||
|
~WindowSettings() override;
|
||||||
|
|
||||||
|
std::optional<QRect> load(const QString &screen) const;
|
||||||
|
void save(const QString &screen, const QRect &geometry);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void flush();
|
||||||
|
|
||||||
|
QTimer m_debounce;
|
||||||
|
QString m_pendingScreen;
|
||||||
|
QRect m_pendingGeometry;
|
||||||
|
};
|
||||||
@@ -13,6 +13,7 @@ sloposk_add_test(tst_pointertracker)
|
|||||||
sloposk_add_test(tst_alternatespicker)
|
sloposk_add_test(tst_alternatespicker)
|
||||||
sloposk_add_test(tst_textrules)
|
sloposk_add_test(tst_textrules)
|
||||||
sloposk_add_test(tst_keyrepeater)
|
sloposk_add_test(tst_keyrepeater)
|
||||||
|
sloposk_add_test(tst_visibility)
|
||||||
|
|
||||||
# Needs the QML module itself, not just the C++ backing library.
|
# Needs the QML module itself, not just the C++ backing library.
|
||||||
sloposk_add_test(tst_keyboardsurface)
|
sloposk_add_test(tst_keyboardsurface)
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#include "window/VisibilityController.h"
|
||||||
|
|
||||||
|
#include <QTest>
|
||||||
|
|
||||||
|
class TestVisibility : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void followsFieldFocus()
|
||||||
|
{
|
||||||
|
VisibilityController v;
|
||||||
|
v.setFieldActive(true);
|
||||||
|
QVERIFY(v.isVisible());
|
||||||
|
v.setFieldActive(false);
|
||||||
|
QVERIFY(!v.isVisible());
|
||||||
|
}
|
||||||
|
|
||||||
|
void dismissLastsUntilFocusChanges()
|
||||||
|
{
|
||||||
|
VisibilityController v;
|
||||||
|
v.setFieldActive(true);
|
||||||
|
v.hide();
|
||||||
|
QVERIFY(!v.isVisible());
|
||||||
|
v.setFieldActive(false);
|
||||||
|
v.setFieldActive(true);
|
||||||
|
QVERIFY(v.isVisible());
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleShowsWithoutField()
|
||||||
|
{
|
||||||
|
VisibilityController v;
|
||||||
|
v.toggle();
|
||||||
|
QVERIFY(v.isVisible());
|
||||||
|
v.toggle();
|
||||||
|
QVERIFY(!v.isVisible());
|
||||||
|
}
|
||||||
|
|
||||||
|
void standaloneAlwaysVisible()
|
||||||
|
{
|
||||||
|
VisibilityController v;
|
||||||
|
v.setStandalone(true);
|
||||||
|
QVERIFY(v.isVisible());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QTEST_GUILESS_MAIN(TestVisibility)
|
||||||
|
#include "tst_visibility.moc"
|
||||||
Reference in New Issue
Block a user