refactor combos: states instead of lists

This commit is contained in:
xs5871 2022-08-10 13:05:36 +00:00 committed by Kyle Brown
parent dafc969bf5
commit 1cb4bddce4

View File

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