2022-07-24 14:28:36 +02:00
|
|
|
try:
|
|
|
|
from typing import Optional, Tuple
|
|
|
|
except ImportError:
|
|
|
|
pass
|
2022-03-09 02:27:56 +01:00
|
|
|
import kmk.handlers.stock as handlers
|
2022-07-24 14:28:36 +02:00
|
|
|
from kmk.keys import Key, make_key
|
2022-03-09 02:27:56 +01:00
|
|
|
from kmk.modules import Module
|
|
|
|
|
|
|
|
|
|
|
|
class Combo:
|
2022-06-05 22:51:04 +02:00
|
|
|
fast_reset = False
|
2022-03-09 02:27:56 +01:00
|
|
|
per_key_timeout = False
|
2022-06-05 22:51:04 +02:00
|
|
|
timeout = 50
|
2022-03-09 02:27:56 +01:00
|
|
|
_remaining = []
|
2022-06-05 22:51:04 +02:00
|
|
|
_timeout = None
|
2022-03-09 02:27:56 +01:00
|
|
|
|
2022-06-05 22:51:04 +02:00
|
|
|
def __init__(
|
|
|
|
self,
|
2022-07-24 14:28:36 +02:00
|
|
|
match: Tuple[Key, ...],
|
|
|
|
result: Key,
|
2022-06-05 22:51:04 +02:00
|
|
|
fast_reset=None,
|
|
|
|
per_key_timeout=None,
|
|
|
|
timeout=None,
|
|
|
|
):
|
2022-03-09 02:27:56 +01:00
|
|
|
'''
|
|
|
|
match: tuple of keys (KC.A, KC.B)
|
|
|
|
result: key KC.C
|
|
|
|
'''
|
|
|
|
self.match = match
|
|
|
|
self.result = result
|
2022-06-05 22:51:04 +02:00
|
|
|
if fast_reset is not None:
|
|
|
|
self.fast_reset = fast_reset
|
|
|
|
if per_key_timeout is not None:
|
2022-03-09 02:27:56 +01:00
|
|
|
self.per_key_timeout = per_key_timeout
|
2022-06-05 22:51:04 +02:00
|
|
|
if timeout is not None:
|
|
|
|
self.timeout = timeout
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return f'{self.__class__.__name__}({[k.code for k in self.match]})'
|
2022-03-09 02:27:56 +01:00
|
|
|
|
|
|
|
def matches(self, key):
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def reset(self):
|
|
|
|
self._remaining = list(self.match)
|
|
|
|
|
|
|
|
|
|
|
|
class Chord(Combo):
|
|
|
|
def matches(self, key):
|
|
|
|
try:
|
|
|
|
self._remaining.remove(key)
|
|
|
|
return True
|
|
|
|
except ValueError:
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
class Sequence(Combo):
|
2022-06-05 22:51:04 +02:00
|
|
|
fast_reset = True
|
2022-03-09 02:27:56 +01:00
|
|
|
per_key_timeout = True
|
2022-06-05 22:51:04 +02:00
|
|
|
timeout = 1000
|
2022-03-09 02:27:56 +01:00
|
|
|
|
|
|
|
def matches(self, key):
|
|
|
|
try:
|
|
|
|
return key == self._remaining.pop(0)
|
|
|
|
except IndexError:
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
class Combos(Module):
|
|
|
|
def __init__(self, combos=[]):
|
|
|
|
self.combos = combos
|
|
|
|
self._active = []
|
|
|
|
self._matching = []
|
|
|
|
self._reset = set()
|
|
|
|
self._key_buffer = []
|
2022-07-24 15:15:37 +02:00
|
|
|
|
2022-03-09 02:27:56 +01:00
|
|
|
make_key(
|
2022-07-11 09:56:09 +02:00
|
|
|
names=('LEADER', 'LDR'),
|
2022-03-09 02:27:56 +01:00
|
|
|
on_press=handlers.passthrough,
|
|
|
|
on_release=handlers.passthrough,
|
|
|
|
)
|
|
|
|
|
|
|
|
def during_bootup(self, keyboard):
|
2022-07-27 17:17:50 +02:00
|
|
|
self._next_module = keyboard.modules.index(self) + 1
|
2022-03-09 02:27:56 +01:00
|
|
|
self.reset(keyboard)
|
|
|
|
|
|
|
|
def before_matrix_scan(self, keyboard):
|
|
|
|
return
|
|
|
|
|
|
|
|
def after_matrix_scan(self, keyboard):
|
|
|
|
return
|
|
|
|
|
|
|
|
def before_hid_send(self, keyboard):
|
|
|
|
return
|
|
|
|
|
|
|
|
def after_hid_send(self, keyboard):
|
|
|
|
return
|
|
|
|
|
|
|
|
def on_powersave_enable(self, keyboard):
|
|
|
|
return
|
|
|
|
|
|
|
|
def on_powersave_disable(self, keyboard):
|
|
|
|
return
|
|
|
|
|
2022-07-24 14:28:36 +02:00
|
|
|
def process_key(self, keyboard, key: Key, is_pressed, int_coord):
|
2022-03-09 02:27:56 +01:00
|
|
|
if is_pressed:
|
2022-04-24 19:34:34 +02:00
|
|
|
return self.on_press(keyboard, key, int_coord)
|
2022-03-09 02:27:56 +01:00
|
|
|
else:
|
2022-04-24 19:34:34 +02:00
|
|
|
return self.on_release(keyboard, key, int_coord)
|
2022-03-09 02:27:56 +01:00
|
|
|
|
2022-07-24 14:28:36 +02:00
|
|
|
def on_press(self, keyboard, key: Optional[Key], int_coord):
|
2022-03-09 02:27:56 +01:00
|
|
|
# refill potential matches from timed-out matches
|
|
|
|
if not self._matching:
|
|
|
|
self._matching = list(self._reset)
|
|
|
|
self._reset = set()
|
|
|
|
|
|
|
|
# filter potential matches
|
|
|
|
for combo in self._matching.copy():
|
|
|
|
if combo.matches(key):
|
|
|
|
continue
|
|
|
|
self._matching.remove(combo)
|
|
|
|
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:
|
|
|
|
# At least one combo matches current key: append key to buffer.
|
2022-04-24 19:34:34 +02:00
|
|
|
self._key_buffer.append((int_coord, key, True))
|
2022-03-09 02:27:56 +01:00
|
|
|
key = None
|
|
|
|
|
2022-06-05 22:49:46 +02:00
|
|
|
# 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)
|
|
|
|
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)
|
|
|
|
|
2022-03-09 02:27:56 +01:00
|
|
|
# Start or reset individual combo timeouts.
|
|
|
|
for combo in self._matching:
|
|
|
|
if combo._timeout:
|
|
|
|
if combo.per_key_timeout:
|
|
|
|
keyboard.cancel_timeout(combo._timeout)
|
|
|
|
else:
|
|
|
|
continue
|
|
|
|
combo._timeout = keyboard.set_timeout(
|
|
|
|
combo.timeout, lambda c=combo: self.on_timeout(keyboard, c)
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
# There's no matching combo: send and reset key buffer
|
|
|
|
self.send_key_buffer(keyboard)
|
|
|
|
self._key_buffer = []
|
2022-04-24 19:34:34 +02:00
|
|
|
key = keyboard._find_key_in_map(int_coord)
|
2022-03-09 02:27:56 +01:00
|
|
|
|
|
|
|
return key
|
|
|
|
|
2022-07-24 14:28:36 +02:00
|
|
|
def on_release(self, keyboard, key: Optional[Key], int_coord):
|
2022-03-09 02:27:56 +01:00
|
|
|
for combo in self._active:
|
|
|
|
if key in combo.match:
|
|
|
|
# Deactivate combo if it matches current key.
|
|
|
|
self.deactivate(keyboard, combo)
|
2022-06-05 22:51:04 +02:00
|
|
|
|
|
|
|
if combo.fast_reset:
|
|
|
|
self.reset_combo(keyboard, combo)
|
|
|
|
self._key_buffer = []
|
|
|
|
else:
|
|
|
|
combo._remaining.insert(0, key)
|
|
|
|
self._matching.append(combo)
|
|
|
|
|
2022-03-09 02:27:56 +01:00
|
|
|
key = combo.result
|
|
|
|
break
|
2022-07-24 23:07:05 +02:00
|
|
|
|
2022-03-09 02:27:56 +01:00
|
|
|
else:
|
2022-06-05 22:51:04 +02:00
|
|
|
# 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():
|
|
|
|
if key not in combo.match:
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Combo matches, but first key released before timeout.
|
2022-07-03 17:32:35 +02:00
|
|
|
elif not combo._remaining and len(self._matching) == 1:
|
2022-06-05 22:51:04 +02:00
|
|
|
keyboard.cancel_timeout(combo._timeout)
|
|
|
|
self._matching.remove(combo)
|
|
|
|
self.activate(keyboard, combo)
|
|
|
|
self._key_buffer = []
|
|
|
|
keyboard._send_hid()
|
|
|
|
self.deactivate(keyboard, combo)
|
|
|
|
if combo.fast_reset:
|
|
|
|
self.reset_combo(keyboard, combo)
|
|
|
|
else:
|
|
|
|
combo._remaining.insert(0, key)
|
|
|
|
self._matching.append(combo)
|
2022-07-09 11:50:01 +02:00
|
|
|
self.reset(keyboard)
|
2022-06-05 22:51:04 +02:00
|
|
|
|
2022-07-03 17:32:35 +02:00
|
|
|
elif not combo._remaining:
|
|
|
|
continue
|
|
|
|
|
2022-06-05 22:51:04 +02:00
|
|
|
# Skip combos that allow tapping.
|
|
|
|
elif combo.fast_reset:
|
|
|
|
continue
|
|
|
|
|
|
|
|
# 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)
|
2022-07-03 17:32:35 +02:00
|
|
|
if not self._matching:
|
|
|
|
self.send_key_buffer(keyboard)
|
|
|
|
self._key_buffer = []
|
2022-06-05 22:51:04 +02:00
|
|
|
|
|
|
|
# Anything between first and last key released.
|
|
|
|
else:
|
|
|
|
combo._remaining.insert(0, key)
|
|
|
|
|
|
|
|
# Don't propagate key-release events for keys that have been
|
|
|
|
# buffered. Append release events only if corresponding press is in
|
|
|
|
# buffer.
|
2022-04-24 19:34:34 +02:00
|
|
|
pressed = self._key_buffer.count((int_coord, key, True))
|
|
|
|
released = self._key_buffer.count((int_coord, key, False))
|
2022-03-09 02:27:56 +01:00
|
|
|
if (pressed - released) > 0:
|
2022-04-24 19:34:34 +02:00
|
|
|
self._key_buffer.append((int_coord, key, False))
|
2022-03-09 02:27:56 +01:00
|
|
|
key = None
|
|
|
|
|
2022-07-24 14:28:36 +02:00
|
|
|
# Reset on non-combo key up
|
2022-07-24 15:15:37 +02:00
|
|
|
if not self._matching:
|
|
|
|
self.reset(keyboard)
|
2022-07-24 23:07:05 +02:00
|
|
|
|
2022-03-09 02:27:56 +01:00
|
|
|
return key
|
|
|
|
|
|
|
|
def on_timeout(self, keyboard, combo):
|
|
|
|
# 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:
|
|
|
|
self.activate(keyboard, combo)
|
2022-07-03 17:50:57 +02:00
|
|
|
# check if the last buffered key event was a 'release'
|
|
|
|
if not self._key_buffer[-1][2]:
|
2022-07-03 17:32:35 +02:00
|
|
|
keyboard._send_hid()
|
|
|
|
self.deactivate(keyboard, combo)
|
2022-03-09 02:27:56 +01:00
|
|
|
self._key_buffer = []
|
2022-06-05 22:51:04 +02:00
|
|
|
self.reset(keyboard)
|
2022-03-09 02:27:56 +01:00
|
|
|
else:
|
|
|
|
if not self._matching:
|
|
|
|
# This was the last pending combo: flush key buffer.
|
|
|
|
self.send_key_buffer(keyboard)
|
|
|
|
self._key_buffer = []
|
|
|
|
self.reset_combo(keyboard, combo)
|
|
|
|
|
|
|
|
def send_key_buffer(self, keyboard):
|
2022-04-24 19:34:34 +02:00
|
|
|
for (int_coord, key, is_pressed) in self._key_buffer:
|
2022-08-05 21:15:26 +02:00
|
|
|
new_key = None
|
|
|
|
if not is_pressed:
|
|
|
|
try:
|
|
|
|
new_key = keyboard._coordkeys_pressed[int_coord]
|
|
|
|
except KeyError:
|
|
|
|
new_key = None
|
2022-04-24 19:34:34 +02:00
|
|
|
if new_key is None:
|
|
|
|
new_key = keyboard._find_key_in_map(int_coord)
|
|
|
|
|
2022-08-05 21:22:04 +02:00
|
|
|
keyboard.resume_process_key(new_key, is_pressed, int_coord, self)
|
2022-03-09 02:27:56 +01:00
|
|
|
keyboard._send_hid()
|
|
|
|
|
|
|
|
def activate(self, keyboard, combo):
|
|
|
|
combo.result.on_press(keyboard)
|
|
|
|
self._active.append(combo)
|
|
|
|
|
|
|
|
def deactivate(self, keyboard, combo):
|
|
|
|
combo.result.on_release(keyboard)
|
|
|
|
self._active.remove(combo)
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
def reset(self, keyboard):
|
|
|
|
self._matching = []
|
|
|
|
for combo in self.combos:
|
2022-06-05 22:51:04 +02:00
|
|
|
if combo not in self._active:
|
|
|
|
self.reset_combo(keyboard, combo)
|