pressed keys represented by class

pull/14/head
sezanzeb 4 years ago
parent d09f02d573
commit b382a04be5

@ -98,30 +98,6 @@ def ensure_numlock(func):
return wrapped 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): def is_in_capabilities(key, capabilities):
"""Are this key or all of its sub keys in the capabilities?""" """Are this key or all of its sub keys in the capabilities?"""
if isinstance(key[0], tuple): if isinstance(key[0], tuple):
@ -153,6 +129,7 @@ class KeycodeInjector:
---------- ----------
device : string device : string
the name of the device as available in get_device the name of the device as available in get_device
mapping : Mapping
""" """
self.device = device self.device = device
self.mapping = mapping self.mapping = mapping
@ -168,7 +145,13 @@ class KeycodeInjector:
self.abs_state = [0, 0, 0, 0] self.abs_state = [0, 0, 0, 0]
def _map_keys_to_codes(self): 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 = {} key_to_code = {}
for key, output in self.mapping: for key, output in self.mapping:
if is_this_a_macro(output): if is_this_a_macro(output):
@ -179,7 +162,8 @@ class KeycodeInjector:
logger.error('Don\'t know what %s is', output) logger.error('Don\'t know what %s is', output)
continue 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 return key_to_code
@ -368,7 +352,8 @@ class KeycodeInjector:
if macro is None: if macro is None:
continue continue
store_permutations(macros, key, macro) for permutation in key.get_permutations():
macros[permutation.keys] = macro
if len(macros) == 0: if len(macros) == 0:
logger.debug('No macros configured') logger.debug('No macros configured')

@ -31,6 +31,7 @@ from evdev.events import EV_KEY, EV_ABS
from keymapper.logger import logger from keymapper.logger import logger
from keymapper.util import sign from keymapper.util import sign
from keymapper.key import Key
from keymapper.getdevices import get_devices, refresh_devices from keymapper.getdevices import get_devices, refresh_devices
from keymapper.dev.keycode_mapper import should_map_event_as_btn 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. Does not serve any purpose for the injection service.
When a button was pressed, the newest keycode can be obtained from this 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. has knowledge of buttons like the middle-mouse button.
""" """
def __init__(self): def __init__(self):
@ -197,7 +198,7 @@ class _KeycodeReader:
return len(self._unreleased) > 0 return len(self._unreleased) > 0
def read(self): 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 If the timing of two recent events is very close, prioritize
key events over abs events. key events over abs events.
@ -253,12 +254,8 @@ class _KeycodeReader:
self.newest_event = newest_event self.newest_event = newest_event
if len(self._unreleased) > 1: if len(self._unreleased) > 0:
# a combination return Key(*self._unreleased.values())
return tuple(self._unreleased.values())
elif len(self._unreleased) == 1:
# a single key
return list(self._unreleased.values())[0]
else: else:
# nothing # nothing
return None return None

@ -27,6 +27,7 @@ from gi.repository import Gtk, GLib, Gdk
from keymapper.state import custom_mapping, system_mapping from keymapper.state import custom_mapping, system_mapping
from keymapper.logger import logger from keymapper.logger import logger
from keymapper.key import Key
CTX_KEYCODE = 2 CTX_KEYCODE = 2
@ -39,38 +40,45 @@ for name in system_mapping.list_names():
def to_string(key): def to_string(key):
"""A nice to show description of the pressed 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]) 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 ev_type, code, value = key
try: if ev_type not in evdev.ecodes.bytype:
key_name = evdev.ecodes.bytype[ev_type][code] logger.error('Unknown key type for %s', key)
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:
return 'unknown' 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 IDLE = 0
HOLDING = 1 HOLDING = 1
@ -85,9 +93,11 @@ class Row(Gtk.ListBoxRow):
Parameters Parameters
---------- ----------
key : int, int, int key : Key
event, code, value
""" """
if key is not None and not isinstance(key, Key):
raise TypeError('Expected key to be a Key object')
super().__init__() super().__init__()
self.device = window.selected_device self.device = window.selected_device
self.window = window self.window = window
@ -111,8 +121,8 @@ class Row(Gtk.ListBoxRow):
window = self.window.window window = self.window.window
GLib.idle_add(lambda: window.set_focus(self.character_input)) GLib.idle_add(lambda: window.set_focus(self.character_input))
def get_keycode(self): def get_key(self):
"""Get a tuple of type, code and value from the left column. """Get the Key object from the left column.
Or None if no code is mapped on this row. Or None if no code is mapped on this row.
""" """
@ -123,11 +133,19 @@ class Row(Gtk.ListBoxRow):
character = self.character_input.get_text() character = self.character_input.get_text()
return character if character else None return character if character else None
def set_new_keycode(self, new_key): def set_new_key(self, new_key):
"""Check if a keycode has been pressed and if so, display it.""" """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 # the newest_keycode is populated since the ui regularly polls it
# in order to display it in the status bar. # in order to display it in the status bar.
previous_key = self.get_keycode() previous_key = self.get_key()
# no input # no input
if new_key is None: if new_key is None:
@ -182,7 +200,7 @@ class Row(Gtk.ListBoxRow):
def on_character_input_change(self, _): def on_character_input_change(self, _):
"""When the output character for that keycode is typed in.""" """When the output character for that keycode is typed in."""
key = self.get_keycode() key = self.get_key()
character = self.get_character() character = self.get_character()
if character is None: if character is None:
@ -204,7 +222,7 @@ class Row(Gtk.ListBoxRow):
def show_click_here(self): def show_click_here(self):
"""Show 'click here' on the keycode input button.""" """Show 'click here' on the keycode input button."""
if self.get_keycode() is not None: if self.get_key() is not None:
return return
self.set_keycode_input_label('click here') self.set_keycode_input_label('click here')
@ -212,7 +230,7 @@ class Row(Gtk.ListBoxRow):
def show_press_key(self): def show_press_key(self):
"""Show 'press key' on the keycode input button.""" """Show 'press key' on the keycode input button."""
if self.get_keycode() is not None: if self.get_key() is not None:
return return
self.set_keycode_input_label('press key') self.set_keycode_input_label('press key')
@ -311,7 +329,7 @@ class Row(Gtk.ListBoxRow):
def on_delete_button_clicked(self, *args): def on_delete_button_clicked(self, *args):
"""Destroy the row and remove it from the config.""" """Destroy the row and remove it from the config."""
key = self.get_keycode() key = self.get_key()
if key is not None: if key is not None:
custom_mapping.clear(key) custom_mapping.clear(key)

