Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news.d/feature/1815.linux.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a keyboard selection option on Wayland
34 changes: 34 additions & 0 deletions plover/gui_qt/config_keyboard_widget.ui
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@ If the key is pressed and released again, another chord is sent.</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_keyboard_selection">
<property name="text">
<string>Keyboard Selection</string>
</property>
<property name="toolTip">
<string>Linux Wayland only: Select the keyboard which will be used by Plover</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="combobox_keyboard_selection">
<property name="toolTip">
<string>Linux Wayland only: Select the keyboard which will be used by Plover</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
Expand All @@ -58,6 +75,22 @@ If the key is pressed and released again, another chord is sent.</string>
</hint>
</hints>
</connection>
<connection>
<sender>combobox_keyboard_selection</sender>
<signal>currentTextChanged(QString)</signal>
<receiver>KeyboardWidget</receiver>
<slot>update_keyboard_selection(QString)</slot>
<hints>
<hint type="sourcelabel">
<x>56</x>
<y>17</y>
</hint>
<hint type="destinationlabel">
<x>56</x>
<y>17</y>
</hint>
</hints>
</connection>
<connection>
<sender>first_up_chord_send</sender>
<signal>clicked(bool)</signal>
Expand All @@ -78,5 +111,6 @@ If the key is pressed and released again, another chord is sent.</string>
<slots>
<slot>update_arpeggiate(bool)</slot>
<slot>update_first_up_chord_send(bool)</slot>
<slot>update_keyboard_selection(QString)</slot>
</slots>
</ui>
18 changes: 18 additions & 0 deletions plover/gui_qt/machine_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions plover/machine/keyboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -162,4 +167,5 @@ def get_option_info(cls):
return {
"arpeggiate": (False, boolean),
"first_up_chord_send": (False, boolean),
"keyboard_selection": ("", str),
}
47 changes: 31 additions & 16 deletions plover/oslayer/linux/keyboardcontrol_uinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__()
Expand Down Expand Up @@ -441,6 +461,7 @@ class KeyboardCapture(Capture):

def __init__(self):
super().__init__()
self._keyboard_selection = ""
self._devices = self._get_devices()

self._selector = selectors.DefaultSelector()
Expand All @@ -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.
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion test/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: (
Expand Down
8 changes: 7 additions & 1 deletion test/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -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 =
""",
),
(
Expand Down
31 changes: 24 additions & 7 deletions test/test_keyboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -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.
Expand All @@ -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())
Expand Down
3 changes: 3 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down