19
19
20
20
from __future__ import annotations # needed for the TYPE_CHECKING import
21
21
22
- from typing import TYPE_CHECKING , Dict , Hashable
22
+ from typing import TYPE_CHECKING , Dict , Hashable , Tuple
23
23
24
24
import evdev
25
25
from evdev .ecodes import EV_ABS , EV_REL
@@ -44,9 +44,12 @@ class CombinationHandler(MappingHandler):
44
44
45
45
# map of InputEvent.input_match_hash -> bool , keep track of the combination state
46
46
_pressed_keys : Dict [Hashable , bool ]
47
- _output_state : bool # the last update we sent to a sub-handler
47
+ # the last update we sent to a sub-handler. If this is true, the output key is
48
+ # still being held down.
49
+ _output_previously_active : bool
48
50
_sub_handler : InputEventHandler
49
51
_handled_input_hashes : list [Hashable ]
52
+ _requires_a_release : Dict [Tuple [int , int ], bool ]
50
53
51
54
def __init__ (
52
55
self ,
@@ -59,8 +62,9 @@ def __init__(
59
62
logger .debug (str (mapping ))
60
63
super ().__init__ (combination , mapping , global_uinputs )
61
64
self ._pressed_keys = {}
62
- self ._output_state = False
65
+ self ._output_previously_active = False
63
66
self ._context = context
67
+ self ._requires_a_release = {}
64
68
65
69
# prepare a key map for all events with non-zero value
66
70
for input_config in combination :
@@ -101,58 +105,118 @@ def notify(
101
105
# we are not responsible for the event
102
106
return False
103
107
104
- was_activated = self .is_activated ()
105
-
106
108
# update the state
107
109
# The value of non-key input should have been changed to either 0 or 1 at this
108
110
# point by other handlers.
109
111
is_pressed = event .value == 1
110
112
self ._pressed_keys [event .input_match_hash ] = is_pressed
111
113
# maybe this changes the activation status (triggered/not-triggered)
112
- is_activated = self .is_activated ()
114
+ changed = self ._is_activated () != self . _output_previously_active
113
115
114
- if is_activated == was_activated or is_activated == self ._output_state :
115
- # nothing changed
116
- if self ._output_state :
117
- # combination is active, consume the event
118
- return True
116
+ if changed :
117
+ if is_pressed :
118
+ return self ._handle_freshly_activated (suppress , event , source )
119
119
else :
120
- # combination inactive, forward the event
121
- return False
122
-
123
- if is_activated :
124
- # send key up events to the forwarded uinput
125
- self .forward_release ()
126
- event = event .modify (value = 1 )
120
+ return self ._handle_freshly_deactivated (event , source )
127
121
else :
128
- if self ._output_state or self .mapping .is_axis_mapping ():
129
- # we ignore the suppress argument for release events
130
- # otherwise we might end up with stuck keys
131
- # (test_event_pipeline.test_combination)
132
-
133
- # we also ignore it if the mapping specifies an output axis
134
- # this will enable us to activate multiple axis with the same button
135
- suppress = False
136
- event = event .modify (value = 0 )
122
+ if is_pressed :
123
+ return self ._handle_no_change_press (event )
124
+ else :
125
+ return self ._handle_no_change_release (event )
137
126
127
+ def _handle_no_change_press (self , event : InputEvent ) -> bool :
128
+ """A key was pressed, but this doesn't change the combinations activation state.
129
+ Can only happen if either the combination wasn't already active, or a duplicate
130
+ key-down event arrived (EV_ABS?)
131
+ """
132
+ # self._output_previously_active is negated, because if the output is active, a
133
+ # key-down event triggered it, which then did not get forwarded, therefore
134
+ # it doesn't require a release.
135
+ self ._require_release_later (not self ._output_previously_active , event )
136
+ # output is active: consume the event
137
+ # output inactive: forward the event
138
+ return self ._output_previously_active
139
+
140
+ def _handle_no_change_release (self , event : InputEvent ) -> bool :
141
+ """One of the combinations keys was released, but it didn't untrigger the
142
+ combination yet."""
143
+ # Negate: `False` means that the event-reader will forward the release.
144
+ return not self ._should_release_event (event )
145
+
146
+ def _handle_freshly_activated (
147
+ self ,
148
+ suppress : bool ,
149
+ event : InputEvent ,
150
+ source : evdev .InputDevice ,
151
+ ) -> bool :
152
+ """The combination was deactivated, but is activated now."""
138
153
if suppress :
139
154
return False
140
155
156
+ # Send key up events to the forwarded uinput if configured to do so.
157
+ self ._forward_release ()
158
+
141
159
logger .debug ("Sending %s to sub-handler" , self .mapping .input_combination )
142
- self ._output_state = bool (event .value )
143
- return self ._sub_handler .notify (event , source , suppress )
160
+ self ._output_previously_active = bool (event .value )
161
+ sub_handler_result = self ._sub_handler .notify (event , source , suppress )
162
+
163
+ # Only if the sub-handler return False, we need a release-event later.
164
+ # If it handled the event, the user never sees this key-down event.
165
+ self ._require_release_later (not sub_handler_result , event )
166
+ return sub_handler_result
167
+
168
+ def _handle_freshly_deactivated (
169
+ self ,
170
+ event : InputEvent ,
171
+ source : evdev .InputDevice ,
172
+ ) -> bool :
173
+ """The combination was activated, but is deactivated now."""
174
+ # We ignore the `suppress` argument for release events. Otherwise, we
175
+ # might end up with stuck keys (test_event_pipeline.test_combination).
176
+ # In the case of output axis, this will enable us to activate multiple
177
+ # axis with the same button.
178
+
179
+ logger .debug ("Sending %s to sub-handler" , self .mapping .input_combination )
180
+ self ._output_previously_active = bool (event .value )
181
+ self ._sub_handler .notify (event , source , suppress = False )
182
+
183
+ # Negate: `False` means that the event-reader will forward the release.
184
+ return not self ._should_release_event (event )
185
+
186
+ def _should_release_event (self , event : InputEvent ) -> bool :
187
+ """Check if the key-up event should be forwarded by the event-reader.
188
+
189
+ After this, the release event needs to be injected by someone, otherwise the
190
+ dictionary was modified erroneously. If there is no entry, we assume that there
191
+ was no key-down event to release. Maybe a duplicate event arrived.
192
+ """
193
+ # Ensure that all injected key-down events will get their release event
194
+ # injected eventually.
195
+ # If a key-up event arrives that will inactivate the combination, but
196
+ # for which previously a key-down event was injected (because it was
197
+ # an earlier key in the combination chain), then we need to ensure that its
198
+ # release is injected as well. So we get two release events in that case:
199
+ # one for the key, and one for the output.
200
+ assert event .value == 0
201
+ return self ._requires_a_release .pop (event .type_and_code , False )
202
+
203
+ def _require_release_later (self , require : bool , event : InputEvent ) -> None :
204
+ """Remember if this key-down event will need a release event later on."""
205
+ assert event .value == 1
206
+ self ._requires_a_release [event .type_and_code ] = require
144
207
145
208
def reset (self ) -> None :
146
209
self ._sub_handler .reset ()
147
210
for key in self ._pressed_keys :
148
211
self ._pressed_keys [key ] = False
149
- self ._output_state = False
212
+ self ._requires_a_release = {}
213
+ self ._output_previously_active = False
150
214
151
- def is_activated (self ) -> bool :
215
+ def _is_activated (self ) -> bool :
152
216
"""Return if all keys in the keymap are set to True."""
153
217
return False not in self ._pressed_keys .values ()
154
218
155
- def forward_release (self ) -> None :
219
+ def _forward_release (self ) -> None :
156
220
"""Forward a button release for all keys if this is a combination.
157
221
158
222
This might cause duplicate key-up events but those are ignored by evdev anyway
@@ -168,6 +232,9 @@ def forward_release(self) -> None:
168
232
logger .debug ("Forwarding release for %s" , self .mapping .input_combination )
169
233
170
234
for input_config in keys_to_release :
235
+ if not self ._requires_a_release .get (input_config .type_and_code ):
236
+ continue
237
+
171
238
origin_hash = input_config .origin_hash
172
239
if origin_hash is None :
173
240
logger .error (
@@ -180,6 +247,9 @@ def forward_release(self) -> None:
180
247
forward_to .write (* input_config .type_and_code , 0 )
181
248
forward_to .syn ()
182
249
250
+ # We are done with this key, forget about it
251
+ del self ._requires_a_release [input_config .type_and_code ]
252
+
183
253
def needs_ranking (self ) -> bool :
184
254
return bool (self .input_configs )
185
255
0 commit comments