Skip to content

Commit

Permalink
add ability to change keyboard layout in addition to language
Browse files Browse the repository at this point in the history
such as for example using colemak
  • Loading branch information
LilleAila committed Jul 27, 2024
1 parent 77640a5 commit 1782a7d
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 24 deletions.
38 changes: 23 additions & 15 deletions plover/gui_qt/config_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ class BooleanOption(QCheckBox):

def __init__(self):
super().__init__()
self.stateChanged.connect(lambda: self.valueChanged.emit(self.isChecked()))
self.stateChanged.connect(
lambda: self.valueChanged.emit(self.isChecked()))

def setValue(self, value):
self.setChecked(value)
Expand All @@ -83,6 +84,7 @@ def __init__(self):
def setValue(self, value):
self.setText(value)


class ChoiceOption(QComboBox):

valueChanged = pyqtSignal(str)
Expand Down Expand Up @@ -272,7 +274,8 @@ def setValue(self, value):
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
self.setItem(row, 0, item)
item = QTableWidgetItem()
item.setFlags((item.flags() & ~Qt.ItemIsEditable) | Qt.ItemIsUserCheckable)
item.setFlags((item.flags() & ~Qt.ItemIsEditable)
| Qt.ItemIsUserCheckable)
item.setCheckState(Qt.Checked if choice in value else Qt.Unchecked)
self.setItem(row, 1, item)
self.resizeColumnsToContents()
Expand All @@ -296,7 +299,7 @@ class BooleanAsDualChoiceOption(ChoiceOption):
valueChanged = pyqtSignal(bool)

def __init__(self, choice_false, choice_true):
choices = { False: choice_false, True: choice_true }
choices = {False: choice_false, True: choice_true}
super().__init__(choices)


Expand Down Expand Up @@ -363,10 +366,13 @@ def __init__(self, engine):
(_('Machine'), (
ConfigOption(_('Machine:'), 'machine_type', partial(ChoiceOption, choices=machines),
dependents=(
('machine_specific_options', self._update_machine_options),
('system_keymap', lambda v: self._update_keymap(machine_type=v)),
)),
ConfigOption(_('Options:'), 'machine_specific_options', self._machine_option),
('machine_specific_options',
self._update_machine_options),
('system_keymap', lambda v: self._update_keymap(
machine_type=v)),
)),
ConfigOption(
_('Options:'), 'machine_specific_options', self._machine_option),
ConfigOption(_('Keymap:'), 'system_keymap', KeymapOption),
)),
# i18n: Widget: “ConfigWindow”.
Expand Down Expand Up @@ -404,16 +410,15 @@ def __init__(self, engine):
'programs time to process each key press.\n'
'Setting the delay too high will negatively impact the\n'
'performance of key stroke output.')),
# There are also the rules, model and options, but i don't think they affect the output of alphanumeric characters
ConfigOption(_('Keyboard Layout:'), 'xkb_layout', StrOption,
_('Set the keyboard layout configured in your system.\n'
'Examples: "us", "gb", "fr", "no"\n'
'\n'
'This only applies when using Linux/BSD and not using X11.\n'
'If you\'re unsure, you probably don\'t need to change it.\n'
'If you need to configure more options about your layout,\n'
'such as setting the variant to a different layout like colemak,\n'
'you can set environment variables starting with XKB_DEFAULT_\n'
'for the RULES, MODEL, VARIANT and OPTIONS')),
'If you use a different layout variant, format it as\n'
'"language:layout", for example "us:colemak"')),
)),
# i18n: Widget: “ConfigWindow”.
(_('Plugins'), (
Expand All @@ -432,16 +437,18 @@ def __init__(self, engine):
for plugin in registry.list_plugins('system')
}),
dependents=(
('system_keymap', lambda v: self._update_keymap(system_name=v)),
)),
('system_keymap', lambda v: self._update_keymap(
system_name=v)),
)),
)),
)
# Only keep supported options, to avoid messing with things like
# dictionaries, that are handled by another (possibly concurrent)
# dialog.
self._supported_options = set()
for section, option_list in mappings:
self._supported_options.update(option.option_name for option in option_list)
self._supported_options.update(
option.option_name for option in option_list)
self._update_config()
# Create and fill tabs.
option_by_name = {}
Expand Down Expand Up @@ -471,7 +478,8 @@ def __init__(self, engine):
for option_name, update_fn in option.dependents
]
self.buttons.button(QDialogButtonBox.Ok).clicked.connect(self.on_apply)
self.buttons.button(QDialogButtonBox.Apply).clicked.connect(self.on_apply)
self.buttons.button(
QDialogButtonBox.Apply).clicked.connect(self.on_apply)
self.tabs.currentWidget().setFocus()
self.restore_state()
self.finished.connect(self.save_state)
Expand Down
22 changes: 15 additions & 7 deletions plover/oslayer/linux/keyboardcontrol_uinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,16 +166,21 @@
KEY_TO_KEYCODE = {**keys, **modifiers}
KEYCODE_TO_KEY = dict(zip(KEY_TO_KEYCODE.values(), KEY_TO_KEYCODE.keys()))


