From 7054e2dd38bde5e8ce06990df51cedd0a96dea85 Mon Sep 17 00:00:00 2001 From: cameron Date: Sat, 23 Aug 2025 03:26:46 +1000 Subject: [PATCH 1/3] wayland: input method protocol --- CMakeLists.txt | 1 + src/wayland/CMakeLists.txt | 5 + src/wayland/input_method/CMakeLists.txt | 40 ++ src/wayland/input_method/c_helpers.cpp | 72 +++ src/wayland/input_method/c_helpers.hpp | 38 ++ .../input_method/input-method-unstable-v2.xml | 494 ++++++++++++++++++ src/wayland/input_method/input_method.cpp | 130 +++++ src/wayland/input_method/input_method.hpp | 87 +++ src/wayland/input_method/key_map_state.cpp | 106 ++++ src/wayland/input_method/key_map_state.hpp | 61 +++ src/wayland/input_method/keyboard_grab.cpp | 193 +++++++ src/wayland/input_method/keyboard_grab.hpp | 80 +++ src/wayland/input_method/manager.cpp | 59 +++ src/wayland/input_method/manager.hpp | 53 ++ src/wayland/input_method/qml.cpp | 213 ++++++++ src/wayland/input_method/qml.hpp | 160 ++++++ src/wayland/input_method/qml_helpers.cpp | 110 ++++ src/wayland/input_method/qml_helpers.hpp | 82 +++ src/wayland/input_method/types.cpp | 125 +++++ src/wayland/input_method/types.hpp | 101 ++++ .../virtual-keyboard-unstable-v1.xml | 113 ++++ src/wayland/input_method/virtual_keyboard.cpp | 65 +++ src/wayland/input_method/virtual_keyboard.hpp | 32 ++ 23 files changed, 2420 insertions(+) create mode 100644 src/wayland/input_method/CMakeLists.txt create mode 100644 src/wayland/input_method/c_helpers.cpp create mode 100644 src/wayland/input_method/c_helpers.hpp create mode 100644 src/wayland/input_method/input-method-unstable-v2.xml create mode 100644 src/wayland/input_method/input_method.cpp create mode 100644 src/wayland/input_method/input_method.hpp create mode 100644 src/wayland/input_method/key_map_state.cpp create mode 100644 src/wayland/input_method/key_map_state.hpp create mode 100644 src/wayland/input_method/keyboard_grab.cpp create mode 100644 src/wayland/input_method/keyboard_grab.hpp create mode 100644 src/wayland/input_method/manager.cpp create mode 100644 src/wayland/input_method/manager.hpp create mode 100644 src/wayland/input_method/qml.cpp create mode 100644 src/wayland/input_method/qml.hpp create mode 100644 src/wayland/input_method/qml_helpers.cpp create mode 100644 src/wayland/input_method/qml_helpers.hpp create mode 100644 src/wayland/input_method/types.cpp create mode 100644 src/wayland/input_method/types.hpp create mode 100644 src/wayland/input_method/virtual-keyboard-unstable-v1.xml create mode 100644 src/wayland/input_method/virtual_keyboard.cpp create mode 100644 src/wayland/input_method/virtual_keyboard.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 55b5e5d5..2c57955d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,6 +60,7 @@ boption(SCREENCOPY " Screencopy" ON REQUIRES WAYLAND) boption(SCREENCOPY_ICC " Image Copy Capture" ON REQUIRES WAYLAND) boption(SCREENCOPY_WLR " Wlroots Screencopy" ON REQUIRES WAYLAND) boption(SCREENCOPY_HYPRLAND_TOPLEVEL " Hyprland Toplevel Export" ON REQUIRES WAYLAND) +boption(INPUT_METHOD " Input Method" ON REQUIRES WAYLAND) boption(X11 "X11" ON) boption(I3 "I3/Sway" ON) boption(I3_IPC " I3/Sway IPC" ON REQUIRES I3) diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index ea2a5d8b..563c39dd 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -114,6 +114,11 @@ if (HYPRLAND) add_subdirectory(hyprland) endif() +if (INPUT_METHOD) + add_subdirectory(input_method) + list(APPEND WAYLAND_MODULES Quickshell.Wayland._InputMethod) +endif() + add_subdirectory(idle_inhibit) list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleInhibitor) diff --git a/src/wayland/input_method/CMakeLists.txt b/src/wayland/input_method/CMakeLists.txt new file mode 100644 index 00000000..cdeac372 --- /dev/null +++ b/src/wayland/input_method/CMakeLists.txt @@ -0,0 +1,40 @@ +set(INPUT_METHOD_PRINT_KETS OFF) + +qt_add_library(quickshell-wayland-input-method STATIC + input_method.cpp + keyboard_grab.cpp + virtual_keyboard.cpp + key_map_state.cpp + manager.cpp + qml.cpp + qml_helpers.cpp + c_helpers.cpp + types.cpp +) + +target_compile_definitions(quickshell-wayland-input-method PRIVATE INPUT_METHOD_PRINT=0) + +qt_add_qml_module(quickshell-wayland-input-method + URI Quickshell.Wayland._InputMethod + VERSION 0.1 + DEPENDENCIES QtQml +) + +qs_add_module_deps_light(quickshell-wayland-input-method + Quickshell Quickshell.Wayland +) + +install_qml_module(quickshell-wayland-input-method) + +wl_proto(zwp-input-method input-method-unstable-v2 "${CMAKE_CURRENT_SOURCE_DIR}") +wl_proto(zwp-virtual-keyboard virtual-keyboard-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}") +wl_proto(zwp-text-input text-input-unstable-v3 "${WAYLAND_PROTOCOLS}/unstable/text-input") + +target_link_libraries(quickshell-wayland-input-method PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + zwp-input-method zwp-virtual-keyboard zwp-text-input +) + +qs_module_pch(quickshell-wayland-input-method SET large) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-input-methodplugin) diff --git a/src/wayland/input_method/c_helpers.cpp b/src/wayland/input_method/c_helpers.cpp new file mode 100644 index 00000000..21ffbcdb --- /dev/null +++ b/src/wayland/input_method/c_helpers.cpp @@ -0,0 +1,72 @@ +#include "c_helpers.hpp" +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace qs::wayland::input_method::impl { + +void FreeDeleter::operator()(const char* p) const { + std::free(const_cast*>(p)); // NOLINT +} + +SharedMemory::SharedMemory(const char* shmName, int oFlag, size_t size) + : mShmName(shmName) + , mSize(size) + , fd(shm_open(this->mShmName, oFlag, 0)) + , map(nullptr) { + if (this->fd == -1) { + perror(""); + qDebug() << "Virtual keyboard failed to open shared memory"; + return; + } + if (ftruncate(this->fd, static_cast(size)) == -1) { + this->fd = -1; + perror(""); + qDebug() << "Virtual keyboard failed to resize shared memory to" << size; + return; + } + this->map = static_cast(mmap(nullptr, this->mSize, PROT_WRITE, MAP_SHARED, this->fd, 0)); + if (this->map == MAP_FAILED) { + perror(""); + qDebug() << "Virtual keyboard failed to open shared memory"; + return; + } +} +SharedMemory::~SharedMemory() { + if (this->fd != -1) { + close(this->fd); + shm_unlink(this->mShmName); + } + if (this->map != nullptr) { + munmap(this->map, this->mSize); + } +} +SharedMemory::SharedMemory(SharedMemory&& other) noexcept + : mShmName(std::exchange(other.mShmName, nullptr)) + , mSize(std::exchange(other.mSize, 0)) + , fd(std::exchange(other.fd, -1)) + , map(std::exchange(other.map, nullptr)) {} +SharedMemory& SharedMemory::operator=(SharedMemory&& other) noexcept { + this->mShmName = std::exchange(other.mShmName, nullptr); + this->mSize = std::exchange(other.mSize, 0); + this->fd = std::exchange(other.fd, -1); + this->map = std::exchange(other.map, nullptr); + return *this; +} + +SharedMemory::operator bool() const { return fd != -1 && map != MAP_FAILED; } +[[nodiscard]] int SharedMemory::get() const { return fd; } + +void SharedMemory::write(const char* string) { + if (!this->map) return; + strcpy(this->map, string); +} + +} // namespace qs::wayland::input_method::impl diff --git a/src/wayland/input_method/c_helpers.hpp b/src/wayland/input_method/c_helpers.hpp new file mode 100644 index 00000000..bc59331e --- /dev/null +++ b/src/wayland/input_method/c_helpers.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include + +namespace qs::wayland::input_method::impl { + +struct FreeDeleter { + void operator()(const char* p) const; +}; +// Dont use this for literals, only c strings that were allocated +using uniqueCString = std::unique_ptr; + +// this is a bit half baked but works for what I need it for +/// Handles the lifetime of a memory mapped shm +class SharedMemory { +public: + SharedMemory(const char* shmName, int oFlag, size_t size); + ~SharedMemory(); + SharedMemory(const SharedMemory&) = delete; + SharedMemory(SharedMemory&& other) noexcept; + SharedMemory& operator=(const SharedMemory&) = delete; + SharedMemory& operator=(SharedMemory&& other) noexcept; + + [[nodiscard]] operator bool() const; + [[nodiscard]] int get() const; + + void write(const char* string); + +private: + const char* mShmName; + size_t mSize; + int fd; + char* map = nullptr; +}; + +} // namespace qs::wayland::input_method::impl diff --git a/src/wayland/input_method/input-method-unstable-v2.xml b/src/wayland/input_method/input-method-unstable-v2.xml new file mode 100644 index 00000000..51bccf28 --- /dev/null +++ b/src/wayland/input_method/input-method-unstable-v2.xml @@ -0,0 +1,494 @@ + + + + + Copyright © 2008-2011 Kristian Høgsberg + Copyright © 2010-2011 Intel Corporation + Copyright © 2012-2013 Collabora, Ltd. + Copyright © 2012, 2013 Intel Corporation + Copyright © 2015, 2016 Jan Arne Petersen + Copyright © 2017, 2018 Red Hat, Inc. + Copyright © 2018 Purism SPC + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol allows applications to act as input methods for compositors. + + An input method context is used to manage the state of the input method. + + Text strings are UTF-8 encoded, their indices and lengths are in bytes. + + This document adheres to the RFC 2119 when using words like "must", + "should", "may", etc. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + An input method object allows for clients to compose text. + + The objects connects the client to a text input in an application, and + lets the client to serve as an input method for a seat. + + The zwp_input_method_v2 object can occupy two distinct states: active and + inactive. In the active state, the object is associated to and + communicates with a text input. In the inactive state, there is no + associated text input, and the only communication is with the compositor. + Initially, the input method is in the inactive state. + + Requests issued in the inactive state must be accepted by the compositor. + Because of the serial mechanism, and the state reset on activate event, + they will not have any effect on the state of the next text input. + + There must be no more than one input method object per seat. + + + + + + + + + Notification that a text input focused on this seat requested the input + method to be activated. + + This event serves the purpose of providing the compositor with an + active input method. + + This event resets all state associated with previous enable, disable, + surrounding_text, text_change_cause, and content_type events, as well + as the state associated with set_preedit_string, commit_string, and + delete_surrounding_text requests. In addition, it marks the + zwp_input_method_v2 object as active, and makes any existing + zwp_input_popup_surface_v2 objects visible. + + The surrounding_text, and content_type events must follow before the + next done event if the text input supports the respective + functionality. + + State set with this event is double-buffered. It will get applied on + the next zwp_input_method_v2.done event, and stay valid until changed. + + + + + + Notification that no focused text input currently needs an active + input method on this seat. + + This event marks the zwp_input_method_v2 object as inactive. The + compositor must make all existing zwp_input_popup_surface_v2 objects + invisible until the next activate event. + + State set with this event is double-buffered. It will get applied on + the next zwp_input_method_v2.done event, and stay valid until changed. + + + + + + Updates the surrounding plain text around the cursor, excluding the + preedit text. + + If any preedit text is present, it is replaced with the cursor for the + purpose of this event. + + The argument text is a buffer containing the preedit string, and must + include the cursor position, and the complete selection. It should + contain additional characters before and after these. There is a + maximum length of wayland messages, so text can not be longer than 4000 + bytes. + + cursor is the byte offset of the cursor within the text buffer. + + anchor is the byte offset of the selection anchor within the text + buffer. If there is no selected text, anchor must be the same as + cursor. + + If this event does not arrive before the first done event, the input + method may assume that the text input does not support this + functionality and ignore following surrounding_text events. + + Values set with this event are double-buffered. They will get applied + and set to initial values on the next zwp_input_method_v2.done + event. + + The initial state for affected fields is empty, meaning that the text + input does not support sending surrounding text. If the empty values + get applied, subsequent attempts to change them may have no effect. + + + + + + + + + Tells the input method why the text surrounding the cursor changed. + + Whenever the client detects an external change in text, cursor, or + anchor position, it must issue this request to the compositor. This + request is intended to give the input method a chance to update the + preedit text in an appropriate way, e.g. by removing it when the user + starts typing with a keyboard. + + cause describes the source of the change. + + The value set with this event is double-buffered. It will get applied + and set to its initial value on the next zwp_input_method_v2.done + event. + + The initial value of cause is input_method. + + + + + + + Indicates the content type and hint for the current + zwp_input_method_v2 instance. + + Values set with this event are double-buffered. They will get applied + on the next zwp_input_method_v2.done event. + + The initial value for hint is none, and the initial value for purpose + is normal. + + + + + + + + Atomically applies state changes recently sent to the client. + + The done event establishes and updates the state of the client, and + must be issued after any changes to apply them. + + Text input state (content purpose, content hint, surrounding text, and + change cause) is conceptually double-buffered within an input method + context. + + Events modify the pending state, as opposed to the current state in use + by the input method. A done event atomically applies all pending state, + replacing the current state. After done, the new pending state is as + documented for each related request. + + Events must be applied in the order of arrival. + + Neither current nor pending state are modified unless noted otherwise. + + + + + + Send the commit string text for insertion to the application. + + Inserts a string at current cursor position (see commit event + sequence). The string to commit could be either just a single character + after a key press or the result of some composing. + + The argument text is a buffer containing the string to insert. There is + a maximum length of wayland messages, so text can not be longer than + 4000 bytes. + + Values set with this event are double-buffered. They must be applied + and reset to initial on the next zwp_text_input_v3.commit request. + + The initial value of text is an empty string. + + + + + + + Send the pre-edit string text to the application text input. + + Place a new composing text (pre-edit) at the current cursor position. + Any previously set composing text must be removed. Any previously + existing selected text must be removed. The cursor is moved to a new + position within the preedit string. + + The argument text is a buffer containing the preedit string. There is + a maximum length of wayland messages, so text can not be longer than + 4000 bytes. + + The arguments cursor_begin and cursor_end are counted in bytes relative + to the beginning of the submitted string buffer. Cursor should be + hidden by the text input when both are equal to -1. + + cursor_begin indicates the beginning of the cursor. cursor_end + indicates the end of the cursor. It may be equal or different than + cursor_begin. + + Values set with this event are double-buffered. They must be applied on + the next zwp_input_method_v2.commit event. + + The initial value of text is an empty string. The initial value of + cursor_begin, and cursor_end are both 0. + + + + + + + + + Remove the surrounding text. + + before_length and after_length are the number of bytes before and after + the current cursor index (excluding the preedit text) to delete. + + If any preedit text is present, it is replaced with the cursor for the + purpose of this event. In effect before_length is counted from the + beginning of preedit text, and after_length from its end (see commit + event sequence). + + Values set with this event are double-buffered. They must be applied + and reset to initial on the next zwp_input_method_v2.commit request. + + The initial values of both before_length and after_length are 0. + + + + + + + + Apply state changes from commit_string, set_preedit_string and + delete_surrounding_text requests. + + The state relating to these events is double-buffered, and each one + modifies the pending state. This request replaces the current state + with the pending state. + + The connected text input is expected to proceed by evaluating the + changes in the following order: + + 1. Replace existing preedit string with the cursor. + 2. Delete requested surrounding text. + 3. Insert commit string with the cursor at its end. + 4. Calculate surrounding text to send. + 5. Insert new preedit text in cursor position. + 6. Place cursor inside preedit text. + + The serial number reflects the last state of the zwp_input_method_v2 + object known to the client. The value of the serial argument must be + equal to the number of done events already issued by that object. When + the compositor receives a commit request with a serial different than + the number of past done events, it must proceed as normal, except it + should not change the current state of the zwp_input_method_v2 object. + + + + + + + Creates a new zwp_input_popup_surface_v2 object wrapping a given + surface. + + The surface gets assigned the "input_popup" role. If the surface + already has an assigned role, the compositor must issue a protocol + error. + + + + + + + + Allow an input method to receive hardware keyboard input and process + key events to generate text events (with pre-edit) over the wire. This + allows input methods which compose multiple key events for inputting + text like it is done for CJK languages. + + The compositor should send all keyboard events on the seat to the grab + holder via the returned wl_keyboard object. Nevertheless, the + compositor may decide not to forward any particular event. The + compositor must not further process any event after it has been + forwarded to the grab holder. + + Releasing the resulting wl_keyboard object releases the grab. + + + + + + + The input method ceased to be available. + + The compositor must issue this event as the only event on the object if + there was another input_method object associated with the same seat at + the time of its creation. + + The compositor must issue this request when the object is no longer + usable, e.g. due to seat removal. + + The input method context becomes inert and should be destroyed after + deactivation is handled. Any further requests and events except for the + destroy request must be ignored. + + + + + + Destroys the zwp_text_input_v2 object and any associated child + objects, i.e. zwp_input_popup_surface_v2 and + zwp_input_method_keyboard_grab_v2. + + + + + + + This interface marks a surface as a popup for interacting with an input + method. + + The compositor should place it near the active text input area. It must + be visible if and only if the input method is in the active state. + + The client must not destroy the underlying wl_surface while the + zwp_input_popup_surface_v2 object exists. + + + + + Notify about the position of the area of the text input expressed as a + rectangle in surface local coordinates. + + This is a hint to the input method telling it the relative position of + the text being entered. + + + + + + + + + + + + + + The zwp_input_method_keyboard_grab_v2 interface represents an exclusive + grab of the wl_keyboard interface associated with the seat. + + + + + This event provides a file descriptor to the client which can be + memory-mapped to provide a keyboard mapping description. + + + + + + + + + A key was pressed or released. + The time argument is a timestamp with millisecond granularity, with an + undefined base. + + + + + + + + + + Notifies clients that the modifier and/or group state has changed, and + it should update its local state. + + + + + + + + + + + + + + + Informs the client about the keyboard's repeat rate and delay. + + This event is sent as soon as the zwp_input_method_keyboard_grab_v2 + object has been created, and is guaranteed to be received by the + client before any key press event. + + Negative values for either rate or delay are illegal. A rate of zero + will disable any repeating (regardless of the value of delay). + + This event can be sent later on as well with a new value if necessary, + so clients should continue listening for the event past the creation + of zwp_input_method_keyboard_grab_v2. + + + + + + + + + The input method manager allows the client to become the input method on + a chosen seat. + + No more than one input method must be associated with any seat at any + given time. + + + + + Request a new input zwp_input_method_v2 object associated with a given + seat. + + + + + + + + Destroys the zwp_input_method_manager_v2 object. + + The zwp_input_method_v2 objects originating from it remain valid. + + + + diff --git a/src/wayland/input_method/input_method.cpp b/src/wayland/input_method/input_method.cpp new file mode 100644 index 00000000..207f9113 --- /dev/null +++ b/src/wayland/input_method/input_method.cpp @@ -0,0 +1,130 @@ +#include "input_method.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "keyboard_grab.hpp" +#include "manager.hpp" + +namespace qs::wayland::input_method::impl { + +InputMethodHandle::InputMethodHandle(QObject* parent, ::zwp_input_method_v2* inputMethod) + : QObject(parent) + , zwp_input_method_v2(inputMethod) {} + +InputMethodHandle::~InputMethodHandle() { + this->sendPreeditString(""); + this->destroy(); +} + +void InputMethodHandle::commitString(const QString& text) { commit_string(text); } +void InputMethodHandle::sendPreeditString( + const QString& text, + int32_t cursorBegin, + int32_t cursorEnd +) { + set_preedit_string(text, cursorBegin, cursorEnd); +} +void InputMethodHandle::deleteText(int before, int after) { + zwp_input_method_v2::delete_surrounding_text(before, after); +} +void InputMethodHandle::commit() { zwp_input_method_v2::commit(this->serial++); } + +bool InputMethodHandle::hasKeyboard() const { return this->keyboard != nullptr; } +QPointer InputMethodHandle::grabKeyboard() { + if (this->keyboard) return this->keyboard; + + this->keyboard = new InputMethodKeyboardGrab(this, grab_keyboard()); + + return this->keyboard; +} +void InputMethodHandle::releaseKeyboard() { + if (!this->keyboard) return; + this->keyboard->deleteLater(); + this->keyboard = nullptr; +} + +bool InputMethodHandle::isActive() const { return this->mState.activated; } +bool InputMethodHandle::isAvailable() const { return this->mAvailable; } + +const QString& InputMethodHandle::surroundingText() const { + return this->mState.surroundingText.text; +} +uint32_t InputMethodHandle::surroundingTextCursor() const { + return this->mState.surroundingText.cursor; +} +uint32_t InputMethodHandle::surroundingTextAnchor() const { + return this->mState.surroundingText.anchor; +} + +ContentHint InputMethodHandle::contentHint() const { + return this->mState.contentHint; +} +ContentPurpose InputMethodHandle::contentPurpose() const { + return this->mState.contentPurpose; +} + +void InputMethodHandle::zwp_input_method_v2_activate() { this->mNewState.activated = true; } + +void InputMethodHandle::zwp_input_method_v2_deactivate() { this->mNewState.activated = false; } + +void InputMethodHandle::zwp_input_method_v2_surrounding_text( + const QString& text, + uint32_t cursor, + uint32_t anchor +) { + this->mNewState.surroundingText.text = text; + this->mNewState.surroundingText.cursor = cursor; + this->mNewState.surroundingText.anchor = anchor; +} + +void InputMethodHandle::zwp_input_method_v2_text_change_cause(uint32_t cause) { + this->mNewState.surroundingText.textChangeCause = static_cast(cause); +} + +void InputMethodHandle::zwp_input_method_v2_content_type(uint32_t hint, uint32_t purpose) { + this->mNewState.contentHint = static_cast(hint); + this->mNewState.contentPurpose = static_cast(purpose); +}; + +void InputMethodHandle::zwp_input_method_v2_done() { + auto oldState= mState; + this->mState = this->mNewState; + + + if (this->mState.activated != oldState.activated) { + if (this->mNewState.activated) emit activated(); + else emit deactivated(); + } + + if (this->mState.surroundingText != oldState.surroundingText) { + emit surroundingTextChanged( + this->mNewState.surroundingText.textChangeCause + ); + } + + if (this->mState.contentHint != oldState.contentHint) { + emit contentHintChanged(); + } + + if (this->mState.contentPurpose != oldState.contentPurpose) { + emit contentPurposeChanged(); + } +} + +void InputMethodHandle::zwp_input_method_v2_unavailable() { + if (!this->mAvailable) return; + this->mAvailable = false; + InputMethodManager::instance()->releaseInput(); + qDebug() + << "Compositor denied input method request, likely due to one already existing elsewhere"; +} + +} // namespace qs::wayland::input_method::impl diff --git a/src/wayland/input_method/input_method.hpp b/src/wayland/input_method/input_method.hpp new file mode 100644 index 00000000..6f482e38 --- /dev/null +++ b/src/wayland/input_method/input_method.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include +#include + +#include "types.hpp" + +namespace qs::wayland::input_method::impl { + +class InputMethodKeyboardGrab; + +/// A lightweight handle for the wayland input_method protocol +class InputMethodHandle + : public QObject + , private QtWayland::zwp_input_method_v2 { + Q_OBJECT; + +public: + explicit InputMethodHandle(QObject* parent, ::zwp_input_method_v2* inputMethod); + ~InputMethodHandle() override; + Q_DISABLE_COPY_MOVE(InputMethodHandle); + + void commitString(const QString& text); + // By default hides the cursor + void sendPreeditString(const QString& text, int32_t cursorBegin = -1, int32_t cursorEnd = -1); + void deleteText(int before, int after); + void commit(); + + [[nodiscard]] bool hasKeyboard() const; + QPointer grabKeyboard(); + void releaseKeyboard(); + + [[nodiscard]] bool isActive() const; + [[nodiscard]] bool isAvailable() const; + + [[nodiscard]] const QString& surroundingText() const; + [[nodiscard]] uint32_t surroundingTextCursor() const; + [[nodiscard]] uint32_t surroundingTextAnchor() const; + + [[nodiscard]] ContentHint contentHint() const; + [[nodiscard]] ContentPurpose contentPurpose() const; + +signals: + void activated(); + void deactivated(); + + void surroundingTextChanged(TextChangeCause textChangeCause); + + void contentHintChanged(); + void contentPurposeChanged(); + +private: + void zwp_input_method_v2_activate() override; + void zwp_input_method_v2_deactivate() override; + void zwp_input_method_v2_surrounding_text(const QString& text, uint32_t cursor, uint32_t anchor) override; + void zwp_input_method_v2_text_change_cause(uint32_t cause) override; + void zwp_input_method_v2_content_type(uint32_t hint, uint32_t purpose) override; + void zwp_input_method_v2_done() override; + void zwp_input_method_v2_unavailable() override; + + bool mAvailable = true; + int serial = 0; + + struct State { + bool activated = false; + + struct SurroundingText { + QString text = ""; + uint32_t cursor = 0; + uint32_t anchor = 0; + TextChangeCause textChangeCause = TextChangeCause::INPUT_METHOD; + bool operator==(const SurroundingText& other) const = default; + } surroundingText; + + ContentHint contentHint = ContentHint::NONE; + ContentPurpose contentPurpose = ContentPurpose::NORMAL; + }; + + State mState; + State mNewState; + + InputMethodKeyboardGrab* keyboard = nullptr; +}; + +} // namespace qs::wayland::input_method::impl diff --git a/src/wayland/input_method/key_map_state.cpp b/src/wayland/input_method/key_map_state.cpp new file mode 100644 index 00000000..ed36581e --- /dev/null +++ b/src/wayland/input_method/key_map_state.cpp @@ -0,0 +1,106 @@ +#include "key_map_state.hpp" +#include +#include + +#include +#include + +#include "c_helpers.hpp" + +namespace qs::wayland::input_method::impl { + +KeyMapState::KeyMapState(const char* string) + : xkbKeymap(xkb_keymap_new_from_string( + xkbContext, + string, + XKB_KEYMAP_FORMAT_TEXT_V1, + XKB_KEYMAP_COMPILE_NO_FLAGS + )) + , xkbState(xkb_state_new(this->xkbKeymap)) {} +KeyMapState::KeyMapState(const KeyMapState& state) + : xkbKeymap(xkb_keymap_ref(state.xkbKeymap)) + , xkbState(xkb_state_ref(state.xkbState)) {} +KeyMapState::KeyMapState(KeyMapState&& state) noexcept + : xkbKeymap(std::exchange(state.xkbKeymap, nullptr)) + , xkbState(std::exchange(state.xkbState, nullptr)) {} +KeyMapState& KeyMapState::operator=(const KeyMapState& state) { + if (this == &state) return *this; + this->xkbKeymap = xkb_keymap_ref(state.xkbKeymap); + this->xkbState = xkb_state_ref(state.xkbState); + return *this; +} +KeyMapState& KeyMapState::operator=(KeyMapState&& state) noexcept { + if (this == &state) return *this; + this->xkbKeymap = std::exchange(state.xkbKeymap, nullptr); + this->xkbState = std::exchange(state.xkbState, nullptr); + return *this; +} +KeyMapState::~KeyMapState() { + xkb_keymap_unref(std::exchange(this->xkbKeymap, nullptr)); + xkb_state_unref(std::exchange(this->xkbState, nullptr)); +} + +uniqueCString KeyMapState::keyMapAsString() const { + return uniqueCString(xkb_keymap_get_as_string(this->xkbKeymap, XKB_KEYMAP_FORMAT_TEXT_V1)); +} + +bool KeyMapState::operator==(const KeyMapState& other) const { + return this->xkbKeymap == other.xkbKeymap && this->xkbState == other.xkbState; +} +KeyMapState::operator bool() const { + return this->xkbKeymap != nullptr && this->xkbState != nullptr; +} + +const char* KeyMapState::keyName(xkb_keycode_t key) const { + return xkb_keymap_key_get_name(this->xkbKeymap, key); +} + +xkb_keycode_t KeyMapState::maxKeycode() const { return xkb_keymap_max_keycode(this->xkbKeymap); } +xkb_keycode_t KeyMapState::minKeycode() const { return xkb_keymap_min_keycode(this->xkbKeymap); } + +xkb_keysym_t KeyMapState::getOneSym(xkb_keycode_t key) const { + return xkb_state_key_get_one_sym(this->xkbState, key); +} +QChar KeyMapState::getChar(xkb_keycode_t key) const { + return QChar(xkb_state_key_get_utf32(this->xkbState, key)); +} + +void KeyMapState::setModifiers(ModifierState modifierState) { + if (!*this) return; + xkb_state_update_mask( + this->xkbState, + modifierState.depressed, + modifierState.latched, + modifierState.locked, + modifierState.depressedLayout, + modifierState.latchedLayout, + modifierState.lockedLayout + ); +} + +KeyMapState::ModifierState KeyMapState::serialiseMods() const { + return { + .depressed = xkb_state_serialize_mods(this->xkbState, XKB_STATE_MODS_DEPRESSED), + .latched = xkb_state_serialize_mods(this->xkbState, XKB_STATE_MODS_LATCHED), + .locked = xkb_state_serialize_mods(this->xkbState, XKB_STATE_MODS_LOCKED), + .depressedLayout = xkb_state_serialize_mods(this->xkbState, XKB_STATE_LAYOUT_DEPRESSED), + .latchedLayout = xkb_state_serialize_mods(this->xkbState, XKB_STATE_LAYOUT_LATCHED), + .lockedLayout = xkb_state_serialize_mods(this->xkbState, XKB_STATE_LAYOUT_LOCKED), + }; +} + +std::string_view KeyMapState::keyStateName(wl_keyboard_key_state state) { + switch (state) { + case WL_KEYBOARD_KEY_STATE_RELEASED: return "released"; + case WL_KEYBOARD_KEY_STATE_PRESSED: return "pressed"; + default: return "unknown state"; + } +} + +bool KeyMapState::keyRepeats(xkb_keycode_t key) const { + return xkb_keymap_key_repeats(this->xkbKeymap, key); +} + +xkb_context* KeyMapState::xkbContext = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + +} // namespace qs::wayland::input_method::impl diff --git a/src/wayland/input_method/key_map_state.hpp b/src/wayland/input_method/key_map_state.hpp new file mode 100644 index 00000000..f5617a47 --- /dev/null +++ b/src/wayland/input_method/key_map_state.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include + +#include +#include +#include + +#include "c_helpers.hpp" + +namespace qs::wayland::input_method::impl { + +/// A wrapper that handles xkb_keymap and xkb_state lifetime with some convenience functions +class KeyMapState { +public: + KeyMapState() = default; + KeyMapState(const char* string); + KeyMapState(const KeyMapState& state); + KeyMapState(KeyMapState&& state) noexcept; + KeyMapState& operator=(const KeyMapState& state); + KeyMapState& operator=(KeyMapState&& state) noexcept; + ~KeyMapState(); + + // xkb expects us to free the returned string + [[nodiscard]] uniqueCString keyMapAsString() const; + + [[nodiscard]] bool operator==(const KeyMapState& other) const; + [[nodiscard]] operator bool() const; + + [[nodiscard]] const char* keyName(xkb_keycode_t key) const; + [[nodiscard]] xkb_keycode_t maxKeycode() const; + [[nodiscard]] xkb_keycode_t minKeycode() const; + + [[nodiscard]] xkb_keysym_t getOneSym(xkb_keycode_t key) const; + [[nodiscard]] QChar getChar(xkb_keycode_t key) const; + + struct ModifierState { + xkb_mod_mask_t depressed; + xkb_mod_mask_t latched; + xkb_mod_mask_t locked; + xkb_layout_index_t depressedLayout; + xkb_layout_index_t latchedLayout; + xkb_layout_index_t lockedLayout; + }; + // This is the server version + // if we want to expand virtual keyboard support + void setModifiers(ModifierState modifierState); + [[nodiscard]] ModifierState serialiseMods() const; + + static std::string_view keyStateName(wl_keyboard_key_state state); + + [[nodiscard]] bool keyRepeats(xkb_keycode_t key) const; + +private: + static xkb_context* xkbContext; + xkb_keymap* xkbKeymap = nullptr; + xkb_state* xkbState = nullptr; +}; + +} // namespace qs::wayland::input_method::impl diff --git a/src/wayland/input_method/keyboard_grab.cpp b/src/wayland/input_method/keyboard_grab.cpp new file mode 100644 index 00000000..40ea5a7f --- /dev/null +++ b/src/wayland/input_method/keyboard_grab.cpp @@ -0,0 +1,193 @@ +#include "keyboard_grab.hpp" +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "manager.hpp" +#include "types.hpp" +#include "virtual_keyboard.hpp" + +namespace qs::wayland::input_method::impl { + +InputMethodKeyboardGrab::InputMethodKeyboardGrab( + QObject* parent, + ::zwp_input_method_keyboard_grab_v2* keyboard +) + : QObject(parent) + , zwp_input_method_keyboard_grab_v2(keyboard) { + this->mRepeatTimer.callOnTimeout(this, [&](){ + this->mRepeatTimer.setInterval(1000 / this->mRepeatRate); + handleKey(mRepeatKey); + }); +} + +InputMethodKeyboardGrab::~InputMethodKeyboardGrab() { + this->release(); + + // Release forward the pressed keys to the text input + for (xkb_keycode_t key = 0; key < this->mKeyState.size(); ++key) { + if (this->mKeyState[key] == KeyState::PRESSED) { + this->mVirturalKeyboard->sendKey( + key + this->mKeyMapState.minKeycode(), + WL_KEYBOARD_KEY_STATE_PRESSED + ); + } + } +} + +void InputMethodKeyboardGrab::zwp_input_method_keyboard_grab_v2_keymap( + uint32_t format [[maybe_unused]], + int32_t fd, + uint32_t size +) { + // https://wayland-book.com/seat/example.html + assert(format == WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1); + + char* mapShm = static_cast(mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0)); + assert(mapShm != MAP_FAILED); + + this->mKeyMapState = KeyMapState(mapShm); + + munmap(mapShm, size); + close(fd); + + this->mVirturalKeyboard = + VirtualKeyboardManager::instance()->createVirtualKeyboard(this->mKeyMapState); + this->mKeyState = std::vector( + this->mKeyMapState.maxKeycode() - this->mKeyMapState.minKeycode() - WAYLAND_KEY_OFFSET, + KeyState::RELEASED + ); + + // Tell the text input to release all the keys + for (xkb_keycode_t key = 0; key < this->mKeyState.size(); ++key) { + this->mVirturalKeyboard->sendKey( + key + this->mKeyMapState.minKeycode(), + WL_KEYBOARD_KEY_STATE_RELEASED + ); + } +} + +void InputMethodKeyboardGrab::zwp_input_method_keyboard_grab_v2_key( + uint32_t serial, + uint32_t /*time*/, + uint32_t key, + uint32_t state +) { + if (serial <= this->mSerial) return; + this->mSerial = serial; + + key += WAYLAND_KEY_OFFSET; + +#if INPUT_METHOD_PRINT + qDebug() << KeyMapState::keyStateName(static_cast(state)) + << this->mKeyMapState.keyName(key) << "[" << key << "]" + << this->mKeyMapState.getChar(key); +#endif + + const xkb_keysym_t sym = this->mKeyMapState.getOneSym(key); + + if (sym == XKB_KEY_Escape) { + if (state == WL_KEYBOARD_KEY_STATE_PRESSED) emit escapePress(); + return; + } + if (sym == XKB_KEY_Return) { + if (state == WL_KEYBOARD_KEY_STATE_PRESSED) emit returnPress(); + return; + } + + // Skip adding the control keys because we've consumed them + if (state == WL_KEYBOARD_KEY_STATE_PRESSED) { + this->mKeyState[key - this->mKeyMapState.minKeycode()] = KeyState::PRESSED; + } else if (state == WL_KEYBOARD_KEY_STATE_RELEASED) { + this->mKeyState[key - this->mKeyMapState.minKeycode()] = KeyState::RELEASED; + } + + if (state == WL_KEYBOARD_KEY_STATE_PRESSED) { + bool keyHandled = handleKey(key); + if (keyHandled){ + if (this->mKeyMapState.keyRepeats(key) && this->mRepeatRate > 0) { + this->mRepeatKey = key; + this->mRepeatTimer.setInterval(this->mRepeatDelay); + this->mRepeatTimer.start(); + } + return; + } + } + + if (this->mRepeatKey == key) { + this->mRepeatTimer.stop(); + } + + this->mVirturalKeyboard->sendKey(key, static_cast(state)); +} + +bool InputMethodKeyboardGrab::handleKey(xkb_keycode_t key){ + const xkb_keysym_t sym = this->mKeyMapState.getOneSym(key); + if (sym == XKB_KEY_Up) { + emit directionPress(DirectionKey::UP); + return true; + } + if (sym == XKB_KEY_Down) { + emit directionPress(DirectionKey::DOWN); + return true; + } + if (sym == XKB_KEY_Left) { + emit directionPress(DirectionKey::LEFT); + return true; + } + if (sym == XKB_KEY_Right) { + emit directionPress(DirectionKey::RIGHT); + return true; + } + if (sym == XKB_KEY_BackSpace) { + emit backspacePress(); + return true; + } + if (sym == XKB_KEY_Delete) { + emit deletePress(); + return true; + } + + const QChar character = this->mKeyMapState.getChar(key); + if (character != '\0') { + emit keyPress(character); + return true; + } + return false; +} + +void InputMethodKeyboardGrab::zwp_input_method_keyboard_grab_v2_modifiers( + uint32_t serial, + uint32_t modsDepressed, + uint32_t modsLatched, + uint32_t modsLocked, + uint32_t group +) { + if (serial <= this->mSerial) return; + this->mSerial = serial; + this->mKeyMapState.setModifiers( + KeyMapState::ModifierState(modsDepressed, modsLatched, modsLocked, group, group, group) + ); + this->mVirturalKeyboard->sendModifiers(); +} + +void InputMethodKeyboardGrab::zwp_input_method_keyboard_grab_v2_repeat_info( + int32_t rate, + int32_t delay +) { + mRepeatRate = rate; + mRepeatDelay = delay; +} + +} // namespace qs::wayland::input_method::impl diff --git a/src/wayland/input_method/keyboard_grab.hpp b/src/wayland/input_method/keyboard_grab.hpp new file mode 100644 index 00000000..e9b0bcb3 --- /dev/null +++ b/src/wayland/input_method/keyboard_grab.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include +#include + +#include "key_map_state.hpp" +#include "types.hpp" + +namespace qs::wayland::input_method::impl { + +class VirtualKeyboardHandle; + +// TODO(cmurtagh-lgtm) Implement the popup-surface part of the protocol +class InputMethodPopupSurface: QObject { + Q_OBJECT; +}; + +/// A wrapper around the input_method_keyboard_grab protocol +/// Only some control keys and keys that are representable by utf32 are captured, +/// other keys are passed back to the compositor via virtual keyboard. +class InputMethodKeyboardGrab + : public QObject + , private QtWayland::zwp_input_method_keyboard_grab_v2 { + Q_OBJECT; + +public: + explicit InputMethodKeyboardGrab(QObject* parent, ::zwp_input_method_keyboard_grab_v2* keyboard); + ~InputMethodKeyboardGrab() override; + Q_DISABLE_COPY_MOVE(InputMethodKeyboardGrab); + +signals: + void keyPress(QChar character); + void escapePress(); + void returnPress(); + void directionPress(DirectionKey); + void backspacePress(); + void deletePress(); + +private: + void + zwp_input_method_keyboard_grab_v2_keymap(uint32_t format, int32_t fd, uint32_t size) override; + void zwp_input_method_keyboard_grab_v2_key( + uint32_t serial, + uint32_t time, + uint32_t key, + uint32_t state + ) override; + void zwp_input_method_keyboard_grab_v2_modifiers( + uint32_t serial, + uint32_t modsDepressed, + uint32_t modsLatched, + uint32_t modsLocked, + uint32_t group + ) override; + void zwp_input_method_keyboard_grab_v2_repeat_info(int32_t rate, int32_t delay) override; + + bool handleKey(xkb_keycode_t key); + + uint32_t mSerial = 0; + + enum class KeyState : bool { + PRESSED = WL_KEYBOARD_KEY_STATE_PRESSED, + RELEASED = WL_KEYBOARD_KEY_STATE_RELEASED + }; + std::vector mKeyState; + KeyMapState mKeyMapState; + + QTimer mRepeatTimer; + // Keys per second + int32_t mRepeatRate = 25; + // milliseconds before first repeat + int32_t mRepeatDelay = 400; + xkb_keycode_t mRepeatKey = 0; + + std::unique_ptr mVirturalKeyboard; +}; + +} // namespace qs::wayland::input_method::impl diff --git a/src/wayland/input_method/manager.cpp b/src/wayland/input_method/manager.cpp new file mode 100644 index 00000000..0f6ee66d --- /dev/null +++ b/src/wayland/input_method/manager.cpp @@ -0,0 +1,59 @@ +#include "manager.hpp" +#include + +#include +#include +#include + +#include "input_method.hpp" +#include "virtual_keyboard.hpp" + +namespace qs::wayland::input_method::impl { + +InputMethodManager::InputMethodManager(): QWaylandClientExtensionTemplate(1) { this->initialize(); } + +InputMethodManager::~InputMethodManager() { this->destroy(); } + +InputMethodManager* InputMethodManager::instance() { + // The OS should free this memory when we exit + static auto* instance = new InputMethodManager(); + return instance; +} + +namespace { +wl_seat* getSeat() { + return dynamic_cast(QGuiApplication::instance()) + ->nativeInterface() + ->seat(); +} +} // namespace + +QPointer InputMethodManager::acquireInput() { + if (this->inputMethod && this->inputMethod->isAvailable()) return this->inputMethod; + + this->inputMethod = new InputMethodHandle(this, get_input_method(getSeat())); + + return this->inputMethod; +} + +void InputMethodManager::releaseInput() { + this->inputMethod->deleteLater(); + this->inputMethod = nullptr; +} + +VirtualKeyboardManager::VirtualKeyboardManager(): QWaylandClientExtensionTemplate(1) { + this->initialize(); +} + +VirtualKeyboardManager* VirtualKeyboardManager::instance() { + // The OS should free this memory when we exit + static auto* instance = new VirtualKeyboardManager(); + return instance; +} + +std::unique_ptr +VirtualKeyboardManager::createVirtualKeyboard(const KeyMapState& keymap) { + return std::make_unique(create_virtual_keyboard(getSeat()), keymap); +} + +} // namespace qs::wayland::input_method::impl diff --git a/src/wayland/input_method/manager.hpp b/src/wayland/input_method/manager.hpp new file mode 100644 index 00000000..add5ee14 --- /dev/null +++ b/src/wayland/input_method/manager.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace qs::wayland::input_method::impl { + +class InputMethodHandle; +class KeyMapState; + +class InputMethodManager + : public QWaylandClientExtensionTemplate + , private QtWayland::zwp_input_method_manager_v2 { + friend class QWaylandClientExtensionTemplate; + +public: + ~InputMethodManager() override; + Q_DISABLE_COPY_MOVE(InputMethodManager); + + static InputMethodManager* instance(); + + QPointer acquireInput(); + void releaseInput(); + +private: + InputMethodManager(); + InputMethodHandle* inputMethod = nullptr; +}; + +class VirtualKeyboardHandle; + +class VirtualKeyboardManager + : public QWaylandClientExtensionTemplate + , private QtWayland::zwp_virtual_keyboard_manager_v1 { + friend class QWaylandClientExtensionTemplate; + +public: + ~VirtualKeyboardManager() override = default; + Q_DISABLE_COPY_MOVE(VirtualKeyboardManager); + + static VirtualKeyboardManager* instance(); + + std::unique_ptr createVirtualKeyboard(const KeyMapState& keymap); + +private: + VirtualKeyboardManager(); +}; + +} // namespace qs::wayland::input_method::impl diff --git a/src/wayland/input_method/qml.cpp b/src/wayland/input_method/qml.cpp new file mode 100644 index 00000000..be1a9ed6 --- /dev/null +++ b/src/wayland/input_method/qml.cpp @@ -0,0 +1,213 @@ +#include "qml.hpp" +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "input_method.hpp" +#include "keyboard_grab.hpp" +#include "manager.hpp" +#include "types.hpp" + +namespace qs::wayland::input_method { + +using namespace impl; + +InputMethod::InputMethod(QObject* parent): QObject(parent) { this->getInput(); } + +void InputMethod::sendString(const QString& text) { + if (!this->isActive()) return; + + this->handle->commitString(text); + this->handle->commit(); +} + +void InputMethod::sendPreeditString(const QString& text, int32_t cursorBegin, int32_t cursorEnd) { + if (!this->isActive()) return; + + this->handle->sendPreeditString(text, cursorBegin, cursorEnd); + this->handle->commit(); +} + +void InputMethod::deleteText(int before, int after) { + if (!this->isActive()) return; + + this->handle->deleteText(before, after); + this->handle->commit(); +} + +bool InputMethod::isActive() const { return this->hasInput() && this->handle->isActive(); } + +QQmlComponent* InputMethod::keyboardComponent() const { return this->mKeyboardComponent; } +void InputMethod::setKeyboardComponent(QQmlComponent* keyboardComponent) { + if (this->mKeyboardComponent == keyboardComponent) return; + this->mKeyboardComponent = keyboardComponent; + emit keyboardComponentChanged(); + + if (this->keyboard) { + this->keyboard->deleteLater(); + this->keyboard = nullptr; + } + + this->handleKeyboardActive(); +} + +bool InputMethod::hasInput() const { return handle && handle->isAvailable(); } + +void InputMethod::getInput() { + if (this->hasInput()) return; + + this->handle = InputMethodManager::instance()->acquireInput(); + + QObject::connect( + this->handle.get(), + &InputMethodHandle::activated, + this, + &InputMethod::onHandleActiveChanged + ); + QObject::connect( + this->handle.get(), + &InputMethodHandle::deactivated, + this, + &InputMethod::onHandleActiveChanged + ); + QObject::connect( + this->handle.get(), + &InputMethodHandle::contentHintChanged, + this, + &InputMethod::contentHintChanged + ); + QObject::connect( + this->handle.get(), + &InputMethodHandle::contentPurposeChanged, + this, + &InputMethod::contentPurposeChanged + ); + QObject::connect( + this->handle.get(), + &InputMethodHandle::surroundingTextChanged, + this, + &InputMethod::surroundingTextChanged + ); + + emit hasInputChanged(); +} + +void InputMethod::releaseInput() { + if (!this->handle) return; + InputMethodManager::instance()->releaseInput(); + emit hasInputChanged(); +} + +bool InputMethod::hasKeyboard() const { + // The lifetime of keyboard should be less than handle's + if (this->keyboard) { + assert(this->handle->hasKeyboard()); + } + return this->keyboard; +} + +void InputMethod::grabKeyboard() { + if (this->hasKeyboard()) return; + if (this->handle->hasKeyboard()) { + qmlDebug(this) << "Only one input method can grad a keyboard at any one time"; + return; + } + auto* instanceObj = + this->mKeyboardComponent->create(QQmlEngine::contextForObject(this->mKeyboardComponent)); + auto* instance = qobject_cast(instanceObj); + + if (instance == nullptr) { + qWarning() << "Failed to create input method keyboard component"; + if (instanceObj != nullptr) instanceObj->deleteLater(); + return; + } + + instance->setParent(this); + instance->setKeyboard(handle->grabKeyboard()); + // Always have a way to release the keyboard + QObject::connect(instance, &Keyboard::escapePress, this, &InputMethod::releaseKeyboard); + + this->keyboard = instance; + emit hasKeyboardChanged(); +} + +void InputMethod::releaseKeyboard() { + if (!this->hasKeyboard()) return; + this->keyboard->deleteLater(); + this->keyboard = nullptr; + this->handle->releaseKeyboard(); + if (this->mClearPreeditOnKeyboardRelease) this->sendPreeditString(""); + emit hasKeyboardChanged(); +} + +void InputMethod::handleKeyboardActive() { + if (!this->mKeyboardComponent) return; + if (this->keyboard) { + this->releaseKeyboard(); + } +} + +const QString& InputMethod::surroundingText() const { + return handle->surroundingText(); +} +uint32_t InputMethod::surroundingTextCursor() const { + return handle->surroundingTextCursor(); +} +uint32_t InputMethod::surroundingTextAnchor() const { + return handle->surroundingTextAnchor(); +} + +QMLContentHint::Enum InputMethod::contentHint() const { + return this->handle->contentHint(); +} +QMLContentPurpose::Enum InputMethod::contentPurpose() const { + return this->handle->contentPurpose(); +} + +void InputMethod::onHandleActiveChanged() { + this->handleKeyboardActive(); + emit activeChanged(); +} + +Keyboard::Keyboard(QObject* parent): QObject(parent) {} + +void Keyboard::setKeyboard(QPointer keyboard) { + if (this->mKeyboard == keyboard) return; + if (keyboard == nullptr) return; + this->mKeyboard = std::move(keyboard); + + QObject::connect(this->mKeyboard, &InputMethodKeyboardGrab::keyPress, this, &Keyboard::keyPress); + QObject::connect( + this->mKeyboard, + &InputMethodKeyboardGrab::escapePress, + this, + &Keyboard::escapePress + ); + QObject::connect( + this->mKeyboard, + &InputMethodKeyboardGrab::returnPress, + this, + &Keyboard::returnPress + ); + QObject::connect( + this->mKeyboard, + &InputMethodKeyboardGrab::backspacePress, + this, + &Keyboard::backspacePress + ); + QObject::connect( + this->mKeyboard, + &InputMethodKeyboardGrab::deletePress, + this, + &Keyboard::deletePress + ); +} + +} // namespace qs::wayland::input_method diff --git a/src/wayland/input_method/qml.hpp b/src/wayland/input_method/qml.hpp new file mode 100644 index 00000000..220aff3b --- /dev/null +++ b/src/wayland/input_method/qml.hpp @@ -0,0 +1,160 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "types.hpp" + +namespace qs::wayland::input_method { + +namespace impl { +class InputMethodManager; +class InputMethodHandle; +class InputMethodKeyboardGrab; +} // namespace impl + +/// Provides keyboard handling logic for an @@InputMethod$'s grab protocol. +/// Use @@KeyboardTextEdit$ for a higher level and easier to use version. +class Keyboard: public QObject { + Q_OBJECT; + /// TODO The surface that will be created for the keyboard. Must create a @@KeyboardSurface$. + // Q_PROPERTY(QQmlComponent* surface READ surfaceComponent WRITE setSurfaceComponent NOTIFY surfaceComponentChanged); + // Q_CLASSINFO("DefaultProperty", "surface"); + QML_ELEMENT; + +public: + explicit Keyboard(QObject* parent = nullptr); + + void setKeyboard(QPointer keyboard); + +signals: + void keyPress(QChar character); + void returnPress(); + /// Note that internally Quickshell will release the keyboard when escape is pressed. + void escapePress(); + void directionPress(QMLDirectionKey::Enum); + void backspacePress(); + void deletePress(); + +private: + QPointer mKeyboard = nullptr; + // QQmlComponent* mSurfaceComponent = nullptr; +}; + +/// Provides the ability to send text input to the compositor +/// +/// A simple example that sends text to the currently focused input method: +/// ``` +/// QSInputMethod { +/// id: input_method +/// } +/// IpcHandler { +/// target: "input_method" +/// function hi(): void { input_method.sendString("hi"); } +/// } +/// ``` +/// We can now call the ipc handler and see that `hi` is printed to the terminal input. +/// `$ qs ipc call input_method hi` +class InputMethod: public QObject { + Q_OBJECT; + /// If a text input is focused + Q_PROPERTY(bool active READ isActive NOTIFY activeChanged); + /// Is false if another input method is already registered to the compositor, otherwise is true + Q_PROPERTY(bool hasInput READ hasInput NOTIFY hasInputChanged); + /// If the input method has grabbed the keyboard + Q_PROPERTY(bool hasKeyboard READ hasKeyboard NOTIFY hasKeyboardChanged); + /// The text around the where we will insert + Q_PROPERTY(QString surroundingText READ surroundingText NOTIFY surroundingTextChanged); + Q_PROPERTY(uint32_t surroundingTextCurosr READ surroundingTextCursor NOTIFY surroundingTextChanged); + Q_PROPERTY(uint32_t surroundingTextAnchor READ surroundingTextAnchor NOTIFY surroundingTextChanged); + /// The content_hint of the text input see https://wayland.app/protocols/text-input-unstable-v3#zwp_text_input_v3:enum:content_hint + Q_PROPERTY(QMLContentHint::Enum contentHint READ contentHint NOTIFY contentHintChanged); + /// The content_purpose of the text input see https://wayland.app/protocols/text-input-unstable-v3#zwp_text_input_v3:enum:content_purpose + Q_PROPERTY(QMLContentPurpose::Enum contentPurpose READ contentPurpose NOTIFY contentPurposeChanged); + /// Clear the preedit text when the keyboard grab is released + Q_PROPERTY( + bool clearPreeditOnKeyboardRelease MEMBER mClearPreeditOnKeyboardRelease NOTIFY + clearPreeditOnKeyboardReleaseChanged + ); + /// The @@Keyboard$ that will handle the grabbed keyboard. + /// Use @@KeyboardTextEdit$ for most cases. + Q_PROPERTY( + QQmlComponent* keyboard READ keyboardComponent WRITE setKeyboardComponent NOTIFY + keyboardComponentChanged + ); + Q_CLASSINFO("DefaultProperty", "keyboard"); + QML_NAMED_ELEMENT(QSInputMethod); + +public: + explicit InputMethod(QObject* parent = nullptr); + + /// Sends a string to the text input currently focused + Q_INVOKABLE void sendString(const QString& text); + /// Provides virtual text in the text input so the user can visualise what they write + /// This will override any previous preedit text + /// If `cursorBegin == cursorEnd == -1` the text input will not show a cursor + /// If `cursorBegin == cursorEnd == n` the text input will show a cursor at n + /// If `cursorBegin == n` and `cursorEnd == m` the text from n to m will be highlighted + Q_INVOKABLE void + sendPreeditString(const QString& text, int32_t cursorBegin = -1, int32_t cursorEnd = -1); + /// Removes text before the cursor by `before` and after by `after`. + /// If preedit text is present, then text will be deleted before the preedit text and after the preedit text instead of the cursor. + Q_INVOKABLE void deleteText(int before = 1, int after = 0); + + /// If there is a focused text input that we can write to + Q_INVOKABLE [[nodiscard]] bool isActive() const; + + /// @@keyboard$ + Q_INVOKABLE [[nodiscard]] QQmlComponent* keyboardComponent() const; + /// @@keyboard$ + Q_INVOKABLE void setKeyboardComponent(QQmlComponent* keyboardComponent); + + /// @@hasInput$ + Q_INVOKABLE [[nodiscard]] bool hasInput() const; + /// Retries getting the input method. + Q_INVOKABLE void getInput(); + /// Releases the input method so another program can use it. + Q_INVOKABLE void releaseInput(); + + /// @@hasKeyboard$ + Q_INVOKABLE [[nodiscard]] bool hasKeyboard() const; + /// Grabs the current keyboard so the input can be intercepted by the @@keyboard$ object + Q_INVOKABLE void grabKeyboard(); + /// Releases the grabbed keyboard so it can be used normally. + Q_INVOKABLE void releaseKeyboard(); + + Q_INVOKABLE [[nodiscard]] const QString& surroundingText() const; + Q_INVOKABLE [[nodiscard]] uint32_t surroundingTextCursor() const; + Q_INVOKABLE [[nodiscard]] uint32_t surroundingTextAnchor() const; + + Q_INVOKABLE [[nodiscard]] QMLContentHint::Enum contentHint() const; + Q_INVOKABLE [[nodiscard]] QMLContentPurpose::Enum contentPurpose() const; + +signals: + void activeChanged(); + void hasInputChanged(); + void hasKeyboardChanged(); + void clearPreeditOnKeyboardReleaseChanged(); + void keyboardComponentChanged(); + void contentHintChanged(); + void contentPurposeChanged(); + + void surroundingTextChanged(QMLTextChangeCause::Enum textChangeCause); + +private slots: + void onHandleActiveChanged(); + +private: + void handleKeyboardActive(); + + QPointer handle; + Keyboard* keyboard = nullptr; + QQmlComponent* mKeyboardComponent = nullptr; + + bool mClearPreeditOnKeyboardRelease = true; +}; + +} // namespace qs::wayland::input_method diff --git a/src/wayland/input_method/qml_helpers.cpp b/src/wayland/input_method/qml_helpers.cpp new file mode 100644 index 00000000..79520795 --- /dev/null +++ b/src/wayland/input_method/qml_helpers.cpp @@ -0,0 +1,110 @@ +#include "qml_helpers.hpp" + +#include +#include +#include + +#include "qml.hpp" + +namespace qs::wayland::input_method { + +KeyboardTextEdit::KeyboardTextEdit(QObject* parent): Keyboard(parent) { + QObject::connect(this, &Keyboard::keyPress, this, &KeyboardTextEdit::onKeyPress); + QObject::connect(this, &Keyboard::returnPress, this, &KeyboardTextEdit::onReturnPress); + QObject::connect(this, &Keyboard::directionPress, this, &KeyboardTextEdit::onDirectionPress); + QObject::connect(this, &Keyboard::backspacePress, this, &KeyboardTextEdit::onBackspacePress); + QObject::connect(this, &Keyboard::deletePress, this, &KeyboardTextEdit::onDeletePress); + + auto* inputMethod = dynamic_cast(parent); + if (inputMethod) + QObject::connect(inputMethod, &InputMethod::surroundingTextChanged, this, &KeyboardTextEdit::onSurroundingTextChanged); +} + +QJSValue KeyboardTextEdit::transform() const { return this->mTransform; } + +void KeyboardTextEdit::setTransform(const QJSValue& callback) { + if (!callback.isCallable()) { + qmlInfo(this) << "Transform must be a callable function"; + return; + } + this->mTransform = callback; +} + +int KeyboardTextEdit::cursor() const { return this->mCursor; } +void KeyboardTextEdit::setCursor(int value) { + value = std::max(value, 0); + value = std::min(value, static_cast(this->mEditText.size())); + if (this->mCursor == value) return; + this->mCursor = value; + this->updatePreedit(); + emit cursorChanged(); +} + +QString KeyboardTextEdit::editText() const { return this->mEditText; } +void KeyboardTextEdit::setEditText(const QString& value) { + if (this->mEditText == value) return; + this->mEditText = value; + this->updatePreedit(); + emit editTextChanged(); +} + +void KeyboardTextEdit::updatePreedit() { + auto* inputMethod = dynamic_cast(parent()); + if (!inputMethod) { + return; + } + inputMethod->sendPreeditString(this->mEditText, this->mCursor, this->mCursor); +} + +void KeyboardTextEdit::onKeyPress(QChar character) { + this->mEditText.insert(this->mCursor, character); + this->updatePreedit(); + emit editTextChanged(); + this->setCursor(this->mCursor + 1); +} +void KeyboardTextEdit::onBackspacePress() { + if (this->mCursor == 0) return; + this->mEditText.remove(this->mCursor - 1, 1); + this->updatePreedit(); + emit editTextChanged(); + this->setCursor(this->mCursor - 1); +} +void KeyboardTextEdit::onDeletePress() { + if (this->mCursor == this->mEditText.size()) return; + this->mEditText.remove(this->mCursor, 1); + this->updatePreedit(); + emit editTextChanged(); +} + +void KeyboardTextEdit::onDirectionPress(QMLDirectionKey::Enum direction) { + switch (direction) { + case QMLDirectionKey::LEFT: { + this->setCursor(this->mCursor - 1); + return; + } + case QMLDirectionKey::RIGHT: { + this->setCursor(this->mCursor + 1); + return; + } + default: return; + } +} + +void KeyboardTextEdit::onReturnPress() { + auto* inputMethod = dynamic_cast(parent()); + if (!inputMethod) return; + QString text = ""; + if (this->mTransform.isCallable()) + text = this->mTransform.call(QJSValueList({this->mEditText})).toString(); + inputMethod->sendString(text); + this->setEditText(""); + inputMethod->releaseKeyboard(); +} + +void KeyboardTextEdit::onSurroundingTextChanged(QMLTextChangeCause::Enum textChangeCause) { + if (textChangeCause == QMLTextChangeCause::OTHER) { + this->setEditText(""); + } +} + +} // namespace qs::wayland::input_method diff --git a/src/wayland/input_method/qml_helpers.hpp b/src/wayland/input_method/qml_helpers.hpp new file mode 100644 index 00000000..351148cf --- /dev/null +++ b/src/wayland/input_method/qml_helpers.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include + +#include "qml.hpp" + +namespace qs::wayland::input_method { + +/// A keyboard handler for @@InputMethod$'s keyboard grab protocol. +/// +/// As text is typed on the keyboard this will be displayed in the input method as virtual/preedit text. +/// A cursor enables some control over the composition of the text. +/// When return is pressed the transform function is called and the returned string is sent to the input text +/// and the keyboard is released. +/// +/// ``` +/// QSInputMethod { +/// id: input_method +/// KeyboardTextEdit { +/// transform: function (text: string): string { +/// return { +/// "cool": "hi" +/// // etc. +/// }[text]; +/// } +/// } +/// } +/// IpcHandler { +/// target: "emoji" +/// function get(): void { input_method.grabKeyboard(); } +/// } +/// ``` +/// +/// If you need a lower level view of the key events use @@Keyboard$. +class KeyboardTextEdit: public Keyboard { + Q_OBJECT; + /// A function that has a string parameter and returns a string. + Q_PROPERTY(QJSValue transform READ transform WRITE setTransform NOTIFY transformChanged FINAL) + /// The position of the cursor within the @@editText$. + Q_PROPERTY(int cursor MEMBER mCursor READ cursor WRITE setCursor NOTIFY cursorChanged); + /// The text that is currently being edited and displayed. + Q_PROPERTY( + QString editText MEMBER mEditText READ editText WRITE setEditText NOTIFY editTextChanged + ); + QML_ELEMENT; + +public: + KeyboardTextEdit(QObject* parent = nullptr); + + Q_INVOKABLE [[nodiscard]] QJSValue transform() const; + Q_INVOKABLE void setTransform(const QJSValue& callback); + + Q_INVOKABLE [[nodiscard]] int cursor() const; + Q_INVOKABLE void setCursor(int value); + + Q_INVOKABLE [[nodiscard]] QString editText() const; + Q_INVOKABLE void setEditText(const QString& value); + +signals: + void transformChanged(); + void cursorChanged(); + void editTextChanged(); + +private slots: + void onKeyPress(QChar character); + void onReturnPress(); + void onDirectionPress(QMLDirectionKey::Enum direction); + void onBackspacePress(); + void onDeletePress(); + void onSurroundingTextChanged(QMLTextChangeCause::Enum textChangeCause); + +private: + void updatePreedit(); + + QJSValue mTransform; + int mCursor = 0; + QString mEditText = ""; +}; + +} // namespace qs::wayland::input_method diff --git a/src/wayland/input_method/types.cpp b/src/wayland/input_method/types.cpp new file mode 100644 index 00000000..674075a4 --- /dev/null +++ b/src/wayland/input_method/types.cpp @@ -0,0 +1,125 @@ +#include "types.hpp" + +#include "qstring.h" + +namespace qs::wayland::input_method { + +QString QMLDirectionKey::toString(Enum direction) { + switch (direction) { + case UP: return "UP"; + case DOWN: return "DOWN"; + case LEFT: return "LEFT"; + case RIGHT: return "RIGHT"; + } + return "UNKNOWN"; +} + +QString QMLContentHint::toString(Enum contentHint) { + QString string = ""; + + bool first = true; + + if (contentHint & COMPLETION) { + if (!first) { + string += " | "; + } + string += "COMPLETION"; + first = false; + } + if (contentHint & SPELLCHECK) { + if (!first) { + string += " | "; + } + string += "SPELLCHECK"; + first = false; + } + if (contentHint & AUTO_CAPITALIZATION) { + if (!first) { + string += " | "; + } + string += "AUTO_CAPITALIZATION"; + first = false; + } + if (contentHint & LOWERCASE) { + if (!first) { + string += " | "; + } + string += "LOWERCASE"; + first = false; + } + if (contentHint & UPPERCASE) { + if (!first) { + string += " | "; + } + string += "UPPERCASE"; + first = false; + } + if (contentHint & TITLECASE) { + if (!first) { + string += " | "; + } + string += "TITLECASE"; + first = false; + } + if (contentHint & HIDDEN_TEXT) { + if (!first) { + string += " | "; + } + string += "HIDDEN_TEXT"; + first = false; + } + if (contentHint & SENSITIVE_DATA) { + if (!first) { + string += " | "; + } + string += "SENSITIVE_DATA"; + first = false; + } + if (contentHint & LATIN) { + if (!first) { + string += " | "; + } + string += "LATIN"; + first = false; + } + if (contentHint & MULTILINE) { + if (!first) { + string += " | "; + } + string += "MULTILINE"; + first = false; + } + + if (string == "") string = "NONE"; + return string; +} + +QString QMLContentPurpose::toString(Enum contentHint) { + switch (contentHint) { + case NORMAL: return "NORMAL"; + case ALPHA: return "ALPHA"; + case DIGITS: return "DIGITS"; + case NUMBER: return "NUMBER"; + case PHONE: return "PHONE"; + case URL: return "URL"; + case EMAIL: return "EMAIL"; + case NAME: return "NAME"; + case PASSWORD: return "PASSWORD"; + case PIN: return "PIN"; + case DATA: return "DATA"; + case TIME: return "TIME"; + case DATETIME: return "DATETIME"; + case TERMINAL: return "TERMINAL"; + } + return "UNKNOWN"; +} + +QString QMLTextChangeCause::toString(Enum textChangeCause) { + switch (textChangeCause) { + case INPUT_METHOD: return "INPUT_METHOD"; + case OTHER: return "OTHER"; + } + return "UNKNOWN"; +} + +} // namespace qs::wayland::input_method diff --git a/src/wayland/input_method/types.hpp b/src/wayland/input_method/types.hpp new file mode 100644 index 00000000..d97e5bde --- /dev/null +++ b/src/wayland/input_method/types.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace qs::wayland::input_method { + +class QMLDirectionKey: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(DirectionKey); + QML_SINGLETON; + +public: + enum Enum : quint8 { + UP, DOWN, LEFT, RIGHT + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(Enum direction); +}; + +class QMLContentHint: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(ContentHint); + QML_SINGLETON; + +public: + enum Enum : quint16 { + NONE = ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE, + COMPLETION = ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION, + SPELLCHECK = ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK, + AUTO_CAPITALIZATION = ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION, + LOWERCASE = ZWP_TEXT_INPUT_V3_CONTENT_HINT_LOWERCASE, + UPPERCASE = ZWP_TEXT_INPUT_V3_CONTENT_HINT_UPPERCASE, + TITLECASE = ZWP_TEXT_INPUT_V3_CONTENT_HINT_TITLECASE, + HIDDEN_TEXT = ZWP_TEXT_INPUT_V3_CONTENT_HINT_HIDDEN_TEXT, + SENSITIVE_DATA = ZWP_TEXT_INPUT_V3_CONTENT_HINT_SENSITIVE_DATA, + LATIN = ZWP_TEXT_INPUT_V3_CONTENT_HINT_LATIN, + MULTILINE = ZWP_TEXT_INPUT_V3_CONTENT_HINT_MULTILINE, + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(Enum contentHint); +}; + +class QMLContentPurpose: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(ContentPurpose); + QML_SINGLETON; + +public: + enum Enum : quint8{ + NORMAL = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL, + ALPHA = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ALPHA, + DIGITS = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DIGITS, + NUMBER = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NUMBER, + PHONE = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE, + URL = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_URL, + EMAIL = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_EMAIL, + NAME = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NAME, + PASSWORD = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PASSWORD, + PIN = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PIN, + DATA = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATE, + TIME = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TIME, + DATETIME = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATETIME, + TERMINAL = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL, + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(Enum contentHint); +}; + +class QMLTextChangeCause: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(TextChangeCause); + QML_SINGLETON; + +public: + enum Enum : bool { + INPUT_METHOD = ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_INPUT_METHOD, + OTHER = ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER, + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(Enum textChangeCause); +}; + +namespace impl { +using DirectionKey = QMLDirectionKey::Enum; + +using ContentHint = QMLContentHint::Enum; +using ContentPurpose = QMLContentPurpose::Enum; +using TextChangeCause = QMLTextChangeCause::Enum; + +inline constexpr xkb_keycode_t WAYLAND_KEY_OFFSET = 8; +} +} // namespace qs::wayland::input_method::impl diff --git a/src/wayland/input_method/virtual-keyboard-unstable-v1.xml b/src/wayland/input_method/virtual-keyboard-unstable-v1.xml new file mode 100644 index 00000000..5095c91b --- /dev/null +++ b/src/wayland/input_method/virtual-keyboard-unstable-v1.xml @@ -0,0 +1,113 @@ + + + + Copyright © 2008-2011 Kristian Høgsberg + Copyright © 2010-2013 Intel Corporation + Copyright © 2012-2013 Collabora, Ltd. + Copyright © 2018 Purism SPC + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + The virtual keyboard provides an application with requests which emulate + the behaviour of a physical keyboard. + + This interface can be used by clients on its own to provide raw input + events, or it can accompany the input method protocol. + + + + + Provide a file descriptor to the compositor which can be + memory-mapped to provide a keyboard mapping description. + + Format carries a value from the keymap_format enumeration. + + + + + + + + + + + + + A key was pressed or released. + The time argument is a timestamp with millisecond granularity, with an + undefined base. All requests regarding a single object must share the + same clock. + + Keymap must be set before issuing this request. + + State carries a value from the key_state enumeration. + + + + + + + + + Notifies the compositor that the modifier and/or group state has + changed, and it should update state. + + The client should use wl_keyboard.modifiers event to synchronize its + internal state with seat state. + + Keymap must be set before issuing this request. + + + + + + + + + + + + + + + A virtual keyboard manager allows an application to provide keyboard + input events as if they came from a physical keyboard. + + + + + + + + + Creates a new virtual keyboard associated to a seat. + + If the compositor enables a keyboard to perform arbitrary actions, it + should present an error when an untrusted client requests a new + keyboard. + + + + + + diff --git a/src/wayland/input_method/virtual_keyboard.cpp b/src/wayland/input_method/virtual_keyboard.cpp new file mode 100644 index 00000000..ad80a148 --- /dev/null +++ b/src/wayland/input_method/virtual_keyboard.cpp @@ -0,0 +1,65 @@ +#include "virtual_keyboard.hpp" +#include +#include +#include + +#include +#include +#include +#include + +#include "c_helpers.hpp" +#include "key_map_state.hpp" +#include "types.hpp" + +namespace qs::wayland::input_method::impl { + +namespace { +using namespace std::chrono; +int64_t now() { + return duration_cast(system_clock::now().time_since_epoch()).count(); +} +} // namespace + +VirtualKeyboardHandle::VirtualKeyboardHandle( + ::zwp_virtual_keyboard_v1* keyboard, + const KeyMapState& keymap +) + : QtWayland::zwp_virtual_keyboard_v1(keyboard) { + setKeymapState(keymap); +} + +VirtualKeyboardHandle::~VirtualKeyboardHandle() { this->destroy(); } + +void VirtualKeyboardHandle::setKeymapState(const KeyMapState& keymap) { + if (this->mKeyMapState == keymap) return; + if (!keymap) return; + this->mKeyMapState = keymap; + + static const char* shmName = "/quickshellvirtualkeyboardformat"; + + auto keymapString = keymap.keyMapAsString(); + const size_t size = strlen(keymapString.get()) + 1; + auto shm = SharedMemory(shmName, O_CREAT | O_RDWR, size); + shm.write(keymapString.get()); + + if (shm) { + this->zwp_virtual_keyboard_v1::keymap(WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1, shm.get(), size); + this->good = true; + } +} + +void VirtualKeyboardHandle::sendKey(xkb_keycode_t keycode, wl_keyboard_key_state state) { + if (!this->good) return; + if (keycode == XKB_KEYCODE_INVALID) return; + this->zwp_virtual_keyboard_v1::key(now(), keycode - WAYLAND_KEY_OFFSET, state); +} + +void VirtualKeyboardHandle::sendModifiers() { + auto mods = this->mKeyMapState.serialiseMods(); + modifiers(mods.depressed, mods.latched, mods.locked, mods.depressedLayout); +} + +bool VirtualKeyboardHandle::getGood() const { return good; } + +}; // namespace qs::wayland::input_method::impl diff --git a/src/wayland/input_method/virtual_keyboard.hpp b/src/wayland/input_method/virtual_keyboard.hpp new file mode 100644 index 00000000..75f58c0a --- /dev/null +++ b/src/wayland/input_method/virtual_keyboard.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include + +#include "key_map_state.hpp" + +namespace qs::wayland::input_method::impl { + +class VirtualKeyboardHandle: private QtWayland::zwp_virtual_keyboard_v1 { + +public: + explicit VirtualKeyboardHandle(::zwp_virtual_keyboard_v1* keyboard, const KeyMapState& keymap); + ~VirtualKeyboardHandle() override; + Q_DISABLE_COPY_MOVE(VirtualKeyboardHandle); + + KeyMapState getKeyMapState(); + void setKeymapState(const KeyMapState& keymap); + + void sendKey(xkb_keycode_t keycode, wl_keyboard_key_state state); + void sendModifiers(); + + [[nodiscard]] bool getGood() const; + +private: + KeyMapState mKeyMapState; + bool good = false; +}; + +} // namespace qs::wayland::input_method::impl From c81f594208ad6bd8d97f679884afcdc0d8b390db Mon Sep 17 00:00:00 2001 From: cameron Date: Sat, 6 Sep 2025 12:19:19 +1000 Subject: [PATCH 2/3] lint errors --- src/wayland/input_method/c_helpers.cpp | 4 +- src/wayland/input_method/input_method.cpp | 18 ++-- src/wayland/input_method/input_method.hpp | 7 +- src/wayland/input_method/keyboard_grab.cpp | 14 +-- src/wayland/input_method/qml.cpp | 42 ++++----- src/wayland/input_method/qml_helpers.cpp | 16 +++- src/wayland/input_method/types.cpp | 86 +++++++++---------- src/wayland/input_method/types.hpp | 56 ++++++------ src/wayland/input_method/virtual_keyboard.cpp | 3 +- 9 files changed, 121 insertions(+), 125 deletions(-) diff --git a/src/wayland/input_method/c_helpers.cpp b/src/wayland/input_method/c_helpers.cpp index 21ffbcdb..6c3fef1b 100644 --- a/src/wayland/input_method/c_helpers.cpp +++ b/src/wayland/input_method/c_helpers.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include namespace qs::wayland::input_method::impl { @@ -19,8 +18,7 @@ void FreeDeleter::operator()(const char* p) const { SharedMemory::SharedMemory(const char* shmName, int oFlag, size_t size) : mShmName(shmName) , mSize(size) - , fd(shm_open(this->mShmName, oFlag, 0)) - , map(nullptr) { + , fd(shm_open(this->mShmName, oFlag, 0)) { if (this->fd == -1) { perror(""); qDebug() << "Virtual keyboard failed to open shared memory"; diff --git a/src/wayland/input_method/input_method.cpp b/src/wayland/input_method/input_method.cpp index 207f9113..c133516d 100644 --- a/src/wayland/input_method/input_method.cpp +++ b/src/wayland/input_method/input_method.cpp @@ -1,5 +1,5 @@ #include "input_method.hpp" -#include +#include #include #include @@ -12,6 +12,7 @@ #include "keyboard_grab.hpp" #include "manager.hpp" +#include "types.hpp" namespace qs::wayland::input_method::impl { @@ -64,12 +65,8 @@ uint32_t InputMethodHandle::surroundingTextAnchor() const { return this->mState.surroundingText.anchor; } -ContentHint InputMethodHandle::contentHint() const { - return this->mState.contentHint; -} -ContentPurpose InputMethodHandle::contentPurpose() const { - return this->mState.contentPurpose; -} +ContentHint InputMethodHandle::contentHint() const { return this->mState.contentHint; } +ContentPurpose InputMethodHandle::contentPurpose() const { return this->mState.contentPurpose; } void InputMethodHandle::zwp_input_method_v2_activate() { this->mNewState.activated = true; } @@ -95,19 +92,16 @@ void InputMethodHandle::zwp_input_method_v2_content_type(uint32_t hint, uint32_t }; void InputMethodHandle::zwp_input_method_v2_done() { - auto oldState= mState; + auto oldState = mState; this->mState = this->mNewState; - if (this->mState.activated != oldState.activated) { if (this->mNewState.activated) emit activated(); else emit deactivated(); } if (this->mState.surroundingText != oldState.surroundingText) { - emit surroundingTextChanged( - this->mNewState.surroundingText.textChangeCause - ); + emit surroundingTextChanged(this->mNewState.surroundingText.textChangeCause); } if (this->mState.contentHint != oldState.contentHint) { diff --git a/src/wayland/input_method/input_method.hpp b/src/wayland/input_method/input_method.hpp index 6f482e38..469aa93d 100644 --- a/src/wayland/input_method/input_method.hpp +++ b/src/wayland/input_method/input_method.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -70,12 +71,12 @@ class InputMethodHandle QString text = ""; uint32_t cursor = 0; uint32_t anchor = 0; - TextChangeCause textChangeCause = TextChangeCause::INPUT_METHOD; + TextChangeCause textChangeCause = TextChangeCause::InputMethod; bool operator==(const SurroundingText& other) const = default; } surroundingText; - ContentHint contentHint = ContentHint::NONE; - ContentPurpose contentPurpose = ContentPurpose::NORMAL; + ContentHint contentHint = ContentHint::None; + ContentPurpose contentPurpose = ContentPurpose::Normal; }; State mState; diff --git a/src/wayland/input_method/keyboard_grab.cpp b/src/wayland/input_method/keyboard_grab.cpp index 40ea5a7f..f13b996c 100644 --- a/src/wayland/input_method/keyboard_grab.cpp +++ b/src/wayland/input_method/keyboard_grab.cpp @@ -1,9 +1,11 @@ #include "keyboard_grab.hpp" #include -#include +#include #include +#if INPUT_METHOD_PRINT #include +#endif #include #include #include @@ -114,7 +116,7 @@ void InputMethodKeyboardGrab::zwp_input_method_keyboard_grab_v2_key( } if (state == WL_KEYBOARD_KEY_STATE_PRESSED) { - bool keyHandled = handleKey(key); + const bool keyHandled = handleKey(key); if (keyHandled){ if (this->mKeyMapState.keyRepeats(key) && this->mRepeatRate > 0) { this->mRepeatKey = key; @@ -135,19 +137,19 @@ void InputMethodKeyboardGrab::zwp_input_method_keyboard_grab_v2_key( bool InputMethodKeyboardGrab::handleKey(xkb_keycode_t key){ const xkb_keysym_t sym = this->mKeyMapState.getOneSym(key); if (sym == XKB_KEY_Up) { - emit directionPress(DirectionKey::UP); + emit directionPress(DirectionKey::Up); return true; } if (sym == XKB_KEY_Down) { - emit directionPress(DirectionKey::DOWN); + emit directionPress(DirectionKey::Down); return true; } if (sym == XKB_KEY_Left) { - emit directionPress(DirectionKey::LEFT); + emit directionPress(DirectionKey::Left); return true; } if (sym == XKB_KEY_Right) { - emit directionPress(DirectionKey::RIGHT); + emit directionPress(DirectionKey::Right); return true; } if (sym == XKB_KEY_BackSpace) { diff --git a/src/wayland/input_method/qml.cpp b/src/wayland/input_method/qml.cpp index be1a9ed6..e4f07fbf 100644 --- a/src/wayland/input_method/qml.cpp +++ b/src/wayland/input_method/qml.cpp @@ -1,6 +1,5 @@ #include "qml.hpp" #include -#include #include #include @@ -9,6 +8,7 @@ #include #include #include +#include #include "input_method.hpp" #include "keyboard_grab.hpp" @@ -78,22 +78,22 @@ void InputMethod::getInput() { &InputMethod::onHandleActiveChanged ); QObject::connect( - this->handle.get(), - &InputMethodHandle::contentHintChanged, - this, - &InputMethod::contentHintChanged + this->handle.get(), + &InputMethodHandle::contentHintChanged, + this, + &InputMethod::contentHintChanged ); QObject::connect( - this->handle.get(), - &InputMethodHandle::contentPurposeChanged, - this, - &InputMethod::contentPurposeChanged + this->handle.get(), + &InputMethodHandle::contentPurposeChanged, + this, + &InputMethod::contentPurposeChanged ); QObject::connect( - this->handle.get(), - &InputMethodHandle::surroundingTextChanged, - this, - &InputMethod::surroundingTextChanged + this->handle.get(), + &InputMethodHandle::surroundingTextChanged, + this, + &InputMethod::surroundingTextChanged ); emit hasInputChanged(); @@ -154,19 +154,11 @@ void InputMethod::handleKeyboardActive() { } } -const QString& InputMethod::surroundingText() const { - return handle->surroundingText(); -} -uint32_t InputMethod::surroundingTextCursor() const { - return handle->surroundingTextCursor(); -} -uint32_t InputMethod::surroundingTextAnchor() const { - return handle->surroundingTextAnchor(); -} +const QString& InputMethod::surroundingText() const { return handle->surroundingText(); } +uint32_t InputMethod::surroundingTextCursor() const { return handle->surroundingTextCursor(); } +uint32_t InputMethod::surroundingTextAnchor() const { return handle->surroundingTextAnchor(); } -QMLContentHint::Enum InputMethod::contentHint() const { - return this->handle->contentHint(); -} +QMLContentHint::Enum InputMethod::contentHint() const { return this->handle->contentHint(); } QMLContentPurpose::Enum InputMethod::contentPurpose() const { return this->handle->contentPurpose(); } diff --git a/src/wayland/input_method/qml_helpers.cpp b/src/wayland/input_method/qml_helpers.cpp index 79520795..419de389 100644 --- a/src/wayland/input_method/qml_helpers.cpp +++ b/src/wayland/input_method/qml_helpers.cpp @@ -1,10 +1,13 @@ #include "qml_helpers.hpp" +#include #include #include #include +#include #include "qml.hpp" +#include "types.hpp" namespace qs::wayland::input_method { @@ -17,7 +20,12 @@ KeyboardTextEdit::KeyboardTextEdit(QObject* parent): Keyboard(parent) { auto* inputMethod = dynamic_cast(parent); if (inputMethod) - QObject::connect(inputMethod, &InputMethod::surroundingTextChanged, this, &KeyboardTextEdit::onSurroundingTextChanged); + QObject::connect( + inputMethod, + &InputMethod::surroundingTextChanged, + this, + &KeyboardTextEdit::onSurroundingTextChanged + ); } QJSValue KeyboardTextEdit::transform() const { return this->mTransform; } @@ -78,11 +86,11 @@ void KeyboardTextEdit::onDeletePress() { void KeyboardTextEdit::onDirectionPress(QMLDirectionKey::Enum direction) { switch (direction) { - case QMLDirectionKey::LEFT: { + case QMLDirectionKey::Left: { this->setCursor(this->mCursor - 1); return; } - case QMLDirectionKey::RIGHT: { + case QMLDirectionKey::Right: { this->setCursor(this->mCursor + 1); return; } @@ -102,7 +110,7 @@ void KeyboardTextEdit::onReturnPress() { } void KeyboardTextEdit::onSurroundingTextChanged(QMLTextChangeCause::Enum textChangeCause) { - if (textChangeCause == QMLTextChangeCause::OTHER) { + if (textChangeCause == QMLTextChangeCause::Other) { this->setEditText(""); } } diff --git a/src/wayland/input_method/types.cpp b/src/wayland/input_method/types.cpp index 674075a4..d975d2a8 100644 --- a/src/wayland/input_method/types.cpp +++ b/src/wayland/input_method/types.cpp @@ -6,10 +6,10 @@ namespace qs::wayland::input_method { QString QMLDirectionKey::toString(Enum direction) { switch (direction) { - case UP: return "UP"; - case DOWN: return "DOWN"; - case LEFT: return "LEFT"; - case RIGHT: return "RIGHT"; + case Up: return "Up"; + case Down: return "Down"; + case Left: return "Left"; + case Right: return "Right"; } return "UNKNOWN"; } @@ -19,107 +19,107 @@ QString QMLContentHint::toString(Enum contentHint) { bool first = true; - if (contentHint & COMPLETION) { + if (contentHint & Completion) { if (!first) { string += " | "; } - string += "COMPLETION"; + string += "Completion"; first = false; } - if (contentHint & SPELLCHECK) { + if (contentHint & Spellcheck) { if (!first) { string += " | "; } - string += "SPELLCHECK"; + string += "Spellcheck"; first = false; } - if (contentHint & AUTO_CAPITALIZATION) { + if (contentHint & AutoCapitalization) { if (!first) { string += " | "; } - string += "AUTO_CAPITALIZATION"; + string += "AutoCapitalization"; first = false; } - if (contentHint & LOWERCASE) { + if (contentHint & Lowercase) { if (!first) { string += " | "; } - string += "LOWERCASE"; + string += "Lowercase"; first = false; } - if (contentHint & UPPERCASE) { + if (contentHint & Uppercase) { if (!first) { string += " | "; } - string += "UPPERCASE"; + string += "Uppercase"; first = false; } - if (contentHint & TITLECASE) { + if (contentHint & Titlecase) { if (!first) { string += " | "; } - string += "TITLECASE"; + string += "Titlecase"; first = false; } - if (contentHint & HIDDEN_TEXT) { + if (contentHint & HiddenText) { if (!first) { string += " | "; } - string += "HIDDEN_TEXT"; + string += "HiddenText"; first = false; } - if (contentHint & SENSITIVE_DATA) { + if (contentHint & SensitiveData) { if (!first) { string += " | "; } - string += "SENSITIVE_DATA"; + string += "SensitiveData"; first = false; } - if (contentHint & LATIN) { + if (contentHint & Latin) { if (!first) { string += " | "; } - string += "LATIN"; + string += "Latin"; first = false; } - if (contentHint & MULTILINE) { + if (contentHint & Multiline) { if (!first) { string += " | "; } - string += "MULTILINE"; + string += "Multiline"; first = false; } - if (string == "") string = "NONE"; + if (string == "") string = "None"; return string; } QString QMLContentPurpose::toString(Enum contentHint) { switch (contentHint) { - case NORMAL: return "NORMAL"; - case ALPHA: return "ALPHA"; - case DIGITS: return "DIGITS"; - case NUMBER: return "NUMBER"; - case PHONE: return "PHONE"; - case URL: return "URL"; - case EMAIL: return "EMAIL"; - case NAME: return "NAME"; - case PASSWORD: return "PASSWORD"; - case PIN: return "PIN"; - case DATA: return "DATA"; - case TIME: return "TIME"; - case DATETIME: return "DATETIME"; - case TERMINAL: return "TERMINAL"; + case Normal: return "Normal"; + case Alpha: return "Alpha"; + case Digits: return "Digits"; + case Number: return "Number"; + case Phone: return "Phone"; + case Url: return "Url"; + case Email: return "Email"; + case Name: return "Name"; + case Password: return "Password"; + case Pin: return "Pin"; + case Data: return "Data"; + case Time: return "Time"; + case Datetime: return "Datetime"; + case Terminal: return "Terminal"; } - return "UNKNOWN"; + return "Unknown"; } QString QMLTextChangeCause::toString(Enum textChangeCause) { switch (textChangeCause) { - case INPUT_METHOD: return "INPUT_METHOD"; - case OTHER: return "OTHER"; + case InputMethod: return "InputMethod"; + case Other: return "Other"; } - return "UNKNOWN"; + return "Unknown"; } } // namespace qs::wayland::input_method diff --git a/src/wayland/input_method/types.hpp b/src/wayland/input_method/types.hpp index d97e5bde..b4e3a7ec 100644 --- a/src/wayland/input_method/types.hpp +++ b/src/wayland/input_method/types.hpp @@ -16,7 +16,7 @@ class QMLDirectionKey: public QObject { public: enum Enum : quint8 { - UP, DOWN, LEFT, RIGHT + Up, Down, Left, Right }; Q_ENUM(Enum); @@ -30,17 +30,17 @@ class QMLContentHint: public QObject { public: enum Enum : quint16 { - NONE = ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE, - COMPLETION = ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION, - SPELLCHECK = ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK, - AUTO_CAPITALIZATION = ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION, - LOWERCASE = ZWP_TEXT_INPUT_V3_CONTENT_HINT_LOWERCASE, - UPPERCASE = ZWP_TEXT_INPUT_V3_CONTENT_HINT_UPPERCASE, - TITLECASE = ZWP_TEXT_INPUT_V3_CONTENT_HINT_TITLECASE, - HIDDEN_TEXT = ZWP_TEXT_INPUT_V3_CONTENT_HINT_HIDDEN_TEXT, - SENSITIVE_DATA = ZWP_TEXT_INPUT_V3_CONTENT_HINT_SENSITIVE_DATA, - LATIN = ZWP_TEXT_INPUT_V3_CONTENT_HINT_LATIN, - MULTILINE = ZWP_TEXT_INPUT_V3_CONTENT_HINT_MULTILINE, + None = ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE, + Completion = ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION, + Spellcheck = ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK, + AutoCapitalization = ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION, + Lowercase = ZWP_TEXT_INPUT_V3_CONTENT_HINT_LOWERCASE, + Uppercase = ZWP_TEXT_INPUT_V3_CONTENT_HINT_UPPERCASE, + Titlecase = ZWP_TEXT_INPUT_V3_CONTENT_HINT_TITLECASE, + HiddenText = ZWP_TEXT_INPUT_V3_CONTENT_HINT_HIDDEN_TEXT, + SensitiveData = ZWP_TEXT_INPUT_V3_CONTENT_HINT_SENSITIVE_DATA, + Latin = ZWP_TEXT_INPUT_V3_CONTENT_HINT_LATIN, + Multiline = ZWP_TEXT_INPUT_V3_CONTENT_HINT_MULTILINE, }; Q_ENUM(Enum); @@ -54,20 +54,20 @@ class QMLContentPurpose: public QObject { public: enum Enum : quint8{ - NORMAL = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL, - ALPHA = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ALPHA, - DIGITS = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DIGITS, - NUMBER = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NUMBER, - PHONE = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE, - URL = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_URL, - EMAIL = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_EMAIL, - NAME = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NAME, - PASSWORD = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PASSWORD, - PIN = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PIN, - DATA = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATE, - TIME = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TIME, - DATETIME = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATETIME, - TERMINAL = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL, + Normal = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL, + Alpha = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ALPHA, + Digits = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DIGITS, + Number = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NUMBER, + Phone = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE, + Url = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_URL, + Email = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_EMAIL, + Name = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NAME, + Password = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PASSWORD, + Pin = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PIN, + Data = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATE, + Time = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TIME, + Datetime = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATETIME, + Terminal = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL, }; Q_ENUM(Enum); @@ -81,8 +81,8 @@ class QMLTextChangeCause: public QObject { public: enum Enum : bool { - INPUT_METHOD = ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_INPUT_METHOD, - OTHER = ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER, + InputMethod = ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_INPUT_METHOD, + Other = ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER, }; Q_ENUM(Enum); diff --git a/src/wayland/input_method/virtual_keyboard.cpp b/src/wayland/input_method/virtual_keyboard.cpp index ad80a148..dc3eeced 100644 --- a/src/wayland/input_method/virtual_keyboard.cpp +++ b/src/wayland/input_method/virtual_keyboard.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "c_helpers.hpp" @@ -16,7 +17,7 @@ namespace qs::wayland::input_method::impl { namespace { using namespace std::chrono; -int64_t now() { +uint32_t now() { return duration_cast(system_clock::now().time_since_epoch()).count(); } } // namespace From 028bf391b2d9268b427c682f817eabea40b65911 Mon Sep 17 00:00:00 2001 From: cameron Date: Sat, 6 Sep 2025 13:14:42 +1000 Subject: [PATCH 3/3] add some more --- src/wayland/input_method/qml.cpp | 1 + src/wayland/input_method/virtual_keyboard.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/src/wayland/input_method/qml.cpp b/src/wayland/input_method/qml.cpp index e4f07fbf..492f23c4 100644 --- a/src/wayland/input_method/qml.cpp +++ b/src/wayland/input_method/qml.cpp @@ -1,5 +1,6 @@ #include "qml.hpp" #include +#include #include #include diff --git a/src/wayland/input_method/virtual_keyboard.cpp b/src/wayland/input_method/virtual_keyboard.cpp index dc3eeced..e2b00276 100644 --- a/src/wayland/input_method/virtual_keyboard.cpp +++ b/src/wayland/input_method/virtual_keyboard.cpp @@ -1,6 +1,7 @@ #include "virtual_keyboard.hpp" #include #include +#include #include #include