1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
|
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2025 sezanzeb <b8x45ygc9@mozmail.com>
#
# 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 handler tree."""
from collections import defaultdict
from typing import Dict, List, Type, Optional, Set, Iterable, Sized, Tuple, Sequence
from evdev.ecodes import EV_KEY, EV_ABS, EV_REL
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.keyboard_layout import DISABLE_CODE, DISABLE_NAME
from inputremapper.configs.mapping import Mapping
from inputremapper.configs.preset import Preset
from inputremapper.exceptions import MappingParsingError
from inputremapper.injection.global_uinputs import GlobalUInputs
from inputremapper.injection.macros.parse import Parser
from inputremapper.injection.mapping_handlers.abs_to_abs_handler import AbsToAbsHandler
from inputremapper.injection.mapping_handlers.abs_to_btn_handler import AbsToBtnHandler
from inputremapper.injection.mapping_handlers.abs_to_rel_handler import AbsToRelHandler
from inputremapper.injection.mapping_handlers.axis_switch_handler import (
AxisSwitchHandler,
)
from inputremapper.injection.mapping_handlers.combination_handler import (
CombinationHandler,
)
from inputremapper.injection.mapping_handlers.hierarchy_handler import HierarchyHandler
from inputremapper.injection.mapping_handlers.key_handler import KeyHandler
from inputremapper.injection.mapping_handlers.macro_handler import MacroHandler
from inputremapper.injection.mapping_handlers.mapping_handler import (
HandlerEnums,
MappingHandler,
ContextProtocol,
InputEventHandler,
)
from inputremapper.injection.mapping_handlers.null_handler import NullHandler
from inputremapper.injection.mapping_handlers.rel_to_abs_handler import RelToAbsHandler
from inputremapper.injection.mapping_handlers.rel_to_btn_handler import RelToBtnHandler
from inputremapper.injection.mapping_handlers.rel_to_rel_handler import RelToRelHandler
from inputremapper.logging.logger import logger
from inputremapper.utils import get_evdev_constant_name
EventPipelines = Dict[InputConfig, Set[InputEventHandler]]
mapping_handler_classes: Dict[HandlerEnums, Optional[Type[MappingHandler]]] = {
# all available mapping_handlers
HandlerEnums.abs2btn: AbsToBtnHandler,
HandlerEnums.rel2btn: RelToBtnHandler,
HandlerEnums.macro: MacroHandler,
HandlerEnums.key: KeyHandler,
HandlerEnums.btn2rel: None, # can be a macro
HandlerEnums.rel2rel: RelToRelHandler,
HandlerEnums.abs2rel: AbsToRelHandler,
HandlerEnums.btn2abs: None, # can be a macro
HandlerEnums.rel2abs: RelToAbsHandler,
HandlerEnums.abs2abs: AbsToAbsHandler,
HandlerEnums.combination: CombinationHandler,
HandlerEnums.hierarchy: HierarchyHandler,
HandlerEnums.axisswitch: AxisSwitchHandler,
HandlerEnums.disable: NullHandler,
}
class MappingParser:
def __init__(
self,
global_uinputs: GlobalUInputs,
):
self.global_uinputs = global_uinputs
def parse_mappings(
self,
preset: Preset,
context: ContextProtocol,
) -> EventPipelines:
"""Create a dict with a list of MappingHandler for each InputEvent."""
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 = self._get_output_handler(mapping)
constructor = mapping_handler_classes[handler_enum]
if not constructor:
logger.warning(
"a mapping handler '%s' for %s is not implemented",
handler_enum,
mapping.format_name(),
)
continue
output_handler = constructor(
mapping.input_combination,
mapping,
context=context,
global_uinputs=self.global_uinputs,
)
# layer other handlers on top until the outer handler needs ranking or can
# directly handle a input event
handlers.extend(self._create_event_pipeline(output_handler, context))
# figure out which handlers need ranking and wrap them with hierarchy_handlers
need_ranking = defaultdict(set)
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].add(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 = self._create_hierarchy_handlers(need_ranking)
for handler in ranked_handlers:
handlers.extend(
self._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 = defaultdict(set)
for handler in handlers:
assert handler.input_configs
for input_config in handler.input_configs:
logger.debug(
"event-pipeline with entry point: %s %s",
get_evdev_constant_name(*input_config.type_and_code),
input_config.input_match_hash,
)
logger.debug_mapping_handler(handler)
event_pipelines[input_config].add(handler)
return event_pipelines
def _create_event_pipeline(
self,
handler: MappingHandler,
context: ContextProtocol,
ignore_ranking=False,
) -> List[MappingHandler]:
"""Recursively wrap a handler with other handlers until the
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,
global_uinputs=self.global_uinputs,
)
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(self._create_event_pipeline(super_handler, context))
if handler.input_configs:
# the handler was only partially wrapped,
# we need to return it as a toplevel handler
handlers.append(handler)
return handlers
def _get_output_handler(self, mapping: Mapping) -> HandlerEnums:
"""Determine the correct output handler.
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 Parser.is_this_a_macro(mapping.output_symbol):
return HandlerEnums.macro
return HandlerEnums.key
if mapping.output_type == EV_KEY:
return HandlerEnums.key
input_event = self._maps_axis(mapping.input_combination)
if not input_event:
raise MappingParsingError(
f"This {mapping = } does not map to an axis, key or macro",
mapping=Mapping,
)
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(self, combination: InputCombination) -> Optional[InputConfig]:
"""Whether this InputCombination contains an InputEvent that is treated as
an axis and not a binary (key or button) event.
"""
for event in combination:
if event.defines_analog_input:
return event
return None
def _create_hierarchy_handlers(
self,
handlers: Dict[InputCombination, Set[MappingHandler]],
) -> Set[MappingHandler]:
"""Sort handlers by input events and create Hierarchy handlers."""
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.update(handlers[combinations_with_event[0]])
continue
# there are multiple handler with the same event.
# rank them and create the HierarchyHandler
sorted_combinations = self._order_combinations(
combinations_with_event,
event,
)
sub_handlers: List[MappingHandler] = []
for combination in sorted_combinations:
sub_handlers.extend(handlers[combination])
sorted_handlers.add(
HierarchyHandler(
sub_handlers,
event,
self.global_uinputs,
)
)
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(
self,
combinations: List[InputCombination],
common_config: InputConfig,
) -> List[InputCombination]:
"""Reorder the keys according to some rules.
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
the list which needs ordering
common_config
the InputConfig all InputCombination's in combinations have in common
"""
combinations.sort(key=len)
for start, end in self._ranges_with_constant_length(combinations.copy()):
sub_list = combinations[start:end]
sub_list.sort(key=lambda x: x.index(common_config))
combinations[start:end] = sub_list
combinations.reverse()
return combinations
def _ranges_with_constant_length(
self,
x: Sequence[Sized],
) -> Iterable[Tuple[int, int]]:
"""Get all ranges of x for which the elements have constant length
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)
|