From 07065cb2f0b9e10342f2b0819da9f3d8f0470074 Mon Sep 17 00:00:00 2001 From: Uwe Date: Fri, 17 Feb 2023 01:52:15 +0100 Subject: [PATCH 01/20] 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 02/20] 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 03/20] 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 04/20] 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 05/20] 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 06/20] 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: From 5460f0cf5f81d5e7ad05e8a66401e09b7581cf57 Mon Sep 17 00:00:00 2001 From: Uwe Date: Tue, 21 Feb 2023 01:12:36 +0100 Subject: [PATCH 07/20] add Gtk.ListBox filter --- .../gui/components/gtkext/__init__.py | 5 + .../gui/components/gtkext/listbox_filter.py | 130 ++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 inputremapper/gui/components/gtkext/__init__.py create mode 100644 inputremapper/gui/components/gtkext/listbox_filter.py diff --git a/inputremapper/gui/components/gtkext/__init__.py b/inputremapper/gui/components/gtkext/__init__.py new file mode 100644 index 000000000..eb5a51112 --- /dev/null +++ b/inputremapper/gui/components/gtkext/__init__.py @@ -0,0 +1,5 @@ +""" +Module with general Gtk enhancements + +All code in this module is indendent from any inputremapper code. +""" diff --git a/inputremapper/gui/components/gtkext/listbox_filter.py b/inputremapper/gui/components/gtkext/listbox_filter.py new file mode 100644 index 000000000..a55e93255 --- /dev/null +++ b/inputremapper/gui/components/gtkext/listbox_filter.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +# input-remapper - GUI for device specific keyboard mappings +# Copyright (C) 2023 sezanzeb +# +# This file is part of input-remapper. +# +# input-remapper is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# input-remapper is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with input-remapper. If not, see . + +import gi + +from gi.repository import Gtk + +from typing import Iterator + + +class ListBoxFilter: + """Implements UI-side filtering of list widgets. + + The following example creates a new ``ListBoxFilter`` for a given ``Gtk.ListBox``. + It also sets all optional arguments to override some default behavior. + + >>> filter = ListBoxFilter( + >>> my_listbox, # Gtk.ListBox to be managed + >>> get_row_name=MyRow.get_name # custom row name getter + >>> ) + + To apply a filter use `set_filter` as follows. + + >>> filter.set_filter("some text") + >>> filter.set_filter("More Text", case_sensitive=True) + + """ + + MAX_WIDGET_TREE_TEXT_SEARCH_DEPTH = 10 + + def __init__( + self, + listbox: Gtk.ListBox, + get_row_name=None, + filter_value="", + case_sensitive=False, + ): + self._controlled_listbox: Gtk.ListBox = listbox + self._get_row_name = get_row_name or self.get_row_name + self._filter_value : str = filter_value + self._case_sensitive = case_sensitive + + @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) + + @property + def filter_value(self): + return self._filter_value + + @property + def case_sensitive(self): + return self._case_sensitive + + # 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 + + if self._case_sensitive: + return self._filter_value in value + else: + return self._filter_value.lower() in value.lower() + + # set and apply filter + def set_filter(self, filter_value: str, case_sensitive=False): + self._filter_value = str(filter_value) + self._case_sensitive = bool(case_sensitive) + self._gtk_apply_filter_to_listbox_children() + + # apply filter to widget tree + def _gtk_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() From 5abf431860d2be779e814e8543b06007ff139bfd Mon Sep 17 00:00:00 2001 From: Uwe Date: Tue, 21 Feb 2023 01:13:53 +0100 Subject: [PATCH 08/20] split mapping filter in list control and entry control --- data/input-remapper.glade | 38 ++++-- inputremapper/gui/components/common.py | 135 +++++++------------- inputremapper/gui/components/editor.py | 11 ++ inputremapper/gui/messages/message_data.py | 15 +++ inputremapper/gui/messages/message_types.py | 1 + inputremapper/gui/user_interface.py | 12 +- tests/integration/test_components.py | 22 +++- 7 files changed, 125 insertions(+), 109 deletions(-) diff --git a/data/input-remapper.glade b/data/input-remapper.glade index 1bac357a6..edd9f6d8a 100644 --- a/data/input-remapper.glade +++ b/data/input-remapper.glade @@ -18,7 +18,7 @@ True False - sweeper + edit-clear-symbolic True @@ -657,32 +657,54 @@ Shortcut: ctrl + del True False - 6 + 3 True True + center + True filter mappings... - True - True + False + False 0 + + + Aa + True + True + False + True + Toggle case sensitive + end + center + + + False + False + 1 + + True True + False True - Clear search filter - start + Clear mapping filter + end + center clear-icon + True False - True - 1 + False + 2 diff --git a/inputremapper/gui/components/common.py b/inputremapper/gui/components/common.py index 320b2ea75..4c9884bdf 100644 --- a/inputremapper/gui/components/common.py +++ b/inputremapper/gui/components/common.py @@ -39,7 +39,11 @@ MessageBroker, MessageType, ) -from inputremapper.gui.messages.message_data import GroupData, PresetData +from inputremapper.gui.messages.message_data import ( + GroupData, + PresetData, + MappingFilter, +) from inputremapper.gui.utils import HandlerDisabled @@ -178,116 +182,69 @@ def _render(self): self._gui.set_label(" / ".join(label)) -class ListFilterControl: - """Implements UI-side filtering of list widgets. +class FilterControl: + """Watches a text input to produce filter events. - 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. + The following example creates a new ``FilterControl`` for a given ``Gtk.Entry`` + for text input. It also sets all optional arguments to override some default behavior. >>> ListFilterControl( - >>> my_gtk_listbox, + >>> message_broker, + >>> message_type, >>> 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 + >>> clear_button=my_gtk_button1, # use an optional clear button + >>> case_toggle=my_gtk_button2, # use optional case sensitivity switch + >>> ) """ - MAX_WIDGET_TREE_TEXT_SEARCH_DEPTH = 10 - def __init__( self, - # message_broker: MessageBroker, - controlled_listbox: Gtk.ListBox, + message_broker: MessageBroker, + message_type: MessageType, filter_entry: Gtk.GtkEntry, clear_button: Gtk.Button = None, - case_sensitive=False, - get_row_name=None, + case_toggle: Gtk.ToggleButton = None, ): - self._controlled_listbox: Gtk.ListBox = controlled_listbox + self._message_broker: MessageBroker = message_broker + self._message_type: MessageType = message_type self._filter_entry: Gtk.Entry = filter_entry self._clear_button: Gtk.Button = clear_button + self._case_toggle: Gtk.ToggleButton = case_toggle self._filter_value: str = "" - self._case_sensitive: bool = bool(case_sensitive) - self._get_row_name = get_row_name or self.get_row_name + self._case_sensitive = case_toggle is None or case_toggle.get_active() 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) + self._update() + + def _update(self, force=False) -> bool: + old_value = self._filter_value + self._filter_value = (self._filter_entry.get_text() or "").strip() + if force or self._filter_value != old_value: + self._message_broker.publish( + MappingFilter( + filter_value=self._filter_value, + case_sensitive=self._case_sensitive, + ) + ) def _connect_gtk_signals(self): + self._filter_entry.connect("changed", self._on_gtk_input_changed) 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 - - 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() + self._clear_button.connect("clicked", self._on_gtk_clear_button_clicked) + if self._case_toggle: + self._case_toggle.connect("toggled", self._on_gtk_case_button_toggled) - def on_gtk_clear_button_clicked(self, *_): + def _on_gtk_case_button_toggled(self, btn: Gtk.ToggleButton): + self._case_sensitive = btn.get_active() + if self._filter_value != "": + self._update(force=True) + + def _on_gtk_clear_button_clicked(self, *_): self._filter_entry.set_text("") - self._filter_value = "" - self.apply_filter() + + def _on_gtk_input_changed(self, *_): + self._update() diff --git a/inputremapper/gui/components/editor.py b/inputremapper/gui/components/editor.py index 15bba0047..3234f4355 100644 --- a/inputremapper/gui/components/editor.py +++ b/inputremapper/gui/components/editor.py @@ -54,12 +54,14 @@ UInputsData, PresetData, CombinationUpdate, + MappingFilter, ) from inputremapper.gui.utils import HandlerDisabled, Colors from inputremapper.injection.mapping_handlers.axis_transform import Transformation from inputremapper.input_event import InputEvent from inputremapper.configs.system_mapping import system_mapping, XKB_KEYCODE_OFFSET from inputremapper.utils import get_evdev_constant_name +from inputremapper.gui.components.gtkext.listbox_filter import ListBoxFilter Capabilities = Dict[int, List] @@ -148,9 +150,13 @@ def __init__( self._controller = controller self._gui = listbox self._gui.set_sort_func(self._sort_func) + self._mapping_filter = ListBoxFilter(listbox) self._message_broker.subscribe(MessageType.preset, self._on_preset_changed) self._message_broker.subscribe(MessageType.mapping, self._on_mapping_changed) + self._message_broker.subscribe( + MessageType.mapping_filter, self._on_mapping_filter_changed + ) self._gui.connect("row-selected", self._on_gtk_mapping_selected) @staticmethod @@ -190,6 +196,11 @@ def _on_mapping_changed(self, mapping: MappingData): if row.combination == combination: self._gui.select_row(row) + def _on_mapping_filter_changed(self, filter: MappingFilter): + self._mapping_filter.set_filter( + filter.filter_value, case_sensitive=filter.case_sensitive + ) + def _on_gtk_mapping_selected(self, _, row: Optional[MappingSelectionLabel]): if not row: return diff --git a/inputremapper/gui/messages/message_data.py b/inputremapper/gui/messages/message_data.py index f24a3b438..b23fb0402 100644 --- a/inputremapper/gui/messages/message_data.py +++ b/inputremapper/gui/messages/message_data.py @@ -124,3 +124,18 @@ class DoStackSwitch: message_type = MessageType.do_stack_switch page_index: int + + +@dataclass(frozen=True) +class FilterData: + """Stores filter data for any kind of text-based filter""" + + filter_value: str + case_sensitive: bool = False + + +@dataclass(frozen=True) +class MappingFilter(FilterData): + """Message sent by the mapping list filter.""" + + message_type = MessageType.mapping_filter diff --git a/inputremapper/gui/messages/message_types.py b/inputremapper/gui/messages/message_types.py index a9619fb62..5eb53108f 100644 --- a/inputremapper/gui/messages/message_types.py +++ b/inputremapper/gui/messages/message_types.py @@ -40,6 +40,7 @@ class MessageType(Enum): preset = "preset" mapping = "mapping" selected_event = "selected_event" + mapping_filter = "mapping_filter" combination_recorded = "combination_recorded" # only the reader_client should send those messages: diff --git a/inputremapper/gui/user_interface.py b/inputremapper/gui/user_interface.py index 91591e27a..776f07a63 100644 --- a/inputremapper/gui/user_interface.py +++ b/inputremapper/gui/user_interface.py @@ -51,10 +51,8 @@ ) from inputremapper.gui.components.presets import PresetSelection from inputremapper.gui.components.main import Stack, StatusBar -from inputremapper.gui.components.common import ( - Breadcrumbs, - ListFilterControl, -) +from inputremapper.gui.components.common import Breadcrumbs, FilterControl +from inputremapper.gui.components.gtkext.listbox_filter import ListBoxFilter from inputremapper.gui.components.device_groups import DeviceGroupSelection from inputremapper.gui.controller import Controller from inputremapper.gui.messages.message_broker import ( @@ -152,10 +150,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"), + FilterControl( + message_broker, + MessageType.mapping_filter, self.get("mapping-filter-input"), clear_button=self.get("mapping-filter-clear-button"), + case_toggle=self.get("mapping-filter-case-button"), ) Breadcrumbs( diff --git a/tests/integration/test_components.py b/tests/integration/test_components.py index 72a7b2369..440e07587 100644 --- a/tests/integration/test_components.py +++ b/tests/integration/test_components.py @@ -81,8 +81,9 @@ from inputremapper.gui.components.common import ( FlowBoxEntry, Breadcrumbs, - ListFilterControl, + FilterControl, ) +from inputremapper.gui.components.gtkext.listbox_filter import ListBoxFilter from inputremapper.gui.components.presets import PresetSelection from inputremapper.gui.components.device_groups import ( DeviceGroupEntry, @@ -522,10 +523,14 @@ def setUp(self) -> None: super().setUp() self.entry = Gtk.Entry() self.button = Gtk.Button() - self.control = ListFilterControl( - self.gui, + self.toggle = Gtk.ToggleButton() + self.filter = ListBoxFilter(self.gui) + self.control = FilterControl( + self.message_broker, + MessageType.mapping_filter, self.entry, clear_button=self.button, + case_toggle=self.toggle, ) def get_num_visible(self): @@ -538,7 +543,7 @@ def test_filter_entry(self): 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) + gtk_iteration() self.assertEqual(self.entry.get_text(), "not in preset") self.assertEqual(self.get_num_visible(), 0, "mappings must not be visible") @@ -549,10 +554,15 @@ def test_filter_entry(self): self.get_num_visible(), n, "all mappings must be visible again" ) - self.entry.set_text("mapping1") - GtkKeyEvent(Gdk.KEY_Escape).emit_to(self.entry) + def test_case_toggle(self): + self.entry.set_text("Mapping1") + gtk_iteration() self.assertEqual(self.get_num_visible(), 1, "only one mapping must be visible") + self.toggle.clicked() + gtk_iteration() + self.assertEqual(self.get_num_visible(), 0, "no mapping must be visible") + class TestMappingSelectionLabel(ComponentBaseTest): def setUp(self) -> None: From b2b0c4687f3d2dac16c06cde469afdbf07e5b10e Mon Sep 17 00:00:00 2001 From: Uwe Date: Tue, 21 Feb 2023 01:26:08 +0100 Subject: [PATCH 09/20] override all optional args, fix format --- inputremapper/gui/components/common.py | 5 ++--- inputremapper/gui/components/gtkext/listbox_filter.py | 7 +++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/inputremapper/gui/components/common.py b/inputremapper/gui/components/common.py index 4c9884bdf..4cb246125 100644 --- a/inputremapper/gui/components/common.py +++ b/inputremapper/gui/components/common.py @@ -192,9 +192,8 @@ class FilterControl: >>> message_broker, >>> message_type, >>> my_gtk_entry, - >>> clear_button=my_gtk_button1, # use an optional clear button - >>> case_toggle=my_gtk_button2, # use optional case sensitivity switch - + >>> clear_button=my_gtk_button, # use an optional clear button + >>> case_toggle=my_gtk_toggle, # use optional case sensitivity switch >>> ) """ diff --git a/inputremapper/gui/components/gtkext/listbox_filter.py b/inputremapper/gui/components/gtkext/listbox_filter.py index a55e93255..580bb740e 100644 --- a/inputremapper/gui/components/gtkext/listbox_filter.py +++ b/inputremapper/gui/components/gtkext/listbox_filter.py @@ -33,6 +33,8 @@ class ListBoxFilter: >>> filter = ListBoxFilter( >>> my_listbox, # Gtk.ListBox to be managed >>> get_row_name=MyRow.get_name # custom row name getter + >>> filter_value="text" # inital value + >>> case_sensitive=True, # override default:False >>> ) To apply a filter use `set_filter` as follows. @@ -53,8 +55,9 @@ def __init__( ): self._controlled_listbox: Gtk.ListBox = listbox self._get_row_name = get_row_name or self.get_row_name - self._filter_value : str = filter_value - self._case_sensitive = case_sensitive + self._filter_value: str = "" + self._case_sensitive = False + self.set_filter(filter_value, case_sensitive=case_sensitive) @classmethod def get_row_name(T, row: Gtk.ListBoxRow) -> str: From eee1752a8cd09fe523fa87249dfcace0aa0370f5 Mon Sep 17 00:00:00 2001 From: Uwe Date: Tue, 21 Feb 2023 01:31:59 +0100 Subject: [PATCH 10/20] remove key event test --- tests/integration/test_gui.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py index 11a1d098f..16fadc47b 100644 --- a/tests/integration/test_gui.py +++ b/tests/integration/test_gui.py @@ -34,7 +34,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, GtkKeyEvent +from tests.integration.test_components import FlowBoxTestUtils import sys import time @@ -1454,12 +1454,12 @@ def test_filtering_mappings(self): text0 = mappings[0].format_name() self.mapping_filter_input.set_text(text0) - GtkKeyEvent(Gdk.KEY_Escape).emit_to(self.mapping_filter_input) + gtk_iteration() 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) + gtk_iteration() self.assertEqual(self.data_manager.active_mapping.format_name(), text1) self.mapping_filter_btn.clicked() From d06dd8d800d46b4eb1516f84a5f0d9dbbda265b4 Mon Sep 17 00:00:00 2001 From: Uwe Date: Tue, 21 Feb 2023 15:04:27 +0100 Subject: [PATCH 11/20] use GtkSearchEntry, rm clear button --- data/input-remapper.glade | 26 ++------------------------ inputremapper/gui/components/common.py | 8 -------- inputremapper/gui/user_interface.py | 2 -- tests/integration/test_components.py | 4 +--- tests/integration/test_gui.py | 15 +++++++++++---- 5 files changed, 14 insertions(+), 41 deletions(-) diff --git a/data/input-remapper.glade b/data/input-remapper.glade index edd9f6d8a..5ac6d61f3 100644 --- a/data/input-remapper.glade +++ b/data/input-remapper.glade @@ -15,11 +15,6 @@ 2 media-playback-start - - True - False - edit-clear-symbolic - True False @@ -659,11 +654,12 @@ Shortcut: ctrl + del False 3 - + True True center True + True filter mappings... @@ -689,24 +685,6 @@ Shortcut: ctrl + del 1 - - - True - True - False - True - Clear mapping filter - end - center - clear-icon - True - - - False - False - 2 - - True diff --git a/inputremapper/gui/components/common.py b/inputremapper/gui/components/common.py index 4cb246125..40ffa20fa 100644 --- a/inputremapper/gui/components/common.py +++ b/inputremapper/gui/components/common.py @@ -192,7 +192,6 @@ class FilterControl: >>> message_broker, >>> message_type, >>> my_gtk_entry, - >>> clear_button=my_gtk_button, # use an optional clear button >>> case_toggle=my_gtk_toggle, # use optional case sensitivity switch >>> ) @@ -203,13 +202,11 @@ def __init__( message_broker: MessageBroker, message_type: MessageType, filter_entry: Gtk.GtkEntry, - clear_button: Gtk.Button = None, case_toggle: Gtk.ToggleButton = None, ): self._message_broker: MessageBroker = message_broker self._message_type: MessageType = message_type self._filter_entry: Gtk.Entry = filter_entry - self._clear_button: Gtk.Button = clear_button self._case_toggle: Gtk.ToggleButton = case_toggle self._filter_value: str = "" @@ -232,8 +229,6 @@ def _update(self, force=False) -> bool: def _connect_gtk_signals(self): self._filter_entry.connect("changed", self._on_gtk_input_changed) - if self._clear_button: - self._clear_button.connect("clicked", self._on_gtk_clear_button_clicked) if self._case_toggle: self._case_toggle.connect("toggled", self._on_gtk_case_button_toggled) @@ -242,8 +237,5 @@ def _on_gtk_case_button_toggled(self, btn: Gtk.ToggleButton): if self._filter_value != "": self._update(force=True) - def _on_gtk_clear_button_clicked(self, *_): - self._filter_entry.set_text("") - def _on_gtk_input_changed(self, *_): self._update() diff --git a/inputremapper/gui/user_interface.py b/inputremapper/gui/user_interface.py index 776f07a63..9f144d9d0 100644 --- a/inputremapper/gui/user_interface.py +++ b/inputremapper/gui/user_interface.py @@ -52,7 +52,6 @@ from inputremapper.gui.components.presets import PresetSelection from inputremapper.gui.components.main import Stack, StatusBar from inputremapper.gui.components.common import Breadcrumbs, FilterControl -from inputremapper.gui.components.gtkext.listbox_filter import ListBoxFilter from inputremapper.gui.components.device_groups import DeviceGroupSelection from inputremapper.gui.controller import Controller from inputremapper.gui.messages.message_broker import ( @@ -154,7 +153,6 @@ def _create_components(self): message_broker, MessageType.mapping_filter, self.get("mapping-filter-input"), - clear_button=self.get("mapping-filter-clear-button"), case_toggle=self.get("mapping-filter-case-button"), ) diff --git a/tests/integration/test_components.py b/tests/integration/test_components.py index 440e07587..ce4ce36b7 100644 --- a/tests/integration/test_components.py +++ b/tests/integration/test_components.py @@ -522,14 +522,12 @@ class TestMappingFilterListbox(TestMappingListboxBase): def setUp(self) -> None: super().setUp() self.entry = Gtk.Entry() - self.button = Gtk.Button() self.toggle = Gtk.ToggleButton() self.filter = ListBoxFilter(self.gui) self.control = FilterControl( self.message_broker, MessageType.mapping_filter, self.entry, - clear_button=self.button, case_toggle=self.toggle, ) @@ -547,7 +545,7 @@ def test_filter_entry(self): self.assertEqual(self.entry.get_text(), "not in preset") self.assertEqual(self.get_num_visible(), 0, "mappings must not be visible") - self.button.clicked() + self.entry.set_text("") gtk_iteration() self.assertEqual(self.entry.get_text(), "", "filter must be cleared") self.assertEqual( diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py index 16fadc47b..cbdaeca7c 100644 --- a/tests/integration/test_gui.py +++ b/tests/integration/test_gui.py @@ -78,6 +78,7 @@ from inputremapper.gui.messages.message_data import StatusData, CombinationRecorded from inputremapper.gui.components.editor import MappingSelectionLabel, SET_KEY_FIRST from inputremapper.gui.components.device_groups import DeviceGroupEntry +from inputremapper.gui.components.gtkext.listbox_filter import ListBoxFilter from inputremapper.gui.controller import Controller from inputremapper.gui.reader_service import ReaderService from inputremapper.gui.utils import gtk_iteration, Colors, debounce, debounce_manager @@ -296,7 +297,7 @@ 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.mapping_filter_btn: Gtk.Button = get("mapping-filter-clear-button") + self.mapping_filter_case_btn: Gtk.Button = get("mapping-filter-case-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") @@ -1441,6 +1442,15 @@ def save(): gtk_iteration() self.assertFalse(os.path.exists(preset_path)) + def test_filter_case_button(self): + self.mapping_filter_case_btn.clicked() + gtk_iteration() + self.assertEqual(self.mapping_filter_case_btn.get_active(), True) + + self.mapping_filter_case_btn.clicked() + gtk_iteration() + self.assertEqual(self.mapping_filter_case_btn.get_active(), False) + def test_filtering_mappings(self): self.controller.load_preset("preset2") self.throttle(20) @@ -1462,9 +1472,6 @@ def test_filtering_mappings(self): gtk_iteration() 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") error_icon = self.user_interface.get("error_status_icon") From ecc7930333903350a299497db5d26b8544f86637 Mon Sep 17 00:00:00 2001 From: sezanzeb Date: Tue, 21 Feb 2023 21:23:26 +0100 Subject: [PATCH 12/20] moving the search to a second row --- data/input-remapper.glade | 146 +++++++++++++++++++++----------------- 1 file changed, 80 insertions(+), 66 deletions(-) diff --git a/data/input-remapper.glade b/data/input-remapper.glade index 5ac6d61f3..8803e5b5a 100644 --- a/data/input-remapper.glade +++ b/data/input-remapper.glade @@ -431,7 +431,6 @@ Shortcut: ctrl + del True False - 6 True @@ -459,6 +458,9 @@ Shortcut: ctrl + del 1 + 1 @@ -502,7 +504,6 @@ Shortcut: ctrl + del False 18 vertical - 6 True @@ -577,70 +578,83 @@ Shortcut: ctrl + del - 463 True False - center - 6 - 6 + 18 + 18 + 18 + vertical 6 - - Add - True - True - True - image3 - True - - - False - False - 0 - - - - - Record - True - True - True - Record a button of your device that should be remapped - image2 - True - - - False - False - 1 - - - - - Advanced - True - True - True - image1 - - - False - False - 2 - - - - - Delete + True - True - True - Delete this entry - icon-delete-row - True - + False + 6 + True + + + Add + True + True + True + image3 + True + + + False + True + 0 + + + + + Record + True + True + True + Record a button of your device that should be remapped + image2 + True + + + False + True + 1 + + + + + Advanced + True + True + True + image1 + + + False + True + 2 + + + + + Delete + True + True + True + Delete this entry + icon-delete-row + True + + + + False + True + 3 + + False @@ -652,19 +666,17 @@ Shortcut: ctrl + del True False - 3 True True - center True True filter mappings... False - False + True 0 @@ -677,7 +689,6 @@ Shortcut: ctrl + del True Toggle case sensitive end - center False @@ -685,6 +696,9 @@ Shortcut: ctrl + del 1 + True @@ -695,7 +709,7 @@ Shortcut: ctrl + del False - True + False 2 From 2a8075f399103e73082547c4d167616e3da75875 Mon Sep 17 00:00:00 2001 From: sezanzeb Date: Wed, 22 Feb 2023 09:23:21 +0100 Subject: [PATCH 13/20] fixed horizontal resizing --- data/input-remapper.glade | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/input-remapper.glade b/data/input-remapper.glade index 8803e5b5a..edac409f6 100644 --- a/data/input-remapper.glade +++ b/data/input-remapper.glade @@ -580,6 +580,7 @@ Shortcut: ctrl + del True False + center 18 18 18 @@ -587,6 +588,7 @@ Shortcut: ctrl + del 6 + 463 True False 6 From 00fec91b8346d99fd7ca72a0b4ac04b05e571565 Mon Sep 17 00:00:00 2001 From: Uwe Jugel <532284+ubunatic@users.noreply.github.com> Date: Wed, 22 Feb 2023 21:29:58 +0100 Subject: [PATCH 14/20] Update inputremapper/gui/components/gtkext/listbox_filter.py Co-authored-by: Tobi --- inputremapper/gui/components/gtkext/listbox_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inputremapper/gui/components/gtkext/listbox_filter.py b/inputremapper/gui/components/gtkext/listbox_filter.py index 580bb740e..6eb320bda 100644 --- a/inputremapper/gui/components/gtkext/listbox_filter.py +++ b/inputremapper/gui/components/gtkext/listbox_filter.py @@ -97,8 +97,8 @@ def filter_value(self): def case_sensitive(self): return self._case_sensitive - # matches the current filter_value and filter_options with the given value def match_filter(self, value: str): + """Match the current filter_value and filter_options with the given value.""" value = (value or "").strip() # if filter is not set, all rows need to match From 5757af2b2218a2d99e8a28b5d744df439fe0cd32 Mon Sep 17 00:00:00 2001 From: Uwe Jugel <532284+ubunatic@users.noreply.github.com> Date: Wed, 22 Feb 2023 21:30:15 +0100 Subject: [PATCH 15/20] Update tests/integration/test_gui.py Co-authored-by: Tobi --- tests/integration/test_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py index cbdaeca7c..a2436e920 100644 --- a/tests/integration/test_gui.py +++ b/tests/integration/test_gui.py @@ -297,7 +297,7 @@ 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.mapping_filter_case_btn: Gtk.Button = get("mapping-filter-case-button") + self.mapping_filter_case_btn: Gtk.ToggleButton = get("mapping-filter-case-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") From f0b0bf5947383fc8dc923bef043f27071e9ab6bd Mon Sep 17 00:00:00 2001 From: Uwe Date: Wed, 22 Feb 2023 21:31:55 +0100 Subject: [PATCH 16/20] use GTK_PATH to allow overriding the data dir for glade and style loading --- inputremapper/configs/data.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/inputremapper/configs/data.py b/inputremapper/configs/data.py index 197301b19..0ab906106 100644 --- a/inputremapper/configs/data.py +++ b/inputremapper/configs/data.py @@ -48,6 +48,12 @@ def _try_standard_locations(): return data +def _try_gtk_path(): + # See GTK_PATH docs at https://docs.gtk.org/gtk3/running.html + if os.environ.get("GTK_PATH"): + data = os.path.join(os.environ.get("GTK_PATH"), "data") + if os.path.exists(data): + return data def _try_python_package_location(): """Look for the data dir at the packages installation location.""" @@ -86,7 +92,7 @@ def get_data_path(filename=""): # prefix path for data # https://docs.python.org/3/distutils/setupscript.html?highlight=package_data#installing-additional-files # noqa pylint: disable=line-too-long - data = _try_python_package_location() or _try_standard_locations() + data = _try_gtk_path() or _try_python_package_location() or _try_standard_locations() if data is None: logger.error("Could not find the application data") From 817c3d9514ab926efe3b6e640b23e366510bf300 Mon Sep 17 00:00:00 2001 From: Uwe Date: Wed, 22 Feb 2023 21:34:03 +0100 Subject: [PATCH 17/20] black fmt --- inputremapper/configs/data.py | 6 +++++- tests/integration/test_gui.py | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/inputremapper/configs/data.py b/inputremapper/configs/data.py index 0ab906106..0d5066412 100644 --- a/inputremapper/configs/data.py +++ b/inputremapper/configs/data.py @@ -48,6 +48,7 @@ def _try_standard_locations(): return data + def _try_gtk_path(): # See GTK_PATH docs at https://docs.gtk.org/gtk3/running.html if os.environ.get("GTK_PATH"): @@ -55,6 +56,7 @@ def _try_gtk_path(): if os.path.exists(data): return data + def _try_python_package_location(): """Look for the data dir at the packages installation location.""" source = None @@ -92,7 +94,9 @@ def get_data_path(filename=""): # prefix path for data # https://docs.python.org/3/distutils/setupscript.html?highlight=package_data#installing-additional-files # noqa pylint: disable=line-too-long - data = _try_gtk_path() or _try_python_package_location() or _try_standard_locations() + data = ( + _try_gtk_path() or _try_python_package_location() or _try_standard_locations() + ) if data is None: logger.error("Could not find the application data") diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py index a2436e920..31adc98de 100644 --- a/tests/integration/test_gui.py +++ b/tests/integration/test_gui.py @@ -297,7 +297,9 @@ 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.mapping_filter_case_btn: Gtk.ToggleButton = get("mapping-filter-case-button") + self.mapping_filter_case_btn: Gtk.ToggleButton = get( + "mapping-filter-case-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") From d8461feacc2a569fda2fad56efdc107dbe156793 Mon Sep 17 00:00:00 2001 From: Uwe Jugel <532284+ubunatic@users.noreply.github.com> Date: Wed, 22 Feb 2023 22:12:39 +0100 Subject: [PATCH 18/20] Update inputremapper/gui/components/gtkext/listbox_filter.py Co-authored-by: Tobi --- inputremapper/gui/components/gtkext/listbox_filter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inputremapper/gui/components/gtkext/listbox_filter.py b/inputremapper/gui/components/gtkext/listbox_filter.py index 6eb320bda..45cf749a2 100644 --- a/inputremapper/gui/components/gtkext/listbox_filter.py +++ b/inputremapper/gui/components/gtkext/listbox_filter.py @@ -110,14 +110,14 @@ def match_filter(self, value: str): else: return self._filter_value.lower() in value.lower() - # set and apply filter def set_filter(self, filter_value: str, case_sensitive=False): + """Set and apply filter.""" self._filter_value = str(filter_value) self._case_sensitive = bool(case_sensitive) self._gtk_apply_filter_to_listbox_children() - # apply filter to widget tree def _gtk_apply_filter_to_listbox_children(self): + """Apply filter to widget tree.""" value = self._filter_value.lower() selected: Gtk.ListBoxRow = None row: Gtk.ListBoxRow = None From dc6b59d604fb2b2ac581be02fbb85bf27061fe6e Mon Sep 17 00:00:00 2001 From: Uwe Date: Thu, 23 Feb 2023 23:53:15 +0100 Subject: [PATCH 19/20] remove root flag, use pkexec only on failure --- bin/input-remapper-gtk | 13 ++++--------- inputremapper/gui/reader_client.py | 5 ++--- inputremapper/gui/reader_service.py | 19 +++++++++++-------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/bin/input-remapper-gtk b/bin/input-remapper-gtk index a4b0bbd5a..30ede3c98 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(ignore_pkexec_errors=False) -> DaemonProxy: +def start_processes() -> DaemonProxy: """Start reader-service and daemon via pkexec to run in the background.""" # this function is overwritten in tests try: - ReaderService.pkexec_reader_service(ingore_errors=ignore_pkexec_errors) + ReaderService.pkexec_reader_service() except Exception as e: logger.error(e) sys.exit(11) @@ -60,11 +60,6 @@ if __name__ == '__main__': 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 - ) options = parser.parse_args(sys.argv[1:]) update_verbosity(options.debug) @@ -90,8 +85,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(), ignore_pkexec_errors=options.no_root) - daemon = start_processes(ignore_pkexec_errors=options.no_root) + reader_client = ReaderClient(message_broker, _Groups()) + daemon = start_processes() data_manager = DataManager( message_broker, GlobalConfig(), reader_client, daemon, GlobalUInputs(), system_mapping diff --git a/inputremapper/gui/reader_client.py b/inputremapper/gui/reader_client.py index f29c0b440..8687c70ea 100644 --- a/inputremapper/gui/reader_client.py +++ b/inputremapper/gui/reader_client.py @@ -73,7 +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 ): self.groups = groups self.message_broker = message_broker @@ -88,14 +88,13 @@ def __init__( 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(ingore_errors=self.ignore_pkexec_errors) + ReaderService.pkexec_reader_service() # wait until the ReaderService is up diff --git a/inputremapper/gui/reader_service.py b/inputremapper/gui/reader_service.py index 5fd2471e0..f465fb140 100644 --- a/inputremapper/gui/reader_service.py +++ b/inputremapper/gui/reader_service.py @@ -136,20 +136,23 @@ def is_running(): return True @staticmethod - def pkexec_reader_service(ingore_errors=False): + def pkexec_reader_service(): """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}" + cmd = f"input-remapper-control --command start-reader-service{debug}" logger.debug("Running `%s`", cmd) exit_code = os.system(cmd) + if exit_code == 0: + return - if exit_code != 0: - ex = Exception(f"Failed to pkexec the reader-service, code {exit_code}") - if ingore_errors: - logger.warn(ex) - else: - raise ex + cmd = f"pkexec {cmd}" + logger.debug("Running `%s`", cmd) + exit_code = os.system(cmd) + if exit_code == 0: + return + + raise Exception(f"Failed to start the reader-service, code {exit_code}") async def run(self): """Start doing stuff.""" From 3026a8e07141cc3d8551e0dd0d2b81f9fcbc2fb6 Mon Sep 17 00:00:00 2001 From: Uwe Date: Fri, 24 Feb 2023 00:45:11 +0100 Subject: [PATCH 20/20] use env var to ignore pkexec errors --- inputremapper/gui/reader_service.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/inputremapper/gui/reader_service.py b/inputremapper/gui/reader_service.py index f465fb140..752be2520 100644 --- a/inputremapper/gui/reader_service.py +++ b/inputremapper/gui/reader_service.py @@ -139,20 +139,19 @@ def is_running(): def pkexec_reader_service(): """Start reader-service via pkexec to run in the background.""" debug = " -d" if logger.level <= logging.DEBUG else "" - cmd = f"input-remapper-control --command start-reader-service{debug}" + cmd = f"pkexec input-remapper-control --command start-reader-service{debug}" logger.debug("Running `%s`", cmd) exit_code = os.system(cmd) if exit_code == 0: return - cmd = f"pkexec {cmd}" - logger.debug("Running `%s`", cmd) - exit_code = os.system(cmd) - if exit_code == 0: + ex = Exception(f"Failed to pkexec the reader-service, code {exit_code}") + if os.environ.get("IGNORE_PKEXEC_ERRORS"): + logger.warn(ex) return - raise Exception(f"Failed to start the reader-service, code {exit_code}") + raise ex async def run(self): """Start doing stuff."""