2022-01-31 19:58:37 +00:00
|
|
|
#!/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/>.
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
2022-04-17 10:19:23 +00:00
|
|
|
import itertools
|
|
|
|
from typing import Tuple, Iterable, Union, List, Callable, Sequence
|
2022-01-31 19:58:37 +00:00
|
|
|
|
|
|
|
from evdev import ecodes
|
|
|
|
|
|
|
|
from inputremapper.logger import logger
|
|
|
|
from inputremapper.configs.system_mapping import system_mapping
|
2022-04-17 10:19:23 +00:00
|
|
|
from inputremapper.input_event import InputEvent, InputEventValidationType
|
2022-01-31 19:58:37 +00:00
|
|
|
|
|
|
|
# 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,
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2022-04-17 10:19:23 +00:00
|
|
|
EventCombinationInitType = Union[
|
|
|
|
InputEventValidationType,
|
|
|
|
Iterable[InputEventValidationType],
|
|
|
|
]
|
|
|
|
|
|
|
|
EventCombinationValidatorType = Union[EventCombinationInitType, str]
|
|
|
|
|
|
|
|
|
2022-01-31 19:58:37 +00:00
|
|
|
class EventCombination(Tuple[InputEvent]):
|
2022-04-18 11:52:59 +00:00
|
|
|
"""One or multiple InputEvent objects for use as an unique identifier for mappings."""
|
2022-01-31 19:58:37 +00:00
|
|
|
|
|
|
|
# tuple is immutable, therefore we need to override __new__()
|
|
|
|
# https://jfine-python-classes.readthedocs.io/en/latest/subclass-tuple.html
|
2022-04-17 10:19:23 +00:00
|
|
|
def __new__(cls, events: EventCombinationInitType) -> EventCombination:
|
|
|
|
validated_events = []
|
|
|
|
try:
|
|
|
|
validated_events.append(InputEvent.validate(events))
|
2022-02-27 14:43:01 +00:00
|
|
|
|
2022-04-17 10:19:23 +00:00
|
|
|
except ValueError:
|
|
|
|
for event in events:
|
|
|
|
validated_events.append(InputEvent.validate(event))
|
2022-01-31 19:58:37 +00:00
|
|
|
|
2022-04-17 10:19:23 +00:00
|
|
|
# mypy bug: https://github.com/python/mypy/issues/8957
|
|
|
|
# https://github.com/python/mypy/issues/8541
|
|
|
|
return super().__new__(cls, validated_events) # type: ignore
|
2022-01-31 19:58:37 +00:00
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
# only used in tests and logging
|
2022-02-27 14:43:01 +00:00
|
|
|
return f"<EventCombination {', '.join([str(e.event_tuple) for e in self])}>"
|
2022-01-31 19:58:37 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def __get_validators__(cls):
|
2022-04-18 11:52:59 +00:00
|
|
|
"""Used by pydantic to create EventCombination objects."""
|
2022-04-17 10:19:23 +00:00
|
|
|
yield cls.validate
|
2022-01-31 19:58:37 +00:00
|
|
|
|
|
|
|
@classmethod
|
2022-04-17 10:19:23 +00:00
|
|
|
def validate(cls, init_arg: EventCombinationValidatorType) -> EventCombination:
|
2022-04-18 11:52:59 +00:00
|
|
|
"""Try all the different methods, and raise an error if none succeed."""
|
2022-04-17 10:19:23 +00:00
|
|
|
if isinstance(init_arg, EventCombination):
|
|
|
|
return init_arg
|
|
|
|
|
|
|
|
combi = None
|
|
|
|
validators: Sequence[Callable[..., EventCombination]] = (cls.from_string, cls)
|
|
|
|
for validator in validators:
|
|
|
|
try:
|
|
|
|
combi = validator(init_arg)
|
|
|
|
break
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
if combi:
|
|
|
|
return combi
|
|
|
|
raise ValueError(f"failed to create EventCombination with {init_arg = }")
|
2022-01-31 19:58:37 +00:00
|
|
|
|
|
|
|
@classmethod
|
2022-04-17 10:19:23 +00:00
|
|
|
def from_string(cls, init_string: str) -> EventCombination:
|
2022-04-18 11:52:59 +00:00
|
|
|
"""Create a EventCombination form a string like '1,2,3+4,5,6'."""
|
2022-04-17 10:19:23 +00:00
|
|
|
try:
|
|
|
|
init_strs = init_string.split("+")
|
|
|
|
return cls(init_strs)
|
|
|
|
except AttributeError:
|
|
|
|
raise ValueError(f"failed to create EventCombination from {init_string = }")
|
2022-01-31 19:58:37 +00:00
|
|
|
|
|
|
|
def is_problematic(self):
|
|
|
|
"""Is this combination going to work properly on all systems?"""
|
|
|
|
if len(self) <= 1:
|
|
|
|
return False
|
|
|
|
|
|
|
|
for event in self:
|
|
|
|
if event.type != ecodes.EV_KEY:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if event.code in DIFFICULT_COMBINATIONS:
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
def get_permutations(self):
|
2022-04-18 11:52:59 +00:00
|
|
|
"""Get a list of EventCombination objects representing all possible permutations.
|
2022-01-31 19:58:37 +00:00
|
|
|
|
|
|
|
combining a + b + c should have the same result as b + a + c.
|
|
|
|
Only the last combination remains the same in the returned result.
|
|
|
|
"""
|
|
|
|
if len(self) <= 2:
|
|
|
|
return [self]
|
|
|
|
|
|
|
|
permutations = []
|
|
|
|
for permutation in itertools.permutations(self[:-1]):
|
2022-04-17 10:19:23 +00:00
|
|
|
permutations.append(EventCombination((*permutation, self[-1])))
|
2022-01-31 19:58:37 +00:00
|
|
|
|
|
|
|
return permutations
|
|
|
|
|
|
|
|
def json_str(self) -> str:
|
|
|
|
return "+".join([event.json_str() for event in self])
|
|
|
|
|
|
|
|
def beautify(self) -> str:
|
|
|
|
"""Get a human readable string representation."""
|
|
|
|
result = []
|
|
|
|
|
|
|
|
for event in self:
|
|
|
|
|
|
|
|
if event.type not in ecodes.bytype:
|
|
|
|
logger.error("Unknown type for %s", event)
|
|
|
|
result.append(str(event.code))
|
|
|
|
continue
|
|
|
|
|
|
|
|
if event.code not in ecodes.bytype[event.type]:
|
|
|
|
logger.error("Unknown combination code for %s", event)
|
|
|
|
result.append(str(event.code))
|
|
|
|
continue
|
|
|
|
|
|
|
|
key_name = None
|
|
|
|
|
|
|
|
# first try to find the name in xmodmap to not display wrong
|
|
|
|
# names due to the keyboard layout
|
|
|
|
if event.type == ecodes.EV_KEY:
|
|
|
|
key_name = system_mapping.get_name(event.code)
|
|
|
|
|
|
|
|
if key_name is None:
|
|
|
|
# if no result, look in the linux combination constants. On a german
|
|
|
|
# keyboard for example z and y are switched, which will therefore
|
|
|
|
# cause the wrong letter to be displayed.
|
|
|
|
key_name = ecodes.bytype[event.type][event.code]
|
|
|
|
if isinstance(key_name, list):
|
|
|
|
key_name = key_name[0]
|
|
|
|
|
|
|
|
if event.type != ecodes.EV_KEY:
|
|
|
|
direction = {
|
|
|
|
# D-Pad
|
|
|
|
(ecodes.ABS_HAT0X, -1): "Left",
|
|
|
|
(ecodes.ABS_HAT0X, 1): "Right",
|
|
|
|
(ecodes.ABS_HAT0Y, -1): "Up",
|
|
|
|
(ecodes.ABS_HAT0Y, 1): "Down",
|
|
|
|
(ecodes.ABS_HAT1X, -1): "Left",
|
|
|
|
(ecodes.ABS_HAT1X, 1): "Right",
|
|
|
|
(ecodes.ABS_HAT1Y, -1): "Up",
|
|
|
|
(ecodes.ABS_HAT1Y, 1): "Down",
|
|
|
|
(ecodes.ABS_HAT2X, -1): "Left",
|
|
|
|
(ecodes.ABS_HAT2X, 1): "Right",
|
|
|
|
(ecodes.ABS_HAT2Y, -1): "Up",
|
|
|
|
(ecodes.ABS_HAT2Y, 1): "Down",
|
|
|
|
# joystick
|
|
|
|
(ecodes.ABS_X, 1): "Right",
|
|
|
|
(ecodes.ABS_X, -1): "Left",
|
|
|
|
(ecodes.ABS_Y, 1): "Down",
|
|
|
|
(ecodes.ABS_Y, -1): "Up",
|
|
|
|
(ecodes.ABS_RX, 1): "Right",
|
|
|
|
(ecodes.ABS_RX, -1): "Left",
|
|
|
|
(ecodes.ABS_RY, 1): "Down",
|
|
|
|
(ecodes.ABS_RY, -1): "Up",
|
|
|
|
# wheel
|
|
|
|
(ecodes.REL_WHEEL, -1): "Down",
|
|
|
|
(ecodes.REL_WHEEL, 1): "Up",
|
|
|
|
(ecodes.REL_HWHEEL, -1): "Left",
|
|
|
|
(ecodes.REL_HWHEEL, 1): "Right",
|
|
|
|
}.get((event.code, event.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)
|