#37 mapping left mouse buttons

xkb
sezanzeb 4 years ago committed by sezanzeb
parent 93aa393051
commit a5b5f562c8

@ -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

@ -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:

@ -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):

@ -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

@ -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)

@ -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

@ -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'

@ -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()

@ -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__":

@ -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),
])

Loading…
Cancel
Save