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
|
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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user