pressed keys represented by class

This commit is contained in:
sezanzeb 2020-12-31 21:47:56 +01:00
parent d09f02d573
commit b382a04be5
10 changed files with 389 additions and 341 deletions

View File

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

View File

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

View File

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

View File

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

128
keymapper/key.py Normal file
View File

@ -0,0 +1,128 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2020 sezanzeb <proxima@hip70890b.de>
#
# 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 <https://www.gnu.org/licenses/>.
"""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

View File

@ -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 not isinstance(key, Key):
raise TypeError('Expected key to be a Key object')
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 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
if not isinstance(key, Key):
raise TypeError('Expected key to be a Key object')
return self._mapping.get(key)
for permutation in key.get_permutations():
existing = self._mapping.get(permutation)
if existing is not None:
return existing

View File

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

View File

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

View File

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

View File

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