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.