@ -364,7 +364,7 @@ class Window:
# inform the currently selected row about the new keycode # inform the currently selected row about the new keycode
row, focused = self.get_focused_row() row, focused = self.get_focused_row()
if isinstance(focused, Gtk.ToggleButton): if isinstance(focused, Gtk.ToggleButton):
row.set_new_keycode(key) row.set_new_key(key)
return True return True

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

@ -30,28 +30,13 @@ import copy
from keymapper.logger import logger from keymapper.logger import logger
from keymapper.paths import touch from keymapper.paths import touch
from keymapper.config import ConfigBase, config from keymapper.config import ConfigBase, config
from keymapper.key import Key
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}')
def split_key(key): def split_key(key):
"""Take a key like "1,2,3" and return a 3-tuple of ints.""" """Take a key like "1,2,3" and return a 3-tuple of ints."""
key = key.strip()
if ',' not in key: if ',' not in key:
logger.error('Found invalid key: "%s"', key) logger.error('Found invalid key: "%s"', key)
return None return None
@ -79,12 +64,12 @@ def split_key(key):
class Mapping(ConfigBase): class Mapping(ConfigBase):
"""Contains and manages mappings and config of a single preset.""" """Contains and manages mappings and config of a single preset."""
def __init__(self): def __init__(self):
self._mapping = {} self._mapping = {} # a mapping of Key objects to strings
self.changed = False self.changed = False
super().__init__(fallback=config) super().__init__(fallback=config)
def __iter__(self): def __iter__(self):
"""Iterate over tuples of unique keycodes and their character.""" """Iterate over Key objects and their character."""
return iter(self._mapping.items()) return iter(self._mapping.items())
def __len__(self): def __len__(self):
@ -105,33 +90,23 @@ class Mapping(ConfigBase):
Parameters Parameters
---------- ----------
new_key : int, int, int new_key : Key
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.
character : string character : string
A single character known to xkb or linux. A single character known to xkb or linux.
Examples: KP_1, Shift_L, a, B, BTN_LEFT. Examples: KP_1, Shift_L, a, B, BTN_LEFT.
previous_key : int, int, int previous_key : Key or None
the previous key, same format as new_key the previous key
If not set, will not remove any previous mapping. If you recently 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 used (1, 10, 1) for new_key and want to overwrite that with
(1, 11, 1), provide (1, 5, 1) here. (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: if character is None:
raise ValueError('Expected `character` not to be None') raise ValueError('Expected `character` not to be None')
verify_key(new_key)
if previous_key:
verify_key(previous_key)
logger.debug( logger.debug(
'%s will map to %s, replacing %s', '%s will map to %s, replacing %s',
new_key, character, previous_key new_key, character, previous_key
@ -153,31 +128,16 @@ class Mapping(ConfigBase):
Parameters Parameters
---------- ----------
key : int, int, int key : Key
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)
""" """
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: for permutation in key.get_permutations():
logger.debug('%s will be cleared', key) if permutation in self._mapping:
del self._mapping[key] logger.debug('%s will be cleared', permutation)
self.changed = True del self._mapping[permutation]
return self.changed = True
logger.error('Unknown key %s', key)
def empty(self): def empty(self):
"""Remove all mappings.""" """Remove all mappings."""
@ -208,15 +168,17 @@ class Mapping(ConfigBase):
return return
for key, character in preset_dict['mapping'].items(): for key, character in preset_dict['mapping'].items():
if '+' in key: try:
chunks = key.split('+') key = Key(*[
key = tuple([split_key(chunk) for chunk in chunks]) split_key(chunk) for chunk in key.split('+')
if None in key: if chunk.strip() != ''
continue ])
else: except ValueError as error:
key = split_key(key) logger.error(str(error))
if key is None: continue
continue
if None in key:
continue
logger.spam('%s maps to %s', key, character) logger.spam('%s maps to %s', key, character)
self._mapping[key] = character self._mapping[key] = character
@ -254,17 +216,13 @@ class Mapping(ConfigBase):
json_ready_mapping = {} json_ready_mapping = {}
# tuple keys are not possible in json, encode them as string # tuple keys are not possible in json, encode them as string
for key, value in self._mapping.items(): for key, value in self._mapping.items():
if isinstance(key[0], tuple): new_key = '+'.join([
# combinations to "1,2,1+1,3,1" ','.join([
new_key = '+'.join([ str(value)
','.join([ for value in sub_key
str(value)
for value in sub_key
])
for sub_key in key
]) ])
else: for sub_key in key
new_key = ','.join([str(value) for value in key]) ])
json_ready_mapping[new_key] = value json_ready_mapping[new_key] = value
preset_dict['mapping'] = json_ready_mapping preset_dict['mapping'] = json_ready_mapping
@ -278,23 +236,12 @@ class Mapping(ConfigBase):
Parameters Parameters
---------- ----------
key : int, int, int key : Key
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.
""" """
if isinstance(key[0], tuple): if not isinstance(key, Key):
for permutation in itertools.permutations(key[:-1]): raise TypeError('Expected key to be a Key object')
permutation += (key[-1],)
existing = self._mapping.get(permutation) for permutation in key.get_permutations():
if existing is not None: existing = self._mapping.get(permutation)
return existing if existing is not None:
return existing
return self._mapping.get(key)

