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 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