diff --git a/news.d/feature/1815.linux.md b/news.d/feature/1815.linux.md new file mode 100644 index 000000000..d587ed8fa --- /dev/null +++ b/news.d/feature/1815.linux.md @@ -0,0 +1 @@ +Add a keyboard selection option on Wayland diff --git a/plover/gui_qt/config_keyboard_widget.ui b/plover/gui_qt/config_keyboard_widget.ui index 66cda4163..5233a9993 100644 --- a/plover/gui_qt/config_keyboard_widget.ui +++ b/plover/gui_qt/config_keyboard_widget.ui @@ -38,6 +38,23 @@ If the key is pressed and released again, another chord is sent. + + + + Keyboard Selection + + + Linux Wayland only: Select the keyboard which will be used by Plover + + + + + + + Linux Wayland only: Select the keyboard which will be used by Plover + + + @@ -58,6 +75,22 @@ If the key is pressed and released again, another chord is sent. + + combobox_keyboard_selection + currentTextChanged(QString) + KeyboardWidget + update_keyboard_selection(QString) + + + 56 + 17 + + + 56 + 17 + + + first_up_chord_send clicked(bool) @@ -78,5 +111,6 @@ If the key is pressed and released again, another chord is sent. update_arpeggiate(bool) update_first_up_chord_send(bool) + update_keyboard_selection(QString) diff --git a/plover/gui_qt/machine_options.py b/plover/gui_qt/machine_options.py index 877a821cf..9f3bc83b6 100644 --- a/plover/gui_qt/machine_options.py +++ b/plover/gui_qt/machine_options.py @@ -22,6 +22,8 @@ from plover import _ from plover.oslayer.serial import patch_ports_info +from plover.oslayer.config import PLATFORM +from plover.oslayer.linux.display_server import DISPLAY_SERVER from plover.gui_qt.config_keyboard_widget_ui import Ui_KeyboardWidget from plover.gui_qt.config_serial_widget_ui import Ui_SerialWidget @@ -203,11 +205,16 @@ def __init__(self): ) ) self._value = {} + if PLATFORM == "linux" and DISPLAY_SERVER == "wayland": + self.setup_keyboard_selection() + else: + self.combobox_keyboard_selection.setDisabled(True) def setValue(self, value): self._value = copy(value) self.arpeggiate.setChecked(value["arpeggiate"]) self.first_up_chord_send.setChecked(value["first_up_chord_send"]) + self.combobox_keyboard_selection.setCurrentText(value["keyboard_selection"]) @Slot(bool) def update_arpeggiate(self, value): @@ -219,6 +226,17 @@ def update_first_up_chord_send(self, value): self._value["first_up_chord_send"] = value self.valueChanged.emit(self._value) + @Slot(str) + def update_keyboard_selection(self, value): + self._value["keyboard_selection"] = value + self.valueChanged.emit(self._value) + + def setup_keyboard_selection(self): + from plover.oslayer.linux.keyboardcontrol_uinput import get_available_devices + + for device in get_available_devices(): + self.combobox_keyboard_selection.addItem(device.name) + class PloverHidOption(QGroupBox, Ui_PloverHidWidget): valueChanged = Signal(object) diff --git a/plover/machine/keyboard.py b/plover/machine/keyboard.py index a876e9fa5..47a4e567c 100644 --- a/plover/machine/keyboard.py +++ b/plover/machine/keyboard.py @@ -8,6 +8,8 @@ from plover.machine.base import StenotypeBase from plover.misc import boolean from plover.oslayer.keyboardcontrol import KeyboardCapture +from plover.oslayer.config import PLATFORM +from plover.oslayer.linux.display_server import DISPLAY_SERVER # i18n: Machine name. @@ -39,6 +41,7 @@ def __init__(self, params): super().__init__() self._arpeggiate = params["arpeggiate"] self._first_up_chord_send = params["first_up_chord_send"] + self._keyboard_selection = params["keyboard_selection"] if self._arpeggiate and self._first_up_chord_send: self._error() raise RuntimeError( @@ -89,6 +92,8 @@ def start_capture(self): self._initializing() try: self._keyboard_capture = KeyboardCapture() + if PLATFORM == "linux" and DISPLAY_SERVER == "wayland": + self._keyboard_capture.set_keyboard_selection(self._keyboard_selection) self._keyboard_capture.key_down = self._key_down self._keyboard_capture.key_up = self._key_up self._keyboard_capture.start() @@ -162,4 +167,5 @@ def get_option_info(cls): return { "arpeggiate": (False, boolean), "first_up_chord_send": (False, boolean), + "keyboard_selection": ("", str), } diff --git a/plover/oslayer/linux/keyboardcontrol_uinput.py b/plover/oslayer/linux/keyboardcontrol_uinput.py index 8a8a51a4c..7258e819b 100644 --- a/plover/oslayer/linux/keyboardcontrol_uinput.py +++ b/plover/oslayer/linux/keyboardcontrol_uinput.py @@ -342,6 +342,26 @@ } +def get_available_devices(): + input_devices = [InputDevice(path) for path in list_devices()] + keyboard_devices = [dev for dev in input_devices if filter_devices(dev)] + return keyboard_devices + + +def filter_devices(device): + """ + Filter out devices that should not be grabbed and suppressed, to avoid output feeding into itself. + """ + is_uinput = device.name == "py-evdev-uinput" or device.phys == "py-evdev-uinput" + # Check for some common keys to make sure it's really a keyboard + keys = device.capabilities().get(e.EV_KEY, []) + keyboard_keys_present = any( + key in keys for key in [e.KEY_ESC, e.KEY_SPACE, e.KEY_ENTER, e.KEY_LEFTSHIFT] + ) + + return not is_uinput and keyboard_keys_present + + class KeyboardEmulation(GenericKeyboardEmulation): def __init__(self): super().__init__() @@ -441,6 +461,7 @@ class KeyboardCapture(Capture): def __init__(self): super().__init__() + self._keyboard_selection = "" self._devices = self._get_devices() self._selector = selectors.DefaultSelector() @@ -454,22 +475,12 @@ def __init__(self): # The keycodes from evdev, e.g. e.KEY_A refers to the *physical* a, which corresponds with the qwerty layout. def _get_devices(self): - input_devices = [InputDevice(path) for path in list_devices()] - keyboard_devices = [dev for dev in input_devices if self._filter_devices(dev)] - return keyboard_devices - - def _filter_devices(self, device): - """ - Filter out devices that should not be grabbed and suppressed, to avoid output feeding into itself. - """ - is_uinput = device.name == "py-evdev-uinput" or device.phys == "py-evdev-uinput" - # Check for some common keys to make sure it's really a keyboard - keys = device.capabilities().get(e.EV_KEY, []) - keyboard_keys_present = any( - key in keys - for key in [e.KEY_ESC, e.KEY_SPACE, e.KEY_ENTER, e.KEY_LEFTSHIFT] - ) - return not is_uinput and keyboard_keys_present + available_devices = get_available_devices() + return [ + device + for device in available_devices + if self._keyboard_selection == "" or device.name == self._keyboard_selection + ] def _grab_devices(self): """Grab all devices, waiting for each device to stop having keys pressed. @@ -544,6 +555,10 @@ def suppress(self, suppressed_keys=()): """ self._suppressed_keys = set(suppressed_keys) + def set_keyboard_selection(self, keyboard_selection: str): + self._keyboard_selection = keyboard_selection + self._devices = self._get_devices() + def _run(self): keys_pressed_with_modifier: set[int] = set() down_modifier_keys: set[int] = set() diff --git a/test/test_command.py b/test/test_command.py index f730b052c..5c8c09b69 100644 --- a/test/test_command.py +++ b/test/test_command.py @@ -40,7 +40,7 @@ def config(self, options): lambda: ('"machine_type":"Keyboard"', "Keyboard"), lambda: ( '"machine_specific_options":{"arpeggiate":True}', - {"arpeggiate": True, "first_up_chord_send": False}, + {"arpeggiate": True, "first_up_chord_send": False, "keyboard_selection": ""}, ), lambda: ('"system_keymap":' + str(DEFAULT_KEYMAP), DEFAULT_KEYMAP), lambda: ( diff --git a/test/test_config.py b/test/test_config.py index 5f7751a6b..71747cb0d 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -140,7 +140,11 @@ def test_config_dict(): "enabled_extensions": set(), "auto_start": False, "machine_type": "Keyboard", - "machine_specific_options": {"arpeggiate": False, "first_up_chord_send": False}, + "machine_specific_options": { + "arpeggiate": False, + "first_up_chord_send": False, + "keyboard_selection": "", + }, "system_name": config.DEFAULT_SYSTEM_NAME, "system_keymap": DEFAULT_KEYMAP, "dictionaries": [ @@ -279,12 +283,14 @@ def test_config_dict(): "machine_specific_options": { "arpeggiate": True, "first_up_chord_send": False, + "keyboard_selection": "", } }, """ [Keyboard] arpeggiate = True first_up_chord_send = False + keyboard_selection = """, ), ( diff --git a/test/test_keyboard.py b/test/test_keyboard.py index c720dca75..97bccd37b 100644 --- a/test/test_keyboard.py +++ b/test/test_keyboard.py @@ -4,6 +4,8 @@ from plover.machine.keyboard import Keyboard from plover.machine.keymap import Keymap from plover.oslayer.keyboardcontrol import KeyboardCapture +from plover.oslayer.config import PLATFORM +from plover.oslayer.linux.display_server import DISPLAY_SERVER from unittest import mock @@ -26,7 +28,11 @@ def capture(): yield capture -@pytest.fixture(params=[{"arpeggiate": False, "first_up_chord_send": False}]) +@pytest.fixture( + params=[ + {"arpeggiate": False, "first_up_chord_send": False, "keyboard_selection": ""} + ] +) def machine(request, capture): machine = Keyboard(request.param) keymap = Keymap(Keyboard.KEYS_LAYOUT.split(), system.KEYS + Keyboard.ACTIONS) @@ -36,10 +42,14 @@ def machine(request, capture): arpeggiate = pytest.mark.parametrize( - "machine", [{"arpeggiate": True, "first_up_chord_send": False}], indirect=True + "machine", + [{"arpeggiate": True, "first_up_chord_send": False, "keyboard_selection": ""}], + indirect=True, ) first_up_chord_send = pytest.mark.parametrize( - "machine", [{"arpeggiate": False, "first_up_chord_send": True}], indirect=True + "machine", + [{"arpeggiate": False, "first_up_chord_send": True, "keyboard_selection": ""}], + indirect=True, ) """ These are decorators to be applied on test functions to modify the machine configuration. @@ -57,10 +67,17 @@ def strokes(machine): def test_lifecycle(capture, machine, strokes): # Start machine. machine.start_capture() - assert capture.mock_calls == [ - mock.call.start(), - mock.call.suppress(()), - ] + if PLATFORM == "linux" and DISPLAY_SERVER == "wayland": + assert capture.mock_calls == [ + mock.call.set_keyboard_selection(""), + mock.call.start(), + mock.call.suppress(()), + ] + else: + assert capture.mock_calls == [ + mock.call.start(), + mock.call.suppress(()), + ] capture.reset_mock() machine.set_suppression(True) suppressed_keys = dict(machine.keymap.get_bindings()) diff --git a/tox.ini b/tox.ini index 2cf3edb9e..606025923 100644 --- a/tox.ini +++ b/tox.ini @@ -60,6 +60,9 @@ setenv = # Linux: add to library path so ctypes can find hidapi libraries LD_LIBRARY_PATH = {env:HIDAPI_DEV_LIB_LNX}{:}{env:LD_LIBRARY_PATH:} + # Linux: pass XDG_SESSION_TYPE to distinguish between X and Wayland + XDG_SESSION_TYPE = {env:XDG_SESSION_TYPE} + # Windows: not done via path; see dev/write_hidapi_pth.py # Lightweight tests only environments.