From a5b5f562c8d280b572d2c4d628ee874f36137824 Mon Sep 17 00:00:00 2001 From: sezanzeb Date: Fri, 2 Apr 2021 16:33:20 +0200 Subject: [PATCH] #37 mapping left mouse buttons --- keymapper/getdevices.py | 2 +- keymapper/gui/helper.py | 8 ++--- keymapper/gui/row.py | 6 ++-- keymapper/gui/window.py | 29 ++++++++++++----- keymapper/key.py | 4 +++ keymapper/mapping.py | 14 +++++++-- tests/testcases/test_integration.py | 49 ++++++++++++++++++++++++++++- tests/testcases/test_key.py | 16 ++++++++++ tests/testcases/test_mapping.py | 30 +++++++++--------- tests/testcases/test_reader.py | 8 ++--- 10 files changed, 124 insertions(+), 42 deletions(-) diff --git a/keymapper/getdevices.py b/keymapper/getdevices.py index 539e2c14..95d7890c 100644 --- a/keymapper/getdevices.py +++ b/keymapper/getdevices.py @@ -29,7 +29,7 @@ import asyncio import evdev from evdev.ecodes import EV_KEY, EV_ABS, KEY_CAMERA, EV_REL, BTN_STYLUS, \ - BTN_A, ABS_MT_POSITION_X, REL_X, KEY_A, BTN_LEFT, REL_Y, REL_WHEEL + ABS_MT_POSITION_X, REL_X, KEY_A, BTN_LEFT, REL_Y, REL_WHEEL from keymapper.logger import logger diff --git a/keymapper/gui/helper.py b/keymapper/gui/helper.py index 3a6c3365..1e2eb184 100644 --- a/keymapper/gui/helper.py +++ b/keymapper/gui/helper.py @@ -192,15 +192,11 @@ class RootHelper: # ignore hold-down events return - click_events = [ - evdev.ecodes.BTN_LEFT, + blacklisted_keys = [ evdev.ecodes.BTN_TOOL_DOUBLETAP ] - if event.type == EV_KEY and event.code in click_events: - # disable mapping the left mouse button because it would break - # the mouse. Also it is emitted right when focusing the row - # which breaks the current workflow. + if event.type == EV_KEY and event.code in blacklisted_keys: return if event.type == EV_ABS: diff --git a/keymapper/gui/row.py b/keymapper/gui/row.py index 592271da..d8415961 100644 --- a/keymapper/gui/row.py +++ b/keymapper/gui/row.py @@ -329,12 +329,12 @@ class Row(Gtk.ListBoxRow): label.set_justify(Gtk.Justification.CENTER) self.keycode_input.set_opacity(1) - def on_character_input_unfocus(self, input, _): + def on_character_input_unfocus(self, character_input, _): """Save the preset and correct the input casing.""" - character = input.get_text() + character = character_input.get_text() correct_case = system_mapping.correct_case(character) if character != correct_case: - input.set_text(correct_case) + character_input.set_text(correct_case) self.window.save_preset() def put_together(self, character): diff --git a/keymapper/gui/window.py b/keymapper/gui/window.py index e1f383be..4a0ebc38 100755 --- a/keymapper/gui/window.py +++ b/keymapper/gui/window.py @@ -38,6 +38,7 @@ from keymapper.logger import logger, COMMIT_HASH, VERSION, EVDEV_VERSION, \ from keymapper.getdevices import get_devices, GAMEPAD, KEYBOARD, UNKNOWN, \ GRAPHICS_TABLET, TOUCHPAD, MOUSE from keymapper.gui.row import Row, to_string +from keymapper.key import Key from keymapper.gui.reader import reader from keymapper.gui.helper import is_helper_running from keymapper.injection.injector import RUNNING, FAILED, NO_GRAB @@ -200,7 +201,8 @@ class Window: self.get('vertical-wrapper').set_opacity(1) self.ctrl = False - self.unreleased_warn = 0 + self.unreleased_warn = False + self.button_left_warn = False if not is_helper_running(): self.show_status(CTX_ERROR, 'The helper did not start') @@ -357,7 +359,6 @@ class Window: device_selection = self.get('device_selection') with HandlerDisabled(device_selection, self.on_select_device): - print('clearing device_store') self.device_store.clear() for device in devices: types = devices[device]['types'] @@ -589,9 +590,20 @@ class Window: logger.info('Applying preset "%s" for "%s"', preset, device) + if not self.button_left_warn: + if custom_mapping.dangerously_mapped_btn_left(): + self.show_status( + CTX_ERROR, + 'This would disable your click button', + 'Map a button to BTN_Left to avoid this.\n' + 'To overwrite this warning, press apply again.' + ) + self.button_left_warn = True + return + if not self.unreleased_warn: unreleased = reader.get_unreleased_keys() - if unreleased is not None: + if unreleased is not None and unreleased != Key.btn_left(): # it's super annoying if that happens and may break the user # input in such a way to prevent disabling the mapping logger.error( @@ -608,6 +620,7 @@ class Window: return self.unreleased_warn = False + self.button_left_warn = False self.dbus.set_config_dir(get_config_path()) self.dbus.start_injecting(device, preset) @@ -659,10 +672,12 @@ class Window: state = self.dbus.get_state(self.selected_device) if state == RUNNING: - self.show_status( - CTX_APPLY, - f'Applied preset "{self.selected_preset}"' - ) + msg = f'Applied preset "{self.selected_preset}"' + + if custom_mapping.get_character(Key.btn_left()): + msg += ', CTRL + DEL to stop' + + self.show_status(CTX_APPLY, msg) self.show_device_mapping_status() return False diff --git a/keymapper/key.py b/keymapper/key.py index ac5ff9d6..15d229d1 100644 --- a/keymapper/key.py +++ b/keymapper/key.py @@ -87,6 +87,10 @@ class Key: self.keys = tuple(keys) self.release = (*self.keys[-1][:2], 0) + @classmethod + def btn_left(cls): + return cls(ecodes.EV_KEY, ecodes.BTN_LEFT, 1) + def __iter__(self): return iter(self.keys) diff --git a/keymapper/mapping.py b/keymapper/mapping.py index 9ad819a9..41ff2738 100644 --- a/keymapper/mapping.py +++ b/keymapper/mapping.py @@ -26,6 +26,8 @@ import os import json import copy +from evdev.ecodes import EV_KEY, BTN_LEFT + from keymapper.logger import logger from keymapper.paths import touch from keymapper.config import ConfigBase, config @@ -253,9 +255,7 @@ class Mapping(ConfigBase): Parameters ---------- - key : Key or InputEvent - If an InputEvent, will test if that event is mapped - and take the sign of the value. + key : Key """ if not isinstance(key, Key): raise TypeError('Expected key to be a Key object') @@ -266,3 +266,11 @@ class Mapping(ConfigBase): return existing return None + + def dangerously_mapped_btn_left(self): + """Return True if this mapping disables BTN_Left.""" + if self.get_character(Key(EV_KEY, BTN_LEFT, 1)) is not None: + values = [value.lower() for value in self._mapping.values()] + return 'btn_left' not in values + + return False diff --git a/tests/testcases/test_integration.py b/tests/testcases/test_integration.py index 91388a4e..87429ca0 100644 --- a/tests/testcases/test_integration.py +++ b/tests/testcases/test_integration.py @@ -41,7 +41,7 @@ from keymapper.state import custom_mapping, system_mapping, XMODMAP_FILENAME from keymapper.paths import CONFIG_PATH, get_preset_path, get_config_path from keymapper.config import config, WHEEL, MOUSE, BUTTONS from keymapper.gui.reader import reader -from keymapper.injection.injector import RUNNING, FAILED +from keymapper.injection.injector import RUNNING, FAILED, UNKNOWN from keymapper.gui.row import Row, to_string, HOLDING, IDLE from keymapper.gui.window import Window from keymapper.key import Key @@ -1170,12 +1170,59 @@ class TestIntegration(unittest.TestCase): wait() text = self.get_status_text() self.assertIn('Applied', text) + text = self.get_status_text() + self.assertNotIn('CTRL + DEL', text) # only shown if btn_left mapped self.assertFalse(error_icon.get_visible()) self.assertEqual(self.window.dbus.get_state(device_name), RUNNING) # because this test managed to reproduce some minor bug: self.assertNotIn('mapping', custom_mapping._config) + def test_wont_start_2(self): + preset_name = 'foo preset' + device_name = 'device 2' + self.window.selected_preset = preset_name + self.window.selected_device = device_name + + def wait(): + """Wait for the injector process to finish doing stuff.""" + for _ in range(10): + time.sleep(0.1) + gtk_iteration() + if 'Starting' not in self.get_status_text(): + return + + # btn_left mapped + custom_mapping.change(Key.btn_left(), 'a') + self.window.save_preset() + + # and key held down + send_event_to_reader(new_event(EV_KEY, KEY_A, 1)) + reader.read() + self.assertEqual(len(reader._unreleased), 1) + self.assertFalse(self.window.unreleased_warn) + + # first apply, shows btn_left warning + self.window.on_apply_preset_clicked(None) + text = self.get_status_text() + self.assertIn('click', text) + self.assertEqual(self.window.dbus.get_state(device_name), UNKNOWN) + + # second apply, shows unreleased warning + self.window.on_apply_preset_clicked(None) + text = self.get_status_text() + self.assertIn('release', text) + self.assertEqual(self.window.dbus.get_state(device_name), UNKNOWN) + + # third apply, overwrites both warnings + self.window.on_apply_preset_clicked(None) + wait() + self.assertEqual(self.window.dbus.get_state(device_name), RUNNING) + text = self.get_status_text() + # because btn_left is mapped, shows help on how to stop + # injecting via the keyboard + self.assertIn('CTRL + DEL', text) + def test_can_modify_mapping(self): preset_name = 'foo preset' device_name = 'device 2' diff --git a/tests/testcases/test_key.py b/tests/testcases/test_key.py index 1239daa6..7a2608dc 100644 --- a/tests/testcases/test_key.py +++ b/tests/testcases/test_key.py @@ -100,6 +100,22 @@ class TestKey(unittest.TestCase): key_5 = Key((1, 3, 1), (1, 5, 1)) self.assertFalse(key_5.is_problematic()) + def test_raises(self): + self.assertRaises(ValueError, lambda: Key(1)) + self.assertRaises(ValueError, lambda: Key(None)) + self.assertRaises(ValueError, lambda: Key([1])) + self.assertRaises(ValueError, lambda: Key((1,))) + self.assertRaises(ValueError, lambda: Key((1, 2))) + self.assertRaises(ValueError, lambda: Key(('1', '2', '3'))) + self.assertRaises(ValueError, lambda: Key('1')) + self.assertRaises(ValueError, lambda: Key('(1,2,3)')) + self.assertRaises(ValueError, lambda: Key((1, 2, 3), (1, 2, '3'))) + self.assertRaises(ValueError, lambda: Key((1, 2, 3), (1, 2, 3), None)) + + # those don't raise errors + Key((1, 2, 3), (1, 2, 3)) + Key((1, 2, 3)) + if __name__ == "__main__": unittest.main() diff --git a/tests/testcases/test_mapping.py b/tests/testcases/test_mapping.py index 51dc4669..7012e6cf 100644 --- a/tests/testcases/test_mapping.py +++ b/tests/testcases/test_mapping.py @@ -364,21 +364,21 @@ class TestMapping(unittest.TestCase): self.mapping.empty() self.assertEqual(len(self.mapping), 0) - def test_verify_key(self): - self.assertRaises(ValueError, lambda: Key(1)) - self.assertRaises(ValueError, lambda: Key(None)) - self.assertRaises(ValueError, lambda: Key([1])) - self.assertRaises(ValueError, lambda: Key((1,))) - self.assertRaises(ValueError, lambda: Key((1, 2))) - self.assertRaises(ValueError, lambda: Key(('1', '2', '3'))) - self.assertRaises(ValueError, lambda: Key('1')) - self.assertRaises(ValueError, lambda: Key('(1,2,3)')) - self.assertRaises(ValueError, lambda: Key((1, 2, 3), (1, 2, '3'))) - self.assertRaises(ValueError, lambda: Key((1, 2, 3), (1, 2, 3), None)) - - # those don't raise errors - Key((1, 2, 3), (1, 2, 3)) - Key((1, 2, 3)) + def test_dangerously_mapped_btn_left(self): + self.mapping.change(Key.btn_left(), '1') + self.assertTrue(self.mapping.dangerously_mapped_btn_left()) + + self.mapping.change(Key(EV_KEY, 41, 1), '2') + self.assertTrue(self.mapping.dangerously_mapped_btn_left()) + + self.mapping.change(Key(EV_KEY, 42, 1), 'btn_left') + self.assertFalse(self.mapping.dangerously_mapped_btn_left()) + + self.mapping.change(Key(EV_KEY, 42, 1), 'BTN_Left') + self.assertFalse(self.mapping.dangerously_mapped_btn_left()) + + self.mapping.change(Key(EV_KEY, 42, 1), '3') + self.assertTrue(self.mapping.dangerously_mapped_btn_left()) if __name__ == "__main__": diff --git a/tests/testcases/test_reader.py b/tests/testcases/test_reader.py index 01f2f1df..8f30118f 100644 --- a/tests/testcases/test_reader.py +++ b/tests/testcases/test_reader.py @@ -340,13 +340,9 @@ class TestReader(unittest.TestCase): # and by doing that keep the previous combination. self.assertEqual(reader.read(), None) - def test_ignore_btn_left(self): - # click events are ignored because overwriting them would render the - # mouse useless, but a mouse is needed to stop the injection - # comfortably. Furthermore, reading mouse events breaks clicking - # around in the table. It can still be changed in the config files. + def test_blacklist(self): push_events('device 1', [ - new_event(EV_KEY, BTN_LEFT, 1), + new_event(EV_KEY, BTN_TOOL_DOUBLETAP, 1), new_event(EV_KEY, CODE_2, 1), new_event(EV_KEY, BTN_TOOL_DOUBLETAP, 1), ])