input-remapper/inputremapper/key.py

266 lines
9.1 KiB
Python

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2022 sezanzeb <proxima@sezanzeb.de>
#
# This file is part of input-remapper.
#
# input-remapper 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.
#
# input-remapper 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 input-remapper. If not, see <https://www.gnu.org/licenses/>.
"""A button or a key combination."""
import itertools
import evdev
from evdev import ecodes
from inputremapper.system_mapping import system_mapping
from inputremapper.logger import logger
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 integers, but got {key}")
# having shift in combinations modifies the configured output,
# ctrl might not work at all
DIFFICULT_COMBINATIONS = [
ecodes.KEY_LEFTSHIFT,
ecodes.KEY_RIGHTSHIFT,
ecodes.KEY_LEFTCTRL,
ecodes.KEY_RIGHTCTRL,
ecodes.KEY_LEFTALT,
ecodes.KEY_RIGHTALT,
]
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 # pylint: disable=no-member
else:
flattened += (key,)
keys = flattened
for key in keys:
verify(key)
self.keys = tuple(keys)
self.release = (*self.keys[-1][:2], 0)
@classmethod
def btn_left(cls):
"""Construct a Key object representing a left click on a mouse."""
return cls(ecodes.EV_KEY, ecodes.BTN_LEFT, 1)
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 __repr__(self):
# used in the AssertionError output of tests
return self.__str__()
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 is_problematic(self):
"""Is this combination going to work properly on all systems?"""
if len(self.keys) <= 1:
return False
for sub_key in self.keys:
if sub_key[0] != ecodes.EV_KEY:
continue
if sub_key[1] in DIFFICULT_COMBINATIONS:
return True
return False
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
def beautify(self):
"""Get a human readable string representation."""
result = []
for sub_key in self:
if isinstance(sub_key[0], tuple):
raise Exception("deprecated stuff")
ev_type, code, value = sub_key
if ev_type not in evdev.ecodes.bytype:
logger.error("Unknown key type for %s", sub_key)
result.append(str(code))
continue
if code not in evdev.ecodes.bytype[ev_type]:
logger.error("Unknown key code for %s", sub_key)
result.append(str(code))
continue
key_name = None
# first try to find the name in xmodmap to not display wrong
# names due to the keyboard layout
if ev_type == evdev.ecodes.EV_KEY:
key_name = system_mapping.get_name(code)
if key_name is None:
# if no result, look in the linux key constants. On a german
# keyboard for example z and y are switched, which will therefore
# cause the wrong letter to be displayed.
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 = {
# D-Pad
(evdev.ecodes.ABS_HAT0X, -1): "Left",
(evdev.ecodes.ABS_HAT0X, 1): "Right",
(evdev.ecodes.ABS_HAT0Y, -1): "Up",
(evdev.ecodes.ABS_HAT0Y, 1): "Down",
(evdev.ecodes.ABS_HAT1X, -1): "Left",
(evdev.ecodes.ABS_HAT1X, 1): "Right",
(evdev.ecodes.ABS_HAT1Y, -1): "Up",
(evdev.ecodes.ABS_HAT1Y, 1): "Down",
(evdev.ecodes.ABS_HAT2X, -1): "Left",
(evdev.ecodes.ABS_HAT2X, 1): "Right",
(evdev.ecodes.ABS_HAT2Y, -1): "Up",
(evdev.ecodes.ABS_HAT2Y, 1): "Down",
# joystick
(evdev.ecodes.ABS_X, 1): "Right",
(evdev.ecodes.ABS_X, -1): "Left",
(evdev.ecodes.ABS_Y, 1): "Down",
(evdev.ecodes.ABS_Y, -1): "Up",
(evdev.ecodes.ABS_RX, 1): "Right",
(evdev.ecodes.ABS_RX, -1): "Left",
(evdev.ecodes.ABS_RY, 1): "Down",
(evdev.ecodes.ABS_RY, -1): "Up",
# wheel
(evdev.ecodes.REL_WHEEL, -1): "Down",
(evdev.ecodes.REL_WHEEL, 1): "Up",
(evdev.ecodes.REL_HWHEEL, -1): "Left",
(evdev.ecodes.REL_HWHEEL, 1): "Right",
}.get((code, value))
if direction is not None:
key_name += f" {direction}"
key_name = key_name.replace("ABS_Z", "Trigger Left")
key_name = key_name.replace("ABS_RZ", "Trigger Right")
key_name = key_name.replace("ABS_HAT0X", "DPad")
key_name = key_name.replace("ABS_HAT0Y", "DPad")
key_name = key_name.replace("ABS_HAT1X", "DPad 2")
key_name = key_name.replace("ABS_HAT1Y", "DPad 2")
key_name = key_name.replace("ABS_HAT2X", "DPad 3")
key_name = key_name.replace("ABS_HAT2Y", "DPad 3")
key_name = key_name.replace("ABS_X", "Joystick")
key_name = key_name.replace("ABS_Y", "Joystick")
key_name = key_name.replace("ABS_RX", "Joystick 2")
key_name = key_name.replace("ABS_RY", "Joystick 2")
key_name = key_name.replace("BTN_", "Button ")
key_name = key_name.replace("KEY_", "")
key_name = key_name.replace("REL_", "")
key_name = key_name.replace("HWHEEL", "Wheel")
key_name = key_name.replace("WHEEL", "Wheel")
key_name = key_name.replace("_", " ")
key_name = key_name.replace(" ", " ")
result.append(key_name)
return " + ".join(result)