refactor combos: states instead of lists
This commit is contained in:
parent
dafc969bf5
commit
1cb4bddce4
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user