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
|
# -*- 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/>.
"""Provides protocols for mapping handlers
*** The architecture behind mapping handlers ***
Handling an InputEvent is done in 3 steps:
1. Input Event Handling
A MappingHandler that does Input event handling receives Input Events directly
from the EventReader.
To do so it must implement the InputEventHandler protocol.
An InputEventHandler may handle multiple events (InputEvent.type_and_code)
2. Event Transformation
The event gets transformed as described by the mapping.
e.g.: combining multiple events to a single one
transforming EV_ABS to EV_REL
macros
...
Multiple transformations may get chained
3. Event Injection
The transformed event gets injected to a global_uinput
MappingHandlers can implement one or more of these steps.
Overview of implemented handlers and the steps they implement:
Step 1:
- HierarchyHandler
Step 1 and 2:
- CombinationHandler
- AbsToBtnHandler
- RelToBtnHandler
Step 1, 2 and 3:
- AbsToRelHandler
- NullHandler
Step 2 and 3:
- KeyHandler
- MacroHandler
"""
from __future__ import annotations
import enum
from typing import Dict, Protocol, Set, Optional, List
import evdev
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.mapping import Mapping
from inputremapper.exceptions import MappingParsingError
from inputremapper.injection.global_uinputs import GlobalUInputs
from inputremapper.input_event import InputEvent
from inputremapper.logging.logger import logger
class EventListener(Protocol):
async def __call__(self, event: evdev.InputEvent) -> None: ...
class ContextProtocol(Protocol):
"""The parts from context needed for handlers."""
listeners: Set[EventListener]
def get_forward_uinput(self, origin_hash) -> evdev.UInput:
pass
class NotifyCallback(Protocol):
"""Type signature of InputEventHandler.notify
return True if the event was actually taken care of
"""
def __call__(
self,
event: InputEvent,
source: evdev.InputDevice,
suppress: bool = False,
) -> bool: ...
class InputEventHandler(Protocol):
"""The protocol any handler, which can be part of an event pipeline, must follow."""
def notify(
self,
event: InputEvent,
source: evdev.InputDevice,
suppress: bool = False,
) -> bool: ...
def reset(self) -> None:
"""Reset the state of the handler e.g. release any buttons."""
...
class HandlerEnums(enum.Enum):
# converting to btn
abs2btn = enum.auto()
rel2btn = enum.auto()
macro = enum.auto()
key = enum.auto()
# converting to "analog"
btn2rel = enum.auto()
rel2rel = enum.auto()
abs2rel = enum.auto()
btn2abs = enum.auto()
rel2abs = enum.auto()
abs2abs = enum.auto()
# special handlers
combination = enum.auto()
hierarchy = enum.auto()
axisswitch = enum.auto()
disable = enum.auto()
class MappingHandler:
"""The protocol an InputEventHandler must follow if it should be
dynamically integrated in an event-pipeline by the mapping parser
"""
mapping: Mapping
# all input events this handler cares about
# should always be a subset of mapping.input_combination
input_configs: List[InputConfig]
_sub_handler: Optional[InputEventHandler]
# https://bugs.python.org/issue44807
def __init__(
self,
combination: InputCombination,
mapping: Mapping,
global_uinputs: GlobalUInputs,
**_,
) -> None:
"""Initialize the handler
Parameters
----------
combination
the combination from sub_handler.wrap_with()
mapping
"""
self.mapping = mapping
self.input_configs = list(combination)
self._sub_handler = None
self.global_uinputs = global_uinputs
def notify(
self,
event: InputEvent,
source: evdev.InputDevice,
suppress: bool = False,
) -> bool:
"""Notify this handler about an incoming event.
Parameters
----------
event
The newest event that came from `source`, and that should be mapped to
something else
source
Where `event` comes from
"""
raise NotImplementedError
def reset(self) -> None:
"""Reset the state of the handler e.g. release any buttons."""
raise NotImplementedError
def needs_wrapping(self) -> bool:
"""If this handler needs to be wrapped in another MappingHandler."""
return len(self.wrap_with()) > 0
def needs_ranking(self) -> bool:
"""If this handler needs ranking and wrapping with a HierarchyHandler."""
return False
def rank_by(self) -> Optional[InputCombination]:
"""The combination for which this handler needs ranking."""
def wrap_with(self) -> Dict[InputCombination, HandlerEnums]:
"""A dict of InputCombination -> HandlerEnums.
for each InputCombination this handler should be wrapped
with the given MappingHandler.
"""
return {}
def set_sub_handler(self, handler: InputEventHandler) -> None:
"""Give this handler a sub_handler."""
self._sub_handler = handler
def occlude_input_event(self, input_config: InputConfig) -> None:
"""Remove the config from self.input_configs."""
if not self.input_configs:
logger.debug_mapping_handler(self)
raise MappingParsingError(
"Cannot remove a non existing config", mapping_handler=self
)
# should be called for each event a wrapping-handler
# has in its input_configs InputCombination
self.input_configs.remove(input_config)
|