From b382a04be55bb53c5e104354f7a8eef308858832 Mon Sep 17 00:00:00 2001 From: sezanzeb Date: Thu, 31 Dec 2020 21:47:56 +0100 Subject: [PATCH] pressed keys represented by class --- keymapper/dev/injector.py | 39 +++----- keymapper/dev/reader.py | 13 +-- keymapper/gtk/row.py | 92 ++++++++++-------- keymapper/gtk/window.py | 2 +- keymapper/key.py | 128 +++++++++++++++++++++++++ keymapper/mapping.py | 141 +++++++++------------------- tests/testcases/test_daemon.py | 9 +- tests/testcases/test_injector.py | 64 ++++--------- tests/testcases/test_integration.py | 130 ++++++++++++------------- tests/testcases/test_mapping.py | 114 +++++++++++----------- 10 files changed, 390 insertions(+), 342 deletions(-) create mode 100644 keymapper/key.py diff --git a/keymapper/dev/injector.py b/keymapper/dev/injector.py index e1c7ce5f..078bb90e 100644 --- a/keymapper/dev/injector.py +++ b/keymapper/dev/injector.py @@ -98,30 +98,6 @@ def ensure_numlock(func): return wrapped -def store_permutations(target, combination, value): - """Store permutations for key combinations. - - Store every permutation of combination, while the last - element needs to remain the last one. It is the finishing - key. E.g. a + b is something different than b + a, but - a + b + c is the same as b + a + c - - a, b and c are tuples of (type, code, value) - - If combination is not a tuple of 3-tuples, it just uses it as key without - permutating anything. - """ - if not isinstance(combination, tuple): - logger.error('Expected a tuple, but got "%s"', combination) - return - - if isinstance(combination[0], tuple): - for permutation in itertools.permutations(combination[:-1]): - target[(*permutation, combination[-1])] = value - else: - target[combination] = value - - def is_in_capabilities(key, capabilities): """Are this key or all of its sub keys in the capabilities?""" if isinstance(key[0], tuple): @@ -153,6 +129,7 @@ class KeycodeInjector: ---------- device : string the name of the device as available in get_device + mapping : Mapping """ self.device = device self.mapping = mapping @@ -168,7 +145,13 @@ class KeycodeInjector: self.abs_state = [0, 0, 0, 0] def _map_keys_to_codes(self): - """To quickly get target keycodes during operation.""" + """To quickly get target keycodes during operation. + + Returns a mapping of one or more 3-tuples to ints. + Examples: + ((1, 2, 1),): 3 + ((1, 5, 1), (1, 4, 1)): 4 + """ key_to_code = {} for key, output in self.mapping: if is_this_a_macro(output): @@ -179,7 +162,8 @@ class KeycodeInjector: logger.error('Don\'t know what %s is', output) continue - store_permutations(key_to_code, key, target_code) + for permutation in key.get_permutations(): + key_to_code[permutation.keys] = target_code return key_to_code @@ -368,7 +352,8 @@ class KeycodeInjector: if macro is None: continue - store_permutations(macros, key, macro) + for permutation in key.get_permutations(): + macros[permutation.keys] = macro if len(macros) == 0: logger.debug('No macros configured') diff --git a/keymapper/dev/reader.py b/keymapper/dev/reader.py index 09728e84..a7c0be84 100644 --- a/keymapper/dev/reader.py +++ b/keymapper/dev/reader.py @@ -31,6 +31,7 @@ from evdev.events import EV_KEY, EV_ABS from keymapper.logger import logger from keymapper.util import sign +from keymapper.key import Key from keymapper.getdevices import get_devices, refresh_devices from keymapper.dev.keycode_mapper import should_map_event_as_btn @@ -60,7 +61,7 @@ class _KeycodeReader: Does not serve any purpose for the injection service. When a button was pressed, the newest keycode can be obtained from this - object. GTK has get_keycode for keyboard keys, but KeycodeReader also + object. GTK has get_key for keyboard keys, but KeycodeReader also has knowledge of buttons like the middle-mouse button. """ def __init__(self): @@ -197,7 +198,7 @@ class _KeycodeReader: return len(self._unreleased) > 0 def read(self): - """Get the newest tuple of event type, keycode or None. + """Get the newest key as Key object If the timing of two recent events is very close, prioritize key events over abs events. @@ -253,12 +254,8 @@ class _KeycodeReader: self.newest_event = newest_event - if len(self._unreleased) > 1: - # a combination - return tuple(self._unreleased.values()) - elif len(self._unreleased) == 1: - # a single key - return list(self._unreleased.values())[0] + if len(self._unreleased) > 0: + return Key(*self._unreleased.values()) else: # nothing return None diff --git a/keymapper/gtk/row.py b/keymapper/gtk/row.py index bd89a859..020cfa9c 100644 --- a/keymapper/gtk/row.py +++ b/keymapper/gtk/row.py @@ -27,6 +27,7 @@ from gi.repository import Gtk, GLib, Gdk from keymapper.state import custom_mapping, system_mapping from keymapper.logger import logger +from keymapper.key import Key CTX_KEYCODE = 2 @@ -39,38 +40,45 @@ for name in system_mapping.list_names(): def to_string(key): """A nice to show description of the pressed key.""" - if isinstance(key[0], tuple): + if isinstance(key, Key): return ' + '.join([to_string(sub_key) for sub_key in key]) + elif isinstance(key[0], tuple): + raise Exception('deprecated stuff') ev_type, code, value = key - try: - key_name = evdev.ecodes.bytype[ev_type][code] - if isinstance(key_name, list): - key_name = key_name[0] - - if ev_type != evdev.ecodes.EV_KEY: - direction = { - (evdev.ecodes.ABS_HAT0X, -1): 'L', - (evdev.ecodes.ABS_HAT0X, 1): 'R', - (evdev.ecodes.ABS_HAT0Y, -1): 'U', - (evdev.ecodes.ABS_HAT0Y, 1): 'D', - (evdev.ecodes.ABS_HAT1X, -1): 'L', - (evdev.ecodes.ABS_HAT1X, 1): 'R', - (evdev.ecodes.ABS_HAT1Y, -1): 'U', - (evdev.ecodes.ABS_HAT1Y, 1): 'D', - (evdev.ecodes.ABS_HAT2X, -1): 'L', - (evdev.ecodes.ABS_HAT2X, 1): 'R', - (evdev.ecodes.ABS_HAT2Y, -1): 'U', - (evdev.ecodes.ABS_HAT2Y, 1): 'D', - }.get((code, value)) - if direction is not None: - key_name += f' {direction}' - - return key_name.replace('KEY_', '') - except KeyError: + if ev_type not in evdev.ecodes.bytype: + logger.error('Unknown key type for %s', key) return 'unknown' + if code not in evdev.ecodes.bytype[ev_type]: + logger.error('Unknown key code for', key) + return 'unknown' + + key_name = evdev.ecodes.bytype[ev_type][code] + if isinstance(key_name, list): + key_name = key_name[0] + + if ev_type != evdev.ecodes.EV_KEY: + direction = { + (evdev.ecodes.ABS_HAT0X, -1): 'L', + (evdev.ecodes.ABS_HAT0X, 1): 'R', + (evdev.ecodes.ABS_HAT0Y, -1): 'U', + (evdev.ecodes.ABS_HAT0Y, 1): 'D', + (evdev.ecodes.ABS_HAT1X, -1): 'L', + (evdev.ecodes.ABS_HAT1X, 1): 'R', + (evdev.ecodes.ABS_HAT1Y, -1): 'U', + (evdev.ecodes.ABS_HAT1Y, 1): 'D', + (evdev.ecodes.ABS_HAT2X, -1): 'L', + (evdev.ecodes.ABS_HAT2X, 1): 'R', + (evdev.ecodes.ABS_HAT2Y, -1): 'U', + (evdev.ecodes.ABS_HAT2Y, 1): 'D', + }.get((code, value)) + if direction is not None: + key_name += f' {direction}' + + return key_name.replace('KEY_', '') + IDLE = 0 HOLDING = 1 @@ -85,9 +93,11 @@ class Row(Gtk.ListBoxRow): Parameters ---------- - key : int, int, int - event, code, value + key : Key """ + if key is not None and not isinstance(key, Key): + raise TypeError('Expected key to be a Key object') + super().__init__() self.device = window.selected_device self.window = window @@ -111,8 +121,8 @@ class Row(Gtk.ListBoxRow): window = self.window.window GLib.idle_add(lambda: window.set_focus(self.character_input)) - def get_keycode(self): - """Get a tuple of type, code and value from the left column. + def get_key(self): + """Get the Key object from the left column. Or None if no code is mapped on this row. """ @@ -123,11 +133,19 @@ class Row(Gtk.ListBoxRow): character = self.character_input.get_text() return character if character else None - def set_new_keycode(self, new_key): - """Check if a keycode has been pressed and if so, display it.""" + def set_new_key(self, new_key): + """Check if a keycode has been pressed and if so, display it. + + Parameters + ---------- + new_key : Key + """ + if new_key is not None and not isinstance(new_key, Key): + raise TypeError('Expected new_key to be a Key object') + # the newest_keycode is populated since the ui regularly polls it # in order to display it in the status bar. - previous_key = self.get_keycode() + previous_key = self.get_key() # no input if new_key is None: @@ -182,7 +200,7 @@ class Row(Gtk.ListBoxRow): def on_character_input_change(self, _): """When the output character for that keycode is typed in.""" - key = self.get_keycode() + key = self.get_key() character = self.get_character() if character is None: @@ -204,7 +222,7 @@ class Row(Gtk.ListBoxRow): def show_click_here(self): """Show 'click here' on the keycode input button.""" - if self.get_keycode() is not None: + if self.get_key() is not None: return self.set_keycode_input_label('click here') @@ -212,7 +230,7 @@ class Row(Gtk.ListBoxRow): def show_press_key(self): """Show 'press key' on the keycode input button.""" - if self.get_keycode() is not None: + if self.get_key() is not None: return self.set_keycode_input_label('press key') @@ -311,7 +329,7 @@ class Row(Gtk.ListBoxRow): def on_delete_button_clicked(self, *args): """Destroy the row and remove it from the config.""" - key = self.get_keycode() + key = self.get_key() if key is not None: custom_mapping.clear(key) diff --git a/keymapper/gtk/window.py b/keymapper/gtk/window.py index 22491983..24ce2b37 100755 --- a/keymapper/gtk/window.py +++ b/keymapper/gtk/window.py @@ -364,7 +364,7 @@ class Window: # inform the currently selected row about the new keycode row, focused = self.get_focused_row() if isinstance(focused, Gtk.ToggleButton): - row.set_new_keycode(key) + row.set_new_key(key) return True diff --git a/keymapper/key.py b/keymapper/key.py new file mode 100644 index 00000000..7475b897 --- /dev/null +++ b/keymapper/key.py @@ -0,0 +1,128 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# key-mapper - GUI for device specific keyboard mappings +# Copyright (C) 2020 sezanzeb +# +# This file is part of key-mapper. +# +# key-mapper 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. +# +# key-mapper 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 key-mapper. If not, see . + + +"""A button or a key combination.""" + + +import itertools + +from keymapper.util import sign + + +def verify(key): + """Check if the key is an int 3-tuple of type, code, value""" + if not isinstance(key, tuple) or len(key) != 3: + raise ValueError(f'Expected key to be a 3-tuple, but got {key}') + if sum([not isinstance(value, int) for value in key]) != 0: + raise ValueError(f'Can only use numbers, but got {key}') + + +class Key: + """Represents one or more pressed down keys. + + Can be used in hashmaps/dicts as key + """ + def __init__(self, *keys): + """ + Parameters + ---------- + Takes an arbitrary number of tuples as arguments. Each one should + be in the format of + 0: type, one of evdev.events, taken from the original source + event. Everything will be mapped to EV_KEY. + 1: The source keycode, what the mouse would report without any + modification. + 2. The value. 1 (down), 0 (up) or any + other value that the device reports. Gamepads use a continuous + space of values for joysticks and triggers. + + or Key objects, which will flatten all of them into one combination + """ + if len(keys) == 0: + raise ValueError('At least one key is required') + + if isinstance(keys[0], int): + # type, code, value was provided instead of a tuple + keys = (keys,) + + # multiple objects of Key get flattened into one tuple + flattened = () + for key in keys: + if isinstance(key, Key): + flattened += key.keys + else: + flattened += (key,) + keys = flattened + + for key in keys: + verify(key) + + self.keys = tuple(keys) + self.release = (*self.keys[-1][:2], 0) + + def __iter__(self): + return iter(self.keys) + + def __getitem__(self, item): + return self.keys[item] + + def __len__(self): + """Get the number of pressed down kes.""" + return len(self.keys) + + def __str__(self): + return f'Key{str(self.keys)}' + + def __hash__(self): + if len(self.keys) == 1: + return hash(self.keys[0]) + + return hash(self.keys) + + def __eq__(self, other): + if isinstance(other, tuple): + if isinstance(other[0], tuple): + # a combination ((1, 5, 1), (1, 3, 1)) + return self.keys == other + + # otherwise, self needs to represent a single key as well + return len(self.keys) == 1 and self.keys[0] == other + + if not isinstance(other, Key): + return False + + # compare two instances of Key + return self.keys == other.keys + + def get_permutations(self): + """Get a list of Key objects representing all possible permutations. + + combining a + b + c should have the same result as b + a + c. + Only the last key remains the same in the returned result. + """ + if len(self.keys) <= 2: + return [self] + + permutations = [] + for permutation in itertools.permutations(self.keys[:-1]): + permutations.append(Key(*permutation, self.keys[-1])) + + return permutations diff --git a/keymapper/mapping.py b/keymapper/mapping.py index ce18a1f5..e389bce2 100644 --- a/keymapper/mapping.py +++ b/keymapper/mapping.py @@ -30,28 +30,13 @@ import copy from keymapper.logger import logger from keymapper.paths import touch from keymapper.config import ConfigBase, config - - -def verify_key(key): - """Check if the key describes a tuple or tuples of (type, code, value). - - For combinations it could be e.g. ((1, 2, 1), (1, 3, 1)). - """ - if not isinstance(key, tuple): - raise ValueError(f'Expected keys to be a 3-tuple, but got {key}') - - if isinstance(key[0], tuple): - for sub_key in key: - verify_key(sub_key) - else: - if len(key) != 3: - raise ValueError(f'Expected key to be a 3-tuple, but got {key}') - if sum([not isinstance(value, int) for value in key]) != 0: - raise ValueError(f'Can only use numbers, but got {key}') +from keymapper.key import Key def split_key(key): """Take a key like "1,2,3" and return a 3-tuple of ints.""" + key = key.strip() + if ',' not in key: logger.error('Found invalid key: "%s"', key) return None @@ -79,12 +64,12 @@ def split_key(key): class Mapping(ConfigBase): """Contains and manages mappings and config of a single preset.""" def __init__(self): - self._mapping = {} + self._mapping = {} # a mapping of Key objects to strings self.changed = False super().__init__(fallback=config) def __iter__(self): - """Iterate over tuples of unique keycodes and their character.""" + """Iterate over Key objects and their character.""" return iter(self._mapping.items()) def __len__(self): @@ -105,33 +90,23 @@ class Mapping(ConfigBase): Parameters ---------- - new_key : int, int, int - the new key. (type, code, value). key as in hashmap-key - - 0: type, one of evdev.events, taken from the original source - event. Everything will be mapped to EV_KEY. - 1: The source keycode, what the mouse would report without any - modification. - 2. The value. 1 (down), 2 (up) or any - other value that the device reports. Gamepads use a continuous - space of values for joysticks and triggers. + new_key : Key character : string A single character known to xkb or linux. Examples: KP_1, Shift_L, a, B, BTN_LEFT. - previous_key : int, int, int - the previous key, same format as new_key + previous_key : Key or None + the previous key If not set, will not remove any previous mapping. If you recently used (1, 10, 1) for new_key and want to overwrite that with (1, 11, 1), provide (1, 5, 1) here. """ + if not isinstance(new_key, Key): + raise TypeError(f'Expected {new_key} to be a Key object') + if character is None: raise ValueError('Expected `character` not to be None') - verify_key(new_key) - if previous_key: - verify_key(previous_key) - logger.debug( '%s will map to %s, replacing %s', new_key, character, previous_key @@ -153,31 +128,16 @@ class Mapping(ConfigBase): Parameters ---------- - key : int, int, int - keycode : int - ev_type : int - one of evdev.events. codes may be the same for various - event types. - value : int - event value. Usually you want 1 (down) + key : Key """ - verify_key(key) - - if isinstance(key[0], tuple): - for permutation in itertools.permutations(key[:-1]): - permutation += (key[-1],) - if permutation in self._mapping: - logger.debug('%s will be cleared', permutation) - del self._mapping[permutation] - return + if not isinstance(key, Key): + raise TypeError('Expected key to be a Key object') - if self._mapping.get(key) is not None: - logger.debug('%s will be cleared', key) - del self._mapping[key] - self.changed = True - return - - logger.error('Unknown key %s', key) + for permutation in key.get_permutations(): + if permutation in self._mapping: + logger.debug('%s will be cleared', permutation) + del self._mapping[permutation] + self.changed = True def empty(self): """Remove all mappings.""" @@ -208,15 +168,17 @@ class Mapping(ConfigBase): return for key, character in preset_dict['mapping'].items(): - if '+' in key: - chunks = key.split('+') - key = tuple([split_key(chunk) for chunk in chunks]) - if None in key: - continue - else: - key = split_key(key) - if key is None: - continue + try: + key = Key(*[ + split_key(chunk) for chunk in key.split('+') + if chunk.strip() != '' + ]) + except ValueError as error: + logger.error(str(error)) + continue + + if None in key: + continue logger.spam('%s maps to %s', key, character) self._mapping[key] = character @@ -254,17 +216,13 @@ class Mapping(ConfigBase): json_ready_mapping = {} # tuple keys are not possible in json, encode them as string for key, value in self._mapping.items(): - if isinstance(key[0], tuple): - # combinations to "1,2,1+1,3,1" - new_key = '+'.join([ - ','.join([ - str(value) - for value in sub_key - ]) - for sub_key in key + new_key = '+'.join([ + ','.join([ + str(value) + for value in sub_key ]) - else: - new_key = ','.join([str(value) for value in key]) + for sub_key in key + ]) json_ready_mapping[new_key] = value preset_dict['mapping'] = json_ready_mapping @@ -278,23 +236,12 @@ class Mapping(ConfigBase): Parameters ---------- - key : int, int, int - keycode : int - ev_type : int - one of evdev.events. codes may be the same for various - event types. - value : int - event value. Usually you want 1 (down) - - Or a tuple of multiple of those. Checks any possible permutation - with the last key being always at the end, to work well with - combinations. + key : Key """ - if isinstance(key[0], tuple): - for permutation in itertools.permutations(key[:-1]): - permutation += (key[-1],) - existing = self._mapping.get(permutation) - if existing is not None: - return existing - - return self._mapping.get(key) + if not isinstance(key, Key): + raise TypeError('Expected key to be a Key object') + + for permutation in key.get_permutations(): + existing = self._mapping.get(permutation) + if existing is not None: + return existing diff --git a/tests/testcases/test_daemon.py b/tests/testcases/test_daemon.py index 849d8c1e..196b4890 100644 --- a/tests/testcases/test_daemon.py +++ b/tests/testcases/test_daemon.py @@ -34,6 +34,7 @@ from keymapper.state import custom_mapping, system_mapping from keymapper.config import config from keymapper.getdevices import get_devices from keymapper.paths import get_preset_path +from keymapper.key import Key from keymapper.daemon import Daemon, get_dbus_interface, BUS_NAME from tests.test import cleanup, uinput_write_history_pipe, InputEvent, \ @@ -125,8 +126,8 @@ class TestDaemon(unittest.TestCase): device = 'device 2' - custom_mapping.change((*ev_1, 1), 'a') - custom_mapping.change((*ev_2, -1), 'b') + custom_mapping.change(Key(*ev_1, 1), 'a') + custom_mapping.change(Key(*ev_2, -1), 'b') system_mapping.clear() system_mapping._set('a', keycode_to_1) @@ -186,7 +187,7 @@ class TestDaemon(unittest.TestCase): device = '9876 name' # this test only makes sense if this device is unknown yet self.assertIsNone(get_devices().get(device)) - custom_mapping.change((*ev, 1), 'a') + custom_mapping.change(Key(*ev, 1), 'a') system_mapping.clear() system_mapping._set('a', keycode_to) preset = 'foo' @@ -231,7 +232,7 @@ class TestDaemon(unittest.TestCase): path = get_preset_path(device, preset) - custom_mapping.change(event, to_name) + custom_mapping.change(Key(event), to_name) custom_mapping.save(path) system_mapping.clear() diff --git a/tests/testcases/test_injector.py b/tests/testcases/test_injector.py index 28b2f12b..1949a59f 100644 --- a/tests/testcases/test_injector.py +++ b/tests/testcases/test_injector.py @@ -27,10 +27,11 @@ import evdev from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X, BTN_A from keymapper.dev.injector import is_numlock_on, set_numlock, \ - ensure_numlock, KeycodeInjector, store_permutations, is_in_capabilities + ensure_numlock, KeycodeInjector, is_in_capabilities from keymapper.state import custom_mapping, system_mapping from keymapper.mapping import Mapping from keymapper.config import config +from keymapper.key import Key from keymapper.dev.macros import parse from tests.test import InputEvent, pending_events, fixtures, \ @@ -76,16 +77,16 @@ class TestInjector(unittest.TestCase): } mapping = Mapping() - mapping.change((EV_KEY, 80, 1), 'a') + mapping.change(Key(EV_KEY, 80, 1), 'a') macro_code = 'r(2, m(sHiFt_l, r(2, k(1).k(2))))' macro = parse(macro_code, mapping) - mapping.change((EV_KEY, 60, 111), macro_code) + mapping.change(Key(EV_KEY, 60, 111), macro_code) # going to be ignored, because EV_REL cannot be mapped, that's # mouse movements. - mapping.change((EV_REL, 1234, 3), 'b') + mapping.change(Key(EV_REL, 1234, 3), 'b') a = system_mapping.get('a') shift_l = system_mapping.get('ShIfT_L') @@ -114,7 +115,7 @@ class TestInjector(unittest.TestCase): def test_grab(self): # path is from the fixtures - custom_mapping.change((EV_KEY, 10, 1), 'a') + custom_mapping.change(Key(EV_KEY, 10, 1), 'a') self.injector = KeycodeInjector('device 1', custom_mapping) path = '/dev/input/event10' @@ -128,7 +129,7 @@ class TestInjector(unittest.TestCase): def test_fail_grab(self): self.make_it_fail = 10 - custom_mapping.change((EV_KEY, 10, 1), 'a') + custom_mapping.change(Key(EV_KEY, 10, 1), 'a') self.injector = KeycodeInjector('device 1', custom_mapping) path = '/dev/input/event10' @@ -145,7 +146,7 @@ class TestInjector(unittest.TestCase): def test_prepare_device_1(self): # according to the fixtures, /dev/input/event30 can do ABS_HAT0X - custom_mapping.change((EV_ABS, ABS_HAT0X, 1), 'a') + custom_mapping.change(Key(EV_ABS, ABS_HAT0X, 1), 'a') self.injector = KeycodeInjector('foobar', custom_mapping) _prepare_device = self.injector._prepare_device @@ -153,7 +154,7 @@ class TestInjector(unittest.TestCase): self.assertIsNotNone(_prepare_device('/dev/input/event30')[0]) def test_prepare_device_non_existing(self): - custom_mapping.change((EV_ABS, ABS_HAT0X, 1), 'a') + custom_mapping.change(Key(EV_ABS, ABS_HAT0X, 1), 'a') self.injector = KeycodeInjector('foobar', custom_mapping) _prepare_device = self.injector._prepare_device @@ -224,7 +225,7 @@ class TestInjector(unittest.TestCase): def test_skip_unused_device(self): # skips a device because its capabilities are not used in the mapping - custom_mapping.change((EV_KEY, 10, 1), 'a') + custom_mapping.change(Key(EV_KEY, 10, 1), 'a') self.injector = KeycodeInjector('device 1', custom_mapping) path = '/dev/input/event11' device, abs_to_rel = self.injector._prepare_device(path) @@ -332,12 +333,12 @@ class TestInjector(unittest.TestCase): numlock_before = is_numlock_on() - combination = ((EV_KEY, 8, 1), (EV_KEY, 9, 1)) + combination = Key((EV_KEY, 8, 1), (EV_KEY, 9, 1)) custom_mapping.change(combination, 'k(KEY_Q).k(w)') - custom_mapping.change((EV_ABS, ABS_HAT0X, -1), 'a') + custom_mapping.change(Key(EV_ABS, ABS_HAT0X, -1), 'a') # one mapping that is unknown in the system_mapping on purpose input_b = 10 - custom_mapping.change((EV_KEY, input_b, 1), 'b') + custom_mapping.change(Key(EV_KEY, input_b, 1), 'b') # stuff the custom_mapping outputs (except for the unknown b) system_mapping.clear() @@ -429,42 +430,13 @@ class TestInjector(unittest.TestCase): numlock_after = is_numlock_on() self.assertEqual(numlock_before, numlock_after) - def test_store_permutations(self): - target = {} - - store_permutations(target, ((1,), (2,), (3,), (4,)), 1234) - self.assertEqual(len(target), 6) - self.assertEqual(target[((1,), (2,), (3,), (4,))], 1234) - self.assertEqual(target[((1,), (3,), (2,), (4,))], 1234) - self.assertEqual(target[((2,), (1,), (3,), (4,))], 1234) - self.assertEqual(target[((2,), (3,), (1,), (4,))], 1234) - self.assertEqual(target[((3,), (1,), (2,), (4,))], 1234) - self.assertEqual(target[((3,), (2,), (1,), (4,))], 1234) - - store_permutations(target, ((1,), (2,)), 5678) - self.assertEqual(len(target), 7) - self.assertEqual(target[((1,), (2,))], 5678) - - store_permutations(target, ((1,),), 3456) - self.assertEqual(len(target), 8) - self.assertEqual(target[((1,),)], 3456) - - store_permutations(target, (1,), 7890) - self.assertEqual(len(target), 9) - self.assertEqual(target[(1,)], 7890) - - # only accepts tuples, because key-mapper always uses tuples - # for this stuff - store_permutations(target, 1, 1357) - self.assertEqual(len(target), 9) - def test_store_permutations_for_macros(self): mapping = Mapping() ev_1 = (EV_KEY, 41, 1) ev_2 = (EV_KEY, 42, 1) ev_3 = (EV_KEY, 43, 1) # a combination - mapping.change((ev_1, ev_2, ev_3), 'k(a)') + mapping.change(Key(ev_1, ev_2, ev_3), 'k(a)') self.injector = KeycodeInjector('device 1', mapping) history = [] @@ -496,17 +468,17 @@ class TestInjector(unittest.TestCase): ev_2 = (EV_KEY, 42, 1) ev_3 = (EV_KEY, 43, 1) ev_4 = (EV_KEY, 44, 1) - mapping.change(ev_1, 'a') + mapping.change(Key(ev_1), 'a') # a combination - mapping.change((ev_2, ev_3, ev_4), 'b') - self.assertEqual(mapping.get_character((ev_2, ev_3, ev_4)), 'b') + mapping.change(Key(ev_2, ev_3, ev_4), 'b') + self.assertEqual(mapping.get_character(Key(ev_2, ev_3, ev_4)), 'b') system_mapping.clear() system_mapping._set('a', 51) system_mapping._set('b', 52) injector = KeycodeInjector('device 1', mapping) - self.assertEqual(injector._key_to_code.get(ev_1), 51) + self.assertEqual(injector._key_to_code.get((ev_1,)), 51) # permutations to make matching combinations easier self.assertEqual(injector._key_to_code.get((ev_2, ev_3, ev_4)), 52) self.assertEqual(injector._key_to_code.get((ev_3, ev_2, ev_4)), 52) diff --git a/tests/testcases/test_integration.py b/tests/testcases/test_integration.py index 3b5d4bde..e2727bda 100644 --- a/tests/testcases/test_integration.py +++ b/tests/testcases/test_integration.py @@ -42,6 +42,7 @@ from keymapper.config import config, WHEEL, MOUSE from keymapper.dev.reader import keycode_reader from keymapper.gtk.row import to_string, HOLDING, IDLE from keymapper.dev import permissions +from keymapper.key import Key from tests.test import tmp, pending_events, InputEvent, \ uinput_write_history_pipe, cleanup @@ -206,9 +207,9 @@ class TestIntegration(unittest.TestCase): def test_select_device(self): # creates a new empty preset when no preset exists for the device self.window.on_select_device(FakeDropdown('device 1')) - custom_mapping.change((EV_KEY, 50, 1), 'q') - custom_mapping.change((EV_KEY, 51, 1), 'u') - custom_mapping.change((EV_KEY, 52, 1), 'x') + custom_mapping.change(Key(EV_KEY, 50, 1), 'q') + custom_mapping.change(Key(EV_KEY, 51, 1), 'u') + custom_mapping.change(Key(EV_KEY, 52, 1), 'x') self.assertEqual(len(custom_mapping), 3) self.window.on_select_device(FakeDropdown('device 2')) self.assertEqual(len(custom_mapping), 0) @@ -227,14 +228,14 @@ class TestIntegration(unittest.TestCase): def test_row_keycode_to_string(self): # not an integration test, but I have all the row tests here already - self.assertEqual(to_string((EV_KEY, evdev.ecodes.KEY_9, 1)), '9') - self.assertEqual(to_string((EV_KEY, evdev.ecodes.KEY_SEMICOLON, 1)), 'SEMICOLON') - self.assertEqual(to_string((EV_ABS, evdev.ecodes.ABS_HAT0X, -1)), 'ABS_HAT0X L') - self.assertEqual(to_string((EV_ABS, evdev.ecodes.ABS_HAT0X, 1)), 'ABS_HAT0X R') - self.assertEqual(to_string((EV_KEY, evdev.ecodes.BTN_A, 1)), 'BTN_A') + self.assertEqual(to_string(Key(EV_KEY, evdev.ecodes.KEY_9, 1)), '9') + self.assertEqual(to_string(Key(EV_KEY, evdev.ecodes.KEY_SEMICOLON, 1)), 'SEMICOLON') + self.assertEqual(to_string(Key(EV_ABS, evdev.ecodes.ABS_HAT0X, -1)), 'ABS_HAT0X L') + self.assertEqual(to_string(Key(EV_ABS, evdev.ecodes.ABS_HAT0X, 1)), 'ABS_HAT0X R') + self.assertEqual(to_string(Key(EV_KEY, evdev.ecodes.BTN_A, 1)), 'BTN_A') # combinations - self.assertEqual(to_string(( + self.assertEqual(to_string(Key( (EV_KEY, evdev.ecodes.BTN_A, 1), (EV_KEY, evdev.ecodes.BTN_B, 1), (EV_KEY, evdev.ecodes.BTN_C, 1) @@ -246,21 +247,21 @@ class TestIntegration(unittest.TestCase): row = rows[0] - row.set_new_keycode(None) - self.assertIsNone(row.get_keycode()) + row.set_new_key(None) + self.assertIsNone(row.get_key()) self.assertEqual(len(custom_mapping), 0) self.assertEqual(row.keycode_input.get_label(), 'click here') - row.set_new_keycode((EV_KEY, 30, 1)) + row.set_new_key(Key(EV_KEY, 30, 1)) self.assertEqual(len(custom_mapping), 0) - self.assertEqual(row.get_keycode(), (EV_KEY, 30, 1)) + self.assertEqual(row.get_key(), (EV_KEY, 30, 1)) # this is KEY_A in linux/input-event-codes.h, # but KEY_ is removed from the text self.assertEqual(row.keycode_input.get_label(), 'A') - row.set_new_keycode((EV_KEY, 30, 1)) + row.set_new_key(Key(EV_KEY, 30, 1)) self.assertEqual(len(custom_mapping), 0) - self.assertEqual(row.get_keycode(), (EV_KEY, 30, 1)) + self.assertEqual(row.get_key(), (EV_KEY, 30, 1)) time.sleep(0.1) gtk_iteration() @@ -273,9 +274,9 @@ class TestIntegration(unittest.TestCase): gtk_iteration() self.assertEqual(len(self.window.get('key_list').get_children()), 2) - self.assertEqual(custom_mapping.get_character((EV_KEY, 30, 1)), 'Shift_L') + self.assertEqual(custom_mapping.get_character(Key(EV_KEY, 30, 1)), 'Shift_L') self.assertEqual(row.get_character(), 'Shift_L') - self.assertEqual(row.get_keycode(), (EV_KEY, 30, 1)) + self.assertEqual(row.get_key(), (EV_KEY, 30, 1)) def change_empty_row(self, key, char, code_first=True, expect_success=True): """Modify the one empty row that always exists. @@ -284,9 +285,7 @@ class TestIntegration(unittest.TestCase): Parameters ---------- - key : int, int, int - type, code, value, - or a tuple of multiple of those + key : Key or None code_first : boolean If True, the code is entered and then the character. If False, the character is entered first. @@ -304,7 +303,7 @@ class TestIntegration(unittest.TestCase): # find the empty row rows = self.get_rows() row = rows[-1] - self.assertIsNone(row.get_keycode()) + self.assertIsNone(row.get_key()) self.assertEqual(row.character_input.get_text(), '') self.assertNotIn('changed', row.get_style_context().list_classes()) self.assertEqual(row.state, IDLE) @@ -322,7 +321,7 @@ class TestIntegration(unittest.TestCase): self.window.window.set_focus(row.keycode_input) gtk_iteration() - self.assertIsNone(row.get_keycode()) + self.assertIsNone(row.get_key()) self.assertEqual(row.keycode_input.get_label(), 'press key') if key: @@ -330,11 +329,8 @@ class TestIntegration(unittest.TestCase): # but by sending an event. Events should be consumed 30 times # per second, so sleep a bit more than 0.033ms each time # press down all the keys of a combination - if isinstance(key[0], tuple): - for sub_key in key: - keycode_reader._pipe[1].send(InputEvent(*sub_key)) - else: - keycode_reader._pipe[1].send(InputEvent(*key)) + for sub_key in key: + keycode_reader._pipe[1].send(InputEvent(*sub_key)) # make the window consume the keycode time.sleep(0.05) @@ -346,11 +342,8 @@ class TestIntegration(unittest.TestCase): self.assertTrue(row.keycode_input.is_focus()) # release all the keys - if isinstance(key[0], tuple): - for sub_key in key: - keycode_reader._pipe[1].send(InputEvent(*sub_key[:2], 0)) - else: - keycode_reader._pipe[1].send(InputEvent(*key[:2], 0)) + for sub_key in key: + keycode_reader._pipe[1].send(InputEvent(*sub_key[:2], 0)) # make the window consume the keycode time.sleep(0.05) @@ -361,14 +354,14 @@ class TestIntegration(unittest.TestCase): self.assertEqual(row.state, IDLE) if expect_success: - self.assertEqual(row.get_keycode(), key) + self.assertEqual(row.get_key(), key) css_classes = row.get_style_context().list_classes() self.assertIn('changed', css_classes) self.assertEqual(row.keycode_input.get_label(), to_string(key)) self.assertFalse(row.keycode_input.is_focus()) if not expect_success: - self.assertIsNone(row.get_keycode()) + self.assertIsNone(row.get_key()) self.assertIsNone(row.get_character()) css_classes = row.get_style_context().list_classes() self.assertNotIn('changed', css_classes) @@ -388,8 +381,8 @@ class TestIntegration(unittest.TestCase): # how many rows there should be in the end num_rows_target = 3 - ev_1 = (EV_KEY, 10, 1) - ev_2 = (EV_ABS, evdev.ecodes.ABS_HAT0X, -1) + ev_1 = Key(EV_KEY, 10, 1) + ev_2 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, -1) """edit""" @@ -440,10 +433,10 @@ class TestIntegration(unittest.TestCase): def test_hat0x(self): # it should be possible to add all of them - ev_1 = (EV_ABS, evdev.ecodes.ABS_HAT0X, -1) - ev_2 = (EV_ABS, evdev.ecodes.ABS_HAT0X, 1) - ev_3 = (EV_ABS, evdev.ecodes.ABS_HAT0Y, -1) - ev_4 = (EV_ABS, evdev.ecodes.ABS_HAT0Y, 1) + ev_1 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, -1) + ev_2 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, 1) + ev_3 = Key(EV_ABS, evdev.ecodes.ABS_HAT0Y, -1) + ev_4 = Key(EV_ABS, evdev.ecodes.ABS_HAT0Y, 1) self.change_empty_row(ev_1, 'a') self.change_empty_row(ev_2, 'b') @@ -471,20 +464,20 @@ class TestIntegration(unittest.TestCase): def test_combination(self): # it should be possible to write a key combination - ev_1 = (EV_KEY, evdev.ecodes.KEY_A, 1) - ev_2 = (EV_ABS, evdev.ecodes.ABS_HAT0X, 1) - ev_3 = (EV_KEY, evdev.ecodes.KEY_C, 1) - ev_4 = (EV_ABS, evdev.ecodes.ABS_HAT0X, -1) - combination_1 = (ev_1, ev_2, ev_3) - combination_2 = (ev_2, ev_1, ev_3) + ev_1 = Key(EV_KEY, evdev.ecodes.KEY_A, 1) + ev_2 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, 1) + ev_3 = Key(EV_KEY, evdev.ecodes.KEY_C, 1) + ev_4 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, -1) + combination_1 = Key(ev_1, ev_2, ev_3) + combination_2 = Key(ev_2, ev_1, ev_3) # same as 1, but different D-Pad direction - combination_3 = (ev_1, ev_4, ev_3) - combination_4 = (ev_4, ev_1, ev_3) + combination_3 = Key(ev_1, ev_4, ev_3) + combination_4 = Key(ev_4, ev_1, ev_3) # same as 1, but the last key is different - combination_5 = (ev_1, ev_3, ev_2) - combination_6 = (ev_3, ev_1, ev_2) + combination_5 = Key(ev_1, ev_3, ev_2) + combination_6 = Key(ev_3, ev_1, ev_2) self.change_empty_row(combination_1, 'a') self.assertEqual(custom_mapping.get_character(combination_1), 'a') @@ -535,8 +528,8 @@ class TestIntegration(unittest.TestCase): """Comprehensive test for rows 2.""" # sleeps are added to be able to visually follow and debug the test. # add two rows by modifiying the one empty row that exists - row_1 = self.change_empty_row((EV_KEY, 10, 1), 'a') - row_2 = self.change_empty_row((EV_KEY, 11, 1), 'b') + row_1 = self.change_empty_row(Key(EV_KEY, 10, 1), 'a') + row_2 = self.change_empty_row(Key(EV_KEY, 11, 1), 'b') row_3 = self.change_empty_row(None, 'c') # no empty row added because one is unfinished @@ -544,7 +537,7 @@ class TestIntegration(unittest.TestCase): gtk_iteration() self.assertEqual(len(self.get_rows()), 3) - self.assertEqual(custom_mapping.get_character((EV_KEY, 11, 1)), 'b') + self.assertEqual(custom_mapping.get_character(Key(EV_KEY, 11, 1)), 'b') def remove(row, code, char, num_rows_after): """Remove a row by clicking the delete button. @@ -560,13 +553,13 @@ class TestIntegration(unittest.TestCase): after deleting, how many rows are expected to still be there """ if code is not None and char is not None: - self.assertEqual(custom_mapping.get_character((EV_KEY, code, 1)), char) + self.assertEqual(custom_mapping.get_character(Key(EV_KEY, code, 1)), char) self.assertEqual(row.get_character(), char) if code is None: - self.assertIsNone(row.get_keycode()) + self.assertIsNone(row.get_key()) else: - self.assertEqual(row.get_keycode(), (EV_KEY, code, 1)) + self.assertEqual(row.get_key(), Key(EV_KEY, code, 1)) row.on_delete_button_clicked() time.sleep(0.2) @@ -575,9 +568,10 @@ class TestIntegration(unittest.TestCase): # if a reference to the row is held somewhere and it is # accidentally used again, make sure to not provide any outdated # information that is supposed to be deleted - self.assertIsNone(row.get_keycode()) + self.assertIsNone(row.get_key()) self.assertIsNone(row.get_character()) - self.assertIsNone(custom_mapping.get_character((EV_KEY, code, 1))) + if code is not None: + self.assertIsNone(custom_mapping.get_character(Key(EV_KEY, code, 1))) self.assertEqual(len(self.get_rows()), num_rows_after) remove(row_1, 10, 'a', 2) @@ -588,17 +582,17 @@ class TestIntegration(unittest.TestCase): remove(row_3, None, 'c', 1) def test_rename_and_save(self): - custom_mapping.change((EV_KEY, 14, 1), 'a', None) + custom_mapping.change(Key(EV_KEY, 14, 1), 'a', None) self.assertEqual(self.window.selected_preset, 'new preset') self.window.on_save_preset_clicked(None) - self.assertEqual(custom_mapping.get_character((EV_KEY, 14, 1)), 'a') + self.assertEqual(custom_mapping.get_character(Key(EV_KEY, 14, 1)), 'a') - custom_mapping.change((EV_KEY, 14, 1), 'b', None) + custom_mapping.change(Key(EV_KEY, 14, 1), 'b', None) self.window.get('preset_name_input').set_text('asdf') self.window.on_save_preset_clicked(None) self.assertEqual(self.window.selected_preset, 'asdf') self.assertTrue(os.path.exists(f'{CONFIG_PATH}/presets/device 1/asdf.json')) - self.assertEqual(custom_mapping.get_character((EV_KEY, 14, 1)), 'b') + self.assertEqual(custom_mapping.get_character(Key(EV_KEY, 14, 1)), 'b') error_icon = self.window.get('error_status_icon') status = self.window.get('status_bar') @@ -610,20 +604,20 @@ class TestIntegration(unittest.TestCase): status = self.window.get('status_bar') error_icon = self.window.get('error_status_icon') - custom_mapping.change((EV_KEY, 9, 1), 'k(1))', None) + custom_mapping.change(Key(EV_KEY, 9, 1), 'k(1))', None) self.window.on_save_preset_clicked(None) tooltip = status.get_tooltip_text().lower() self.assertIn('brackets', tooltip) self.assertTrue(error_icon.get_visible()) - custom_mapping.change((EV_KEY, 9, 1), 'k(1)', None) + custom_mapping.change(Key(EV_KEY, 9, 1), 'k(1)', None) self.window.on_save_preset_clicked(None) tooltip = status.get_tooltip_text().lower() self.assertNotIn('brackets', tooltip) self.assertIn('saved', tooltip) self.assertFalse(error_icon.get_visible()) - self.assertEqual(custom_mapping.get_character((EV_KEY, 9, 1)), 'k(1)') + self.assertEqual(custom_mapping.get_character(Key(EV_KEY, 9, 1)), 'k(1)') def test_select_device_and_preset(self): # created on start because the first device is selected and some empty @@ -653,7 +647,7 @@ class TestIntegration(unittest.TestCase): gtk_iteration() self.assertEqual(self.window.selected_preset, 'new preset') self.assertFalse(os.path.exists(f'{CONFIG_PATH}/presets/device 1/abc 123.json')) - custom_mapping.change((EV_KEY, 10, 1), '1', None) + custom_mapping.change(Key(EV_KEY, 10, 1), '1', None) self.window.on_save_preset_clicked(None) gtk_iteration() self.assertEqual(self.window.selected_preset, 'abc 123') @@ -706,7 +700,7 @@ class TestIntegration(unittest.TestCase): keycode_from = 9 keycode_to = 200 - self.change_empty_row((EV_KEY, keycode_from, 1), 'a') + self.change_empty_row(Key(EV_KEY, keycode_from, 1), 'a') system_mapping.clear() system_mapping._set('a', keycode_to) @@ -746,7 +740,7 @@ class TestIntegration(unittest.TestCase): keycode_from = 16 keycode_to = 90 - self.change_empty_row((EV_KEY, keycode_from, 1), 't') + self.change_empty_row(Key(EV_KEY, keycode_from, 1), 't') system_mapping.clear() system_mapping._set('t', keycode_to) diff --git a/tests/testcases/test_mapping.py b/tests/testcases/test_mapping.py index 14816680..f3670f0a 100644 --- a/tests/testcases/test_mapping.py +++ b/tests/testcases/test_mapping.py @@ -24,10 +24,11 @@ import unittest import json from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X, KEY_A -from keymapper.mapping import Mapping, verify_key +from keymapper.mapping import Mapping from keymapper.state import SystemMapping, XMODMAP_FILENAME from keymapper.config import config from keymapper.paths import get_preset_path +from keymapper.key import Key from tests.test import tmp, cleanup @@ -133,12 +134,12 @@ class TestMapping(unittest.TestCase): # setting mapping.whatever does not overwrite the mapping # after saving. It should be ignored. - self.mapping.change((EV_KEY, 81, 1), 'a') + self.mapping.change(Key(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.assertEqual(self.mapping.get_character(Key(EV_KEY, 81, 1)), 'a') self.assertIsNone(self.mapping.get('mapping.a')) self.assertFalse(self.mapping.changed) @@ -156,8 +157,8 @@ class TestMapping(unittest.TestCase): self.assertEqual(self.mapping.get('d.e.f'), 3) def test_clone(self): - ev_1 = (EV_KEY, 1, 1) - ev_2 = (EV_KEY, 2, 0) + ev_1 = Key(EV_KEY, 1, 1) + ev_2 = Key(EV_KEY, 2, 0) mapping1 = Mapping() mapping1.change(ev_1, 'a') @@ -170,17 +171,17 @@ class TestMapping(unittest.TestCase): self.assertEqual(mapping2.get_character(ev_1), 'a') self.assertIsNone(mapping2.get_character(ev_2)) - self.assertIsNone(mapping2.get_character((EV_KEY, 2, 3))) - self.assertIsNone(mapping2.get_character((EV_KEY, 1, 3))) + self.assertIsNone(mapping2.get_character(Key(EV_KEY, 2, 3))) + self.assertIsNone(mapping2.get_character(Key(EV_KEY, 1, 3))) def test_save_load(self): - one = (EV_KEY, 10, 1) - two = (EV_KEY, 11, 1) - three = (EV_KEY, 12, 1) + one = Key(EV_KEY, 10, 1) + two = Key(EV_KEY, 11, 1) + three = Key(EV_KEY, 12, 1) self.mapping.change(one, '1') self.mapping.change(two, '2') - self.mapping.change((two, three), '3') + self.mapping.change(Key(two, three), '3') self.mapping._config['foo'] = 'bar' self.mapping.save(get_preset_path('device 1', 'test')) @@ -194,7 +195,7 @@ class TestMapping(unittest.TestCase): self.assertEqual(len(loaded), 3) self.assertEqual(loaded.get_character(one), '1') self.assertEqual(loaded.get_character(two), '2') - self.assertEqual(loaded.get_character((two, three)), '3') + self.assertEqual(loaded.get_character(Key(two, three)), '3') self.assertEqual(loaded._config['foo'], 'bar') def test_save_load_2(self): @@ -209,10 +210,8 @@ class TestMapping(unittest.TestCase): f'{EV_ABS},{ABS_HAT0X},-1': 'b', f'{EV_ABS},1,1+{EV_ABS},2,-1+{EV_ABS},3,1': 'c', # ignored because broken - f'3,1,1+': 'd', f'3,1,1,2': 'e', f'3': 'e', - f'+3,1,2': 'f', f',,+3,1,2': 'g', f'': 'h', } @@ -221,19 +220,21 @@ class TestMapping(unittest.TestCase): loaded = Mapping() loaded.load(get_preset_path('device 1', 'test')) self.assertEqual(len(loaded), 3) - self.assertEqual(loaded.get_character((EV_KEY, 3, 1)), 'a') - self.assertEqual(loaded.get_character((EV_ABS, ABS_HAT0X, -1)), 'b') - self.assertEqual(loaded.get_character( - ((EV_ABS, 1, 1), (EV_ABS, 2, -1), (EV_ABS, 3, 1)) + self.assertEqual(loaded.get_character(Key(EV_KEY, 3, 1)), 'a') + self.assertEqual(loaded.get_character(Key(EV_ABS, ABS_HAT0X, -1)), 'b') + self.assertEqual(loaded.get_character(Key( + (EV_ABS, 1, 1), + (EV_ABS, 2, -1), + Key(EV_ABS, 3, 1)) ), 'c') def test_change(self): # the reader would not report values like 111 or 222, only 1 or -1. # the mapping just does what it is told, so it accepts them. - ev_1 = (EV_KEY, 1, 111) - ev_2 = (EV_KEY, 1, 222) - ev_3 = (EV_KEY, 2, 111) - ev_4 = (EV_ABS, 1, 111) + ev_1 = Key(EV_KEY, 1, 111) + ev_2 = Key(EV_KEY, 1, 222) + ev_3 = Key(EV_KEY, 2, 111) + ev_4 = Key(EV_ABS, 1, 111) # 1 is not assigned yet, ignore it self.mapping.change(ev_1, 'a', ev_2) @@ -265,13 +266,13 @@ class TestMapping(unittest.TestCase): self.assertEqual(len(self.mapping), 2) def test_combinations(self): - ev_1 = (EV_KEY, 1, 111) - ev_2 = (EV_KEY, 1, 222) - ev_3 = (EV_KEY, 2, 111) - ev_4 = (EV_ABS, 1, 111) - combi_1 = (ev_1, ev_2, ev_3) - combi_2 = (ev_2, ev_1, ev_3) - combi_3 = (ev_1, ev_2, ev_4) + ev_1 = Key(EV_KEY, 1, 111) + ev_2 = Key(EV_KEY, 1, 222) + ev_3 = Key(EV_KEY, 2, 111) + ev_4 = Key(EV_ABS, 1, 111) + combi_1 = Key(ev_1, ev_2, ev_3) + combi_2 = Key(ev_2, ev_1, ev_3) + combi_3 = Key(ev_1, ev_2, ev_4) self.mapping.change(combi_1, 'a') self.assertEqual(self.mapping.get_character(combi_1), 'a') @@ -293,50 +294,55 @@ class TestMapping(unittest.TestCase): def test_clear(self): # does nothing - self.mapping.clear((EV_KEY, 40, 1)) + ev_1 = Key(EV_KEY, 40, 1) + ev_2 = Key(EV_KEY, 30, 1) + ev_3 = Key(EV_KEY, 20, 1) + ev_4 = Key(EV_KEY, 10, 1) + + self.mapping.clear(ev_1) self.assertFalse(self.mapping.changed) self.assertEqual(len(self.mapping), 0) - self.mapping._mapping[(EV_KEY, 40, 1)] = 'b' + self.mapping._mapping[ev_1] = 'b' self.assertEqual(len(self.mapping), 1) - self.mapping.clear((EV_KEY, 40, 1)) + self.mapping.clear(ev_1) self.assertEqual(len(self.mapping), 0) self.assertTrue(self.mapping.changed) - self.mapping.change((EV_KEY, 10, 1), 'KP_1', None) + self.mapping.change(ev_4, 'KP_1', None) self.assertTrue(self.mapping.changed) - self.mapping.change((EV_KEY, 20, 1), 'KP_2', None) - self.mapping.change((EV_KEY, 30, 1), 'KP_3', None) + self.mapping.change(ev_3, 'KP_2', None) + self.mapping.change(ev_2, 'KP_3', None) self.assertEqual(len(self.mapping), 3) - self.mapping.clear((EV_KEY, 20, 1)) + self.mapping.clear(ev_3) self.assertEqual(len(self.mapping), 2) - self.assertEqual(self.mapping.get_character((EV_KEY, 10, 1)), 'KP_1') - self.assertIsNone(self.mapping.get_character((EV_KEY, 20, 1))) - self.assertEqual(self.mapping.get_character((EV_KEY, 30, 1)), 'KP_3') + self.assertEqual(self.mapping.get_character(ev_4), 'KP_1') + self.assertIsNone(self.mapping.get_character(ev_3)) + self.assertEqual(self.mapping.get_character(ev_2), 'KP_3') def test_empty(self): - self.mapping.change((EV_KEY, 10, 1), '1') - self.mapping.change((EV_KEY, 11, 1), '2') - self.mapping.change((EV_KEY, 12, 1), '3') + self.mapping.change(Key(EV_KEY, 10, 1), '1') + self.mapping.change(Key(EV_KEY, 11, 1), '2') + self.mapping.change(Key(EV_KEY, 12, 1), '3') self.assertEqual(len(self.mapping), 3) self.mapping.empty() self.assertEqual(len(self.mapping), 0) def test_verify_key(self): - self.assertRaises(ValueError, lambda: verify_key(1)) - self.assertRaises(ValueError, lambda: verify_key(None)) - self.assertRaises(ValueError, lambda: verify_key([1])) - self.assertRaises(ValueError, lambda: verify_key((1,))) - self.assertRaises(ValueError, lambda: verify_key((1, 2))) - self.assertRaises(ValueError, lambda: verify_key(('1', '2', '3'))) - self.assertRaises(ValueError, lambda: verify_key('1')) - self.assertRaises(ValueError, lambda: verify_key('(1,2,3)')) - self.assertRaises(ValueError, lambda: verify_key(((1, 2, 3), (1, 2, '3')))) - self.assertRaises(ValueError, lambda: verify_key(((1, 2, 3), (1, 2, 3), None))) + 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 - verify_key(((1, 2, 3), (1, 2, 3))) - verify_key((1, 2, 3)) + Key((1, 2, 3), (1, 2, 3)) + Key((1, 2, 3)) if __name__ == "__main__":