From 99aacf45dcfc05ff8e113020cc350f69515dd08c Mon Sep 17 00:00:00 2001 From: DropDemBits Date: Sat, 17 Dec 2022 22:57:46 -0500 Subject: [PATCH 01/11] config: correctly handle new missing int options getint apparently gives `None` if the config value is missing instead of throwing a `KeyValue` exception, so it eventually ends up falling through the `InvalidConfigOption` path and wrongly reports an error --- plover/config.py | 8 ++++--- test/test_config.py | 56 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/plover/config.py b/plover/config.py index fac9048e1..8acb942ea 100644 --- a/plover/config.py +++ b/plover/config.py @@ -105,12 +105,14 @@ def setter(config, key, value): def int_option(name, default, minimum, maximum, section, option=None): option = option or name def getter(config, key): - return config._config[section].getint(option) + return config._config[section][option] def setter(config, key, value): config._set(section, option, str(value)) def validate(config, key, value): - if not isinstance(value, int): - raise InvalidConfigOption(value, default) + try: + value = int(value) + except ValueError as e: + raise InvalidConfigOption(value, default) from e if (minimum is not None and value < minimum) or \ (maximum is not None and value > maximum): message = '%s not in [%s, %s]' % (value, minimum or '-∞', maximum or '∞') diff --git a/test/test_config.py b/test/test_config.py index 8a06c4bbe..614a8b302 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -394,6 +394,17 @@ def test_config_dict(): None, ), + ('invalid_options_3', + ''' + [Output Configuration] + undo_levels = foobar + ''', + DEFAULTS, + {}, + {}, + None, + ), + ('invalid_update_1', ''' [Translation Frame] @@ -505,6 +516,51 @@ def test_config(original_contents, original_config, assert config_file.read_text(encoding='utf-8').strip() == dedent_strip(resulting_contents) +CONFIG_MISSING_INTS_TESTS = ( + ('int_option', + config.OUTPUT_CONFIG_SECTION, + 'undo_levels', + config.DEFAULT_UNDO_LEVELS, + ), + + ('opacity_option', + 'Translation Frame', + 'translation_frame_opacity', + 100, + ), +) + + +@pytest.mark.parametrize(('which_section', 'which_option', 'fixed_value'), + [t[1:] for t in CONFIG_MISSING_INTS_TESTS], + ids=[t[0] for t in CONFIG_MISSING_INTS_TESTS]) +def test_config_missing_ints(which_section, which_option, fixed_value, + monkeypatch, tmpdir, caplog): + registry = Registry() + registry.register_plugin('machine', 'Keyboard', Keyboard) + registry.register_plugin('system', 'English Stenotype', english_stenotype) + monkeypatch.setattr('plover.config.registry', registry) + config_file = tmpdir / 'config.cfg' + + # Make config with the appropriate empty section + contents = f''' + [{which_section}] + ''' + config_file.write_text(contents, encoding='utf-8') + cfg = config.Config(config_file.strpath) + cfg.load() + + # Try to access an option under that section + # (should trigger validation) + assert cfg[which_option] == fixed_value + + # Ensure that missing options are handled + assert 'InvalidConfigOption: None' not in caplog.text + # ... or any that there aren't any unhandled errors + for record in caplog.records: + assert record.levelname != 'ERROR' + + CONFIG_DIR_TESTS = ( # Default to `user_config_dir`. (''' From 02d2e946eb10a54605161efdf212e8166a69dd6a Mon Sep 17 00:00:00 2001 From: DropDemBits Date: Sat, 17 Dec 2022 23:46:43 -0500 Subject: [PATCH 02/11] (gui_qt/)config: allow configuring key press delay --- plover/config.py | 3 +++ plover/gui_qt/config_window.py | 13 ++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/plover/config.py b/plover/config.py index 8acb942ea..7ae6c3330 100644 --- a/plover/config.py +++ b/plover/config.py @@ -26,6 +26,8 @@ OUTPUT_CONFIG_SECTION = 'Output Configuration' DEFAULT_UNDO_LEVELS = 100 MINIMUM_UNDO_LEVELS = 1 +DEFAULT_TIME_BETWEEN_KEY_PRESSES = 0 +MINIMUM_TIME_BETWEEN_KEY_PRESSES = 0 DEFAULT_SYSTEM_NAME = 'English Stenotype' @@ -335,6 +337,7 @@ def _set(self, section, option, value): boolean_option('start_attached', False, OUTPUT_CONFIG_SECTION), boolean_option('start_capitalized', False, OUTPUT_CONFIG_SECTION), int_option('undo_levels', DEFAULT_UNDO_LEVELS, MINIMUM_UNDO_LEVELS, None, OUTPUT_CONFIG_SECTION), + int_option('time_between_key_presses', DEFAULT_TIME_BETWEEN_KEY_PRESSES, MINIMUM_TIME_BETWEEN_KEY_PRESSES, None, OUTPUT_CONFIG_SECTION), # Logging. path_option('log_file_name', expand_path('strokes.log'), LOGGING_CONFIG_SECTION, 'log_file'), boolean_option('enable_stroke_logging', False, LOGGING_CONFIG_SECTION), diff --git a/plover/gui_qt/config_window.py b/plover/gui_qt/config_window.py index 7cc4b6948..544e372ad 100644 --- a/plover/gui_qt/config_window.py +++ b/plover/gui_qt/config_window.py @@ -26,7 +26,7 @@ ) from plover import _ -from plover.config import MINIMUM_UNDO_LEVELS +from plover.config import MINIMUM_UNDO_LEVELS, MINIMUM_TIME_BETWEEN_KEY_PRESSES from plover.misc import expand_path, shorten_path from plover.registry import registry @@ -381,6 +381,17 @@ def __init__(self, engine): '\n' 'Note: the effective value will take into account the\n' 'dictionaries entry with the maximum number of strokes.')), + ConfigOption(_('Time between key presses:'), 'time_between_key_presses', + partial(IntOption, + maximum=100000, + minimum=MINIMUM_TIME_BETWEEN_KEY_PRESSES), + _('Set the delay between emulated key presses (in milliseconds).\n' + '\n' + 'Some programs may drop key presses if too many are sent\n' + 'within a short period of time. Increasing the delay gives\n' + 'programs time to process each key press.\n' + 'Setting the delay too high will negatively impact the\n' + 'performance of key stroke output.')), )), # i18n: Widget: “ConfigWindow”. (_('Plugins'), ( From b0cac46cd579b96127a69214e8be70705bec6d94 Mon Sep 17 00:00:00 2001 From: DropDemBits Date: Sun, 18 Dec 2022 00:55:43 -0500 Subject: [PATCH 03/11] engine,oslayer,output: add output event delay --- plover/engine.py | 1 + plover/oslayer/linux/keyboardcontrol_x11.py | 20 +++++++++++++++++--- plover/oslayer/osx/keyboardcontrol.py | 15 +++++++++++++-- plover/oslayer/windows/keyboardcontrol.py | 13 +++++++++++++ plover/output/__init__.py | 4 ++++ 5 files changed, 48 insertions(+), 5 deletions(-) diff --git a/plover/engine.py b/plover/engine.py index bc47d669c..cc83c5259 100644 --- a/plover/engine.py +++ b/plover/engine.py @@ -211,6 +211,7 @@ def _update(self, config_update=None, full=False, reset_machine=False): self._formatter.start_attached = config['start_attached'] self._formatter.start_capitalized = config['start_capitalized'] self._translator.set_min_undo_length(config['undo_levels']) + self._keyboard_emulation.set_key_press_delay(config['time_between_key_presses']) # Update system. system_name = config['system_name'] if system.NAME != system_name: diff --git a/plover/oslayer/linux/keyboardcontrol_x11.py b/plover/oslayer/linux/keyboardcontrol_x11.py index 1482f0f8d..cc38db15d 100644 --- a/plover/oslayer/linux/keyboardcontrol_x11.py +++ b/plover/oslayer/linux/keyboardcontrol_x11.py @@ -24,6 +24,7 @@ import os import select import threading +from time import sleep from Xlib import X, XK from Xlib.display import Display @@ -1157,6 +1158,7 @@ def __init__(self): super().__init__() self._display = Display() self._update_keymap() + self._key_press_delay = 0 def _update_keymap(self): '''Analyse keymap, build a mapping of keysym to (keycode + modifiers), @@ -1216,11 +1218,17 @@ def _update_keymap(self): # Get modifier mapping. self.modifier_mapping = self._display.get_modifier_mapping() + def set_key_press_delay(self, delay_ms): + self._key_press_delay = delay_ms + def send_backspaces(self, count): for x in range(count): self._send_keycode(self._backspace_mapping.keycode, self._backspace_mapping.modifiers) - self._display.sync() + self._display.sync() + + if self._key_press_delay > 0: + sleep(self._key_press_delay / 1000) def send_string(self, string): for char in string: @@ -1230,7 +1238,10 @@ def send_string(self, string): continue self._send_keycode(mapping.keycode, mapping.modifiers) - self._display.sync() + self._display.sync() + + if self._key_press_delay > 0: + sleep(self._key_press_delay / 1000) def send_key_combination(self, combo): # Parse and validate combo. @@ -1241,7 +1252,10 @@ def send_key_combination(self, combo): # Emulate the key combination by sending key events. for keycode, event_type in key_events: xtest.fake_input(self._display, event_type, keycode) - self._display.sync() + self._display.sync() + + if self._key_press_delay > 0: + sleep(self._key_press_delay / 1000) def _send_keycode(self, keycode, modifiers=0): """Emulate a key press and release. diff --git a/plover/oslayer/osx/keyboardcontrol.py b/plover/oslayer/osx/keyboardcontrol.py index f3d52f929..0b50eb088 100644 --- a/plover/oslayer/osx/keyboardcontrol.py +++ b/plover/oslayer/osx/keyboardcontrol.py @@ -309,9 +309,12 @@ class KeyboardEmulation(Output): def __init__(self): super().__init__() self._layout = KeyboardLayout() + self._key_press_delay = 0 - @staticmethod - def send_backspaces(count): + def set_key_press_delay(self, delay_ms): + self._key_press_delay = delay_ms + + def send_backspaces(self, count): for _ in range(count): backspace_down = CGEventCreateKeyboardEvent( OUTPUT_SOURCE, BACK_SPACE, True) @@ -319,6 +322,8 @@ def send_backspaces(count): OUTPUT_SOURCE, BACK_SPACE, False) CGEventPost(kCGSessionEventTap, backspace_down) CGEventPost(kCGSessionEventTap, backspace_up) + if self._key_press_delay > 0: + sleep(self._key_press_delay / 1000) def send_string(self, string): # Key plan will store the type of output @@ -367,6 +372,9 @@ def apply_raw(): elif press_type is self.RAW_PRESS: self._send_sequence(sequence) + if self._key_press_delay > 0: + sleep(self._key_press_delay / 1000) + @staticmethod def _send_string_press(c): event = CGEventCreateKeyboardEvent(OUTPUT_SOURCE, 0, True) @@ -397,6 +405,9 @@ def name_to_code(name): # Send events... self._send_sequence(key_events) + if self._key_press_delay > 0: + sleep(self._key_press_delay / 1000) + @staticmethod def _modifier_to_keycodes(modifier): keycodes = [] diff --git a/plover/oslayer/windows/keyboardcontrol.py b/plover/oslayer/windows/keyboardcontrol.py index 14067456d..db087b801 100644 --- a/plover/oslayer/windows/keyboardcontrol.py +++ b/plover/oslayer/windows/keyboardcontrol.py @@ -430,6 +430,7 @@ class KeyboardEmulation(Output): def __init__(self): super().__init__() self.keyboard_layout = KeyboardLayout() + self._key_press_delay = 0 # Sends input types to buffer @staticmethod @@ -497,10 +498,16 @@ def _key_unicode(self, char): for code in pairs] self._send_input(*inputs) + def set_key_press_delay(self, delay_ms): + self._key_press_delay = delay_ms + def send_backspaces(self, count): for _ in range(count): self._key_press('\x08') + if self._key_press_delay > 0: + sleep(self._key_press_delay / 1000) + def send_string(self, string): self._refresh_keyboard_layout() for char in string: @@ -511,6 +518,9 @@ def send_string(self, string): # Otherwise, we send it as a Unicode string. self._key_unicode(char) + if self._key_press_delay > 0: + sleep(self._key_press_delay / 1000) + def send_key_combination(self, combo): # Make sure keyboard layout is up-to-date. self._refresh_keyboard_layout() @@ -519,3 +529,6 @@ def send_key_combination(self, combo): # Send events... for keycode, pressed in key_events: self._key_event(keycode, pressed) + + if self._key_press_delay > 0: + sleep(self._key_press_delay / 1000) diff --git a/plover/output/__init__.py b/plover/output/__init__.py index 0546696af..ffb04fd3f 100644 --- a/plover/output/__init__.py +++ b/plover/output/__init__.py @@ -16,3 +16,7 @@ def send_key_combination(self, combo): See `plover.key_combo` for the format of the `combo` string. """ raise NotImplementedError() + + def set_key_press_delay(self, delay_ms): + """Sets the delay between outputting key press events.""" + raise NotImplementedError() From f2a5e8e4132d1b8730929c208af0a50ca5021027 Mon Sep 17 00:00:00 2001 From: DropDemBits Date: Sun, 18 Dec 2022 19:39:22 -0500 Subject: [PATCH 04/11] oslayer/linux: add delay when switching key maps See https://github.com/openstenoproject/plover/pull/1132#issuecomment-782479959 --- plover/oslayer/linux/keyboardcontrol_x11.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/plover/oslayer/linux/keyboardcontrol_x11.py b/plover/oslayer/linux/keyboardcontrol_x11.py index cc38db15d..5c01d824a 100644 --- a/plover/oslayer/linux/keyboardcontrol_x11.py +++ b/plover/oslayer/linux/keyboardcontrol_x11.py @@ -1233,15 +1233,28 @@ def send_backspaces(self, count): def send_string(self, string): for char in string: keysym = uchr_to_keysym(char) - mapping = self._get_mapping(keysym) + # TODO: can we find mappings for multiple keys at a time? + mapping = self._get_mapping(keysym, automatically_map=False) + mapping_changed = False if mapping is None: - continue + mapping = self._get_mapping(keysym, automatically_map=True) + if mapping is None: + continue + if self._key_press_delay > 0: + self._display.sync() + sleep(self._key_press_delay / 2000) + mapping_changed = True + self._send_keycode(mapping.keycode, mapping.modifiers) + self._display.sync() if self._key_press_delay > 0: - sleep(self._key_press_delay / 1000) + if mapping_changed: + sleep(self._key_press_delay / 2000) + else: + sleep(self._key_press_delay / 1000) def send_key_combination(self, combo): # Parse and validate combo. From dfca03c9565642611f4ed8dd071382608fc3978a Mon Sep 17 00:00:00 2001 From: DropDemBits Date: Sun, 18 Dec 2022 19:50:52 -0500 Subject: [PATCH 05/11] test_engine: add stub `set_key_press_delay` --- test/test_engine.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/test_engine.py b/test/test_engine.py index dfb9535b5..1809341a4 100644 --- a/test/test_engine.py +++ b/test/test_engine.py @@ -16,6 +16,7 @@ from plover.machine.keymap import Keymap from plover.misc import normalize_path from plover.oslayer.controller import Controller +from plover.output import Output from plover.registry import Registry from plover.steno_dictionary import StenoDictionaryCollection @@ -50,7 +51,7 @@ def stop_capture(self): def set_suppression(self, enabled): self.is_suppressed = enabled -class FakeKeyboardEmulation: +class FakeKeyboardEmulation(Output): def send_backspaces(self, b): pass @@ -61,6 +62,9 @@ def send_string(self, s): def send_key_combination(self, c): pass + def set_key_press_delay(self, delay_ms): + pass + class FakeEngine(StenoEngine): def __init__(self, *args, **kwargs): From c3f24585280e5887071b25ca11dc65866f9d8759 Mon Sep 17 00:00:00 2001 From: DropDemBits Date: Mon, 19 Dec 2022 15:35:03 -0500 Subject: [PATCH 06/11] oslayer/windows: add missing sleep import --- plover/oslayer/windows/keyboardcontrol.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plover/oslayer/windows/keyboardcontrol.py b/plover/oslayer/windows/keyboardcontrol.py index db087b801..f144e57d7 100644 --- a/plover/oslayer/windows/keyboardcontrol.py +++ b/plover/oslayer/windows/keyboardcontrol.py @@ -14,6 +14,7 @@ """ from ctypes import windll, wintypes +from time import sleep import atexit import ctypes import multiprocessing From 6a88de0674b2896d7e58e5c456c5b02611ef16e4 Mon Sep 17 00:00:00 2001 From: Sammi De Guzman Date: Tue, 26 Sep 2023 17:35:20 -0700 Subject: [PATCH 07/11] plover/machine/keyboard: Refactor key press delay code into generic keyboard emulation machine --- plover/oslayer/linux/keyboardcontrol_x11.py | 30 ++++----------------- plover/oslayer/osx/keyboardcontrol.py | 19 ++++--------- plover/oslayer/windows/keyboardcontrol.py | 24 ++++------------- plover/output/keyboard.py | 21 +++++++++++++++ 4 files changed, 36 insertions(+), 58 deletions(-) create mode 100644 plover/output/keyboard.py diff --git a/plover/oslayer/linux/keyboardcontrol_x11.py b/plover/oslayer/linux/keyboardcontrol_x11.py index 5c01d824a..c100a36b5 100644 --- a/plover/oslayer/linux/keyboardcontrol_x11.py +++ b/plover/oslayer/linux/keyboardcontrol_x11.py @@ -24,7 +24,6 @@ import os import select import threading -from time import sleep from Xlib import X, XK from Xlib.display import Display @@ -34,7 +33,7 @@ from plover import log from plover.key_combo import add_modifiers_aliases, parse_key_combo from plover.machine.keyboard_capture import Capture -from plover.output import Output +from plover.output.keyboard import GenericKeyboardEmulation # Enable support for media keys. @@ -1128,7 +1127,7 @@ def keysym_to_string(keysym): return chr(code) -class KeyboardEmulation(Output): +class KeyboardEmulation(GenericKeyboardEmulation): class Mapping: @@ -1158,7 +1157,6 @@ def __init__(self): super().__init__() self._display = Display() self._update_keymap() - self._key_press_delay = 0 def _update_keymap(self): '''Analyse keymap, build a mapping of keysym to (keycode + modifiers), @@ -1218,20 +1216,14 @@ def _update_keymap(self): # Get modifier mapping. self.modifier_mapping = self._display.get_modifier_mapping() - def set_key_press_delay(self, delay_ms): - self._key_press_delay = delay_ms - def send_backspaces(self, count): - for x in range(count): + for x in self.with_delay(range(count)): self._send_keycode(self._backspace_mapping.keycode, self._backspace_mapping.modifiers) self._display.sync() - if self._key_press_delay > 0: - sleep(self._key_press_delay / 1000) - def send_string(self, string): - for char in string: + for char in self.with_delay(string): keysym = uchr_to_keysym(char) # TODO: can we find mappings for multiple keys at a time? mapping = self._get_mapping(keysym, automatically_map=False) @@ -1240,9 +1232,6 @@ def send_string(self, string): mapping = self._get_mapping(keysym, automatically_map=True) if mapping is None: continue - if self._key_press_delay > 0: - self._display.sync() - sleep(self._key_press_delay / 2000) mapping_changed = True self._send_keycode(mapping.keycode, @@ -1250,12 +1239,6 @@ def send_string(self, string): self._display.sync() - if self._key_press_delay > 0: - if mapping_changed: - sleep(self._key_press_delay / 2000) - else: - sleep(self._key_press_delay / 1000) - def send_key_combination(self, combo): # Parse and validate combo. key_events = [ @@ -1263,13 +1246,10 @@ def send_key_combination(self, combo): in parse_key_combo(combo, self._get_keycode_from_keystring) ] # Emulate the key combination by sending key events. - for keycode, event_type in key_events: + for keycode, event_type in self.with_delay(key_events): xtest.fake_input(self._display, event_type, keycode) self._display.sync() - if self._key_press_delay > 0: - sleep(self._key_press_delay / 1000) - def _send_keycode(self, keycode, modifiers=0): """Emulate a key press and release. diff --git a/plover/oslayer/osx/keyboardcontrol.py b/plover/oslayer/osx/keyboardcontrol.py index 0b50eb088..644d1d1b4 100644 --- a/plover/oslayer/osx/keyboardcontrol.py +++ b/plover/oslayer/osx/keyboardcontrol.py @@ -46,7 +46,7 @@ from plover import log from plover.key_combo import add_modifiers_aliases, parse_key_combo, KEYNAME_TO_CHAR from plover.machine.keyboard_capture import Capture -from plover.output import Output +from plover.output.keyboard import GenericKeyboardEmulation from .keyboardlayout import KeyboardLayout @@ -302,28 +302,25 @@ def _run(self): self.key_down(key) -class KeyboardEmulation(Output): +class KeyboardEmulation(GenericKeyboardEmulation): RAW_PRESS, STRING_PRESS = range(2) def __init__(self): super().__init__() self._layout = KeyboardLayout() - self._key_press_delay = 0 def set_key_press_delay(self, delay_ms): self._key_press_delay = delay_ms def send_backspaces(self, count): - for _ in range(count): + for _ in self.with_delay(range(count)): backspace_down = CGEventCreateKeyboardEvent( OUTPUT_SOURCE, BACK_SPACE, True) backspace_up = CGEventCreateKeyboardEvent( OUTPUT_SOURCE, BACK_SPACE, False) CGEventPost(kCGSessionEventTap, backspace_down) CGEventPost(kCGSessionEventTap, backspace_up) - if self._key_press_delay > 0: - sleep(self._key_press_delay / 1000) def send_string(self, string): # Key plan will store the type of output @@ -366,15 +363,12 @@ def apply_raw(): apply_raw() # We have a key plan for the whole string, grouping modifiers. - for press_type, sequence in key_plan: + for press_type, sequence in self.with_delay(key_plan): if press_type is self.STRING_PRESS: self._send_string_press(sequence) elif press_type is self.RAW_PRESS: self._send_sequence(sequence) - if self._key_press_delay > 0: - sleep(self._key_press_delay / 1000) - @staticmethod def _send_string_press(c): event = CGEventCreateKeyboardEvent(OUTPUT_SOURCE, 0, True) @@ -405,9 +399,6 @@ def name_to_code(name): # Send events... self._send_sequence(key_events) - if self._key_press_delay > 0: - sleep(self._key_press_delay / 1000) - @staticmethod def _modifier_to_keycodes(modifier): keycodes = [] @@ -456,7 +447,7 @@ def _send_sequence(sequence): # If mods_flags is not zero at the end then bad things might happen. mods_flags = 0 - for keycode, key_down in sequence: + for keycode, key_down in self.with_delay(sequence): if keycode >= NX_KEY_OFFSET: # Handle media (NX) key. event = KeyboardEmulation._get_media_event( diff --git a/plover/oslayer/windows/keyboardcontrol.py b/plover/oslayer/windows/keyboardcontrol.py index f144e57d7..bb8db9e7b 100644 --- a/plover/oslayer/windows/keyboardcontrol.py +++ b/plover/oslayer/windows/keyboardcontrol.py @@ -14,7 +14,6 @@ """ from ctypes import windll, wintypes -from time import sleep import atexit import ctypes import multiprocessing @@ -27,7 +26,7 @@ from plover.key_combo import parse_key_combo from plover.machine.keyboard_capture import Capture from plover.misc import to_surrogate_pair -from plover.output import Output +from plover.output.keyboard import GenericKeyboardEmulation from .keyboardlayout import KeyboardLayout @@ -426,12 +425,11 @@ def suppress(self, suppressed_keys=()): self._proc.suppress(self._suppressed_keys) -class KeyboardEmulation(Output): +class KeyboardEmulation(GenericKeyboardEmulation): def __init__(self): super().__init__() self.keyboard_layout = KeyboardLayout() - self._key_press_delay = 0 # Sends input types to buffer @staticmethod @@ -499,19 +497,13 @@ def _key_unicode(self, char): for code in pairs] self._send_input(*inputs) - def set_key_press_delay(self, delay_ms): - self._key_press_delay = delay_ms - def send_backspaces(self, count): - for _ in range(count): + for _ in self.with_delay(range(count)): self._key_press('\x08') - if self._key_press_delay > 0: - sleep(self._key_press_delay / 1000) - def send_string(self, string): self._refresh_keyboard_layout() - for char in string: + for char in self.with_delay(string): if char in self.keyboard_layout.char_to_vk_ss: # We know how to simulate the character. self._key_press(char) @@ -519,17 +511,11 @@ def send_string(self, string): # Otherwise, we send it as a Unicode string. self._key_unicode(char) - if self._key_press_delay > 0: - sleep(self._key_press_delay / 1000) - def send_key_combination(self, combo): # Make sure keyboard layout is up-to-date. self._refresh_keyboard_layout() # Parse and validate combo. key_events = parse_key_combo(combo, self.keyboard_layout.keyname_to_vk.get) # Send events... - for keycode, pressed in key_events: + for keycode, pressed in self.with_delay(key_events): self._key_event(keycode, pressed) - - if self._key_press_delay > 0: - sleep(self._key_press_delay / 1000) diff --git a/plover/output/keyboard.py b/plover/output/keyboard.py new file mode 100644 index 000000000..58efabe4d --- /dev/null +++ b/plover/output/keyboard.py @@ -0,0 +1,21 @@ +from time import sleep + +from plover.output import Output + + +class GenericKeyboardEmulation(Output): + def __init__(self): + super().__init__() + self._key_press_delay_ms = 0 + + def set_key_press_delay(self, delay_ms): + self._key_press_delay_ms = delay_ms + + def delay(self): + if self._key_press_delay_ms > 0: + sleep(self._key_press_delay_ms / 1000) + + def with_delay(self, iterable): + for item in iterable: + yield item + self.delay() From 69bacb7a94eb253df7583d8b0b30c1fe9d7413be Mon Sep 17 00:00:00 2001 From: Sammi De Guzman Date: Tue, 26 Sep 2023 17:50:38 -0700 Subject: [PATCH 08/11] mac: Make _send_sequence not static in order to apply delay --- plover/oslayer/osx/keyboardcontrol.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plover/oslayer/osx/keyboardcontrol.py b/plover/oslayer/osx/keyboardcontrol.py index 644d1d1b4..ed60f8b05 100644 --- a/plover/oslayer/osx/keyboardcontrol.py +++ b/plover/oslayer/osx/keyboardcontrol.py @@ -437,8 +437,7 @@ def _get_media_event(key_id, key_down): -1 ).CGEvent() - @staticmethod - def _send_sequence(sequence): + def _send_sequence(self, sequence): # There is a bug in the event system that seems to cause inconsistent # modifiers on key events: # http://stackoverflow.com/questions/2008126/cgeventpost-possible-bug-when-simulating-keyboard-events From b7905f0d6ed281a461764245fca2451fecf3c3d4 Mon Sep 17 00:00:00 2001 From: Sammi De Guzman Date: Tue, 26 Sep 2023 17:54:14 -0700 Subject: [PATCH 09/11] Add news blurbs for #1633 --- news.d/api/1633.new.md | 1 + news.d/feature/1633.core.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 news.d/api/1633.new.md create mode 100644 news.d/feature/1633.core.md diff --git a/news.d/api/1633.new.md b/news.d/api/1633.new.md new file mode 100644 index 000000000..510033df6 --- /dev/null +++ b/news.d/api/1633.new.md @@ -0,0 +1 @@ +Introduces the `GenericKeyboardEmulation` interface which automatically handles output delay. diff --git a/news.d/feature/1633.core.md b/news.d/feature/1633.core.md new file mode 100644 index 000000000..fbae304b0 --- /dev/null +++ b/news.d/feature/1633.core.md @@ -0,0 +1 @@ +Added a configurable delay between key presses, to accommodate applications that can't handle fast keyboard emulation. From c845e0ed9a799254ac404a23ae7dda4e467b41f7 Mon Sep 17 00:00:00 2001 From: Sammi De Guzman Date: Tue, 26 Sep 2023 18:06:52 -0700 Subject: [PATCH 10/11] debugging is hard --- plover/oslayer/osx/keyboardcontrol.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/plover/oslayer/osx/keyboardcontrol.py b/plover/oslayer/osx/keyboardcontrol.py index ed60f8b05..f230ffb92 100644 --- a/plover/oslayer/osx/keyboardcontrol.py +++ b/plover/oslayer/osx/keyboardcontrol.py @@ -310,9 +310,6 @@ def __init__(self): super().__init__() self._layout = KeyboardLayout() - def set_key_press_delay(self, delay_ms): - self._key_press_delay = delay_ms - def send_backspaces(self, count): for _ in self.with_delay(range(count)): backspace_down = CGEventCreateKeyboardEvent( From a12769478bb81e05ad3b5587fc26f75ff7af5d92 Mon Sep 17 00:00:00 2001 From: Sammi De Guzman Date: Tue, 26 Sep 2023 20:04:55 -0700 Subject: [PATCH 11/11] Reimplement half-delay on X11 as covered in #1132 --- plover/oslayer/linux/keyboardcontrol_x11.py | 9 ++++++++- plover/output/keyboard.py | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/plover/oslayer/linux/keyboardcontrol_x11.py b/plover/oslayer/linux/keyboardcontrol_x11.py index c100a36b5..a60eb4c25 100644 --- a/plover/oslayer/linux/keyboardcontrol_x11.py +++ b/plover/oslayer/linux/keyboardcontrol_x11.py @@ -24,6 +24,7 @@ import os import select import threading +from time import sleep from Xlib import X, XK from Xlib.display import Display @@ -1223,7 +1224,7 @@ def send_backspaces(self, count): self._display.sync() def send_string(self, string): - for char in self.with_delay(string): + for char in string: keysym = uchr_to_keysym(char) # TODO: can we find mappings for multiple keys at a time? mapping = self._get_mapping(keysym, automatically_map=False) @@ -1232,12 +1233,18 @@ def send_string(self, string): mapping = self._get_mapping(keysym, automatically_map=True) if mapping is None: continue + self._display.sync() + self.half_delay() mapping_changed = True self._send_keycode(mapping.keycode, mapping.modifiers) self._display.sync() + if mapping_changed: + self.half_delay() + else: + self.delay() def send_key_combination(self, combo): # Parse and validate combo. diff --git a/plover/output/keyboard.py b/plover/output/keyboard.py index 58efabe4d..740f0e6ca 100644 --- a/plover/output/keyboard.py +++ b/plover/output/keyboard.py @@ -15,6 +15,10 @@ def delay(self): if self._key_press_delay_ms > 0: sleep(self._key_press_delay_ms / 1000) + def half_delay(self): + if self._key_press_delay_ms > 0: + sleep(self._key_press_delay_ms / 2000) + def with_delay(self, iterable): for item in iterable: yield item