Spike: KWin input-method backend with floating layer-shell window
Registers as KWin's virtual keyboard (input-method-unstable-v1), shows a non-focusable layer-shell overlay, and commits Unicode text. Verified in a nested headless KWin with the textprobe tool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
build/
|
||||||
|
.cache/
|
||||||
|
*.user
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.21)
|
||||||
|
project(sloposk VERSION 0.1 LANGUAGES C CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
find_package(Qt6 6.8 REQUIRED COMPONENTS Gui Quick Widgets WaylandClient)
|
||||||
|
find_package(LayerShellQt REQUIRED)
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(XKB REQUIRED IMPORTED_TARGET xkbcommon)
|
||||||
|
pkg_check_modules(WAYLAND REQUIRED IMPORTED_TARGET wayland-client)
|
||||||
|
pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)
|
||||||
|
|
||||||
|
qt_standard_project_setup(REQUIRES 6.8)
|
||||||
|
|
||||||
|
add_subdirectory(src)
|
||||||
|
add_subdirectory(tools)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
|
||||||
|
// Spike UI: proves text commits, keysyms and dragging. Replaced by the real
|
||||||
|
// keyboard in milestone 2.
|
||||||
|
Window {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var backend
|
||||||
|
property var placement: null
|
||||||
|
|
||||||
|
width: 720
|
||||||
|
height: 120
|
||||||
|
visible: false
|
||||||
|
color: "#d1d3d9"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: handle
|
||||||
|
anchors { horizontalCenter: parent.horizontalCenter; bottom: parent.bottom; bottomMargin: 6 }
|
||||||
|
width: 120; height: 6; radius: 3
|
||||||
|
color: "#8e8e93"
|
||||||
|
|
||||||
|
DragHandler {
|
||||||
|
target: null
|
||||||
|
margin: 12
|
||||||
|
property point last
|
||||||
|
onActiveChanged: last = centroid.scenePressPosition
|
||||||
|
onCentroidChanged: {
|
||||||
|
if (!active || !root.placement)
|
||||||
|
return
|
||||||
|
const p = centroid.scenePosition
|
||||||
|
root.placement.moveBy(p.x - last.x, p.y - last.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 8
|
||||||
|
Button { text: "héllo👍"; focusPolicy: Qt.NoFocus; onClicked: root.backend.commitText(text) }
|
||||||
|
Button { text: "⌫"; focusPolicy: Qt.NoFocus; onClicked: root.backend.sendKeysym(0xff08) }
|
||||||
|
Button { text: "⏎"; focusPolicy: Qt.NoFocus; onClicked: root.backend.sendKeysym(0xff0d) }
|
||||||
|
Label { anchors.verticalCenter: parent.verticalCenter; text: root.backend.active ? "active" : "inactive" }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
qt_add_executable(sloposk
|
||||||
|
main.cpp
|
||||||
|
backend/TextBackend.h
|
||||||
|
backend/InputMethodProtocol.h
|
||||||
|
backend/KWinInputMethod.h backend/KWinInputMethod.cpp
|
||||||
|
backend/KWinInputContext.h backend/KWinInputContext.cpp
|
||||||
|
window/FloatingWindow.h window/FloatingWindow.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
qt6_generate_wayland_protocol_client_sources(sloposk PRIVATE_CODE FILES
|
||||||
|
${WAYLAND_PROTOCOLS_DIR}/unstable/input-method/input-method-unstable-v1.xml
|
||||||
|
)
|
||||||
|
# Generated code lacks the core-protocol include (see backend/InputMethodProtocol.h).
|
||||||
|
set_source_files_properties(${CMAKE_CURRENT_BINARY_DIR}/qwayland-input-method-unstable-v1.cpp
|
||||||
|
PROPERTIES COMPILE_OPTIONS "-include;wayland-client-protocol.h")
|
||||||
|
|
||||||
|
# QML lives in the top-level qml/ dir; alias each file so the module sees it at its root.
|
||||||
|
set(SLOPOSK_QML_FILES
|
||||||
|
Main.qml
|
||||||
|
)
|
||||||
|
list(TRANSFORM SLOPOSK_QML_FILES PREPEND ${PROJECT_SOURCE_DIR}/qml/ OUTPUT_VARIABLE sloposk_qml_paths)
|
||||||
|
foreach(file IN LISTS SLOPOSK_QML_FILES)
|
||||||
|
set_source_files_properties(${PROJECT_SOURCE_DIR}/qml/${file} PROPERTIES QT_RESOURCE_ALIAS ${file})
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
qt_add_qml_module(sloposk
|
||||||
|
URI Sloposk
|
||||||
|
QML_FILES ${sloposk_qml_paths}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(sloposk PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
||||||
|
target_link_libraries(sloposk PRIVATE
|
||||||
|
Qt6::Gui
|
||||||
|
Qt6::Quick
|
||||||
|
Qt6::WaylandClient
|
||||||
|
LayerShellQt::Interface
|
||||||
|
PkgConfig::XKB
|
||||||
|
PkgConfig::WAYLAND
|
||||||
|
)
|
||||||
|
|
||||||
|
install(TARGETS sloposk)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// The generated protocol header references core interfaces (wl_keyboard,
|
||||||
|
// wl_surface) without including their declarations. Include this instead.
|
||||||
|
#include <wayland-client-protocol.h>
|
||||||
|
|
||||||
|
#include "qwayland-input-method-unstable-v1.h"
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#include "KWinInputContext.h"
|
||||||
|
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Order defines the bit positions used in keysym()'s modifier mask.
|
||||||
|
constexpr const char *ModifierNames[] = {"Shift", "Control", "Mod1", "Mod4"};
|
||||||
|
constexpr Qt::KeyboardModifier ModifierFlags[] = {
|
||||||
|
Qt::ShiftModifier, Qt::ControlModifier, Qt::AltModifier, Qt::MetaModifier};
|
||||||
|
|
||||||
|
uint32_t toModifierMask(Qt::KeyboardModifiers modifiers)
|
||||||
|
{
|
||||||
|
uint32_t mask = 0;
|
||||||
|
for (size_t bit = 0; bit < std::size(ModifierFlags); ++bit) {
|
||||||
|
if (modifiers & ModifierFlags[bit])
|
||||||
|
mask |= 1u << bit;
|
||||||
|
}
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t timestampMs()
|
||||||
|
{
|
||||||
|
static QElapsedTimer clock = [] { QElapsedTimer t; t.start(); return t; }();
|
||||||
|
return uint32_t(clock.elapsed());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The protocol reports cursor offsets in UTF-8 bytes.
|
||||||
|
int utf8OffsetToIndex(const QString &text, uint32_t byteOffset)
|
||||||
|
{
|
||||||
|
return QString::fromUtf8(text.toUtf8().left(int(byteOffset))).size();
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr uint32_t KeyReleased = 0;
|
||||||
|
constexpr uint32_t KeyPressed = 1;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
KWinInputContext::KWinInputContext(::zwp_input_method_context_v1 *object, QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, QtWayland::zwp_input_method_context_v1(object)
|
||||||
|
{
|
||||||
|
announceModifiers();
|
||||||
|
}
|
||||||
|
|
||||||
|
KWinInputContext::~KWinInputContext()
|
||||||
|
{
|
||||||
|
destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void KWinInputContext::commit(const QString &text)
|
||||||
|
{
|
||||||
|
commit_string(m_serial, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void KWinInputContext::preedit(const QString &text)
|
||||||
|
{
|
||||||
|
preedit_string(m_serial, text, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void KWinInputContext::sendKeysym(quint32 sym, Qt::KeyboardModifiers modifiers)
|
||||||
|
{
|
||||||
|
const uint32_t mask = toModifierMask(modifiers);
|
||||||
|
keysym(m_serial, timestampMs(), sym, KeyPressed, mask);
|
||||||
|
keysym(m_serial, timestampMs(), sym, KeyReleased, mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
void KWinInputContext::zwp_input_method_context_v1_surrounding_text(const QString &text, uint32_t cursor, uint32_t)
|
||||||
|
{
|
||||||
|
m_surroundingText = text;
|
||||||
|
m_cursorPosition = utf8OffsetToIndex(text, cursor);
|
||||||
|
Q_EMIT surroundingChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void KWinInputContext::zwp_input_method_context_v1_commit_state(uint32_t serial)
|
||||||
|
{
|
||||||
|
m_serial = serial;
|
||||||
|
}
|
||||||
|
|
||||||
|
void KWinInputContext::zwp_input_method_context_v1_reset()
|
||||||
|
{
|
||||||
|
m_surroundingText.clear();
|
||||||
|
m_cursorPosition = -1;
|
||||||
|
Q_EMIT surroundingChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void KWinInputContext::announceModifiers()
|
||||||
|
{
|
||||||
|
QByteArray map;
|
||||||
|
for (const char *name : ModifierNames)
|
||||||
|
map.append(name).append('\0');
|
||||||
|
modifiers_map(map);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "InputMethodProtocol.h"
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
// One input-method context: exists while a text field is focused.
|
||||||
|
// Owns the serial bookkeeping and the UTF-8 <-> QString offset conversion.
|
||||||
|
class KWinInputContext : public QObject, public QtWayland::zwp_input_method_context_v1
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit KWinInputContext(::zwp_input_method_context_v1 *object, QObject *parent = nullptr);
|
||||||
|
~KWinInputContext() override;
|
||||||
|
|
||||||
|
QString surroundingText() const { return m_surroundingText; }
|
||||||
|
int cursorPosition() const { return m_cursorPosition; }
|
||||||
|
|
||||||
|
void commit(const QString &text);
|
||||||
|
void preedit(const QString &text);
|
||||||
|
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void surroundingChanged();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void zwp_input_method_context_v1_surrounding_text(const QString &text, uint32_t cursor, uint32_t anchor) override;
|
||||||
|
void zwp_input_method_context_v1_commit_state(uint32_t serial) override;
|
||||||
|
void zwp_input_method_context_v1_reset() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void announceModifiers();
|
||||||
|
|
||||||
|
uint32_t m_serial = 0;
|
||||||
|
QString m_surroundingText;
|
||||||
|
int m_cursorPosition = -1;
|
||||||
|
};
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#include "KWinInputMethod.h"
|
||||||
|
#include "KWinInputContext.h"
|
||||||
|
|
||||||
|
KWinInputMethodGlobal::KWinInputMethodGlobal()
|
||||||
|
: QWaylandClientExtensionTemplate(1)
|
||||||
|
{
|
||||||
|
initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
void KWinInputMethodGlobal::zwp_input_method_v1_activate(::zwp_input_method_context_v1 *context)
|
||||||
|
{
|
||||||
|
Q_EMIT activated(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
void KWinInputMethodGlobal::zwp_input_method_v1_deactivate(::zwp_input_method_context_v1 *)
|
||||||
|
{
|
||||||
|
Q_EMIT deactivated();
|
||||||
|
}
|
||||||
|
|
||||||
|
KWinInputMethod::KWinInputMethod(QObject *parent)
|
||||||
|
: TextBackend(parent)
|
||||||
|
{
|
||||||
|
connect(&m_global, &KWinInputMethodGlobal::activated, this, &KWinInputMethod::activate);
|
||||||
|
connect(&m_global, &KWinInputMethodGlobal::deactivated, this, &KWinInputMethod::deactivate);
|
||||||
|
connect(&m_global, &QWaylandClientExtension::activeChanged, this, &KWinInputMethod::availableChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
KWinInputMethod::~KWinInputMethod() = default;
|
||||||
|
|
||||||
|
bool KWinInputMethod::isAvailable() const
|
||||||
|
{
|
||||||
|
return m_global.isActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool KWinInputMethod::isActive() const
|
||||||
|
{
|
||||||
|
return m_context != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString KWinInputMethod::surroundingText() const
|
||||||
|
{
|
||||||
|
return m_context ? m_context->surroundingText() : QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
int KWinInputMethod::cursorPosition() const
|
||||||
|
{
|
||||||
|
return m_context ? m_context->cursorPosition() : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void KWinInputMethod::commitText(const QString &text)
|
||||||
|
{
|
||||||
|
if (m_context)
|
||||||
|
m_context->commit(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void KWinInputMethod::setPreedit(const QString &text)
|
||||||
|
{
|
||||||
|
if (m_context)
|
||||||
|
m_context->preedit(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void KWinInputMethod::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers)
|
||||||
|
{
|
||||||
|
if (m_context)
|
||||||
|
m_context->sendKeysym(keysym, modifiers);
|
||||||
|
}
|
||||||
|
|
||||||
|
void KWinInputMethod::activate(::zwp_input_method_context_v1 *context)
|
||||||
|
{
|
||||||
|
m_context = std::make_unique<KWinInputContext>(context);
|
||||||
|
connect(m_context.get(), &KWinInputContext::surroundingChanged, this, &TextBackend::surroundingChanged);
|
||||||
|
Q_EMIT activeChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void KWinInputMethod::deactivate()
|
||||||
|
{
|
||||||
|
m_context.reset();
|
||||||
|
Q_EMIT activeChanged();
|
||||||
|
Q_EMIT surroundingChanged();
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "TextBackend.h"
|
||||||
|
#include "InputMethodProtocol.h"
|
||||||
|
|
||||||
|
#include <QWaylandClientExtensionTemplate>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
class KWinInputContext;
|
||||||
|
|
||||||
|
// Binds zwp_input_method_v1, which KWin only offers to the process it launched
|
||||||
|
// as the virtual keyboard (see --inputmethod / System Settings → Virtual Keyboard).
|
||||||
|
class KWinInputMethodGlobal
|
||||||
|
: public QWaylandClientExtensionTemplate<KWinInputMethodGlobal>
|
||||||
|
, public QtWayland::zwp_input_method_v1
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
KWinInputMethodGlobal();
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void activated(::zwp_input_method_context_v1 *context);
|
||||||
|
void deactivated();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void zwp_input_method_v1_activate(::zwp_input_method_context_v1 *context) override;
|
||||||
|
void zwp_input_method_v1_deactivate(::zwp_input_method_context_v1 *context) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
class KWinInputMethod : public TextBackend
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit KWinInputMethod(QObject *parent = nullptr);
|
||||||
|
~KWinInputMethod() override;
|
||||||
|
|
||||||
|
// False when the compositor didn't hand us the input-method global.
|
||||||
|
bool isAvailable() const;
|
||||||
|
|
||||||
|
bool isActive() const override;
|
||||||
|
QString surroundingText() const override;
|
||||||
|
int cursorPosition() const override;
|
||||||
|
|
||||||
|
void commitText(const QString &text) override;
|
||||||
|
void setPreedit(const QString &text) override;
|
||||||
|
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) override;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void availableChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void activate(::zwp_input_method_context_v1 *context);
|
||||||
|
void deactivate();
|
||||||
|
|
||||||
|
KWinInputMethodGlobal m_global;
|
||||||
|
std::unique_ptr<KWinInputContext> m_context;
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
// Delivers text produced by the keyboard to the focused application.
|
||||||
|
// Implementations hide the transport (KWin input method, uinput, XTest, ...).
|
||||||
|
// Keys are identified by XKB keysyms so every backend speaks the same language.
|
||||||
|
class TextBackend : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(bool active READ isActive NOTIFY activeChanged)
|
||||||
|
Q_PROPERTY(QString surroundingText READ surroundingText NOTIFY surroundingChanged)
|
||||||
|
Q_PROPERTY(int cursorPosition READ cursorPosition NOTIFY surroundingChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
using QObject::QObject;
|
||||||
|
|
||||||
|
// True while a text field is focused and accepts input.
|
||||||
|
virtual bool isActive() const = 0;
|
||||||
|
|
||||||
|
// Text around the cursor, if the backend knows it (empty otherwise).
|
||||||
|
virtual QString surroundingText() const { return {}; }
|
||||||
|
virtual int cursorPosition() const { return -1; }
|
||||||
|
|
||||||
|
Q_INVOKABLE virtual void commitText(const QString &text) = 0;
|
||||||
|
Q_INVOKABLE virtual void setPreedit(const QString &text) = 0;
|
||||||
|
Q_INVOKABLE virtual void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers = Qt::NoModifier) = 0;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void activeChanged();
|
||||||
|
void surroundingChanged();
|
||||||
|
};
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#include "backend/KWinInputMethod.h"
|
||||||
|
#include "window/FloatingWindow.h"
|
||||||
|
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QQmlApplicationEngine>
|
||||||
|
#include <QQuickWindow>
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
QGuiApplication app(argc, argv);
|
||||||
|
QGuiApplication::setApplicationName(QStringLiteral("sloposk"));
|
||||||
|
QGuiApplication::setDesktopFileName(QStringLiteral("org.sloposk.keyboard"));
|
||||||
|
|
||||||
|
KWinInputMethod backend;
|
||||||
|
|
||||||
|
QQmlApplicationEngine engine;
|
||||||
|
engine.setInitialProperties({{QStringLiteral("backend"), QVariant::fromValue<TextBackend *>(&backend)}});
|
||||||
|
engine.loadFromModule("Sloposk", "Main");
|
||||||
|
auto *window = engine.rootObjects().isEmpty() ? nullptr : qobject_cast<QQuickWindow *>(engine.rootObjects().first());
|
||||||
|
if (!window) {
|
||||||
|
qCritical("Failed to load the keyboard UI");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingWindow placement(window);
|
||||||
|
window->setProperty("placement", QVariant::fromValue(&placement));
|
||||||
|
|
||||||
|
// Show while a text field is focused; with no input-method global (e.g. run
|
||||||
|
// standalone for UI work) stay visible so the keyboard can still be tried out.
|
||||||
|
auto updateVisibility = [&] {
|
||||||
|
if (backend.isActive() || !backend.isAvailable())
|
||||||
|
placement.show();
|
||||||
|
else
|
||||||
|
placement.hide();
|
||||||
|
};
|
||||||
|
QObject::connect(&backend, &TextBackend::activeChanged, &app, updateVisibility);
|
||||||
|
QObject::connect(&backend, &KWinInputMethod::availableChanged, &app, updateVisibility);
|
||||||
|
updateVisibility();
|
||||||
|
|
||||||
|
// Dev aid for headless testing: type a string as soon as a field is focused.
|
||||||
|
if (const QString autotype = qEnvironmentVariable("SLOPOSK_AUTOTYPE"); !autotype.isEmpty()) {
|
||||||
|
QObject::connect(&backend, &TextBackend::activeChanged, &app, [&backend, autotype] {
|
||||||
|
if (backend.isActive())
|
||||||
|
backend.commitText(autotype);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.exec();
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#include "FloatingWindow.h"
|
||||||
|
|
||||||
|
#include <LayerShellQt/Window>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QScreen>
|
||||||
|
#include <QWindow>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool isWayland()
|
||||||
|
{
|
||||||
|
return QGuiApplication::platformName().startsWith(QLatin1String("wayland"));
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int BottomGap = 24;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
FloatingWindow::FloatingWindow(QWindow *window, QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_window(window)
|
||||||
|
{
|
||||||
|
if (isWayland()) {
|
||||||
|
m_layerSurface = LayerShellQt::Window::get(window);
|
||||||
|
m_layerSurface->setLayer(LayerShellQt::Window::LayerOverlay);
|
||||||
|
m_layerSurface->setKeyboardInteractivity(LayerShellQt::Window::KeyboardInteractivityNone);
|
||||||
|
m_layerSurface->setAnchors({LayerShellQt::Window::AnchorTop | LayerShellQt::Window::AnchorLeft});
|
||||||
|
m_layerSurface->setExclusiveZone(-1);
|
||||||
|
m_layerSurface->setScope(QStringLiteral("sloposk-keyboard"));
|
||||||
|
m_layerSurface->setActivateOnShow(false);
|
||||||
|
} else {
|
||||||
|
window->setFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint
|
||||||
|
| Qt::WindowDoesNotAcceptFocus);
|
||||||
|
}
|
||||||
|
m_position = defaultPosition();
|
||||||
|
apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FloatingWindow::setPosition(QPoint position)
|
||||||
|
{
|
||||||
|
position = clamped(position);
|
||||||
|
if (position == m_position)
|
||||||
|
return;
|
||||||
|
m_position = position;
|
||||||
|
apply();
|
||||||
|
Q_EMIT positionChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FloatingWindow::moveBy(qreal dx, qreal dy)
|
||||||
|
{
|
||||||
|
setPosition(m_position + QPoint(qRound(dx), qRound(dy)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void FloatingWindow::show()
|
||||||
|
{
|
||||||
|
m_window->setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FloatingWindow::hide()
|
||||||
|
{
|
||||||
|
m_window->setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
QPoint FloatingWindow::clamped(QPoint position) const
|
||||||
|
{
|
||||||
|
const QRect area = m_window->screen()->geometry();
|
||||||
|
const int maxX = qMax(0, area.width() - m_window->width());
|
||||||
|
const int maxY = qMax(0, area.height() - m_window->height());
|
||||||
|
return {qBound(0, position.x(), maxX), qBound(0, position.y(), maxY)};
|
||||||
|
}
|
||||||
|
|
||||||
|
QPoint FloatingWindow::defaultPosition() const
|
||||||
|
{
|
||||||
|
const QRect area = m_window->screen()->geometry();
|
||||||
|
return clamped({(area.width() - m_window->width()) / 2,
|
||||||
|
area.height() - m_window->height() - BottomGap});
|
||||||
|
}
|
||||||
|
|
||||||
|
void FloatingWindow::apply()
|
||||||
|
{
|
||||||
|
if (m_layerSurface)
|
||||||
|
m_layerSurface->setMargins({m_position.x(), m_position.y(), 0, 0});
|
||||||
|
else
|
||||||
|
m_window->setPosition(m_window->screen()->geometry().topLeft() + m_position);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
class QWindow;
|
||||||
|
namespace LayerShellQt { class Window; }
|
||||||
|
|
||||||
|
// 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 X11: a frameless, non-focusable, always-on-top tool window.
|
||||||
|
class FloatingWindow : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(QPoint position READ position WRITE setPosition NOTIFY positionChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Must be called before the window is shown for the first time.
|
||||||
|
explicit FloatingWindow(QWindow *window, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QPoint position() const { return m_position; }
|
||||||
|
void setPosition(QPoint position);
|
||||||
|
|
||||||
|
Q_INVOKABLE void moveBy(qreal dx, qreal dy);
|
||||||
|
Q_INVOKABLE void show();
|
||||||
|
Q_INVOKABLE void hide();
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void positionChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QPoint clamped(QPoint position) const;
|
||||||
|
QPoint defaultPosition() const;
|
||||||
|
void apply();
|
||||||
|
|
||||||
|
QWindow *m_window;
|
||||||
|
LayerShellQt::Window *m_layerSurface = nullptr;
|
||||||
|
QPoint m_position;
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Test client: a focused line edit that prints every text change to stdout.
|
||||||
|
# Lets us verify the input-method path headlessly under a nested KWin.
|
||||||
|
qt_add_executable(sloposk-textprobe textprobe.cpp)
|
||||||
|
target_link_libraries(sloposk-textprobe PRIVATE Qt6::Widgets)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#include <QApplication>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
QApplication app(argc, argv);
|
||||||
|
QLineEdit edit;
|
||||||
|
QObject::connect(&edit, &QLineEdit::textChanged, [](const QString &text) {
|
||||||
|
std::printf("TEXT:%s\n", qPrintable(text));
|
||||||
|
std::fflush(stdout);
|
||||||
|
});
|
||||||
|
edit.resize(400, 40);
|
||||||
|
edit.show();
|
||||||
|
edit.setFocus();
|
||||||
|
return app.exec();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user