Skip to content

Commit 982e480

Browse files
committed
refactor fix, fix incorrect test
1 parent 0a46a12 commit 982e480

File tree

2 files changed

+69
-59
lines changed

2 files changed

+69
-59
lines changed

inputremapper/injection/mapping_handlers/combination_handler.py

Lines changed: 56 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,19 @@ class CombinationHandler(MappingHandler):
4242

4343
# map of InputEvent.input_match_hash -> bool , keep track of the combination state
4444
_pressed_keys: Dict[Hashable, bool]
45-
_output_state: bool # the last update we sent to a sub-handler
45+
46+
# the last update we sent to a sub-handler. If this is true, the output key is
47+
# still being held down.
48+
_output_active: bool
4649
_sub_handler: InputEventHandler
4750
_handled_input_hashes: list[Hashable]
48-
_notify_results: Dict[Tuple[int, int], bool]
51+
52+
# If a key-up event arrives that will inactivate the combination, but
53+
# for which previously a key-down event was injected (because it was
54+
# an earlier key in the combination chain), then we need to ensure that its
55+
# release is injected as well. So we get two release events in that case:
56+
# one for the key, and one for the output.
57+
_requires_a_release: Dict[Tuple[int, int], bool]
4958

5059
def __init__(
5160
self,
@@ -57,9 +66,9 @@ def __init__(
5766
logger.debug(str(mapping))
5867
super().__init__(combination, mapping)
5968
self._pressed_keys = {}
60-
self._output_state = False
69+
self._output_active = False
6170
self._context = context
62-
self._notify_results = {}
71+
self._requires_a_release = {}
6372

6473
# prepare a key map for all events with non-zero value
6574
for input_config in combination:
@@ -95,58 +104,39 @@ def notify(
95104
event: InputEvent,
96105
source: evdev.InputDevice,
97106
suppress: bool = False,
98-
) -> bool:
99-
result = self._notify(event, source, suppress)
100-
101-
if event.type_and_code in self._notify_results:
102-
# The return value is always the same as for the key-down event.
103-
# If a key-up event arrives that will inactivate the combination, but
104-
# for which previously a key-down event was injected (because it was
105-
# an earlier key in the combination chain), then we need to ensure that its
106-
# release is injected as well.
107-
result = self._notify_results[event.type_and_code]
108-
del self._notify_results[event.type_and_code]
109-
return result
110-
111-
self._notify_results[event.type_and_code] = result
112-
return result
113-
114-
def _notify(
115-
self,
116-
event: InputEvent,
117-
source: evdev.InputDevice,
118-
suppress: bool = False,
119107
) -> bool:
120108
if event.input_match_hash not in self._handled_input_hashes:
121109
# we are not responsible for the event
122110
return False
123111

124-
was_activated = self.is_activated()
125-
126112
# update the state
127113
# The value of non-key input should have been changed to either 0 or 1 at this
128114
# point by other handlers.
129115
is_pressed = event.value == 1
116+
is_released = event.value == 0
130117
self._pressed_keys[event.input_match_hash] = is_pressed
131118
# maybe this changes the activation status (triggered/not-triggered)
132119
is_activated = self.is_activated()
133120

134-
if is_activated == was_activated or is_activated == self._output_state:
121+
if is_activated == self._output_active:
135122
# nothing changed
136-
if self._output_state:
137-
# combination is active, consume the event
138-
return True
139-
else:
140-
# combination inactive, forward the event
141-
return False
123+
# combination is active: consume the event
124+
# combination inactive: forward the event
125+
if is_pressed:
126+
self.remember(self._output_active, event)
127+
return self._output_active
128+
129+
if is_released:
130+
return self.should_release_event(event)
142131

143132
if is_activated:
144133
# send key up events to the forwarded uinput
145134
self.forward_release()
146135
event = event.modify(value=1)
147-
else:
148-
if self._output_state or self.mapping.is_axis_mapping():
149-
# we ignore the suppress argument for release events
136+
137+
if not is_activated:
138+
if self._output_active or self.mapping.is_axis_mapping():
139+
# we ignore the `suppress` argument for release events
150140
# otherwise we might end up with stuck keys
151141
# (test_event_pipeline.test_combination)
152142

@@ -159,14 +149,34 @@ def _notify(
159149
return False
160150

161151
logger.debug("Sending %s to sub-handler", self.mapping.input_combination)
162-
self._output_state = bool(event.value)
163-
return self._sub_handler.notify(event, source, suppress)
152+
self._output_active = bool(event.value)
153+
sub_handler_result = self._sub_handler.notify(event, source, suppress)
154+
155+
if is_pressed:
156+
self.remember(sub_handler_result, event)
157+
return sub_handler_result
158+
159+
if is_released:
160+
return self.should_release_event(event)
161+
162+
def should_release_event(self, event):
163+
if event.value == 0 and event.type_and_code in self._requires_a_release:
164+
forward_release = self._requires_a_release[event.type_and_code]
165+
del self._requires_a_release[event.type_and_code]
166+
# False means "please forward this, event-reader", therefore we negate
167+
# this.
168+
return not forward_release
169+
170+
return True
171+
172+
def remember(self, handled, event):
173+
self._requires_a_release[event.type_and_code] = not handled
164174

165175
def reset(self) -> None:
166176
self._sub_handler.reset()
167177
for key in self._pressed_keys:
168178
self._pressed_keys[key] = False
169-
self._output_state = False
179+
self._output_active = False
170180

171181
def is_activated(self) -> bool:
172182
"""Return if all keys in the keymap are set to True."""
@@ -188,6 +198,9 @@ def forward_release(self) -> None:
188198
logger.debug("Forwarding release for %s", self.mapping.input_combination)
189199

190200
for input_config in keys_to_release:
201+
if not self._requires_a_release.get(input_config.type_and_code):
202+
continue
203+
191204
origin_hash = input_config.origin_hash
192205
if origin_hash is None:
193206
logger.error(
@@ -200,6 +213,9 @@ def forward_release(self) -> None:
200213
forward_to.write(*input_config.type_and_code, 0)
201214
forward_to.syn()
202215

216+
# We are done with this key, forget about it
217+
del self._requires_a_release[input_config.type_and_code]
218+
203219
def needs_ranking(self) -> bool:
204220
return bool(self.input_configs)
205221

tests/unit/test_injector.py

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,14 @@
2323
except ImportError:
2424
from pydantic import ValidationError
2525

26-
from inputremapper.input_event import InputEvent
27-
from tests.lib.global_uinputs import (
28-
reset_global_uinputs_for_service,
29-
reset_global_uinputs_for_gui,
30-
)
3126
from tests.lib.patches import uinputs
3227
from tests.lib.cleanup import quick_cleanup
3328
from tests.lib.constants import EVENT_READ_TIMEOUT
3429
from tests.lib.fixtures import fixtures
3530
from tests.lib.pipes import uinput_write_history_pipe
3631
from tests.lib.pipes import read_write_history_pipe, push_events
3732
from tests.lib.fixtures import keyboard_keys
33+
from inputremapper.input_event import InputEvent
3834

3935
import unittest
4036
from unittest import mock
@@ -438,9 +434,10 @@ def test_injector(self):
438434
[
439435
# should execute a macro...
440436
InputEvent.key(8, 1), # forwarded
441-
InputEvent.key(9, 1), # triggers macro
442-
InputEvent.key(8, 0), # releases macro
443-
InputEvent.key(9, 0), # forwarded
437+
InputEvent.key(9, 1), # triggers macro, not forwarding
438+
# macro runs now and injects a few more keys
439+
InputEvent.key(8, 0), # releases macro (needs to be forwarded as well)
440+
InputEvent.key(9, 0), # not forwarded, just like the down-event
444441
],
445442
)
446443

@@ -477,20 +474,15 @@ def test_injector(self):
477474
history = read_write_history_pipe()
478475

479476
# 1 event before the combination was triggered
480-
# 2 events for releasing the combination trigger (by combination handler)
481477
# 4 events for the macro
482-
# 1 release of the event that didn't release the macro
478+
# 1 event for releasing the previous key-down event
483479
# 2 for mapped keys
484480
# 3 for forwarded events
485-
self.assertEqual(len(history), 13)
481+
self.assertEqual(len(history), 11)
486482

487483
# the first bit is ordered properly
488484
self.assertEqual(history[0], (EV_KEY, 8, 1)) # forwarded
489485
del history[0]
490-
self.assertIn((EV_KEY, 8, 0), history[0:2]) # released by combination handler
491-
self.assertIn((EV_KEY, 9, 0), history[0:2]) # released by combination handler
492-
del history[0]
493-
del history[0]
494486

495487
# since the macro takes a little bit of time to execute, its
496488
# keystrokes are all over the place.
@@ -512,10 +504,12 @@ def test_injector(self):
512504
del history[index_q_0]
513505
del history[index_q_1]
514506

515-
# the rest should be in order now.
516-
# first the released combination key which did not release the macro.
517-
# the combination key which released the macro won't appear here.
518-
self.assertEqual(history[0], (EV_KEY, 9, 0))
507+
# The rest should be in order now.
508+
# First the released combination key which did not release the macro.
509+
# The combination key which released the macro won't appear here, because
510+
# it also didn't have a key-down event and therefore doesn't need to be
511+
# released itself.
512+
self.assertEqual(history[0], (EV_KEY, 8, 0))
519513
# value should be 1, even if the input event was -1.
520514
# Injected keycodes should always be either 0 or 1
521515
self.assertEqual(history[1], (EV_KEY, code_a, 1))

0 commit comments

Comments
 (0)