refactor combos: states instead of lists
This commit is contained in:
		| @@ -2,18 +2,28 @@ try: | |||||||
|     from typing import Optional, Tuple |     from typing import Optional, Tuple | ||||||
| except ImportError: | except ImportError: | ||||||
|     pass |     pass | ||||||
|  | from micropython import const | ||||||
|  |  | ||||||
| import kmk.handlers.stock as handlers | import kmk.handlers.stock as handlers | ||||||
| from kmk.keys import Key, make_key | from kmk.keys import Key, make_key | ||||||
| from kmk.kmk_keyboard import KMKKeyboard | from kmk.kmk_keyboard import KMKKeyboard | ||||||
| from kmk.modules import Module | from kmk.modules import Module | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class _ComboState: | ||||||
|  |     RESET = const(0) | ||||||
|  |     MATCHING = const(1) | ||||||
|  |     ACTIVE = const(2) | ||||||
|  |     IDLE = const(3) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Combo: | class Combo: | ||||||
|     fast_reset = False |     fast_reset = False | ||||||
|     per_key_timeout = False |     per_key_timeout = False | ||||||
|     timeout = 50 |     timeout = 50 | ||||||
|     _remaining = [] |     _remaining = [] | ||||||
|     _timeout = None |     _timeout = None | ||||||
|  |     _state = _ComboState.IDLE | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
| @@ -71,9 +81,6 @@ class Sequence(Combo): | |||||||
| class Combos(Module): | class Combos(Module): | ||||||
|     def __init__(self, combos=[]): |     def __init__(self, combos=[]): | ||||||
|         self.combos = combos |         self.combos = combos | ||||||
|         self._active = [] |  | ||||||
|         self._matching = [] |  | ||||||
|         self._reset = set() |  | ||||||
|         self._key_buffer = [] |         self._key_buffer = [] | ||||||
|  |  | ||||||
|         make_key( |         make_key( | ||||||
| @@ -111,41 +118,49 @@ class Combos(Module): | |||||||
|  |  | ||||||
|     def on_press(self, keyboard: KMKKeyboard, key: Key, int_coord: Optional[int]): |     def on_press(self, keyboard: KMKKeyboard, key: Key, int_coord: Optional[int]): | ||||||
|         # refill potential matches from timed-out matches |         # refill potential matches from timed-out matches | ||||||
|         if not self._matching: |         if self.count_matching() == 0: | ||||||
|             self._matching = list(self._reset) |             for combo in self.combos: | ||||||
|             self._reset = set() |                 if combo._state == _ComboState.RESET: | ||||||
|  |                     combo._state = _ComboState.MATCHING | ||||||
|  |  | ||||||
|         # filter potential matches |         # filter potential matches | ||||||
|         for combo in self._matching.copy(): |         for combo in self.combos: | ||||||
|  |             if combo._state != _ComboState.MATCHING: | ||||||
|  |                 continue | ||||||
|             if combo.matches(key): |             if combo.matches(key): | ||||||
|                 continue |                 continue | ||||||
|             self._matching.remove(combo) |             combo._state = _ComboState.IDLE | ||||||
|             if combo._timeout: |             if combo._timeout: | ||||||
|                 keyboard.cancel_timeout(combo._timeout) |                 keyboard.cancel_timeout(combo._timeout) | ||||||
|             combo._timeout = keyboard.set_timeout( |             combo._timeout = keyboard.set_timeout( | ||||||
|                 combo.timeout, lambda c=combo: self.reset_combo(keyboard, c) |                 combo.timeout, lambda c=combo: self.reset_combo(keyboard, c) | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         if self._matching: |         match_count = self.count_matching() | ||||||
|  |  | ||||||
|  |         if match_count: | ||||||
|             # At least one combo matches current key: append key to buffer. |             # At least one combo matches current key: append key to buffer. | ||||||
|             self._key_buffer.append((int_coord, key, True)) |             self._key_buffer.append((int_coord, key, True)) | ||||||
|             key = None |             key = None | ||||||
|  |  | ||||||
|  |             for first_match in self.combos: | ||||||
|  |                 if first_match._state == _ComboState.MATCHING: | ||||||
|  |                     break | ||||||
|  |  | ||||||
|             # Single match left: don't wait on timeout to activate |             # Single match left: don't wait on timeout to activate | ||||||
|             if len(self._matching) == 1 and not self._matching[0]._remaining: |             if match_count == 1 and not any(first_match._remaining): | ||||||
|                 combo = self._matching.pop(0) |                 combo = first_match | ||||||
|                 self.activate(keyboard, combo) |                 self.activate(keyboard, combo) | ||||||
|                 if combo._timeout: |                 if combo._timeout: | ||||||
|                     keyboard.cancel_timeout(combo._timeout) |                     keyboard.cancel_timeout(combo._timeout) | ||||||
|                     combo._timeout = None |                     combo._timeout = None | ||||||
|                 for _combo in self._matching: |  | ||||||
|                     self.reset_combo(keyboard, _combo) |  | ||||||
|                 self._matching = [] |  | ||||||
|                 self._key_buffer = [] |                 self._key_buffer = [] | ||||||
|                 self.reset(keyboard) |                 self.reset(keyboard) | ||||||
|  |  | ||||||
|             # Start or reset individual combo timeouts. |             # Start or reset individual combo timeouts. | ||||||
|             for combo in self._matching: |             for combo in self.combos: | ||||||
|  |                 if combo._state != _ComboState.MATCHING: | ||||||
|  |                     continue | ||||||
|                 if combo._timeout: |                 if combo._timeout: | ||||||
|                     if combo.per_key_timeout: |                     if combo.per_key_timeout: | ||||||
|                         keyboard.cancel_timeout(combo._timeout) |                         keyboard.cancel_timeout(combo._timeout) | ||||||
| @@ -164,7 +179,9 @@ class Combos(Module): | |||||||
|         return key |         return key | ||||||
|  |  | ||||||
|     def on_release(self, keyboard: KMKKeyboard, key: Key, int_coord: Optional[int]): |     def on_release(self, keyboard: KMKKeyboard, key: Key, int_coord: Optional[int]): | ||||||
|         for combo in self._active: |         for combo in self.combos: | ||||||
|  |             if combo._state != _ComboState.ACTIVE: | ||||||
|  |                 continue | ||||||
|             if key in combo.match: |             if key in combo.match: | ||||||
|                 # Deactivate combo if it matches current key. |                 # Deactivate combo if it matches current key. | ||||||
|                 self.deactivate(keyboard, combo) |                 self.deactivate(keyboard, combo) | ||||||
| @@ -174,7 +191,7 @@ class Combos(Module): | |||||||
|                     self._key_buffer = [] |                     self._key_buffer = [] | ||||||
|                 else: |                 else: | ||||||
|                     combo._remaining.insert(0, key) |                     combo._remaining.insert(0, key) | ||||||
|                     self._matching.append(combo) |                     combo._state = _ComboState.MATCHING | ||||||
|  |  | ||||||
|                 key = combo.result |                 key = combo.result | ||||||
|                 break |                 break | ||||||
| @@ -183,14 +200,15 @@ class Combos(Module): | |||||||
|             # Non-active but matching combos can either activate on key release |             # Non-active but matching combos can either activate on key release | ||||||
|             # if they're the only match, or "un-match" the released key but stay |             # if they're the only match, or "un-match" the released key but stay | ||||||
|             # matching if they're a repeatable combo. |             # matching if they're a repeatable combo. | ||||||
|             for combo in self._matching.copy(): |             for combo in self.combos: | ||||||
|  |                 if combo._state != _ComboState.MATCHING: | ||||||
|  |                     continue | ||||||
|                 if key not in combo.match: |                 if key not in combo.match: | ||||||
|                     continue |                     continue | ||||||
|  |  | ||||||
|                 # Combo matches, but first key released before timeout. |                 # Combo matches, but first key released before timeout. | ||||||
|                 elif not combo._remaining and len(self._matching) == 1: |                 elif not any(combo._remaining) and self.count_matching() == 1: | ||||||
|                     keyboard.cancel_timeout(combo._timeout) |                     keyboard.cancel_timeout(combo._timeout) | ||||||
|                     self._matching.remove(combo) |  | ||||||
|                     self.activate(keyboard, combo) |                     self.activate(keyboard, combo) | ||||||
|                     self._key_buffer = [] |                     self._key_buffer = [] | ||||||
|                     keyboard._send_hid() |                     keyboard._send_hid() | ||||||
| @@ -199,10 +217,10 @@ class Combos(Module): | |||||||
|                         self.reset_combo(keyboard, combo) |                         self.reset_combo(keyboard, combo) | ||||||
|                     else: |                     else: | ||||||
|                         combo._remaining.insert(0, key) |                         combo._remaining.insert(0, key) | ||||||
|                         self._matching.append(combo) |                         combo._state = _ComboState.MATCHING | ||||||
|                     self.reset(keyboard) |                     self.reset(keyboard) | ||||||
|  |  | ||||||
|                 elif not combo._remaining: |                 elif not any(combo._remaining): | ||||||
|                     continue |                     continue | ||||||
|  |  | ||||||
|                 # Skip combos that allow tapping. |                 # Skip combos that allow tapping. | ||||||
| @@ -211,9 +229,8 @@ class Combos(Module): | |||||||
|  |  | ||||||
|                 # This was the last key released of a repeatable combo. |                 # This was the last key released of a repeatable combo. | ||||||
|                 elif len(combo._remaining) == len(combo.match) - 1: |                 elif len(combo._remaining) == len(combo.match) - 1: | ||||||
|                     self._matching.remove(combo) |  | ||||||
|                     self.reset_combo(keyboard, combo) |                     self.reset_combo(keyboard, combo) | ||||||
|                     if not self._matching: |                     if not self.count_matching(): | ||||||
|                         self.send_key_buffer(keyboard) |                         self.send_key_buffer(keyboard) | ||||||
|                         self._key_buffer = [] |                         self._key_buffer = [] | ||||||
|  |  | ||||||
| @@ -231,7 +248,7 @@ class Combos(Module): | |||||||
|                 key = None |                 key = None | ||||||
|  |  | ||||||
|         # Reset on non-combo key up |         # Reset on non-combo key up | ||||||
|         if not self._matching: |         if not self.count_matching(): | ||||||
|             self.reset(keyboard) |             self.reset(keyboard) | ||||||
|  |  | ||||||
|         return key |         return key | ||||||
| @@ -240,9 +257,8 @@ class Combos(Module): | |||||||
|         # If combo reaches timeout and has no remaining keys, activate it; |         # If combo reaches timeout and has no remaining keys, activate it; | ||||||
|         # else, drop it from the match list. |         # else, drop it from the match list. | ||||||
|         combo._timeout = None |         combo._timeout = None | ||||||
|         self._matching.remove(combo) |  | ||||||
|  |  | ||||||
|         if not combo._remaining: |         if not any(combo._remaining): | ||||||
|             self.activate(keyboard, combo) |             self.activate(keyboard, combo) | ||||||
|             # check if the last buffered key event was a 'release' |             # check if the last buffered key event was a 'release' | ||||||
|             if not self._key_buffer[-1][2]: |             if not self._key_buffer[-1][2]: | ||||||
| @@ -251,7 +267,7 @@ class Combos(Module): | |||||||
|             self._key_buffer = [] |             self._key_buffer = [] | ||||||
|             self.reset(keyboard) |             self.reset(keyboard) | ||||||
|         else: |         else: | ||||||
|             if not self._matching: |             if self.count_matching() == 1: | ||||||
|                 # This was the last pending combo: flush key buffer. |                 # This was the last pending combo: flush key buffer. | ||||||
|                 self.send_key_buffer(keyboard) |                 self.send_key_buffer(keyboard) | ||||||
|                 self._key_buffer = [] |                 self._key_buffer = [] | ||||||
| @@ -273,21 +289,27 @@ class Combos(Module): | |||||||
|  |  | ||||||
|     def activate(self, keyboard, combo): |     def activate(self, keyboard, combo): | ||||||
|         combo.result.on_press(keyboard) |         combo.result.on_press(keyboard) | ||||||
|         self._active.append(combo) |         combo._state = _ComboState.ACTIVE | ||||||
|  |  | ||||||
|     def deactivate(self, keyboard, combo): |     def deactivate(self, keyboard, combo): | ||||||
|         combo.result.on_release(keyboard) |         combo.result.on_release(keyboard) | ||||||
|         self._active.remove(combo) |         combo._state = _ComboState.IDLE | ||||||
|  |  | ||||||
|     def reset_combo(self, keyboard, combo): |     def reset_combo(self, keyboard, combo): | ||||||
|         combo.reset() |         combo.reset() | ||||||
|         if combo._timeout is not None: |         if combo._timeout is not None: | ||||||
|             keyboard.cancel_timeout(combo._timeout) |             keyboard.cancel_timeout(combo._timeout) | ||||||
|             combo._timeout = None |             combo._timeout = None | ||||||
|         self._reset.add(combo) |         combo._state = _ComboState.RESET | ||||||
|  |  | ||||||
|     def reset(self, keyboard): |     def reset(self, keyboard): | ||||||
|         self._matching = [] |  | ||||||
|         for combo in self.combos: |         for combo in self.combos: | ||||||
|             if combo not in self._active: |             if combo._state != _ComboState.ACTIVE: | ||||||
|                 self.reset_combo(keyboard, combo) |                 self.reset_combo(keyboard, combo) | ||||||
|  |  | ||||||
|  |     def count_matching(self): | ||||||
|  |         match_count = 0 | ||||||
|  |         for combo in self.combos: | ||||||
|  |             if combo._state == _ComboState.MATCHING: | ||||||
|  |                 match_count += 1 | ||||||
|  |         return match_count | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user