@ -34,6 +34,7 @@ from keymapper.state import custom_mapping, system_mapping
from keymapper.config import config from keymapper.config import config
from keymapper.getdevices import get_devices from keymapper.getdevices import get_devices
from keymapper.paths import get_preset_path from keymapper.paths import get_preset_path
from keymapper.key import Key
from keymapper.daemon import Daemon, get_dbus_interface, BUS_NAME from keymapper.daemon import Daemon, get_dbus_interface, BUS_NAME
from tests.test import cleanup, uinput_write_history_pipe, InputEvent, \ from tests.test import cleanup, uinput_write_history_pipe, InputEvent, \
@ -125,8 +126,8 @@ class TestDaemon(unittest.TestCase):
device = 'device 2' device = 'device 2'
custom_mapping.change((*ev_1, 1), 'a') custom_mapping.change(Key(*ev_1, 1), 'a')
custom_mapping.change((*ev_2, -1), 'b') custom_mapping.change(Key(*ev_2, -1), 'b')
system_mapping.clear() system_mapping.clear()
system_mapping._set('a', keycode_to_1) system_mapping._set('a', keycode_to_1)
@ -186,7 +187,7 @@ class TestDaemon(unittest.TestCase):
device = '9876 name' device = '9876 name'
# this test only makes sense if this device is unknown yet # this test only makes sense if this device is unknown yet
self.assertIsNone(get_devices().get(device)) self.assertIsNone(get_devices().get(device))
custom_mapping.change((*ev, 1), 'a') custom_mapping.change(Key(*ev, 1), 'a')
system_mapping.clear() system_mapping.clear()
system_mapping._set('a', keycode_to) system_mapping._set('a', keycode_to)
preset = 'foo' preset = 'foo'
@ -231,7 +232,7 @@ class TestDaemon(unittest.TestCase):
path = get_preset_path(device, preset) path = get_preset_path(device, preset)
custom_mapping.change(event, to_name) custom_mapping.change(Key(event), to_name)
custom_mapping.save(path) custom_mapping.save(path)
system_mapping.clear() system_mapping.clear()

