From 9a6eaff28bde065436823407b98cbfa65e2501df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Delabre?= Date: Sat, 1 Jan 2022 15:16:15 -0500 Subject: [PATCH 1/6] Add support for wlroots-based Wayland compositors like Sway --- MANIFEST.in | 1 + news.d/feature/1459.linux.md | 1 + plover/oslayer/config.py | 10 + plover/oslayer/keyboardcontrol.py | 12 +- plover/oslayer/waykeyboardcontrol.py | 422 +++++++++++++++ plover/oslayer/wayland/.gitignore | 3 + .../wayland/input-method-unstable-v2.xml | 494 ++++++++++++++++++ .../wayland/virtual-keyboard-unstable-v1.xml | 113 ++++ plover_build_utils/setup.py | 35 ++ reqs/dist.txt | 1 + setup.cfg | 2 + setup.py | 4 +- tox.ini | 1 + 13 files changed, 1093 insertions(+), 6 deletions(-) create mode 100644 news.d/feature/1459.linux.md create mode 100644 plover/oslayer/waykeyboardcontrol.py create mode 100644 plover/oslayer/wayland/.gitignore create mode 100644 plover/oslayer/wayland/input-method-unstable-v2.xml create mode 100644 plover/oslayer/wayland/virtual-keyboard-unstable-v1.xml diff --git a/MANIFEST.in b/MANIFEST.in index 01c1941a7..b228f1339 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -20,6 +20,7 @@ include plover/gui_qt/resources/*.qrc include plover/gui_qt/resources/*.svg include plover/messages/*/LC_MESSAGES/*.po include plover/messages/plover.pot +include plover/oslayer/wayland/*.xml include plover_build_utils/*.sh include pyproject.toml include pytest.ini diff --git a/news.d/feature/1459.linux.md b/news.d/feature/1459.linux.md new file mode 100644 index 000000000..9c04de6b6 --- /dev/null +++ b/news.d/feature/1459.linux.md @@ -0,0 +1 @@ +Add support for wlroots-based Wayland compositors like Sway, and other compositors that implement the `virtual_keyboard_unstable_v1` and `input_method_unstable_v2` protocols. diff --git a/plover/oslayer/config.py b/plover/oslayer/config.py index f4b9dee70..eec0a5ae6 100644 --- a/plover/oslayer/config.py +++ b/plover/oslayer/config.py @@ -21,6 +21,16 @@ else: PLATFORM = None +if PLATFORM in {'linux', 'bsd'}: + if os.environ.get('WAYLAND_DISPLAY', None): + DISPLAY_SERVER = 'wayland' + else: + DISPLAY_SERVER = 'xorg' +elif PLATFORM in {'win', 'mac'}: + DISPLAY_SERVER = PLATFORM +else: + DISPLAY_SERVER = None + # If the program's working directory has a plover.cfg file then run in # "portable mode", i.e. store all data in the same directory. This allows # keeping all Plover files in a portable drive. diff --git a/plover/oslayer/keyboardcontrol.py b/plover/oslayer/keyboardcontrol.py index 1b892c4e3..2a563371b 100644 --- a/plover/oslayer/keyboardcontrol.py +++ b/plover/oslayer/keyboardcontrol.py @@ -16,16 +16,18 @@ """ -from plover.oslayer.config import PLATFORM +from plover.oslayer.config import DISPLAY_SERVER KEYBOARDCONTROL_NOT_FOUND_FOR_OS = \ - "No keyboard control module was found for platform: %s" % PLATFORM + "No keyboard control module was found for platform: %s" % DISPLAY_SERVER -if PLATFORM in {'linux', 'bsd'}: +if DISPLAY_SERVER == 'xorg': from plover.oslayer import xkeyboardcontrol as keyboardcontrol -elif PLATFORM == 'win': +elif DISPLAY_SERVER == 'wayland': + from plover.oslayer import waykeyboardcontrol as keyboardcontrol +elif DISPLAY_SERVER == 'win': from plover.oslayer import winkeyboardcontrol as keyboardcontrol -elif PLATFORM == 'mac': +elif DISPLAY_SERVER == 'mac': from plover.oslayer import osxkeyboardcontrol as keyboardcontrol else: raise Exception(KEYBOARDCONTROL_NOT_FOUND_FOR_OS) diff --git a/plover/oslayer/waykeyboardcontrol.py b/plover/oslayer/waykeyboardcontrol.py new file mode 100644 index 000000000..5a697196b --- /dev/null +++ b/plover/oslayer/waykeyboardcontrol.py @@ -0,0 +1,422 @@ +"""Keyboard capture and control on Wayland. + +This module provides an interface for capturing and emulating keyboard events +on Wayland compositors that support the 'virtual_keyboard_unstable_v1' and +'input_method_unstable_v2' protocols (that is, wlroots-based compositors +like Sway, as of January 2022). +""" + +import os +import time +import select +from threading import Thread +from pywayland.client.display import Display + +# Protocol modules generated from XML description files at build time +from plover.oslayer.wayland.wayland.wl_seat import WlSeat +from plover.oslayer.wayland.input_method_unstable_v2.zwp_input_method_manager_v2 \ + import ZwpInputMethodManagerV2 +from plover.oslayer.wayland.virtual_keyboard_unstable_v1.zwp_virtual_keyboard_manager_v1 \ + import ZwpVirtualKeyboardManagerV1 + +from plover.oslayer.xkeyboardcontrol import KEYCODE_TO_KEY, KEY_TO_KEYSYM +from plover.key_combo import parse_key_combo, add_modifiers_aliases + + +# Taken from the default XKB modifier mapping +MOD_NAME_TO_INDEX = { + 'shift_l': 0, + 'shift_r': 0, + 'lock': 1, + 'caps_lock': 1, + 'control_l': 2, + 'control_r': 2, + 'mod1': 3, + 'alt_l': 3, + 'meta_l': 3, + 'alt_r': 3, + 'meta_r': 3, + 'mod2': 4, + 'num_lock': 4, + 'mod3': 5, + 'mod4': 6, + 'super_l': 6, + 'super_r': 6, + 'hyper_l': 6, + 'hyper_r': 6, + 'mod5': 7, + 'iso_level3_shift': 7, + 'mode_switch': 7, +} +add_modifiers_aliases(MOD_NAME_TO_INDEX) + +XKB_KEYCODE_OFFSET = 8 +PLOVER_TAG = '' + + +def keymap_generate(keysyms): + """Generate a keymap that can send the given list of keysyms. + + Argument: + + keysyms -- List of keysyms to support. + + Returns: A file descriptor for the new keymap, and the new keymap’s size. + """ + keycodes = '\n'.join([ + # Special keycode recognized by the keyboard capture class to + # avoid processing generated keys + f'{PLOVER_TAG} = {XKB_KEYCODE_OFFSET + len(keysyms)};' + ] + [ + f' = {XKB_KEYCODE_OFFSET + keycode};' + for keycode, _ in enumerate(keysyms) + ]) + symbols = '\n'.join([ + f'key {PLOVER_TAG} {{[]}};' + ] + [ + f'key {{[{keysym}]}};' \ + for keycode, keysym in enumerate(keysyms) + ]) + keymap = f'''xkb_keymap {{ +xkb_keycodes {{ +minimum = {XKB_KEYCODE_OFFSET}; +maximum = {XKB_KEYCODE_OFFSET + len(keysyms)}; +{keycodes} +}}; +xkb_types {{}}; +xkb_compatibility {{}}; +xkb_symbols {{ +{symbols} +}}; +}};''' + fd = os.memfd_create('emulated_keymap.xkb') + os.truncate(fd, len(keymap)) + file = open(fd, 'w', closefd=False) + file.write(keymap) + file.flush() + return fd, len(keymap) + + +def keymap_is_generated(fd): + """Check whether a keymap was generated from this module.""" + file = open(fd, closefd=False) + keymap = file.read() + return keymap.find(PLOVER_TAG) >= 0 + + +class KeyboardCapture: + """Listen to keyboard press and release events. + + This uses the 'input_method_unstable_v2' protocol to grab the Wayland + keyboard. This grab is global and unconditional, therefore a virtual + keyboard input is also created (using the 'virtual_keyboard_unstable_v1' + protocol) to forward events that do not need to be captured by Plover. + Note that this grab will also capture events generated by the + KeyboardEmulation class, those events need to be actively filtered out + to avoid infinite feedback loops. + """ + def __init__(self): + # Callbacks that receive keypresses + self.key_down = lambda key: None + self.key_up = lambda key: None + + # True if the event loop is running + self._running = False + self._loop_thread = None + + # Global Wayland objects + self._display = None + self._seat = None + self._keyboard = None + + # True if the keyboard has been grabbed + self._grabbed = False + + # Keyboard grab and virtual keyboard objects + self._input_method_manager = None + self._input_method = None + self._grabbed_keyboard = None + self._virtual_keyboard_manager = None + self._virtual_keyboard = None + + # Current modifier state, depressed, latched and locked + self._mod_state = (0, 0, 0) + + # True if the next received keypresses should be ignored + # because they are generated by KeyboardEmulation + self._is_generated = False + + # Set of keys to capture and transmit to Plover - other keys + # are forwarded to the client transparently + self._suppressed_keys = set() + + def start(self): + """Connect to the Wayland compositor and start the event loop.""" + if not self._running: + self._display = Display() + self._display.connect() + + # Query protocols available in the current compositor + reg = self._display.get_registry() + reg.dispatcher['global'] = self._on_registry_global + self._display.roundtrip() + + for obj, interface in ( + (self._seat, WlSeat), + (self._input_method_manager, ZwpInputMethodManagerV2), + (self._virtual_keyboard_manager, ZwpVirtualKeyboardManagerV1), + ): + if not obj: + raise RuntimeError(f'Cannot capture keyboard events: your \ +Wayland compositor does not support the \'{interface.name}\' interface') + + # Wait for an active keyboard to be ready before grabbing (in some + # cases, there might not be any active keyboard, for example if the + # last active keyboard was just unplugged - trying to grab the + # keyboard in this scenario would crash the compositor) + self._keyboard = self._seat.get_keyboard() + self._keyboard.dispatcher['keymap'] = self._on_keyboard_ready + + self._running = True + self._loop_thread = Thread(target=self._event_loop) + self._loop_thread.start() + + def cancel(self): + """Cancel grabbing the keyboard and free resources.""" + if self._running: + self._running = False + self._loop_thread.join() + self._loop_thread = None + + if self._grabbed: + self._grabbed = False + self._virtual_keyboard.destroy() + self._virtual_keyboard = None + self._input_method.destroy() + self._input_method = None + self._grabbed_keyboard = None + self._virtual_keyboard_manager = None + self._input_method_manager = None + + if self._keyboard: + self._keyboard.release() + self._keyboard = None + + if self._seat: + self._seat.release() + self._seat = None + + self._display.disconnect() + self._display = None + + def _event_loop(self): + """Read incoming events repeatedly.""" + fd = self._display.get_fd() + + while self._running: + # Send any remaining requests + self._display.flush() + + # Wait for events from the server and process them + read, _, _ = select.select((fd,), (), (), 1) + if read: self._display.dispatch(block=True) + + def _on_registry_global(self, obj, name, interface, version): + """Listener for global objects advertised by the Wayland compositor.""" + if interface == WlSeat.name: + self._seat = obj.bind(name, WlSeat, version) + elif interface == ZwpInputMethodManagerV2.name: + self._input_method_manager = \ + obj.bind(name, ZwpInputMethodManagerV2, version) + elif interface == ZwpVirtualKeyboardManagerV1.name: + self._virtual_keyboard_manager = \ + obj.bind(name, ZwpVirtualKeyboardManagerV1, version) + + def _on_keyboard_ready(self, _, fmt, fd, size): + if not self._grabbed: + # Now that the source keyboard is ready, try grabbing its events + self._grabbed = True + self._input_method = \ + self._input_method_manager.get_input_method(self._seat) + self._grabbed_keyboard = self._input_method.grab_keyboard() + self._virtual_keyboard = \ + self._virtual_keyboard_manager.create_virtual_keyboard( \ + self._seat) + + self._grabbed_keyboard.dispatcher['keymap'] = self._on_grab_keymap + self._grabbed_keyboard.dispatcher['modifiers'] = \ + self._on_grab_modifiers + self._grabbed_keyboard.dispatcher['key'] = self._on_grab_key + + os.close(fd) + + def _on_grab_keymap(self, _, fmt, fd, size): + """Callback for when the active keymap changes.""" + self._is_generated = fmt == 1 and keymap_is_generated(fd) + self._virtual_keyboard.keymap(fmt, fd, size) + self._display.flush() + os.close(fd) + + def _on_grab_key(self, _, serial, origtime, keycode, state): + """Callback for when a new key event arrives.""" + key = KEYCODE_TO_KEY.get(keycode + 8) + + if not self._is_generated and key in self._suppressed_keys and \ + self._mod_state == (0, 0, 0): + # Signal and suppress changes for watched keys + if state == 1: self.key_down(key) + else: self.key_up(key) + else: + # Forward other keys unchanged + self._virtual_keyboard.key(origtime, keycode, state) + self._display.flush() + + def _on_grab_modifiers(self, _, serial, depressed, latched, locked, layout): + """Callback for when the set of active modifiers changes.""" + self._mod_state = (depressed, latched, locked) + self._virtual_keyboard.modifiers(depressed, latched, locked, layout) + self._display.flush() + + def suppress_keyboard(self, keys=()): + """Change the set of keys to capture.""" + self._suppressed_keys = set(keys) + + +class KeyboardEmulation: + """Emulate keyboard events to send strings on Wayland. + + This emulation layer uses the 'virtual_keyboard_unstable_v1' protocol. + Since the protocol allows using any XKB layout, a new layout is generated + each time a string needs to be sent, containing just the needed symbols. + This makes the emulation independent of the user’s current keyboard layout. + To signal emulated events to KeyboardCapture, a special tag is inserted in + generated XKB layouts. + """ + def __init__(self): + # True if the required interfaces for sending key events are setup + self._ready = False + + # Global Wayland objects + self._display = Display() + self._display.connect() + self._seat = None + + # Virtual keyboard objects + self._virtual_keyboard_manager = None + self._virtual_keyboard = None + + # Query protocols available in the current compositor + reg = self._display.get_registry() + reg.dispatcher['global'] = self._on_registry_global + self._display.roundtrip() + + for obj, interface in ( + (self._seat, WlSeat), + (self._virtual_keyboard_manager, ZwpVirtualKeyboardManagerV1), + ): + if not obj: + raise RuntimeError(f'Cannot emulate keyboard events: your \ +Wayland compositor does not support the \'{interface.name}\' interface') + + self._virtual_keyboard = \ + self._virtual_keyboard_manager.create_virtual_keyboard(self._seat) + self._ready = True + + def close(self, type, value, traceback): + """Destroy the virtual keyboard and free resources.""" + self._ready = False + + if self._virtual_keyboard: + self._virtual_keyboard.destroy() + self._virtual_keyboard = None + + if self._seat: + self._seat.release() + self._seat = None + + if self._display: + self._display.disconnect() + self._display = None + + def _on_registry_global(self, obj, name, interface, version): + """Listener for global objects advertised by the Wayland compositor.""" + if interface == WlSeat.name: + self._seat = obj.bind(name, WlSeat, version) + elif interface == ZwpVirtualKeyboardManagerV1.name: + self._virtual_keyboard_manager = \ + obj.bind(name, ZwpVirtualKeyboardManagerV1, version) + + def _send_keymap(self, keysyms): + """Set virtual keymap to support a given list of keysyms.""" + fd, size = keymap_generate(keysyms) + self._virtual_keyboard.keymap(1, fd, size) + self._display.flush() + os.close(fd) + + def _send_key(self, keycode, state): + """Emulate a single keypress.""" + timestamp = time.thread_time_ns() // (10 ** 3) + self._virtual_keyboard.key(timestamp, keycode, state) + + def _send_modifiers(self, mods): + """Emulate changing the active modifiers.""" + self._virtual_keyboard.modifiers( + mods_depressed=mods, + mods_latched=0, + mods_locked=0, + group=0, + ) + + def send_string(self, string): + """Emulate a complete string.""" + if not self._ready: + raise RuntimeError('Cannot send string: keyboard emulation \ +is not available') + + letterset = list(set(string)) + keysyms = [f'U{ord(letter):04X}' for letter in letterset] + self._send_keymap(keysyms) + + for letter in string: + self._send_key(letterset.index(letter), 1) + self._send_key(letterset.index(letter), 0) + + self._display.flush() + + def send_backspaces(self, count): + """Emulate a sequence of backspaces.""" + if not self._ready: + raise RuntimeError('Cannot send backspaces: keyboard emulation \ +is not available') + + self._send_keymap(['BackSpace']) + + for _ in range(count): + self._send_key(0, 1) + self._send_key(0, 0) + + self._display.flush() + + def send_key_combination(self, combo_string): + """Emulate a key combo.""" + combo = parse_key_combo(combo_string) + + keyset = list(set([ + key[0] for key in combo + if key[0] not in MOD_NAME_TO_INDEX + ])) + keysyms = [str(KEY_TO_KEYSYM[key]) for key in keyset] + self._send_keymap(keysyms) + + mods = 0 + + for key, state in combo: + if key in MOD_NAME_TO_INDEX: + index = MOD_NAME_TO_INDEX[key] + if state: mods |= (1 << index) + else: mods &= ~(1 << index) + self._send_modifiers(mods) + else: + self._send_key(keyset.index(key), state) + + self._display.flush() diff --git a/plover/oslayer/wayland/.gitignore b/plover/oslayer/wayland/.gitignore new file mode 100644 index 000000000..81a5fb927 --- /dev/null +++ b/plover/oslayer/wayland/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!*.xml diff --git a/plover/oslayer/wayland/input-method-unstable-v2.xml b/plover/oslayer/wayland/input-method-unstable-v2.xml new file mode 100644 index 000000000..51bccf281 --- /dev/null +++ b/plover/oslayer/wayland/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/plover/oslayer/wayland/virtual-keyboard-unstable-v1.xml b/plover/oslayer/wayland/virtual-keyboard-unstable-v1.xml new file mode 100644 index 000000000..5095c91b8 --- /dev/null +++ b/plover/oslayer/wayland/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/plover_build_utils/setup.py b/plover_build_utils/setup.py index 7a655edd8..257bd32e4 100644 --- a/plover_build_utils/setup.py +++ b/plover_build_utils/setup.py @@ -150,6 +150,41 @@ def run(self): # }}} +# Wayland protocols generation. {{{ + +class BuildWayland(Command): + + description = 'build Wayland protocol modules' + user_options = [ + ('force', 'f', + 'force re-generation of all Wayland protocol modules'), + ] + + def initialize_options(self): + self.force = False + + def finalize_options(self): + pass + + def run(self): + self.run_command('egg_info') + log.info('generating Wayland protocol modules') + ei_cmd = self.get_finalized_command('egg_info') + base = 'plover/oslayer/wayland' + defs = [ + src for src in ei_cmd.filelist.files + if src.startswith(base) + and src.endswith('.xml') + ] + cmd = ( + sys.executable, '-m', 'pywayland.scanner', + '-i', *defs, '/usr/share/wayland/wayland.xml', + '-o', base, + ) + subprocess.check_call(cmd) + +# }}} + # Patched `build_py` command. {{{ class BuildPy(build_py): diff --git a/reqs/dist.txt b/reqs/dist.txt index 8e8e3b2d5..a397fabb1 100644 --- a/reqs/dist.txt +++ b/reqs/dist.txt @@ -7,6 +7,7 @@ pyobjc-framework-Quartz>=4.0; "darwin" in sys_platform pyserial>=2.7 python-xlib>=0.16; ("linux" in sys_platform or "bsd" in sys_platform) and python_version < "3.9" python-xlib>=0.29; ("linux" in sys_platform or "bsd" in sys_platform) and python_version >= "3.9" +pywayland==0.4.7 rtf_tokenize setuptools wcwidth diff --git a/setup.cfg b/setup.cfg index 77b8af72c..288e51893 100644 --- a/setup.cfg +++ b/setup.cfg @@ -113,5 +113,7 @@ plover = plover.gui_qt = *.ui resources/* +plover.oslayer = + wayland/*.xml # vim: commentstring=#\ %s list diff --git a/setup.py b/setup.py index ea1597e52..12aa4d931 100755 --- a/setup.py +++ b/setup.py @@ -24,15 +24,17 @@ exec(fp.read()) from plover_build_utils.setup import ( - BuildPy, BuildUi, Command, Develop, babel_options + BuildPy, BuildUi, BuildWayland, Command, Develop, babel_options ) BuildPy.build_dependencies.append('build_ui') +BuildPy.build_dependencies.append('build_wayland') Develop.build_dependencies.append('build_py') cmdclass = { 'build_py': BuildPy, 'build_ui': BuildUi, + 'build_wayland': BuildWayland, 'develop': Develop, } options = {} diff --git a/tox.ini b/tox.ini index e32ed5500..7732f8181 100644 --- a/tox.ini +++ b/tox.ini @@ -82,6 +82,7 @@ description = launch plover passenv = {[testenv]passenv} DISPLAY + WAYLAND_DISPLAY XDG_RUNTIME_DIR commands = {envpython} setup.py launch -- {posargs} From f5dfe28998db8b3283e6c54f6c6149f5ec9ac4d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Delabre?= Date: Sun, 2 Jan 2022 15:42:33 -0500 Subject: [PATCH 2/6] Fix PR number in news.d entry --- news.d/feature/{1459.linux.md => 1461.linux.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news.d/feature/{1459.linux.md => 1461.linux.md} (100%) diff --git a/news.d/feature/1459.linux.md b/news.d/feature/1461.linux.md similarity index 100% rename from news.d/feature/1459.linux.md rename to news.d/feature/1461.linux.md From fb911d1b3da89c43955b9c1d7775954a89adeef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Delabre?= Date: Mon, 3 Jan 2022 15:10:22 -0500 Subject: [PATCH 3/6] setup.cfg: Add generated Wayland modules --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index 288e51893..1d80ab8e2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,6 +45,9 @@ packages = plover.macro plover.meta plover.oslayer + plover.oslayer.wayland.input_method_unstable_v2 + plover.oslayer.wayland.virtual_keyboard_unstable_v1 + plover.oslayer.wayland.wayland plover.scripts plover.system plover_build_utils From 299cff564e3570123996e9bf2c0a2f339be4521c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Delabre?= Date: Mon, 3 Jan 2022 15:10:40 -0500 Subject: [PATCH 4/6] setup.py: Remove dep on egg_info BuildWayland --- plover_build_utils/setup.py | 12 +++--------- setup.py | 2 +- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/plover_build_utils/setup.py b/plover_build_utils/setup.py index 257bd32e4..4c7ded4ec 100644 --- a/plover_build_utils/setup.py +++ b/plover_build_utils/setup.py @@ -1,5 +1,6 @@ from distutils import log import contextlib +import glob import importlib import os import subprocess @@ -167,19 +168,12 @@ def finalize_options(self): pass def run(self): - self.run_command('egg_info') log.info('generating Wayland protocol modules') - ei_cmd = self.get_finalized_command('egg_info') base = 'plover/oslayer/wayland' - defs = [ - src for src in ei_cmd.filelist.files - if src.startswith(base) - and src.endswith('.xml') - ] + defs = glob.glob(base + '/*.xml') + ['/usr/share/wayland/wayland.xml'] cmd = ( sys.executable, '-m', 'pywayland.scanner', - '-i', *defs, '/usr/share/wayland/wayland.xml', - '-o', base, + '-i', *defs, '-o', base, ) subprocess.check_call(cmd) diff --git a/setup.py b/setup.py index 12aa4d931..61dfcba79 100755 --- a/setup.py +++ b/setup.py @@ -28,8 +28,8 @@ ) -BuildPy.build_dependencies.append('build_ui') BuildPy.build_dependencies.append('build_wayland') +BuildPy.build_dependencies.append('build_ui') Develop.build_dependencies.append('build_py') cmdclass = { 'build_py': BuildPy, From 1a0c3735a07fbc8a8f7558f0a88a7e26b45e6e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Delabre?= Date: Mon, 3 Jan 2022 15:13:33 -0500 Subject: [PATCH 5/6] setup.py: Remove unused force flag in BuildWayland --- plover_build_utils/setup.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plover_build_utils/setup.py b/plover_build_utils/setup.py index 4c7ded4ec..b61fd44a1 100644 --- a/plover_build_utils/setup.py +++ b/plover_build_utils/setup.py @@ -156,13 +156,9 @@ def run(self): class BuildWayland(Command): description = 'build Wayland protocol modules' - user_options = [ - ('force', 'f', - 'force re-generation of all Wayland protocol modules'), - ] def initialize_options(self): - self.force = False + pass def finalize_options(self): pass From fd5668a3ad9bd091289dd2e5e8e2c1dec063d51f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Delabre?= Date: Fri, 14 Jan 2022 08:56:14 -0500 Subject: [PATCH 6/6] Fix compatibility with Xwayland --- plover/oslayer/waykeyboardcontrol.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plover/oslayer/waykeyboardcontrol.py b/plover/oslayer/waykeyboardcontrol.py index 5a697196b..d3cb2920e 100644 --- a/plover/oslayer/waykeyboardcontrol.py +++ b/plover/oslayer/waykeyboardcontrol.py @@ -77,14 +77,17 @@ def keymap_generate(keysyms): f'key {{[{keysym}]}};' \ for keycode, keysym in enumerate(keysyms) ]) + # Sway is more permissive than Xwayland on what an XKB keymap must + # or must not include. We need to take care if we want to ensure + # compatibility with both. See keymap = f'''xkb_keymap {{ xkb_keycodes {{ minimum = {XKB_KEYCODE_OFFSET}; maximum = {XKB_KEYCODE_OFFSET + len(keysyms)}; {keycodes} }}; -xkb_types {{}}; -xkb_compatibility {{}}; +xkb_types {{ include \"complete\" }}; +xkb_compatibility {{ include \"complete\" }}; xkb_symbols {{ {symbols} }};