1919
2020from __future__ import annotations # needed for the TYPE_CHECKING import
2121
22- from typing import TYPE_CHECKING , Dict , Hashable
22+ from typing import TYPE_CHECKING , Dict , Hashable , Tuple
2323
2424import evdev
2525from evdev .ecodes import EV_ABS , EV_REL
@@ -44,9 +44,12 @@ class CombinationHandler(MappingHandler):
4444
4545 # map of InputEvent.input_match_hash -> bool , keep track of the combination state
4646 _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
4850 _sub_handler : InputEventHandler
4951 _handled_input_hashes : list [Hashable ]
52+ _requires_a_release : Dict [Tuple [int , int ], bool ]
5053
5154 def __init__ (
5255 self ,
@@ -59,8 +62,9 @@ def __init__(
5962 logger .debug (str (mapping ))
6063 super ().__init__ (combination , mapping , global_uinputs )
6164 self ._pressed_keys = {}
62- self ._output_state = False
65+ self ._output_previously_active = False
6366 self ._context = context
67+ self ._requires_a_release = {}
6468
6569 # prepare a key map for all events with non-zero value
6670 for input_config in combination :
@@ -101,58 +105,118 @@ def notify(
101105 # we are not responsible for the event
102106 return False
103107
104- was_activated = self .is_activated ()
105-
106108 # update the state
107109 # The value of non-key input should have been changed to either 0 or 1 at this
108110 # point by other handlers.
109111 is_pressed = event .value == 1
110112 self ._pressed_keys [event .input_match_hash ] = is_pressed
111113 # maybe this changes the activation status (triggered/not-triggered)
112- is_activated = self .is_activated ()
114+ changed = self ._is_activated () != self . _output_previously_active
113115
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 )
119119 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 )
127121 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 )
137126
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."""
138153 if suppress :
139154 return False
140155
156+ # Send key up events to the forwarded uinput if configured to do so.
157+ self ._forward_release ()
158+
141159 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
144207
145208 def reset (self ) -> None :
146209 self ._sub_handler .reset ()
147210 for key in self ._pressed_keys :
148211 self ._pressed_keys [key ] = False
149- self ._output_state = False
212+ self ._requires_a_release = {}
213+ self ._output_previously_active = False
150214
151- def is_activated (self ) -> bool :
215+ def _is_activated (self ) -> bool :
152216 """Return if all keys in the keymap are set to True."""
153217 return False not in self ._pressed_keys .values ()
154218
155- def forward_release (self ) -> None :
219+ def _forward_release (self ) -> None :
156220 """Forward a button release for all keys if this is a combination.
157221
158222 This might cause duplicate key-up events but those are ignored by evdev anyway
@@ -168,6 +232,9 @@ def forward_release(self) -> None:
168232 logger .debug ("Forwarding release for %s" , self .mapping .input_combination )
169233
170234 for input_config in keys_to_release :
235+ if not self ._requires_a_release .get (input_config .type_and_code ):
236+ continue
237+
171238 origin_hash = input_config .origin_hash
172239 if origin_hash is None :
173240 logger .error (
@@ -180,6 +247,9 @@ def forward_release(self) -> None:
180247 forward_to .write (* input_config .type_and_code , 0 )
181248 forward_to .syn ()
182249
250+ # We are done with this key, forget about it
251+ del self ._requires_a_release [input_config .type_and_code ]
252+
183253 def needs_ranking (self ) -> bool :
184254 return bool (self .input_configs )
185255
0 commit comments