@ -27,10 +27,11 @@ import evdev
from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X, BTN_A from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X, BTN_A
from keymapper.dev.injector import is_numlock_on, set_numlock, \ 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.state import custom_mapping, system_mapping
from keymapper.mapping import Mapping from keymapper.mapping import Mapping
from keymapper.config import config from keymapper.config import config
from keymapper.key import Key
from keymapper.dev.macros import parse from keymapper.dev.macros import parse
from tests.test import InputEvent, pending_events, fixtures, \ from tests.test import InputEvent, pending_events, fixtures, \
@ -76,16 +77,16 @@ class TestInjector(unittest.TestCase):
} }
mapping = Mapping() 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_code = 'r(2, m(sHiFt_l, r(2, k(1).k(2))))'
macro = parse(macro_code, mapping) 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 # going to be ignored, because EV_REL cannot be mapped, that's
# mouse movements. # mouse movements.
mapping.change((EV_REL, 1234, 3), 'b') mapping.change(Key(EV_REL, 1234, 3), 'b')
a = system_mapping.get('a') a = system_mapping.get('a')
shift_l = system_mapping.get('ShIfT_L') shift_l = system_mapping.get('ShIfT_L')
@ -114,7 +115,7 @@ class TestInjector(unittest.TestCase):
def test_grab(self): def test_grab(self):
# path is from the fixtures # 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) self.injector = KeycodeInjector('device 1', custom_mapping)
path = '/dev/input/event10' path = '/dev/input/event10'
@ -128,7 +129,7 @@ class TestInjector(unittest.TestCase):
def test_fail_grab(self): def test_fail_grab(self):
self.make_it_fail = 10 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) self.injector = KeycodeInjector('device 1', custom_mapping)
path = '/dev/input/event10' path = '/dev/input/event10'
@ -145,7 +146,7 @@ class TestInjector(unittest.TestCase):
def test_prepare_device_1(self): def test_prepare_device_1(self):
# according to the fixtures, /dev/input/event30 can do ABS_HAT0X # 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) self.injector = KeycodeInjector('foobar', custom_mapping)
_prepare_device = self.injector._prepare_device _prepare_device = self.injector._prepare_device
@ -153,7 +154,7 @@ class TestInjector(unittest.TestCase):
self.assertIsNotNone(_prepare_device('/dev/input/event30')[0]) self.assertIsNotNone(_prepare_device('/dev/input/event30')[0])
def test_prepare_device_non_existing(self): 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) self.injector = KeycodeInjector('foobar', custom_mapping)
_prepare_device = self.injector._prepare_device _prepare_device = self.injector._prepare_device
@ -224,7 +225,7 @@ class TestInjector(unittest.TestCase):
def test_skip_unused_device(self): def test_skip_unused_device(self):
# skips a device because its capabilities are not used in the mapping # 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) self.injector = KeycodeInjector('device 1', custom_mapping)
path = '/dev/input/event11' path = '/dev/input/event11'
device, abs_to_rel = self.injector._prepare_device(path) device, abs_to_rel = self.injector._prepare_device(path)
@ -332,12 +333,12 @@ class TestInjector(unittest.TestCase):
numlock_before = is_numlock_on() 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(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 # one mapping that is unknown in the system_mapping on purpose
input_b = 10 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) # stuff the custom_mapping outputs (except for the unknown b)
system_mapping.clear() system_mapping.clear()
@ -429,42 +430,13 @@ class TestInjector(unittest.TestCase):
numlock_after = is_numlock_on() numlock_after = is_numlock_on()
self.assertEqual(numlock_before, numlock_after) 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): def test_store_permutations_for_macros(self):
mapping = Mapping() mapping = Mapping()
ev_1 = (EV_KEY, 41, 1) ev_1 = (EV_KEY, 41, 1)
ev_2 = (EV_KEY, 42, 1) ev_2 = (EV_KEY, 42, 1)
ev_3 = (EV_KEY, 43, 1) ev_3 = (EV_KEY, 43, 1)
# a combination # 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) self.injector = KeycodeInjector('device 1', mapping)
history = [] history = []
@ -496,17 +468,17 @@ class TestInjector(unittest.TestCase):
ev_2 = (EV_KEY, 42, 1) ev_2 = (EV_KEY, 42, 1)
ev_3 = (EV_KEY, 43, 1) ev_3 = (EV_KEY, 43, 1)
ev_4 = (EV_KEY, 44, 1) ev_4 = (EV_KEY, 44, 1)
mapping.change(ev_1, 'a') mapping.change(Key(ev_1), 'a')
# a combination # a combination
mapping.change((ev_2, ev_3, ev_4), 'b') mapping.change(Key(ev_2, ev_3, ev_4), 'b')
self.assertEqual(mapping.get_character((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.clear()
system_mapping._set('a', 51) system_mapping._set('a', 51)
system_mapping._set('b', 52) system_mapping._set('b', 52)
injector = KeycodeInjector('device 1', mapping) 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 # 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_2, ev_3, ev_4)), 52)
self.assertEqual(injector._key_to_code.get((ev_3, ev_2, ev_4)), 52) self.assertEqual(injector._key_to_code.get((ev_3, ev_2, ev_4)), 52)

@ -42,6 +42,7 @@ from keymapper.config import config, WHEEL, MOUSE
from keymapper.dev.reader import keycode_reader from keymapper.dev.reader import keycode_reader
from keymapper.gtk.row import to_string, HOLDING, IDLE from keymapper.gtk.row import to_string, HOLDING, IDLE
from keymapper.dev import permissions from keymapper.dev import permissions
from keymapper.key import Key
from tests.test import tmp, pending_events, InputEvent, \ from tests.test import tmp, pending_events, InputEvent, \
uinput_write_history_pipe, cleanup uinput_write_history_pipe, cleanup
@ -206,9 +207,9 @@ class TestIntegration(unittest.TestCase):
def test_select_device(self): def test_select_device(self):
# creates a new empty preset when no preset exists for the device # creates a new empty preset when no preset exists for the device
self.window.on_select_device(FakeDropdown('device 1')) self.window.on_select_device(FakeDropdown('device 1'))
custom_mapping.change((EV_KEY, 50, 1), 'q') custom_mapping.change(Key(EV_KEY, 50, 1), 'q')
custom_mapping.change((EV_KEY, 51, 1), 'u') custom_mapping.change(Key(EV_KEY, 51, 1), 'u')
custom_mapping.change((EV_KEY, 52, 1), 'x') custom_mapping.change(Key(EV_KEY, 52, 1), 'x')
self.assertEqual(len(custom_mapping), 3) self.assertEqual(len(custom_mapping), 3)
self.window.on_select_device(FakeDropdown('device 2')) self.window.on_select_device(FakeDropdown('device 2'))
self.assertEqual(len(custom_mapping), 0) self.assertEqual(len(custom_mapping), 0)
@ -227,14 +228,14 @@ class TestIntegration(unittest.TestCase):
def test_row_keycode_to_string(self): def test_row_keycode_to_string(self):
# not an integration test, but I have all the row tests here already # 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(Key(EV_KEY, evdev.ecodes.KEY_9, 1)), '9')
self.assertEqual(to_string((EV_KEY, evdev.ecodes.KEY_SEMICOLON, 1)), 'SEMICOLON') self.assertEqual(to_string(Key(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(Key(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(Key(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.BTN_A, 1)), 'BTN_A')
# combinations # combinations
self.assertEqual(to_string(( self.assertEqual(to_string(Key(
(EV_KEY, evdev.ecodes.BTN_A, 1), (EV_KEY, evdev.ecodes.BTN_A, 1),
(EV_KEY, evdev.ecodes.BTN_B, 1), (EV_KEY, evdev.ecodes.BTN_B, 1),
(EV_KEY, evdev.ecodes.BTN_C, 1) (EV_KEY, evdev.ecodes.BTN_C, 1)
@ -246,21 +247,21 @@ class TestIntegration(unittest.TestCase):
row = rows[0] row = rows[0]
row.set_new_keycode(None) row.set_new_key(None)
self.assertIsNone(row.get_keycode()) self.assertIsNone(row.get_key())
self.assertEqual(len(custom_mapping), 0) self.assertEqual(len(custom_mapping), 0)
self.assertEqual(row.keycode_input.get_label(), 'click here') 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(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, # this is KEY_A in linux/input-event-codes.h,
# but KEY_ is removed from the text # but KEY_ is removed from the text
self.assertEqual(row.keycode_input.get_label(), 'A') 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(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) time.sleep(0.1)
gtk_iteration() gtk_iteration()
@ -273,9 +274,9 @@ class TestIntegration(unittest.TestCase):
gtk_iteration() gtk_iteration()
self.assertEqual(len(self.window.get('key_list').get_children()), 2) 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_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): def change_empty_row(self, key, char, code_first=True, expect_success=True):
"""Modify the one empty row that always exists. """Modify the one empty row that always exists.
@ -284,9 +285,7 @@ class TestIntegration(unittest.TestCase):
Parameters Parameters
---------- ----------
key : int, int, int key : Key or None
type, code, value,
or a tuple of multiple of those
code_first : boolean code_first : boolean
If True, the code is entered and then the character. If True, the code is entered and then the character.
If False, the character is entered first. If False, the character is entered first.
@ -304,7 +303,7 @@ class TestIntegration(unittest.TestCase):
# find the empty row # find the empty row
rows = self.get_rows() rows = self.get_rows()
row = rows[-1] row = rows[-1]
self.assertIsNone(row.get_keycode()) self.assertIsNone(row.get_key())
self.assertEqual(row.character_input.get_text(), '') self.assertEqual(row.character_input.get_text(), '')
self.assertNotIn('changed', row.get_style_context().list_classes()) self.assertNotIn('changed', row.get_style_context().list_classes())
self.assertEqual(row.state, IDLE) self.assertEqual(row.state, IDLE)
@ -322,7 +321,7 @@ class TestIntegration(unittest.TestCase):
self.window.window.set_focus(row.keycode_input) self.window.window.set_focus(row.keycode_input)
gtk_iteration() gtk_iteration()
self.assertIsNone(row.get_keycode()) self.assertIsNone(row.get_key())
self.assertEqual(row.keycode_input.get_label(), 'press key') self.assertEqual(row.keycode_input.get_label(), 'press key')
if key: if key:
@ -330,11 +329,8 @@ class TestIntegration(unittest.TestCase):
# but by sending an event. Events should be consumed 30 times # but by sending an event. Events should be consumed 30 times
# per second, so sleep a bit more than 0.033ms each time # per second, so sleep a bit more than 0.033ms each time
# press down all the keys of a combination # press down all the keys of a combination
if isinstance(key[0], tuple): for sub_key in key:
for sub_key in key: keycode_reader._pipe[1].send(InputEvent(*sub_key))
keycode_reader._pipe[1].send(InputEvent(*sub_key))
else:
keycode_reader._pipe[1].send(InputEvent(*key))
# make the window consume the keycode # make the window consume the keycode
time.sleep(0.05) time.sleep(0.05)
@ -346,11 +342,8 @@ class TestIntegration(unittest.TestCase):
self.assertTrue(row.keycode_input.is_focus()) self.assertTrue(row.keycode_input.is_focus())
# release all the keys # release all the keys
if isinstance(key[0], tuple): for sub_key in key:
for sub_key in key: keycode_reader._pipe[1].send(InputEvent(*sub_key[:2], 0))
keycode_reader._pipe[1].send(InputEvent(*sub_key[:2], 0))
else:
keycode_reader._pipe[1].send(InputEvent(*key[:2], 0))
# make the window consume the keycode # make the window consume the keycode
time.sleep(0.05) time.sleep(0.05)
@ -361,14 +354,14 @@ class TestIntegration(unittest.TestCase):
self.assertEqual(row.state, IDLE) self.assertEqual(row.state, IDLE)
if expect_success: if expect_success:
self.assertEqual(row.get_keycode(), key) self.assertEqual(row.get_key(), key)
css_classes = row.get_style_context().list_classes() css_classes = row.get_style_context().list_classes()
self.assertIn('changed', css_classes) self.assertIn('changed', css_classes)
self.assertEqual(row.keycode_input.get_label(), to_string(key)) self.assertEqual(row.keycode_input.get_label(), to_string(key))
self.assertFalse(row.keycode_input.is_focus()) self.assertFalse(row.keycode_input.is_focus())
if not expect_success: if not expect_success:
self.assertIsNone(row.get_keycode()) self.assertIsNone(row.get_key())
self.assertIsNone(row.get_character()) self.assertIsNone(row.get_character())
css_classes = row.get_style_context().list_classes() css_classes = row.get_style_context().list_classes()
self.assertNotIn('changed', css_classes) self.assertNotIn('changed', css_classes)
@ -388,8 +381,8 @@ class TestIntegration(unittest.TestCase):
# how many rows there should be in the end # how many rows there should be in the end
num_rows_target = 3 num_rows_target = 3
ev_1 = (EV_KEY, 10, 1) ev_1 = Key(EV_KEY, 10, 1)
ev_2 = (EV_ABS, evdev.ecodes.ABS_HAT0X, -1) ev_2 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, -1)
"""edit""" """edit"""
@ -440,10 +433,10 @@ class TestIntegration(unittest.TestCase):
def test_hat0x(self): def test_hat0x(self):
# it should be possible to add all of them # it should be possible to add all of them
ev_1 = (EV_ABS, evdev.ecodes.ABS_HAT0X, -1) ev_1 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, -1)
ev_2 = (EV_ABS, evdev.ecodes.ABS_HAT0X, 1) ev_2 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, 1)
ev_3 = (EV_ABS, evdev.ecodes.ABS_HAT0Y, -1) ev_3 = Key(EV_ABS, evdev.ecodes.ABS_HAT0Y, -1)
ev_4 = (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_1, 'a')
self.change_empty_row(ev_2, 'b') self.change_empty_row(ev_2, 'b')
@ -471,20 +464,20 @@ class TestIntegration(unittest.TestCase):
def test_combination(self): def test_combination(self):
# it should be possible to write a key combination # it should be possible to write a key combination
ev_1 = (EV_KEY, evdev.ecodes.KEY_A, 1) ev_1 = Key(EV_KEY, evdev.ecodes.KEY_A, 1)
ev_2 = (EV_ABS, evdev.ecodes.ABS_HAT0X, 1) ev_2 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, 1)
ev_3 = (EV_KEY, evdev.ecodes.KEY_C, 1) ev_3 = Key(EV_KEY, evdev.ecodes.KEY_C, 1)
ev_4 = (EV_ABS, evdev.ecodes.ABS_HAT0X, -1) ev_4 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, -1)
combination_1 = (ev_1, ev_2, ev_3) combination_1 = Key(ev_1, ev_2, ev_3)
combination_2 = (ev_2, ev_1, ev_3) combination_2 = Key(ev_2, ev_1, ev_3)
# same as 1, but different D-Pad direction # same as 1, but different D-Pad direction
combination_3 = (ev_1, ev_4, ev_3) combination_3 = Key(ev_1, ev_4, ev_3)
combination_4 = (ev_4, ev_1, ev_3) combination_4 = Key(ev_4, ev_1, ev_3)
# same as 1, but the last key is different # same as 1, but the last key is different
combination_5 = (ev_1, ev_3, ev_2) combination_5 = Key(ev_1, ev_3, ev_2)
combination_6 = (ev_3, ev_1, ev_2) combination_6 = Key(ev_3, ev_1, ev_2)
self.change_empty_row(combination_1, 'a') self.change_empty_row(combination_1, 'a')
self.assertEqual(custom_mapping.get_character(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.""" """Comprehensive test for rows 2."""
# sleeps are added to be able to visually follow and debug the test. # sleeps are added to be able to visually follow and debug the test.
# add two rows by modifiying the one empty row that exists # add two rows by modifiying the one empty row that exists
row_1 = self.change_empty_row((EV_KEY, 10, 1), 'a') row_1 = self.change_empty_row(Key(EV_KEY, 10, 1), 'a')
row_2 = self.change_empty_row((EV_KEY, 11, 1), 'b') row_2 = self.change_empty_row(Key(EV_KEY, 11, 1), 'b')
row_3 = self.change_empty_row(None, 'c') row_3 = self.change_empty_row(None, 'c')
# no empty row added because one is unfinished # no empty row added because one is unfinished
@ -544,7 +537,7 @@ class TestIntegration(unittest.TestCase):
gtk_iteration() gtk_iteration()
self.assertEqual(len(self.get_rows()), 3) 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): def remove(row, code, char, num_rows_after):
"""Remove a row by clicking the delete button. """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 after deleting, how many rows are expected to still be there
""" """
if code is not None and char is not None: 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) self.assertEqual(row.get_character(), char)
if code is None: if code is None:
self.assertIsNone(row.get_keycode()) self.assertIsNone(row.get_key())
else: 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() row.on_delete_button_clicked()
time.sleep(0.2) time.sleep(0.2)
@ -575,9 +568,10 @@ class TestIntegration(unittest.TestCase):
# if a reference to the row is held somewhere and it is # if a reference to the row is held somewhere and it is
# accidentally used again, make sure to not provide any outdated # accidentally used again, make sure to not provide any outdated
# information that is supposed to be deleted # information that is supposed to be deleted
self.assertIsNone(row.get_keycode()) self.assertIsNone(row.get_key())
self.assertIsNone(row.get_character()) 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) self.assertEqual(len(self.get_rows()), num_rows_after)
remove(row_1, 10, 'a', 2) remove(row_1, 10, 'a', 2)
@ -588,17 +582,17 @@ class TestIntegration(unittest.TestCase):
remove(row_3, None, 'c', 1) remove(row_3, None, 'c', 1)
def test_rename_and_save(self): 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.assertEqual(self.window.selected_preset, 'new preset')
self.window.on_save_preset_clicked(None) 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.get('preset_name_input').set_text('asdf')
self.window.on_save_preset_clicked(None) self.window.on_save_preset_clicked(None)
self.assertEqual(self.window.selected_preset, 'asdf') self.assertEqual(self.window.selected_preset, 'asdf')
self.assertTrue(os.path.exists(f'{CONFIG_PATH}/presets/device 1/asdf.json')) 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') error_icon = self.window.get('error_status_icon')
status = self.window.get('status_bar') status = self.window.get('status_bar')
@ -610,20 +604,20 @@ class TestIntegration(unittest.TestCase):
status = self.window.get('status_bar') status = self.window.get('status_bar')
error_icon = self.window.get('error_status_icon') 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) self.window.on_save_preset_clicked(None)
tooltip = status.get_tooltip_text().lower() tooltip = status.get_tooltip_text().lower()
self.assertIn('brackets', tooltip) self.assertIn('brackets', tooltip)
self.assertTrue(error_icon.get_visible()) 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) self.window.on_save_preset_clicked(None)
tooltip = status.get_tooltip_text().lower() tooltip = status.get_tooltip_text().lower()
self.assertNotIn('brackets', tooltip) self.assertNotIn('brackets', tooltip)
self.assertIn('saved', tooltip) self.assertIn('saved', tooltip)
self.assertFalse(error_icon.get_visible()) 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): def test_select_device_and_preset(self):
# created on start because the first device is selected and some empty # created on start because the first device is selected and some empty
@ -653,7 +647,7 @@ class TestIntegration(unittest.TestCase):
gtk_iteration() gtk_iteration()
self.assertEqual(self.window.selected_preset, 'new preset') self.assertEqual(self.window.selected_preset, 'new preset')
self.assertFalse(os.path.exists(f'{CONFIG_PATH}/presets/device 1/abc 123.json')) 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) self.window.on_save_preset_clicked(None)
gtk_iteration() gtk_iteration()
self.assertEqual(self.window.selected_preset, 'abc 123') self.assertEqual(self.window.selected_preset, 'abc 123')
@ -706,7 +700,7 @@ class TestIntegration(unittest.TestCase):
keycode_from = 9 keycode_from = 9
keycode_to = 200 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.clear()
system_mapping._set('a', keycode_to) system_mapping._set('a', keycode_to)
@ -746,7 +740,7 @@ class TestIntegration(unittest.TestCase):
keycode_from = 16 keycode_from = 16
keycode_to = 90 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.clear()
system_mapping._set('t', keycode_to) system_mapping._set('t', keycode_to)

@ -24,10 +24,11 @@ import unittest
import json import json
from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X, KEY_A 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.state import SystemMapping, XMODMAP_FILENAME
from keymapper.config import config from keymapper.config import config
from keymapper.paths import get_preset_path from keymapper.paths import get_preset_path
from keymapper.key import Key
from tests.test import tmp, cleanup from tests.test import tmp, cleanup
@ -133,12 +134,12 @@ class TestMapping(unittest.TestCase):
# setting mapping.whatever does not overwrite the mapping # setting mapping.whatever does not overwrite the mapping
# after saving. It should be ignored. # 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.set('mapping.a', 2)
self.mapping.save(get_preset_path('foo', 'bar')) self.mapping.save(get_preset_path('foo', 'bar'))
self.assertFalse(self.mapping.changed) self.assertFalse(self.mapping.changed)
self.mapping.load(get_preset_path('foo', 'bar')) 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.assertIsNone(self.mapping.get('mapping.a'))
self.assertFalse(self.mapping.changed) self.assertFalse(self.mapping.changed)
@ -156,8 +157,8 @@ class TestMapping(unittest.TestCase):
self.assertEqual(self.mapping.get('d.e.f'), 3) self.assertEqual(self.mapping.get('d.e.f'), 3)
def test_clone(self): def test_clone(self):
ev_1 = (EV_KEY, 1, 1) ev_1 = Key(EV_KEY, 1, 1)
ev_2 = (EV_KEY, 2, 0) ev_2 = Key(EV_KEY, 2, 0)
mapping1 = Mapping() mapping1 = Mapping()
mapping1.change(ev_1, 'a') mapping1.change(ev_1, 'a')
@ -170,17 +171,17 @@ class TestMapping(unittest.TestCase):
self.assertEqual(mapping2.get_character(ev_1), 'a') self.assertEqual(mapping2.get_character(ev_1), 'a')
self.assertIsNone(mapping2.get_character(ev_2)) self.assertIsNone(mapping2.get_character(ev_2))
self.assertIsNone(mapping2.get_character((EV_KEY, 2, 3))) self.assertIsNone(mapping2.get_character(Key(EV_KEY, 2, 3)))
self.assertIsNone(mapping2.get_character((EV_KEY, 1, 3))) self.assertIsNone(mapping2.get_character(Key(EV_KEY, 1, 3)))
def test_save_load(self): def test_save_load(self):
one = (EV_KEY, 10, 1) one = Key(EV_KEY, 10, 1)
two = (EV_KEY, 11, 1) two = Key(EV_KEY, 11, 1)
three = (EV_KEY, 12, 1) three = Key(EV_KEY, 12, 1)
self.mapping.change(one, '1') self.mapping.change(one, '1')
self.mapping.change(two, '2') 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._config['foo'] = 'bar'
self.mapping.save(get_preset_path('device 1', 'test')) self.mapping.save(get_preset_path('device 1', 'test'))
@ -194,7 +195,7 @@ class TestMapping(unittest.TestCase):
self.assertEqual(len(loaded), 3) self.assertEqual(len(loaded), 3)
self.assertEqual(loaded.get_character(one), '1') self.assertEqual(loaded.get_character(one), '1')
self.assertEqual(loaded.get_character(two), '2') 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') self.assertEqual(loaded._config['foo'], 'bar')
def test_save_load_2(self): def test_save_load_2(self):
@ -209,10 +210,8 @@ class TestMapping(unittest.TestCase):
f'{EV_ABS},{ABS_HAT0X},-1': 'b', f'{EV_ABS},{ABS_HAT0X},-1': 'b',
f'{EV_ABS},1,1+{EV_ABS},2,-1+{EV_ABS},3,1': 'c', f'{EV_ABS},1,1+{EV_ABS},2,-1+{EV_ABS},3,1': 'c',
# ignored because broken # ignored because broken
f'3,1,1+': 'd',
f'3,1,1,2': 'e', f'3,1,1,2': 'e',
f'3': 'e', f'3': 'e',
f'+3,1,2': 'f',
f',,+3,1,2': 'g', f',,+3,1,2': 'g',
f'': 'h', f'': 'h',
} }
@ -221,19 +220,21 @@ class TestMapping(unittest.TestCase):
loaded = Mapping() loaded = Mapping()
loaded.load(get_preset_path('device 1', 'test')) loaded.load(get_preset_path('device 1', 'test'))
self.assertEqual(len(loaded), 3) self.assertEqual(len(loaded), 3)
self.assertEqual(loaded.get_character((EV_KEY, 3, 1)), 'a') self.assertEqual(loaded.get_character(Key(EV_KEY, 3, 1)), 'a')
self.assertEqual(loaded.get_character((EV_ABS, ABS_HAT0X, -1)), 'b') self.assertEqual(loaded.get_character(Key(EV_ABS, ABS_HAT0X, -1)), 'b')
self.assertEqual(loaded.get_character( self.assertEqual(loaded.get_character(Key(
((EV_ABS, 1, 1), (EV_ABS, 2, -1), (EV_ABS, 3, 1)) (EV_ABS, 1, 1),
(EV_ABS, 2, -1),
Key(EV_ABS, 3, 1))
), 'c') ), 'c')
def test_change(self): def test_change(self):
# the reader would not report values like 111 or 222, only 1 or -1. # 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. # the mapping just does what it is told, so it accepts them.
ev_1 = (EV_KEY, 1, 111) ev_1 = Key(EV_KEY, 1, 111)
ev_2 = (EV_KEY, 1, 222) ev_2 = Key(EV_KEY, 1, 222)
ev_3 = (EV_KEY, 2, 111) ev_3 = Key(EV_KEY, 2, 111)
ev_4 = (EV_ABS, 1, 111) ev_4 = Key(EV_ABS, 1, 111)
# 1 is not assigned yet, ignore it # 1 is not assigned yet, ignore it
self.mapping.change(ev_1, 'a', ev_2) self.mapping.change(ev_1, 'a', ev_2)
@ -265,13 +266,13 @@ class TestMapping(unittest.TestCase):
self.assertEqual(len(self.mapping), 2) self.assertEqual(len(self.mapping), 2)
def test_combinations(self): def test_combinations(self):
ev_1 = (EV_KEY, 1, 111) ev_1 = Key(EV_KEY, 1, 111)
ev_2 = (EV_KEY, 1, 222) ev_2 = Key(EV_KEY, 1, 222)
ev_3 = (EV_KEY, 2, 111) ev_3 = Key(EV_KEY, 2, 111)
ev_4 = (EV_ABS, 1, 111) ev_4 = Key(EV_ABS, 1, 111)
combi_1 = (ev_1, ev_2, ev_3) combi_1 = Key(ev_1, ev_2, ev_3)
combi_2 = (ev_2, ev_1, ev_3) combi_2 = Key(ev_2, ev_1, ev_3)
combi_3 = (ev_1, ev_2, ev_4) combi_3 = Key(ev_1, ev_2, ev_4)
self.mapping.change(combi_1, 'a') self.mapping.change(combi_1, 'a')
self.assertEqual(self.mapping.get_character(combi_1), 'a') self.assertEqual(self.mapping.get_character(combi_1), 'a')
@ -293,50 +294,55 @@ class TestMapping(unittest.TestCase):
def test_clear(self): def test_clear(self):
# does nothing # 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.assertFalse(self.mapping.changed)
self.assertEqual(len(self.mapping), 0) 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.assertEqual(len(self.mapping), 1)
self.mapping.clear((EV_KEY, 40, 1)) self.mapping.clear(ev_1)
self.assertEqual(len(self.mapping), 0) self.assertEqual(len(self.mapping), 0)
self.assertTrue(self.mapping.changed) 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.assertTrue(self.mapping.changed)
self.mapping.change((EV_KEY, 20, 1), 'KP_2', None) self.mapping.change(ev_3, 'KP_2', None)
self.mapping.change((EV_KEY, 30, 1), 'KP_3', None) self.mapping.change(ev_2, 'KP_3', None)
self.assertEqual(len(self.mapping), 3) 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(len(self.mapping), 2)
self.assertEqual(self.mapping.get_character((EV_KEY, 10, 1)), 'KP_1') self.assertEqual(self.mapping.get_character(ev_4), 'KP_1')
self.assertIsNone(self.mapping.get_character((EV_KEY, 20, 1))) self.assertIsNone(self.mapping.get_character(ev_3))
self.assertEqual(self.mapping.get_character((EV_KEY, 30, 1)), 'KP_3') self.assertEqual(self.mapping.get_character(ev_2), 'KP_3')
def test_empty(self): def test_empty(self):
self.mapping.change((EV_KEY, 10, 1), '1') self.mapping.change(Key(EV_KEY, 10, 1), '1')
self.mapping.change((EV_KEY, 11, 1), '2') self.mapping.change(Key(EV_KEY, 11, 1), '2')
self.mapping.change((EV_KEY, 12, 1), '3') self.mapping.change(Key(EV_KEY, 12, 1), '3')
self.assertEqual(len(self.mapping), 3) self.assertEqual(len(self.mapping), 3)
self.mapping.empty() self.mapping.empty()
self.assertEqual(len(self.mapping), 0) self.assertEqual(len(self.mapping), 0)
def test_verify_key(self): def test_verify_key(self):
self.assertRaises(ValueError, lambda: verify_key(1)) self.assertRaises(ValueError, lambda: Key(1))
self.assertRaises(ValueError, lambda: verify_key(None)) self.assertRaises(ValueError, lambda: Key(None))
self.assertRaises(ValueError, lambda: verify_key([1])) self.assertRaises(ValueError, lambda: Key([1]))
self.assertRaises(ValueError, lambda: verify_key((1,))) self.assertRaises(ValueError, lambda: Key((1,)))
self.assertRaises(ValueError, lambda: verify_key((1, 2))) self.assertRaises(ValueError, lambda: Key((1, 2)))
self.assertRaises(ValueError, lambda: verify_key(('1', '2', '3'))) self.assertRaises(ValueError, lambda: Key(('1', '2', '3')))
self.assertRaises(ValueError, lambda: verify_key('1')) self.assertRaises(ValueError, lambda: Key('1'))
self.assertRaises(ValueError, lambda: verify_key('(1,2,3)')) self.assertRaises(ValueError, lambda: Key('(1,2,3)'))
self.assertRaises(ValueError, lambda: verify_key(((1, 2, 3), (1, 2, '3')))) self.assertRaises(ValueError, lambda: 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, 2, 3), (1, 2, 3), None))
# those don't raise errors # those don't raise errors
verify_key(((1, 2, 3), (1, 2, 3))) Key((1, 2, 3), (1, 2, 3))
verify_key((1, 2, 3)) Key((1, 2, 3))
if __name__ == "__main__": if __name__ == "__main__":

Loading…
Cancel
Save