From bdebee3ce95523bcc179ea2edba39802703f2543 Mon Sep 17 00:00:00 2001 From: sezanzeb Date: Sun, 27 Dec 2020 15:38:08 +0100 Subject: [PATCH] small gamepad improvements --- data/key-mapper.glade | 33 ++++++++++++++----- keymapper/dev/ev_abs_mapper.py | 2 ++ keymapper/dev/injector.py | 10 +++--- keymapper/dev/permissions.py | 5 +-- keymapper/gtk/window.py | 51 +++++++++++++++++++++++++---- keymapper/mapping.py | 11 +++++++ tests/testcases/test_injector.py | 22 ++++++++++--- tests/testcases/test_integration.py | 4 +++ tests/testcases/test_mapping.py | 7 ++++ tests/testcases/test_permissions.py | 2 +- 10 files changed, 120 insertions(+), 27 deletions(-) diff --git a/data/key-mapper.glade b/data/key-mapper.glade index 95660bcd..4646ae54 100644 --- a/data/key-mapper.glade +++ b/data/key-mapper.glade @@ -14,6 +14,7 @@ False + 10 Key Mapper True center @@ -30,6 +31,7 @@ True False center + 10 end @@ -63,9 +65,8 @@ True False + 10 0 - 12 - 12 dialog-error 6 @@ -137,6 +138,7 @@ False + 10 Key Mapper True center @@ -153,6 +155,7 @@ True False center + 10 end @@ -199,10 +202,9 @@ True False + 10 0 - 12 - 12 - dialog-error + dialog-warning 6 @@ -770,6 +772,19 @@ True False + + + False + 10 + 10 + dialog-warning + + + False + True + 0 + + False @@ -779,7 +794,7 @@ False True - 0 + 1 @@ -798,7 +813,7 @@ True True - 1 + 2 @@ -809,7 +824,7 @@ False True - 2 + 3 @@ -824,7 +839,7 @@ False True - 3 + 4 diff --git a/keymapper/dev/ev_abs_mapper.py b/keymapper/dev/ev_abs_mapper.py index 92a74033..f15b911b 100644 --- a/keymapper/dev/ev_abs_mapper.py +++ b/keymapper/dev/ev_abs_mapper.py @@ -46,6 +46,8 @@ WHEEL_THRESHOLD = 0.3 def _write(device, ev_type, keycode, value): """Inject.""" + # if the mouse won't move even though correct stuff is written here, the + # capabilities are probably wrong device.write(ev_type, keycode, value) device.syn() diff --git a/keymapper/dev/injector.py b/keymapper/dev/injector.py index 8f87a845..48a3df2c 100644 --- a/keymapper/dev/injector.py +++ b/keymapper/dev/injector.py @@ -257,10 +257,12 @@ class KeycodeInjector: evdev.ecodes.REL_WHEEL, ] keys = capabilities.get(EV_KEY) - if keys is None or len(keys) == 0: - # for reasons I don't know, it is also required to have - # any keyboard button in capabilities. Maybe they intended to - # check for the mouse-button code. + if keys is None: + capabilities[EV_KEY] = [] + + if ecodes.BTN_MOUSE not in capabilities[EV_KEY]: + # to be able to move the cursor, this key capability is + # needed capabilities[EV_KEY] = [ecodes.BTN_MOUSE] # just like what python-evdev does in from_device diff --git a/keymapper/dev/permissions.py b/keymapper/dev/permissions.py index d6744c32..ffa2aa9a 100644 --- a/keymapper/dev/permissions.py +++ b/keymapper/dev/permissions.py @@ -49,7 +49,7 @@ def check_group(group): if not in_group: msg = ( - 'Some devices may not be visible without being in the ' + 'Some devices may not be accessible without being in the ' f'"{group}" user group.' ) logger.warning(msg) @@ -66,7 +66,7 @@ def check_group(group): if in_group and not group_active: msg = ( f'You are in the "{group}" group, but your session is not yet ' - 'using it. Some devices may not be visible. Please log out and ' + 'using it. Some devices may not be accessible. Please log out and ' 'back in or restart' ) logger.warning(msg) @@ -97,6 +97,7 @@ def can_read_devices(): plugdev_check = check_group('plugdev') # ubuntu. funnily, individual devices in /dev/input/ have write permitted. + print(is_service_running(), check_injection_rights()) if not is_service_running(): can_write = check_injection_rights() else: diff --git a/keymapper/gtk/window.py b/keymapper/gtk/window.py index 7507420e..3b423b7a 100755 --- a/keymapper/gtk/window.py +++ b/keymapper/gtk/window.py @@ -54,6 +54,7 @@ def gtk_iteration(): CTX_SAVE = 0 CTX_APPLY = 1 CTX_ERROR = 3 +CTX_WARNING = 4 def get_selected_row_bg(): @@ -74,6 +75,30 @@ def get_selected_row_bg(): return color.to_string() +def with_selected_device(func): + """Decorate a function to only execute if a device is selected.""" + # this should only happen if no device was found at all + def wrapped(window, *args): + if window.selected_device is None: + return True # work with timeout_add + + return func(window, *args) + + return wrapped + + +def with_selected_preset(func): + """Decorate a function to only execute if a preset is selected.""" + # this should only happen if no device was found at all + def wrapped(window, *args): + if window.selected_preset is None or window.selected_device is None: + return True # work with timeout_add + + return func(window, *args) + + return wrapped + + class HandlerDisabled: """Safely modify a widget without causing handlers to be called. @@ -142,6 +167,11 @@ class Window: '\n\n'.join(permission_errors) ) + # this is not set to invisible in glade to give the ui a default + # height that doesn't jump when a gamepad is selected + self.get('gamepad_separator').hide() + self.get('gamepad_config').hide() + self.populate_devices() self.select_newest_preset() @@ -313,6 +343,7 @@ class Window: return row, focused + @with_selected_device def consume_newest_keycode(self): """To capture events from keyboards, mice and gamepads.""" # the "event" event of Gtk.Window wouldn't trigger on gamepad @@ -332,6 +363,7 @@ class Window: return True + @with_selected_device def on_apply_system_layout_clicked(self, _): """Load the mapping.""" self.dbus.stop_injecting(self.selected_device) @@ -343,10 +375,14 @@ class Window: if tooltip is None: tooltip = message + self.get('error_status_icon').hide() + self.get('warning_status_icon').hide() + if context_id == CTX_ERROR: self.get('error_status_icon').show() - else: - self.get('error_status_icon').hide() + + if context_id == CTX_WARNING: + self.get('warning_status_icon').show() status_bar = self.get('status_bar') status_bar.push(context_id, message) @@ -366,6 +402,7 @@ class Window: msg = f'Syntax error at {position}, hover for info' self.show_status(CTX_ERROR, msg, error) + @with_selected_preset def on_save_preset_clicked(self, button): """Save changes to a preset to the file system.""" new_name = self.get('preset_name_input').get_text() @@ -389,11 +426,13 @@ class Window: self.show_status(CTX_ERROR, 'Error: Permission denied!', error) logger.error(error) + @with_selected_preset def on_delete_preset_clicked(self, _): """Delete a preset from the file system.""" delete_preset(self.selected_device, self.selected_preset) self.populate_presets() + @with_selected_preset def on_apply_preset_clicked(self, _): """Apply a preset without saving changes.""" preset = self.selected_preset @@ -403,7 +442,7 @@ class Window: if custom_mapping.changed: self.show_status( - CTX_APPLY, + CTX_WARNING, f'Applied outdated preset "{preset}"', 'Click "Save" first for changes to take effect' ) @@ -462,6 +501,7 @@ class Window: else: self.get('apply_system_layout').set_opacity(0.4) + @with_selected_device def on_create_preset_clicked(self, _): """Create a new preset and select it.""" if custom_mapping.changed: @@ -520,6 +560,8 @@ class Window: self.initialize_gamepad_config() + custom_mapping.changed = False + def on_left_joystick_purpose_changed(self, dropdown): """Set the purpose of the left joystick.""" purpose = dropdown.get_active_id() @@ -557,9 +599,6 @@ class Window: def save_preset(self): """Write changes to presets to disk.""" - if self.selected_device is None or self.selected_preset is None: - return - logger.info( 'Updating configs for "%s", "%s"', self.selected_device, diff --git a/keymapper/mapping.py b/keymapper/mapping.py index 92b4c0b0..9a28d0bb 100644 --- a/keymapper/mapping.py +++ b/keymapper/mapping.py @@ -54,6 +54,17 @@ class Mapping(ConfigBase): def __len__(self): return len(self._mapping) + def set(self, *args): + """Set a config value. See `ConfigBase.set`.""" + print('set', args) + self.changed = True + return super().set(*args) + + def remove(self, *args): + """Remove a config value. See `ConfigBase.remove`.""" + self.changed = True + return super().remove(*args) + def change(self, new_key, character, previous_key=None): """Replace the mapping of a keycode with a different one. diff --git a/tests/testcases/test_injector.py b/tests/testcases/test_injector.py index 9a3d3445..ca262c91 100644 --- a/tests/testcases/test_injector.py +++ b/tests/testcases/test_injector.py @@ -180,6 +180,8 @@ class TestInjector(unittest.TestCase): # be able to control the mouse. it probably wants the mouse click. self.injector = KeycodeInjector('gamepad 2', custom_mapping) + """gamepad without any existing key capability""" + path = self.new_gamepad gamepad_template = copy.deepcopy(fixtures['/dev/input/event30']) fixtures[path] = { @@ -189,19 +191,29 @@ class TestInjector(unittest.TestCase): 'capabilities': gamepad_template['capabilities'] } del fixtures[path]['capabilities'][EV_KEY] - device, abs_to_rel = self.injector._prepare_device(path) + self.assertNotIn(EV_KEY, device.capabilities()) + capabilities = self.injector._modify_capabilities( + {}, + device, + abs_to_rel + ) + self.assertIn(EV_KEY, capabilities) + self.assertIn(evdev.ecodes.BTN_MOUSE, capabilities[EV_KEY]) - self.assertNotIn(evdev.ecodes.EV_KEY, device.capabilities()) + """gamepad with existing key capabilities, but not btn_mouse""" + path = '/dev/input/event30' + device, abs_to_rel = self.injector._prepare_device(path) + self.assertIn(EV_KEY, device.capabilities()) + self.assertNotIn(evdev.ecodes.BTN_MOUSE, device.capabilities()[EV_KEY]) capabilities = self.injector._modify_capabilities( {}, device, abs_to_rel ) - - self.assertIn(evdev.ecodes.EV_KEY, capabilities) - self.assertEqual(len(capabilities[evdev.ecodes.EV_KEY]), 1) + self.assertIn(EV_KEY, capabilities) + self.assertIn(evdev.ecodes.BTN_MOUSE, capabilities[EV_KEY]) def test_skip_unused_device(self): # skips a device because its capabilities are not used in the mapping diff --git a/tests/testcases/test_integration.py b/tests/testcases/test_integration.py index fe70f75c..efe3494b 100644 --- a/tests/testcases/test_integration.py +++ b/tests/testcases/test_integration.py @@ -567,10 +567,12 @@ class TestIntegration(unittest.TestCase): # select a device that is not a gamepad self.window.on_select_device(FakeDropdown('device 1')) self.assertFalse(self.window.get('gamepad_config').is_visible()) + self.assertFalse(custom_mapping.changed) # select a gamepad self.window.on_select_device(FakeDropdown('gamepad')) self.assertTrue(self.window.get('gamepad_config').is_visible()) + self.assertFalse(custom_mapping.changed) # set stuff self.window.get('left_joystick_purpose').set_active_id(WHEEL) @@ -583,6 +585,7 @@ class TestIntegration(unittest.TestCase): config.set('gamepad.joystick.left_purpose', MOUSE) config.set('gamepad.joystick.right_purpose', MOUSE) config.set('gamepad.joystick.pointer_speed', 50) + self.assertTrue(custom_mapping.changed) left_purpose = custom_mapping.get('gamepad.joystick.left_purpose') right_purpose = custom_mapping.get('gamepad.joystick.right_purpose') pointer_speed = custom_mapping.get('gamepad.joystick.pointer_speed') @@ -593,6 +596,7 @@ class TestIntegration(unittest.TestCase): # select a device that is not a gamepad again self.window.on_select_device(FakeDropdown('device 1')) self.assertFalse(self.window.get('gamepad_config').is_visible()) + self.assertFalse(custom_mapping.changed) def test_start_injecting(self): keycode_from = 9 diff --git a/tests/testcases/test_mapping.py b/tests/testcases/test_mapping.py index 552d85d1..991fd012 100644 --- a/tests/testcases/test_mapping.py +++ b/tests/testcases/test_mapping.py @@ -115,8 +115,11 @@ class TestMapping(unittest.TestCase): self.assertEqual(self.mapping.get('a'), None) + self.assertFalse(self.mapping.changed) + self.mapping.set('a', 1) self.assertEqual(self.mapping.get('a'), 1) + self.assertTrue(self.mapping.changed) self.mapping.remove('a') self.mapping.set('a.b', 2) @@ -133,11 +136,15 @@ class TestMapping(unittest.TestCase): self.mapping.change((EV_KEY, 81, 1), 'a') self.mapping.set('mapping.a', 2) self.mapping.save(get_preset_path('foo', 'bar')) + self.assertFalse(self.mapping.changed) self.mapping.load(get_preset_path('foo', 'bar')) self.assertEqual(self.mapping.get_character((EV_KEY, 81, 1)), 'a') self.assertIsNone(self.mapping.get('mapping.a')) + self.assertFalse(self.mapping.changed) # loading a different preset also removes the configs from memory + self.mapping.remove('a') + self.assertTrue(self.mapping.changed) self.mapping.set('a.b.c', 6) self.mapping.load(get_preset_path('foo', 'bar2')) self.assertIsNone(self.mapping.get('a.b.c')) diff --git a/tests/testcases/test_permissions.py b/tests/testcases/test_permissions.py index ada40aeb..b83b2129 100644 --- a/tests/testcases/test_permissions.py +++ b/tests/testcases/test_permissions.py @@ -102,7 +102,7 @@ class TestPermissions(unittest.TestCase): def fake_check_output(cmd): # fake the `groups` output to act like the current session only # has input and a_unused active - if cmd[0] == 'groups': + if cmd == 'groups' or cmd[0] == 'groups': return b'foo input a_unused bar' return original_check_output(cmd)