File: rel_to_btn_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 (145 lines) | stat: -rw-r--r-- 4,910 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
# -*- 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/>.

import asyncio
import time

import evdev
from evdev.ecodes import EV_REL

from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.mapping import Mapping
from inputremapper.injection.global_uinputs import GlobalUInputs
from inputremapper.injection.mapping_handlers.mapping_handler import (
    MappingHandler,
    InputEventHandler,
)
from inputremapper.input_event import InputEvent, EventActions
from inputremapper.logging.logger import logger


class RelToBtnHandler(MappingHandler):
    """Handler which transforms an EV_REL to a button event
    and sends that to a sub_handler

    adheres to the MappingHandler protocol
    """

    _active: bool
    _input_config: InputConfig
    _last_activation: float
    _sub_handler: InputEventHandler

    def __init__(
        self,
        combination: InputCombination,
        mapping: Mapping,
        global_uinputs: GlobalUInputs,
        **_,
    ) -> None:
        super().__init__(combination, mapping, global_uinputs)

        self._active = False
        self._input_config = combination[0]
        self._last_activation = time.time()
        self._abort_release = False
        assert self._input_config.analog_threshold != 0
        assert len(combination) == 1

    def __str__(self):
        return f'RelToBtnHandler for "{self._input_config}"'

    def __repr__(self):
        return f"<{str(self)} at {hex(id(self))}>"

    @property
    def child(self):  # used for logging
        return self._sub_handler

    async def _stage_release(
        self,
        source: InputEvent,
        suppress: bool,
    ):
        while time.time() < self._last_activation + self.mapping.release_timeout:
            await asyncio.sleep(1 / self.mapping.rel_rate)

        if self._abort_release:
            self._abort_release = False
            return

        event = InputEvent(
            0,
            0,
            *self._input_config.type_and_code,
            value=0,
            actions=(EventActions.as_key,),
            origin_hash=self._input_config.origin_hash,
        )
        logger.debug("Sending %s to sub_handler", event)
        self._sub_handler.notify(event, source, suppress)
        self._active = False

    def notify(
        self,
        event: InputEvent,
        source: evdev.InputDevice,
        suppress: bool = False,
    ) -> bool:
        assert event.type == EV_REL
        if event.input_match_hash != self._input_config.input_match_hash:
            return False

        assert (threshold := self._input_config.analog_threshold)
        value = event.value
        if (value < threshold > 0) or (value > threshold < 0):
            if self._active:
                # the axis is below the threshold and the stage_release
                # function is running
                if self.mapping.force_release_timeout:
                    # consume the event
                    return True
                event = event.modify(value=0, actions=(EventActions.as_key,))
                logger.debug("Sending %s to sub_handler", event)
                self._abort_release = True
            else:
                # don't consume the event.
                # We could return True to consume events
                return False
        else:
            # the axis is above the threshold
            if not self._active:
                asyncio.ensure_future(self._stage_release(source, suppress))
            if value >= threshold > 0:
                direction = EventActions.positive_trigger
            else:
                direction = EventActions.negative_trigger
            self._last_activation = time.time()
            event = event.modify(value=1, actions=(EventActions.as_key, direction))

        self._active = bool(event.value)
        # logger.debug("Sending %s to sub_handler", event)
        return self._sub_handler.notify(event, source=source, suppress=suppress)

    def reset(self) -> None:
        if self._active:
            self._abort_release = True

        self._active = False
        self._sub_handler.reset()