File: axis_switch_handler.py

package info (click to toggle)
input-remapper 2.1.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 2,856 kB
  • sloc: python: 27,277; sh: 191; xml: 33; makefile: 3
file content (187 lines) | stat: -rw-r--r-- 6,358 bytes parent folder | download | duplicates (3)
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}