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
|
# -*- 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/>.
from typing import Dict, Tuple, Hashable
import evdev
from inputremapper.configs.input_config import InputCombination
from inputremapper.configs.input_config import InputConfig
from inputremapper.configs.mapping import Mapping
from inputremapper.injection.global_uinputs import GlobalUInputs
from inputremapper.injection.mapping_handlers.mapping_handler import (
MappingHandler,
HandlerEnums,
InputEventHandler,
ContextProtocol,
)
from inputremapper.input_event import InputEvent, EventActions
from inputremapper.logging.logger import logger
from inputremapper.utils import get_device_hash
class AxisSwitchHandler(MappingHandler):
"""Enables or disables an axis.
Generally, if multiple events are mapped to something in a combination, all of
them need to be triggered in order to map to the output.
If an analog input is combined with a key input, then the same thing should happen.
The key needs to be pressed and the joystick needs to be moved in order to generate
output.
"""
_map_axis: InputConfig # the InputConfig for the axis we switch on or off
_trigger_keys: Tuple[Hashable, ...] # all events that can switch the axis
_active: bool # whether the axis is on or off
_last_value: int # the value of the last axis event that arrived
_axis_source: evdev.InputDevice # the cached source of the axis input events
_forward_device: evdev.UInput # the cached forward uinput
_sub_handler: InputEventHandler
def __init__(
self,
combination: InputCombination,
mapping: Mapping,
context: ContextProtocol,
global_uinputs: GlobalUInputs,
**_,
):
super().__init__(combination, mapping, global_uinputs)
trigger_keys = tuple(
event.input_match_hash
for event in combination
if not event.defines_analog_input
)
assert len(trigger_keys) >= 1
assert (map_axis := combination.find_analog_input_config())
self._map_axis = map_axis
self._trigger_keys = trigger_keys
self._active = False
self._last_value = 0
self._axis_source = None
self._forward_device = None
self.context = context
def __str__(self):
return f"AxisSwitchHandler for {self._map_axis.type_and_code}"
def __repr__(self):
return f"<{str(self)} at {hex(id(self))}>"
@property
def child(self):
return self._sub_handler
def _handle_key_input(self, event: InputEvent):
"""If a key is pressed, allow mapping analog events in subhandlers.
Analog events (e.g. ABS_X, REL_Y) that have gone through Handlers that
transform them to buttons also count as keys.
"""
key_is_pressed = bool(event.value)
if self._active == key_is_pressed:
# nothing changed
return False
self._active = key_is_pressed
if self._axis_source is None:
return True
if not key_is_pressed:
# recenter the axis
logger.debug("Stopping axis for %s", self.mapping.input_combination)
event = InputEvent(
0,
0,
*self._map_axis.type_and_code,
0,
actions=(EventActions.recenter,),
origin_hash=self._map_axis.origin_hash,
)
self._sub_handler.notify(event, self._axis_source)
return True
if self._map_axis.type == evdev.ecodes.EV_ABS:
# send the last cached value so that the abs axis
# is at the correct position
logger.debug("Starting axis for %s", self.mapping.input_combination)
event = InputEvent(
0,
0,
*self._map_axis.type_and_code,
self._last_value,
origin_hash=self._map_axis.origin_hash,
)
self._sub_handler.notify(event, self._axis_source)
return True
return True
def _should_map(self, event: InputEvent):
return (
event.input_match_hash in self._trigger_keys
or event.input_match_hash == self._map_axis.input_match_hash
)
def notify(
self,
event: InputEvent,
source: evdev.InputDevice,
suppress: bool = False,
) -> bool:
if not self._should_map(event):
return False
if event.is_key_event:
return self._handle_key_input(event)
# do some caching so that we can generate the
# recenter event and an initial abs event
if self._axis_source is None:
self._axis_source = source
if self._forward_device is None:
device_hash = get_device_hash(source)
self._forward_device = self.context.get_forward_uinput(device_hash)
# always cache the value
self._last_value = event.value
if self._active:
return self._sub_handler.notify(event, source, suppress)
return False
def reset(self) -> None:
self._last_value = 0
self._active = False
self._sub_handler.reset()
def needs_wrapping(self) -> bool:
return True
def wrap_with(self) -> Dict[InputCombination, HandlerEnums]:
combination = [
config for config in self.input_configs if not config.defines_analog_input
]
return {InputCombination(combination): HandlerEnums.combination}
|