class KeyboardEmulation(GenericKeyboardEmulation):
def __init__(self):
super().__init__()
# Initialize UInput with all keys available
self._res = util.find_ecodes_by_regex(r"KEY_.*")
self._ui = UInput(self._res)

def _update_layout(self, layout):
log.info("Using keyboard layout " + layout + " for keyboard emulation.")
symbols = generate_symbols(layout)
def _update_layout(self, _layout):
log.info("Using keyboard layout " +
_layout + " for keyboard emulation.")
_layout_options = _layout.split(":")
layout = _layout_options[0]
variant = _layout_options[1] if len(_layout_options) > 1 else ""
symbols = generate_symbols(layout, variant)
# Remove symbols not in KEY_TO_KEYCODE
syms_to_remove = []
for sym in symbols:
Expand Down Expand Up @@ -203,6 +208,7 @@ def _send_key(self, key):
the same keybinding, it's too fast for it to handle and ends up writing random stuff. I don't
think there is a way to fix that other than increasing the delay.
"""

def _send_unicode(self, hex):
self.send_key_combination("ctrl_l(shift(u))")
self.delay()
Expand Down Expand Up @@ -248,7 +254,7 @@ def send_key_combination(self, combo):
else:
k = KEY_TO_KEYCODE[key.lower()]
self._press_key(k, pressed)
except KeyError: # In case the key does not exist
except KeyError: # In case the key does not exist
log.warning("Key " + key + " is not valid!")


Expand All @@ -265,7 +271,8 @@ def __init__(self):

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)]
keyboard_devices = [
dev for dev in input_devices if self._filter_devices(dev)]
return {dev.fd: dev for dev in keyboard_devices}

def _filter_devices(self, device):
Expand All @@ -274,7 +281,7 @@ def _filter_devices(self, device):
"""
is_uinput = device.name == "py-evdev-uinput" or device.phys == "py-evdev-uinput"
capabilities = device.capabilities()
is_mouse = e.EV_REL in capabilities or e.EV_ABS in capabilities
is_mouse = e.EV_REL in capabilities or e.EV_ABS in capabilities
return not is_uinput and not is_mouse

def start(self):
Expand Down Expand Up @@ -321,6 +328,7 @@ def _run(self):
if key_name in self._suppressed_keys:
pressed = event.value == 1
(self.key_down if pressed else self.key_up)(key_name)
continue # Go to the next iteration, skipping the below code:
# Go to the next iteration, skipping the below code:
continue
self._ui.write(e.EV_KEY, event.code, event.value)
self._ui.syn()
4 changes: 2 additions & 2 deletions plover/oslayer/linux/xkb_symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@


# layout can be "no", "us", "gb", "fr" or any other xkb layout
def generate_symbols(layout="us"):
def generate_symbols(layout="us", variant=""):
ctx = xkb.Context()
keymap = ctx.keymap_new_from_names(layout=layout)
keymap = ctx.keymap_new_from_names(layout=layout, variant=variant)
# The keymaps have to be "translated" to a US layout keyboard for evdev
keymap_us = ctx.keymap_new_from_names(layout="us")

Expand Down

0 comments on commit 1782a7d

Please sign in to comment.