input-remapper/inputremapper/injection/mapping_handlers/mapping_parser.py

320 lines
12 KiB
Python
Raw Normal View History

2022-04-17 10:19:23 +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/>.
"""Functions to assemble the mapping handlers"""
2022-04-17 10:19:23 +00:00
from typing import Dict, List, Type, Optional, Set, Iterable, Sized, Tuple, Sequence
from evdev.ecodes import (
EV_KEY,
EV_ABS,
EV_REL,
)
from inputremapper.exceptions import MappingParsingError
from inputremapper.logger import logger
from inputremapper.event_combination import EventCombination
from inputremapper.input_event import InputEvent
from inputremapper.injection.mapping_handlers.mapping_handler import (
HandlerEnums,
MappingHandler,
ContextProtocol,
)
from inputremapper.injection.mapping_handlers.combination_handler import (
CombinationHandler,
)
from inputremapper.injection.mapping_handlers.hierarchy_handler import HierarchyHandler
from inputremapper.injection.mapping_handlers.abs_to_btn_handler import AbsToBtnHandler
from inputremapper.injection.mapping_handlers.rel_to_btn_handler import RelToBtnHandler
from inputremapper.injection.mapping_handlers.abs_to_rel_handler import AbsToRelHandler
from inputremapper.injection.mapping_handlers.macro_handler import MacroHandler
from inputremapper.injection.mapping_handlers.key_handler import KeyHandler
from inputremapper.injection.mapping_handlers.axis_switch_handler import (
AxisSwitchHandler,
)
from inputremapper.injection.mapping_handlers.null_handler import NullHandler
from inputremapper.injection.macros.parse import is_this_a_macro
from inputremapper.configs.preset import Preset
from inputremapper.configs.mapping import Mapping
from inputremapper.configs.system_mapping import DISABLE_CODE, DISABLE_NAME
EventPipelines = Dict[InputEvent, List[MappingHandler]]
mapping_handler_classes: Dict[HandlerEnums, Type[MappingHandler]] = {
# all available mapping_handlers
HandlerEnums.abs2btn: AbsToBtnHandler,
HandlerEnums.rel2btn: RelToBtnHandler,
HandlerEnums.macro: MacroHandler,
HandlerEnums.key: KeyHandler,
HandlerEnums.btn2rel: None, # type: ignore
HandlerEnums.rel2rel: None, # type: ignore
HandlerEnums.abs2rel: AbsToRelHandler,
HandlerEnums.btn2abs: None, # type: ignore
HandlerEnums.rel2abs: None, # type: ignore
HandlerEnums.abs2abs: None, # type: ignore
HandlerEnums.combination: CombinationHandler,
HandlerEnums.hierarchy: HierarchyHandler,
HandlerEnums.axisswitch: AxisSwitchHandler,
HandlerEnums.disable: NullHandler,
}
def parse_mappings(preset: Preset, context: ContextProtocol) -> EventPipelines:
"""Create a dict with a list of MappingHandler for each InputEvent."""
2022-04-17 10:19:23 +00:00
handlers = []
for mapping in preset:
# start with the last handler in the chain, each mapping only has one output,
# but may have multiple inputs, therefore the last handler is a good starting
# point to assemble the pipeline
handler_enum = _get_output_handler(mapping)
constructor = mapping_handler_classes[handler_enum]
if not constructor:
raise NotImplementedError(
f"mapping handler {handler_enum} is not implemented"
)
output_handler = constructor(
mapping.event_combination,
mapping,
context=context,
2022-04-17 10:19:23 +00:00
)
# layer other handlers on top until the outer handler needs ranking or can
# directly handle a input event
handlers.extend(_create_event_pipeline(output_handler, context))
# figure out which handlers need ranking and wrap them with hierarchy_handlers
need_ranking = {}
for handler in handlers.copy():
if handler.needs_ranking():
combination = handler.rank_by()
if not combination:
raise MappingParsingError(
f"{type(handler).__name__} claims to need ranking but does not "
f"return a combination to rank by",
mapping_handler=handler,
)
need_ranking[combination] = handler
handlers.remove(handler)
# the HierarchyHandler's might not be the starting point of the event pipeline
# layer other handlers on top again.
ranked_handlers = _create_hierarchy_handlers(need_ranking)
for handler in ranked_handlers:
handlers.extend(_create_event_pipeline(handler, context, ignore_ranking=True))
# group all handlers by the input events they take care of. One handler might end
# up in multiple groups if it takes care of multiple InputEvents
event_pipelines: EventPipelines = {}
for handler in handlers:
assert handler.input_events
for event in handler.input_events:
if event in event_pipelines.keys():
logger.debug("event-pipeline with entry point: %s", event.type_and_code)
logger.debug_mapping_handler(handler)
event_pipelines[event].append(handler)
else:
logger.debug("event-pipeline with entry point: %s", event.type_and_code)
logger.debug_mapping_handler(handler)
event_pipelines[event] = [handler]
return event_pipelines
def _create_event_pipeline(
handler: MappingHandler, context: ContextProtocol, ignore_ranking=False
) -> List[MappingHandler]:
"""Recursively wrap a handler with other handlers until the
2022-04-17 10:19:23 +00:00
outer handler needs ranking or is finished wrapping
"""
if not handler.needs_wrapping() or (handler.needs_ranking() and not ignore_ranking):
return [handler]
handlers = []
for combination, handler_enum in handler.wrap_with().items():
constructor = mapping_handler_classes[handler_enum]
if not constructor:
raise NotImplementedError(
f"mapping handler {handler_enum} is not implemented"
)
super_handler = constructor(combination, handler.mapping, context=context)
super_handler.set_sub_handler(handler)
for event in combination:
# the handler now has a super_handler which takes care about the events.
# so we need to hide them on the handler
handler.occlude_input_event(event)
handlers.extend(_create_event_pipeline(super_handler, context))
if handler.input_events:
# the handler was only partially wrapped,
# we need to return it as a toplevel handler
handlers.append(handler)
return handlers
def _get_output_handler(mapping: Mapping) -> HandlerEnums:
"""Determine the correct output handler
2022-04-17 10:19:23 +00:00
this is used as a starting point for the mapping parser
"""
if mapping.output_code == DISABLE_CODE or mapping.output_symbol == DISABLE_NAME:
return HandlerEnums.disable
if mapping.output_symbol:
if is_this_a_macro(mapping.output_symbol):
return HandlerEnums.macro
else:
return HandlerEnums.key
if mapping.output_type == EV_KEY:
return HandlerEnums.key
input_event = _maps_axis(mapping.event_combination)
if not input_event:
raise MappingParsingError(
f"this {mapping = } does not map to an axis, key or macro",
mapping=Mapping,
2022-04-17 10:19:23 +00:00
)
if mapping.output_type == EV_REL:
if input_event.type == EV_KEY:
return HandlerEnums.btn2rel
if input_event.type == EV_REL:
return HandlerEnums.rel2rel
if input_event.type == EV_ABS:
return HandlerEnums.abs2rel
if mapping.output_type == EV_ABS:
if input_event.type == EV_KEY:
return HandlerEnums.btn2abs
if input_event.type == EV_REL:
return HandlerEnums.rel2abs
if input_event.type == EV_ABS:
return HandlerEnums.abs2abs
raise MappingParsingError(f"the output of {mapping = } is unknown", mapping=Mapping)
def _maps_axis(combination: EventCombination) -> Optional[InputEvent]:
"""Whether this EventCombination contains an InputEvent that is treated as
2022-04-17 10:19:23 +00:00
an axis and not a binary (key or button) event.
"""
for event in combination:
if event.value == 0:
return event
return None
def _create_hierarchy_handlers(
handlers: Dict[EventCombination, MappingHandler]
) -> Set[MappingHandler]:
"""Sort handlers by input events and create Hierarchy handlers."""
2022-04-17 10:19:23 +00:00
sorted_handlers = set()
all_combinations = handlers.keys()
events = set()
# gather all InputEvents from all handlers
for combination in all_combinations:
for event in combination:
events.add(event)
# create a ranking for each event
for event in events:
# find all combinations (from handlers) which contain the event
combinations_with_event = [
combination for combination in all_combinations if event in combination
]
if len(combinations_with_event) == 1:
# there was only one handler containing that event return it as is
sorted_handlers.add(handlers[combinations_with_event[0]])
continue
# there are multiple handler with the same event.
# rank them and create the HierarchyHandler
sorted_combinations = _order_combinations(combinations_with_event, event)
sub_handlers = []
for combination in sorted_combinations:
sub_handlers.append(handlers[combination])
sorted_handlers.add(HierarchyHandler(sub_handlers, event))
for handler in sub_handlers:
# the handler now has a HierarchyHandler which takes care about this event.
# so we hide need to hide it on the handler
handler.occlude_input_event(event)
return sorted_handlers
def _order_combinations(
combinations: List[EventCombination], common_event: InputEvent
) -> List[EventCombination]:
"""Reorder the keys according to some rules
2022-04-17 10:19:23 +00:00
such that a combination a+b+c is in front of a+b which is in front of b
for a+b+c vs. b+d+e: a+b+c would be in front of b+d+e, because the common key b
has the higher index in the a+b+c (1), than in the b+c+d (0) list
in this example b would be the common key
as for combinations like a+b+c and e+d+c with the common key c: ¯\\_()_/¯
Parameters
----------
combinations : List[Key]
the list which needs ordering
common_event : InputEvent
the Key all members of Keys have in common
"""
combinations.sort(key=len)
for start, end in ranges_with_constant_length(combinations.copy()):
sub_list = combinations[start:end]
sub_list.sort(key=lambda x: x.index(common_event))
combinations[start:end] = sub_list
combinations.reverse()
return combinations
def ranges_with_constant_length(x: Sequence[Sized]) -> Iterable[Tuple[int, int]]:
"""Get all ranges of x for which the elements have constant length
2022-04-17 10:19:23 +00:00
Parameters
----------
x: Sequence[Sized]
l must be ordered by increasing length of elements
"""
start_idx = 0
last_len = 0
for idx, y in enumerate(x):
if len(y) > last_len and idx - start_idx > 1:
yield start_idx, idx
if len(y) == last_len and idx + 1 == len(x):
yield start_idx, idx + 1
if len(y) > last_len:
start_idx = idx
if len(y) < last_len:
raise MappingParsingError(
"ranges_with_constant_length " "was called with an unordered list"
)
last_len = len(y)