From 07065cb2f0b9e10342f2b0819da9f3d8f0470074 Mon Sep 17 00:00:00 2001 From: Uwe Date: Fri, 17 Feb 2023 01:52:15 +0100 Subject: [PATCH 1/6] add listbox filter component and use to for filtering mappings --- bin/input-remapper-gtk | 15 ++- data/input-remapper.glade | 115 +++++++++++++----- inputremapper/gui/components/common.py | 126 +++++++++++++++++++- inputremapper/gui/messages/message_types.py | 1 + inputremapper/gui/reader_client.py | 11 +- inputremapper/gui/reader_service.py | 8 +- inputremapper/gui/user_interface.py | 11 +- inputremapper/injection/global_uinputs.py | 1 + tests/integration/test_gui.py | 14 +++ 9 files changed, 263 insertions(+), 39 deletions(-) diff --git a/bin/input-remapper-gtk b/bin/input-remapper-gtk index 6da510bdc..a4b0bbd5a 100755 --- a/bin/input-remapper-gtk +++ b/bin/input-remapper-gtk @@ -41,11 +41,11 @@ from inputremapper.daemon import DaemonProxy from inputremapper.logger import logger, update_verbosity, log_info -def start_processes() -> DaemonProxy: +def start_processes(ignore_pkexec_errors=False) -> DaemonProxy: """Start reader-service and daemon via pkexec to run in the background.""" # this function is overwritten in tests try: - ReaderService.pkexec_reader_service() + ReaderService.pkexec_reader_service(ingore_errors=ignore_pkexec_errors) except Exception as e: logger.error(e) sys.exit(11) @@ -57,7 +57,12 @@ if __name__ == '__main__': parser = ArgumentParser() parser.add_argument( '-d', '--debug', action='store_true', dest='debug', - help=_('Displays additional debug information'), + help=_('displays additional debug information'), + default=False + ) + parser.add_argument( + '-R', '--no-root', action='store_true', dest='no_root', + help=_('allow rejecting root access (by cancelling the pkexec dialog)'), default=False ) @@ -85,8 +90,8 @@ if __name__ == '__main__': # create the reader before we start the reader-service (start_processes) otherwise # it can come to race conditions with the creation of pipes - reader_client = ReaderClient(message_broker, _Groups()) - daemon = start_processes() + reader_client = ReaderClient(message_broker, _Groups(), ignore_pkexec_errors=options.no_root) + daemon = start_processes(ignore_pkexec_errors=options.no_root) data_manager = DataManager( message_broker, GlobalConfig(), reader_client, daemon, GlobalUInputs(), system_mapping diff --git a/data/input-remapper.glade b/data/input-remapper.glade index 00a6daee4..1bac357a6 100644 --- a/data/input-remapper.glade +++ b/data/input-remapper.glade @@ -15,6 +15,11 @@ 2 media-playback-start + + True + False + sweeper + True False @@ -436,6 +441,7 @@ Shortcut: ctrl + del True True + enter preset name... True @@ -501,6 +507,7 @@ Shortcut: ctrl + del False 18 vertical + 6 True @@ -521,14 +528,44 @@ Shortcut: ctrl + del True False - center - 18 + 6 + 6 + 6 + True - + True False - no input configured - True + center + 18 + + + True + False + no input configured + True + + + False + True + 0 + + + + + False + 0.5 + (recording ...) + + + + + + False + True + 1 + + False @@ -536,21 +573,6 @@ Shortcut: ctrl + del 0 - - - False - 0.5 - (recording ...) - - - - - - False - True - 1 - - False @@ -564,11 +586,9 @@ Shortcut: ctrl + del True False center - 18 - 18 - 18 + 6 + 6 6 - True Add @@ -580,8 +600,8 @@ Shortcut: ctrl + del False - True - 1 + False + 0 @@ -596,7 +616,7 @@ Shortcut: ctrl + del False - True + False 1 @@ -610,7 +630,7 @@ Shortcut: ctrl + del False - True + False 2 @@ -629,6 +649,45 @@ Shortcut: ctrl + del False + False + 3 + + + + + True + False + 6 + + + True + True + filter mappings... + + + True + True + 0 + + + + + True + True + True + Clear search filter + start + clear-icon + + + False + True + 1 + + + + + True True 4 diff --git a/inputremapper/gui/components/common.py b/inputremapper/gui/components/common.py index 7110113ec..ed24af573 100644 --- a/inputremapper/gui/components/common.py +++ b/inputremapper/gui/components/common.py @@ -27,7 +27,10 @@ from gi.repository import Gtk -from typing import Optional +from typing import ( + Optional, + Iterator, +) from inputremapper.configs.mapping import MappingData @@ -173,3 +176,124 @@ def _render(self): label.append(self._mapping_name or "?") self._gui.set_label(" / ".join(label)) + + +class ListFilterControl: + """Implements UI-side filtering of list widgets. + + The following example creates a new ``ListFilterControl`` for a given + ``Gtk.ListBox`` and a given ``Gtk.Entry`` for text input. It also sets all + optional arguments to override some default behavior. + + >>> ListFilterControl( + >>> my_gtk_listbox, + >>> my_gtk_entry, + >>> clear_button=my_gtk_button, # use an optional clear button + >>> case_sensitive=True, # change default behavior + >>> get_row_name=MyRow.get_name # custom row name getter + >>> ) + + """ + + MAX_WIDGET_TREE_TEXT_SEARCH_DEPTH = 10 + + def __init__( + self, + # message_broker: MessageBroker, + controlled_listbox: Gtk.ListBox, + filter_entry: Gtk.GtkEntry, + clear_button: Gtk.Button = None, + case_sensitive=False, + get_row_name=None, + ): + self._controlled_listbox: Gtk.ListBox = controlled_listbox + self._filter_entry: Gtk.Entry = filter_entry + self._clear_button: Gtk.Button = clear_button + + self._filter_value:str = "" + self._case_sensitive:bool = bool(case_sensitive) + self._get_row_name = get_row_name or self.get_row_name + + self._connect_gtk_signals() + + @classmethod + def get_row_name(T, row:Gtk.ListBoxRow) -> str: + """ + Returns the visible text of a Gtk.ListBoxRow from both the row's `name` + attribute or the row's text in the UI. + """ + text = getattr(row, "name", "") + + # find and join all text in the ListBoxRow + text += " ".join(v for v in T.get_widget_tree_text(row) if v != "") + + return text.strip() + + @classmethod + def get_widget_tree_text(T, widget:Gtk.Widget, level=0) -> Iterator[str]: + """ + Recursively traverses the tree of child widgets starting from the given + widget, and yields the text of all text-containing widgets. + """ + if level > T.MAX_WIDGET_TREE_TEXT_SEARCH_DEPTH: + return + + if hasattr(widget, "get_label"): + yield (widget.get_label() or "").strip() + if hasattr(widget, "get_text"): + yield (widget.get_text() or "").strip() + if isinstance(widget, Gtk.Container): + for t in widget.get_children(): + yield from T.get_widget_tree_text(t, level=level+1) + + def _connect_gtk_signals(self): + if self._clear_button: + self._clear_button.connect("clicked", + self.on_gtk_clear_button_clicked + ) + self._filter_entry.connect("key-release-event", + self.on_gtk_filter_entry_input + ) + + # apply defined filter by sending out the corresponding events + def apply_filter(self): + self._apply_filter_to_listbox_children() + + # matches the current filter_value and filter_options with the given value + def match_filter(self, value:str): + value = (value or "").strip() + + # if filter is not set, all rows need to match + if self._filter_value == "": + return True + + print(f"matching filter: {self._filter_value} with value: {value}") + + if self._case_sensitive: + return self._filter_value in value + else: + return self._filter_value.lower() in value.lower() + + def _apply_filter_to_listbox_children(self): + value = self._filter_value.lower() + selected: Gtk.ListBoxRow = None + row: Gtk.ListBoxRow = None + for row in self._controlled_listbox.get_children(): + if self.match_filter(self._get_row_name(row)): + # show matching rows, then select the first row + row.show() + if selected is None: + selected = row + self._controlled_listbox.select_row(selected) + else: + # hide non-matching rows + row.hide() + + def on_gtk_filter_entry_input(self, _, event: Gdk.EventKey): + self._filter_value = (self._filter_entry.get_text() or "").strip() + self.apply_filter() + + def on_gtk_clear_button_clicked(self, *_): + self._filter_entry.set_text("") + self._filter_value = "" + self.apply_filter() diff --git a/inputremapper/gui/messages/message_types.py b/inputremapper/gui/messages/message_types.py index a9619fb62..9ecda3404 100644 --- a/inputremapper/gui/messages/message_types.py +++ b/inputremapper/gui/messages/message_types.py @@ -41,6 +41,7 @@ class MessageType(Enum): mapping = "mapping" selected_event = "selected_event" combination_recorded = "combination_recorded" + filter_changed = "filter_changed" # only the reader_client should send those messages: recording_started = "recording_started" diff --git a/inputremapper/gui/reader_client.py b/inputremapper/gui/reader_client.py index a0b1009f1..8f9d9e525 100644 --- a/inputremapper/gui/reader_client.py +++ b/inputremapper/gui/reader_client.py @@ -23,6 +23,7 @@ see gui.reader_service.ReaderService """ +import os import time from typing import Optional, List, Generator, Dict, Tuple, Set @@ -72,7 +73,12 @@ class ReaderClient: # how long to wait for the reader-service at most _timeout: int = 5 - def __init__(self, message_broker: MessageBroker, groups: _Groups): + def __init__( + self, + message_broker: MessageBroker, + groups: _Groups, + ignore_pkexec_errors=False + ): self.groups = groups self.message_broker = message_broker @@ -86,13 +92,14 @@ def __init__(self, message_broker: MessageBroker, groups: _Groups): self.attach_to_events() self._read_timeout = GLib.timeout_add(30, self._read) + self.ignore_pkexec_errors = ignore_pkexec_errors def ensure_reader_service_running(self): if ReaderService.is_running(): return logger.info("ReaderService not running anymore, restarting") - ReaderService.pkexec_reader_service() + ReaderService.pkexec_reader_service(ingore_errors=self.ignore_pkexec_errors) # wait until the ReaderService is up diff --git a/inputremapper/gui/reader_service.py b/inputremapper/gui/reader_service.py index 2aae26437..31168bfe0 100644 --- a/inputremapper/gui/reader_service.py +++ b/inputremapper/gui/reader_service.py @@ -136,7 +136,7 @@ def is_running(): return True @staticmethod - def pkexec_reader_service(): + def pkexec_reader_service(ingore_errors=False): """Start reader-service via pkexec to run in the background.""" debug = " -d" if logger.level <= logging.DEBUG else "" cmd = f"pkexec input-remapper-control --command start-reader-service{debug}" @@ -145,7 +145,11 @@ def pkexec_reader_service(): exit_code = os.system(cmd) if exit_code != 0: - raise Exception(f"Failed to pkexec the reader-service, code {exit_code}") + ex = Exception(f"Failed to pkexec the reader-service, code {exit_code}") + if ingore_errors: + logger.warn(ex) + else: + raise ex def run(self): """Start doing stuff. Blocks.""" diff --git a/inputremapper/gui/user_interface.py b/inputremapper/gui/user_interface.py index d7c222d96..269a60e69 100644 --- a/inputremapper/gui/user_interface.py +++ b/inputremapper/gui/user_interface.py @@ -51,7 +51,10 @@ ) from inputremapper.gui.components.presets import PresetSelection from inputremapper.gui.components.main import Stack, StatusBar -from inputremapper.gui.components.common import Breadcrumbs +from inputremapper.gui.components.common import ( + Breadcrumbs, + ListFilterControl, +) from inputremapper.gui.components.device_groups import DeviceGroupSelection from inputremapper.gui.controller import Controller from inputremapper.gui.messages.message_broker import ( @@ -149,6 +152,12 @@ def _create_components(self): MappingListBox(message_broker, controller, self.get("selection_label_listbox")) TargetSelection(message_broker, controller, self.get("target-selector")) + ListFilterControl( + self.get("selection_label_listbox"), + self.get("mapping-filter-input"), + clear_button=self.get("mapping-filter-clear-button"), + ) + Breadcrumbs( message_broker, self.get("selected_device_name"), diff --git a/inputremapper/injection/global_uinputs.py b/inputremapper/injection/global_uinputs.py index 4fd2abe14..75719d1f8 100644 --- a/inputremapper/injection/global_uinputs.py +++ b/inputremapper/injection/global_uinputs.py @@ -151,6 +151,7 @@ def write(self, event: Tuple[int, int, int], target_uinput): if not uinput: raise inputremapper.exceptions.UinputNotAvailable(target_uinput) + # TODO: fix: AttributeError: 'FrontendUInput' object has no attribute 'can_emit' if not uinput.can_emit(event): raise inputremapper.exceptions.EventNotHandled(event) diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py index c175225fc..dafefd6a6 100644 --- a/tests/integration/test_gui.py +++ b/tests/integration/test_gui.py @@ -291,6 +291,8 @@ def setUp(self): self.stop_injector_btn: Gtk.Button = get("stop_injection_preset_page") self.rename_btn: Gtk.Button = get("rename-button") self.rename_input: Gtk.Entry = get("preset_name_input") + self.search_btn: Gtk.Button = get("mapping-search-clear-button") + self.search_input: Gtk.Entry = get("mapping-search-input") self.create_mapping_btn: Gtk.Button = get("create_mapping_button") self.delete_mapping_btn: Gtk.Button = get("delete-mapping") @@ -1415,6 +1417,18 @@ def save(): gtk_iteration() self.assertFalse(os.path.exists(preset_path)) + def test_search_input(self): + self.search_input.set_text("foo") + self.set_focus(self.search_input) + # TODO: press a key and check filter value in data manager + gtk_iteration() + # self.assertEquals(self.controller.data_manager.active_filter, "foo") + + self.search_btn.clicked() + gtk_iteration() + self.assertEquals(self.search_input.get_text(), "") + self.assertEquals(self.controller.data_manager.active_filter, "") + def test_check_for_unknown_symbols(self): status = self.user_interface.get("status_bar") error_icon = self.user_interface.get("error_status_icon") From 3d47406bf014047005ea141eb6fc83fd748f4f17 Mon Sep 17 00:00:00 2001 From: Uwe Date: Sun, 19 Feb 2023 00:48:49 +0100 Subject: [PATCH 2/6] add missing can_emit method, used in tests --- inputremapper/injection/global_uinputs.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/inputremapper/injection/global_uinputs.py b/inputremapper/injection/global_uinputs.py index 75719d1f8..99c70f2f9 100644 --- a/inputremapper/injection/global_uinputs.py +++ b/inputremapper/injection/global_uinputs.py @@ -69,7 +69,7 @@ def __init__(self, *args, **kwargs): # gather the capabilities. (can_emit is called regularly) self._capabilities_cache = self.capabilities(absinfo=False) - def can_emit(self, event: Tuple[int, int, int]): + def can_emit(self, event: Tuple[int, int, int]) -> bool: """Check if an event can be emitted by the UIinput. Wrong events might be injected if the group mappings are wrong, @@ -90,6 +90,9 @@ def __init__(self, *args, events=None, name="py-evdev-uinput", **kwargs): def capabilities(self): return self.events + def can_emit(self, event: Tuple[int, int, int]) -> bool: + return False + class GlobalUInputs: """Manages all UInputs that are shared between all injection processes.""" @@ -151,7 +154,6 @@ def write(self, event: Tuple[int, int, int], target_uinput): if not uinput: raise inputremapper.exceptions.UinputNotAvailable(target_uinput) - # TODO: fix: AttributeError: 'FrontendUInput' object has no attribute 'can_emit' if not uinput.can_emit(event): raise inputremapper.exceptions.EventNotHandled(event) From bce087d2cbeced7cf6c128c1c9caaa0b84083101 Mon Sep 17 00:00:00 2001 From: Uwe Date: Sun, 19 Feb 2023 00:49:30 +0100 Subject: [PATCH 3/6] use help func to create combination --- tests/lib/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/fixtures.py b/tests/lib/fixtures.py index e6506cdec..47de57b5c 100644 --- a/tests/lib/fixtures.py +++ b/tests/lib/fixtures.py @@ -341,7 +341,7 @@ def get_key_mapping(combination=None, target_uinput="keyboard", output_symbol="a from inputremapper.configs.mapping import Mapping if not combination: - combination = [{"type": 99, "code": 99, "analog_threshold": 99}] + combination = get_combination_config((99, 99, 99)) return Mapping( input_combination=combination, From 3f3904db8ed61aff74b9fde0badfb1916d9d1c0b Mon Sep 17 00:00:00 2001 From: Uwe Date: Sun, 19 Feb 2023 00:49:46 +0100 Subject: [PATCH 4/6] add tests for listbox filter --- tests/integration/test_components.py | 69 +++++++++++++++++++++++++++- tests/integration/test_gui.py | 46 ++++++++++--------- 2 files changed, 92 insertions(+), 23 deletions(-) diff --git a/tests/integration/test_components.py b/tests/integration/test_components.py index d66e969bf..13d6fbac1 100644 --- a/tests/integration/test_components.py +++ b/tests/integration/test_components.py @@ -78,7 +78,11 @@ GdkEventRecorder, ) from inputremapper.gui.components.main import Stack, StatusBar -from inputremapper.gui.components.common import FlowBoxEntry, Breadcrumbs +from inputremapper.gui.components.common import ( + FlowBoxEntry, + Breadcrumbs, + ListFilterControl, +) from inputremapper.gui.components.presets import PresetSelection from inputremapper.gui.components.device_groups import ( DeviceGroupEntry, @@ -88,6 +92,24 @@ from inputremapper.configs.input_config import InputCombination, InputConfig +class GtkKeyEvent: + + KEY_RELEASE = "key-release-event" + KEY_PRESS = "key-press-event" + + def __init__(self, keyval): + self.keyval = keyval + + def get_keyval(self): + return True, self.keyval + + def emit_to(self, target:Gtk.Widget, event_type=KEY_RELEASE): + ev = Gdk.Event() + ev.key.keyval = self.keyval + target.emit(event_type, ev) + gtk_iteration() + + class ComponentBaseTest(unittest.TestCase): """Test a gui component.""" @@ -343,7 +365,7 @@ def test_loads_preset(self): self.controller_mock.load_preset.assert_called_once_with("preset2") -class TestMappingListbox(ComponentBaseTest): +class TestMappingListboxBase(ComponentBaseTest): def setUp(self) -> None: super().setUp() self.gui = Gtk.ListBox() @@ -395,6 +417,11 @@ def select(label_: MappingSelectionLabel): for label in self.gui.get_children(): select(label) + +class TestMappingListbox(TestMappingListboxBase): + def setUp(self) -> None: + super().setUp() + def test_populates_listbox(self): labels = {row.name for row in self.gui.get_children()} self.assertEqual(labels, {"mapping1", "mapping2", "a + b"}) @@ -485,6 +512,41 @@ def test_sorts_empty_mapping_to_bottom(self): self.assertEqual(bottom_row.combination, InputCombination.empty_combination()) +class TestMappingFilterListbox(TestMappingListboxBase): + def setUp(self) -> None: + super().setUp() + self.entry = Gtk.Entry() + self.button = Gtk.Button() + self.control = ListFilterControl( + self.gui, + self.entry, + clear_button=self.button, + ) + + def get_num_visible(self): + return len([c for c in self.gui.get_children() if c.get_visible()]) + + def test_filter_entry(self): + n = len(list(self.gui.get_children())) + + self.assertGreater(n, 2, "some mappings must be loaded") + self.assertEqual(self.get_num_visible(), n, "all mappings must be visible") + + self.entry.set_text("not in preset") + GtkKeyEvent(Gdk.KEY_Escape).emit_to(self.entry) + self.assertEqual(self.entry.get_text(), "not in preset") + self.assertEqual(self.get_num_visible(), 0 , "mappings must not be visible") + + self.button.clicked() + gtk_iteration() + self.assertEqual(self.entry.get_text(), "", "filter must be cleared") + self.assertEqual(self.get_num_visible(), n, "all mappings must be visible again") + + self.entry.set_text("mapping1") + GtkKeyEvent(Gdk.KEY_Escape).emit_to(self.entry) + self.assertEqual(self.get_num_visible(), 1, "only one mapping must be visible") + + class TestMappingSelectionLabel(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1854,3 +1916,6 @@ def test_breadcrumbs(self): ) self.assertEqual(self.label_4.get_text(), "group / preset / qux") self.assertEqual(self.label_5.get_text(), "qux") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py index dafefd6a6..8a7ac1760 100644 --- a/tests/integration/test_gui.py +++ b/tests/integration/test_gui.py @@ -32,7 +32,7 @@ from tests.lib.logger import logger from tests.lib.fixtures import fixtures from tests.lib.pipes import push_event, push_events, uinput_write_history_pipe -from tests.integration.test_components import FlowBoxTestUtils +from tests.integration.test_components import FlowBoxTestUtils, GtkKeyEvent import sys import time @@ -161,14 +161,6 @@ def clean_up_integration(test): atexit.unregister(test.daemon.stop_all) -class GtkKeyEvent: - def __init__(self, keyval): - self.keyval = keyval - - def get_keyval(self): - return True, self.keyval - - class TestGroupsFromReaderService(unittest.TestCase): def setUp(self): # don't try to connect, return an object instance of it instead @@ -291,8 +283,8 @@ def setUp(self): self.stop_injector_btn: Gtk.Button = get("stop_injection_preset_page") self.rename_btn: Gtk.Button = get("rename-button") self.rename_input: Gtk.Entry = get("preset_name_input") - self.search_btn: Gtk.Button = get("mapping-search-clear-button") - self.search_input: Gtk.Entry = get("mapping-search-input") + self.mapping_filter_btn: Gtk.Button = get("mapping-filter-clear-button") + self.mapping_filter_input: Gtk.Entry = get("mapping-filter-input") self.create_mapping_btn: Gtk.Button = get("create_mapping_button") self.delete_mapping_btn: Gtk.Button = get("delete-mapping") @@ -1417,17 +1409,29 @@ def save(): gtk_iteration() self.assertFalse(os.path.exists(preset_path)) - def test_search_input(self): - self.search_input.set_text("foo") - self.set_focus(self.search_input) - # TODO: press a key and check filter value in data manager - gtk_iteration() - # self.assertEquals(self.controller.data_manager.active_filter, "foo") + def test_filtering_mappings(self): + self.controller.load_preset("preset2") + self.throttle(20) - self.search_btn.clicked() - gtk_iteration() - self.assertEquals(self.search_input.get_text(), "") - self.assertEquals(self.controller.data_manager.active_filter, "") + mappings = list(self.data_manager.get_mappings()) + self.assertGreaterEqual(len(mappings), 2) + + self.assertGreater(len(mappings), 0, "at least one mapping must be loaded") + num_rows = len(self.selection_label_listbox.get_children()) + self.assertEqual(len(mappings), num_rows, "all mappimgs must be in the listbox") + + text0 = mappings[0].format_name() + self.mapping_filter_input.set_text(text0) + GtkKeyEvent(Gdk.KEY_Escape).emit_to(self.mapping_filter_input) + self.assertEqual(self.data_manager.active_mapping.format_name(), text0) + + text1 = mappings[1].format_name() + self.mapping_filter_input.set_text(text1) + GtkKeyEvent(Gdk.KEY_Escape).emit_to(self.mapping_filter_input) + self.assertEqual(self.data_manager.active_mapping.format_name(), text1) + + self.mapping_filter_btn.clicked() + self.assertEqual(self.mapping_filter_input.get_text(), "") def test_check_for_unknown_symbols(self): status = self.user_interface.get("status_bar") From b7820241b6c21c05d27abce2b6a763a3b8f872c8 Mon Sep 17 00:00:00 2001 From: Uwe Date: Sun, 19 Feb 2023 01:05:06 +0100 Subject: [PATCH 5/6] cleanup and black format --- inputremapper/daemon.py | 1 + inputremapper/gui/components/common.py | 20 ++++++++----------- inputremapper/gui/components/editor.py | 1 - inputremapper/gui/messages/message_types.py | 1 - inputremapper/gui/reader_client.py | 6 +----- inputremapper/gui/reader_service.py | 2 +- inputremapper/gui/user_interface.py | 2 +- .../mapping_handlers/abs_to_abs_handler.py | 1 - .../mapping_handlers/axis_switch_handler.py | 1 - .../mapping_handlers/rel_to_btn_handler.py | 1 - tests/integration/test_components.py | 10 ++++++---- tests/unit/test_control.py | 1 + tests/unit/test_macros.py | 14 ++++++------- 13 files changed, 26 insertions(+), 35 deletions(-) diff --git a/inputremapper/daemon.py b/inputremapper/daemon.py index 653395db9..373920460 100644 --- a/inputremapper/daemon.py +++ b/inputremapper/daemon.py @@ -106,6 +106,7 @@ def may_autoload(self, group_key: str, preset: str): def remove_timeout(func): """Remove timeout to ensure the call works if the daemon is not a proxy.""" + # the timeout kwarg is a feature of pydbus. This is needed to make tests work # that create a Daemon by calling its constructor instead of using pydbus. def wrapped(*args, **kwargs): diff --git a/inputremapper/gui/components/common.py b/inputremapper/gui/components/common.py index ed24af573..9d6503a99 100644 --- a/inputremapper/gui/components/common.py +++ b/inputremapper/gui/components/common.py @@ -210,14 +210,14 @@ def __init__( self._filter_entry: Gtk.Entry = filter_entry self._clear_button: Gtk.Button = clear_button - self._filter_value:str = "" - self._case_sensitive:bool = bool(case_sensitive) + self._filter_value: str = "" + self._case_sensitive: bool = bool(case_sensitive) self._get_row_name = get_row_name or self.get_row_name self._connect_gtk_signals() @classmethod - def get_row_name(T, row:Gtk.ListBoxRow) -> str: + def get_row_name(T, row: Gtk.ListBoxRow) -> str: """ Returns the visible text of a Gtk.ListBoxRow from both the row's `name` attribute or the row's text in the UI. @@ -230,7 +230,7 @@ def get_row_name(T, row:Gtk.ListBoxRow) -> str: return text.strip() @classmethod - def get_widget_tree_text(T, widget:Gtk.Widget, level=0) -> Iterator[str]: + def get_widget_tree_text(T, widget: Gtk.Widget, level=0) -> Iterator[str]: """ Recursively traverses the tree of child widgets starting from the given widget, and yields the text of all text-containing widgets. @@ -244,23 +244,19 @@ def get_widget_tree_text(T, widget:Gtk.Widget, level=0) -> Iterator[str]: yield (widget.get_text() or "").strip() if isinstance(widget, Gtk.Container): for t in widget.get_children(): - yield from T.get_widget_tree_text(t, level=level+1) + yield from T.get_widget_tree_text(t, level=level + 1) def _connect_gtk_signals(self): if self._clear_button: - self._clear_button.connect("clicked", - self.on_gtk_clear_button_clicked - ) - self._filter_entry.connect("key-release-event", - self.on_gtk_filter_entry_input - ) + self._clear_button.connect("clicked", self.on_gtk_clear_button_clicked) + self._filter_entry.connect("key-release-event", self.on_gtk_filter_entry_input) # apply defined filter by sending out the corresponding events def apply_filter(self): self._apply_filter_to_listbox_children() # matches the current filter_value and filter_options with the given value - def match_filter(self, value:str): + def match_filter(self, value: str): value = (value or "").strip() # if filter is not set, all rows need to match diff --git a/inputremapper/gui/components/editor.py b/inputremapper/gui/components/editor.py index add7bb188..dc0857a7f 100644 --- a/inputremapper/gui/components/editor.py +++ b/inputremapper/gui/components/editor.py @@ -950,7 +950,6 @@ def _set_model(self, target: str): self.model.clear() self.model.append(["None, None", _("No Axis")]) for type_, code in types_codes: - key_name = bytype[type_][code] if isinstance(key_name, list): key_name = key_name[0] diff --git a/inputremapper/gui/messages/message_types.py b/inputremapper/gui/messages/message_types.py index 9ecda3404..a9619fb62 100644 --- a/inputremapper/gui/messages/message_types.py +++ b/inputremapper/gui/messages/message_types.py @@ -41,7 +41,6 @@ class MessageType(Enum): mapping = "mapping" selected_event = "selected_event" combination_recorded = "combination_recorded" - filter_changed = "filter_changed" # only the reader_client should send those messages: recording_started = "recording_started" diff --git a/inputremapper/gui/reader_client.py b/inputremapper/gui/reader_client.py index 8f9d9e525..f29c0b440 100644 --- a/inputremapper/gui/reader_client.py +++ b/inputremapper/gui/reader_client.py @@ -23,7 +23,6 @@ see gui.reader_service.ReaderService """ -import os import time from typing import Optional, List, Generator, Dict, Tuple, Set @@ -74,10 +73,7 @@ class ReaderClient: _timeout: int = 5 def __init__( - self, - message_broker: MessageBroker, - groups: _Groups, - ignore_pkexec_errors=False + self, message_broker: MessageBroker, groups: _Groups, ignore_pkexec_errors=False ): self.groups = groups self.message_broker = message_broker diff --git a/inputremapper/gui/reader_service.py b/inputremapper/gui/reader_service.py index 31168bfe0..2282247ef 100644 --- a/inputremapper/gui/reader_service.py +++ b/inputremapper/gui/reader_service.py @@ -149,7 +149,7 @@ def pkexec_reader_service(ingore_errors=False): if ingore_errors: logger.warn(ex) else: - raise ex + raise ex def run(self): """Start doing stuff. Blocks.""" diff --git a/inputremapper/gui/user_interface.py b/inputremapper/gui/user_interface.py index 269a60e69..91591e27a 100644 --- a/inputremapper/gui/user_interface.py +++ b/inputremapper/gui/user_interface.py @@ -375,7 +375,7 @@ def connect_shortcuts(self): "key-press-event", self.on_gtk_shortcut ) - def get(self, name: str): + def get(self, name: str) -> Gtk.Widget: """Get a widget from the window.""" return self.builder.get_object(name) diff --git a/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py b/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py index d932168f3..efdf45201 100644 --- a/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py +++ b/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py @@ -90,7 +90,6 @@ def notify( forward: evdev.UInput = None, suppress: bool = False, ) -> bool: - if event.input_match_hash != self._map_axis.input_match_hash: return False diff --git a/inputremapper/injection/mapping_handlers/axis_switch_handler.py b/inputremapper/injection/mapping_handlers/axis_switch_handler.py index ac032e12c..b06617560 100644 --- a/inputremapper/injection/mapping_handlers/axis_switch_handler.py +++ b/inputremapper/injection/mapping_handlers/axis_switch_handler.py @@ -142,7 +142,6 @@ def notify( forward: evdev.UInput, suppress: bool = False, ) -> bool: - if not self._should_map(event): return False diff --git a/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py b/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py index 4f66a5ce4..b86cc2b05 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py @@ -102,7 +102,6 @@ def notify( forward: evdev.UInput, suppress: bool = False, ) -> bool: - assert event.type == EV_REL if event.input_match_hash != self._input_config.input_match_hash: return False diff --git a/tests/integration/test_components.py b/tests/integration/test_components.py index 13d6fbac1..f8606e606 100644 --- a/tests/integration/test_components.py +++ b/tests/integration/test_components.py @@ -93,7 +93,6 @@ class GtkKeyEvent: - KEY_RELEASE = "key-release-event" KEY_PRESS = "key-press-event" @@ -103,7 +102,7 @@ def __init__(self, keyval): def get_keyval(self): return True, self.keyval - def emit_to(self, target:Gtk.Widget, event_type=KEY_RELEASE): + def emit_to(self, target: Gtk.Widget, event_type=KEY_RELEASE): ev = Gdk.Event() ev.key.keyval = self.keyval target.emit(event_type, ev) @@ -535,12 +534,14 @@ def test_filter_entry(self): self.entry.set_text("not in preset") GtkKeyEvent(Gdk.KEY_Escape).emit_to(self.entry) self.assertEqual(self.entry.get_text(), "not in preset") - self.assertEqual(self.get_num_visible(), 0 , "mappings must not be visible") + self.assertEqual(self.get_num_visible(), 0, "mappings must not be visible") self.button.clicked() gtk_iteration() self.assertEqual(self.entry.get_text(), "", "filter must be cleared") - self.assertEqual(self.get_num_visible(), n, "all mappings must be visible again") + self.assertEqual( + self.get_num_visible(), n, "all mappings must be visible again" + ) self.entry.set_text("mapping1") GtkKeyEvent(Gdk.KEY_Escape).emit_to(self.entry) @@ -1917,5 +1918,6 @@ def test_breadcrumbs(self): self.assertEqual(self.label_4.get_text(), "group / preset / qux") self.assertEqual(self.label_5.get_text(), "qux") + if __name__ == "__main__": unittest.main() diff --git a/tests/unit/test_control.py b/tests/unit/test_control.py index db7cde9fb..6a45ea3d6 100644 --- a/tests/unit/test_control.py +++ b/tests/unit/test_control.py @@ -87,6 +87,7 @@ def test_autoload(self): start_history = [] stop_counter = 0 + # using an actual injector is not within the scope of this test class Injector: def stop_injecting(self, *args, **kwargs): diff --git a/tests/unit/test_macros.py b/tests/unit/test_macros.py index 965d0406d..003bade58 100644 --- a/tests/unit/test_macros.py +++ b/tests/unit/test_macros.py @@ -722,17 +722,17 @@ async def test_just_hold(self): macro.press_trigger() asyncio.ensure_future(macro.run(self.handler)) - await (asyncio.sleep(0.1)) + await asyncio.sleep(0.1) self.assertTrue(macro.is_holding()) self.assertEqual(len(self.result), 2) - await (asyncio.sleep(0.1)) + await asyncio.sleep(0.1) # doesn't do fancy stuff, is blocking until the release self.assertEqual(len(self.result), 2) """up""" macro.release_trigger() - await (asyncio.sleep(0.05)) + await asyncio.sleep(0.05) self.assertFalse(macro.is_holding()) self.assertEqual(len(self.result), 4) @@ -745,7 +745,7 @@ async def test_dont_just_hold(self): macro = parse("key(1).hold().key(3)", self.context, DummyMapping) asyncio.ensure_future(macro.run(self.handler)) - await (asyncio.sleep(0.1)) + await asyncio.sleep(0.1) self.assertFalse(macro.is_holding()) # since press_trigger was never called it just does the macro # completely @@ -764,7 +764,7 @@ async def test_hold_down(self): """down""" macro.press_trigger() - await (asyncio.sleep(0.05)) + await asyncio.sleep(0.05) self.assertTrue(macro.is_holding()) asyncio.ensure_future(macro.run(self.handler)) @@ -777,7 +777,7 @@ async def test_hold_down(self): """up""" macro.release_trigger() - await (asyncio.sleep(0.05)) + await asyncio.sleep(0.05) self.assertFalse(macro.is_holding()) self.assertEqual(len(self.result), 2) @@ -969,7 +969,7 @@ async def test_mouse(self): asyncio.ensure_future(macro_2.run(self.handler)) sleep = 0.1 - await (asyncio.sleep(sleep)) + await asyncio.sleep(sleep) self.assertTrue(macro_1.is_holding()) self.assertTrue(macro_2.is_holding()) macro_1.release_trigger() From 6be27002f01e0f7eaf48a8b9e22189518f3dbefd Mon Sep 17 00:00:00 2001 From: Uwe Date: Sun, 19 Feb 2023 01:37:58 +0100 Subject: [PATCH 6/6] remove debug log --- inputremapper/gui/components/common.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/inputremapper/gui/components/common.py b/inputremapper/gui/components/common.py index 9d6503a99..320b2ea75 100644 --- a/inputremapper/gui/components/common.py +++ b/inputremapper/gui/components/common.py @@ -263,8 +263,6 @@ def match_filter(self, value: str): if self._filter_value == "": return True - print(f"matching filter: {self._filter_value} with value: {value}") - if self._case_sensitive: return self._filter_value in value else: