Refactor macros, decouple UI and logic
parent
2f982c7e38
commit
db69e1cd8d
|
|
@ -7,6 +7,8 @@ from collections import OrderedDict
|
|||
|
||||
from keycodes import RESET_KEYCODE, Keycode
|
||||
from kle_serial import Serial as KleSerial
|
||||
from macro_action import SS_TAP_CODE, SS_DOWN_CODE, SS_UP_CODE, ActionText, ActionTap, ActionDown, ActionUp, \
|
||||
SS_QMK_PREFIX, SS_DELAY_CODE, ActionDelay
|
||||
from unlocker import Unlocker
|
||||
from util import MSG_LEN, hid_send, chunks
|
||||
|
||||
|
|
@ -39,6 +41,103 @@ CMD_VIAL_LOCK = 0x08
|
|||
BUFFER_FETCH_CHUNK = 28
|
||||
|
||||
|
||||
def macro_deserialize_v1(data):
|
||||
"""
|
||||
Deserialize a single macro, protocol version 1
|
||||
"""
|
||||
|
||||
out = []
|
||||
sequence = []
|
||||
data = bytearray(data)
|
||||
while len(data) > 0:
|
||||
if data[0] in [SS_TAP_CODE, SS_DOWN_CODE, SS_UP_CODE]:
|
||||
# append to previous *_CODE if it's the same type, otherwise create a new entry
|
||||
if len(sequence) > 0 and isinstance(sequence[-1], list) and sequence[-1][0] == data[0]:
|
||||
sequence[-1][1].append(data[1])
|
||||
else:
|
||||
sequence.append([data[0], [data[1]]])
|
||||
|
||||
data.pop(0)
|
||||
data.pop(0)
|
||||
else:
|
||||
# append to previous string if it is a string, otherwise create a new entry
|
||||
ch = chr(data[0])
|
||||
if len(sequence) > 0 and isinstance(sequence[-1], str):
|
||||
sequence[-1] += ch
|
||||
else:
|
||||
sequence.append(ch)
|
||||
data.pop(0)
|
||||
for s in sequence:
|
||||
if isinstance(s, str):
|
||||
out.append(ActionText(s))
|
||||
else:
|
||||
# map integer values to qmk keycodes
|
||||
keycodes = []
|
||||
for code in s[1]:
|
||||
keycode = Keycode.find_outer_keycode(code)
|
||||
if keycode:
|
||||
keycodes.append(keycode)
|
||||
cls = {SS_TAP_CODE: ActionTap, SS_DOWN_CODE: ActionDown, SS_UP_CODE: ActionUp}[s[0]]
|
||||
out.append(cls(keycodes))
|
||||
return out
|
||||
|
||||
|
||||
def macro_deserialize_v2(data):
|
||||
"""
|
||||
Deserialize a single macro, protocol version 2
|
||||
"""
|
||||
|
||||
out = []
|
||||
sequence = []
|
||||
data = bytearray(data)
|
||||
while len(data) > 0:
|
||||
if data[0] == SS_QMK_PREFIX:
|
||||
if data[1] in [SS_TAP_CODE, SS_DOWN_CODE, SS_UP_CODE]:
|
||||
# append to previous *_CODE if it's the same type, otherwise create a new entry
|
||||
if len(sequence) > 0 and isinstance(sequence[-1], list) and sequence[-1][0] == data[1]:
|
||||
sequence[-1][1].append(data[2])
|
||||
else:
|
||||
sequence.append([data[1], [data[2]]])
|
||||
|
||||
for x in range(3):
|
||||
data.pop(0)
|
||||
elif data[1] == SS_DELAY_CODE:
|
||||
# decode the delay
|
||||
delay = (data[2] - 1) + (data[3] - 1) * 255
|
||||
sequence.append([SS_DELAY_CODE, delay])
|
||||
|
||||
for x in range(4):
|
||||
data.pop(0)
|
||||
else:
|
||||
# append to previous string if it is a string, otherwise create a new entry
|
||||
ch = chr(data[0])
|
||||
if len(sequence) > 0 and isinstance(sequence[-1], str):
|
||||
sequence[-1] += ch
|
||||
else:
|
||||
sequence.append(ch)
|
||||
data.pop(0)
|
||||
for s in sequence:
|
||||
if isinstance(s, str):
|
||||
out.append(ActionText(s))
|
||||
else:
|
||||
args = None
|
||||
if s[0] in [SS_TAP_CODE, SS_DOWN_CODE, SS_UP_CODE]:
|
||||
# map integer values to qmk keycodes
|
||||
args = []
|
||||
for code in s[1]:
|
||||
keycode = Keycode.find_outer_keycode(code)
|
||||
if keycode:
|
||||
args.append(keycode)
|
||||
elif s[0] == SS_DELAY_CODE:
|
||||
args = s[1]
|
||||
|
||||
if args is not None:
|
||||
cls = {SS_TAP_CODE: ActionTap, SS_DOWN_CODE: ActionDown, SS_UP_CODE: ActionUp,
|
||||
SS_DELAY_CODE: ActionDelay}[s[0]]
|
||||
out.append(cls(args))
|
||||
return out
|
||||
|
||||
|
||||
class Keyboard:
|
||||
""" Low-level communication with a vial-enabled keyboard """
|
||||
|
||||
|
|
@ -277,14 +376,16 @@ class Keyboard:
|
|||
data["layout"] = layout
|
||||
data["encoder_layout"] = encoder_layout
|
||||
data["layout_options"] = self.layout_options
|
||||
# TODO: macros should be serialized in a portable format instead of base64 string
|
||||
# i.e. use a custom structure (as keycodes numbers can change, etc)
|
||||
data["macro"] = base64.b64encode(self.macro).decode("utf-8")
|
||||
data["macro"] = self.save_macro()
|
||||
data["vial_protocol"] = self.vial_protocol
|
||||
data["via_protocol"] = self.via_protocol
|
||||
|
||||
return json.dumps(data).encode("utf-8")
|
||||
|
||||
def save_macro(self):
|
||||
# TODO: decouple macros from GUI, should be able to serialize/deserialize just with keyboard interface
|
||||
return []
|
||||
|
||||
def restore_layout(self, data):
|
||||
""" Restores saved layout """
|
||||
|
||||
|
|
@ -304,16 +405,12 @@ class Keyboard:
|
|||
self.set_encoder(l, e, 1, Keycode.deserialize(encoder[1]))
|
||||
|
||||
self.set_layout_options(data["layout_options"])
|
||||
self.restore_macros(data.get("macro"))
|
||||
|
||||
json_vial_protocol = data.get("vial_protocol", 1)
|
||||
if json_vial_protocol == self.vial_protocol:
|
||||
# we need to unlock the keyboard before we can restore the macros, lock it afterwards
|
||||
# only do that if it's different from current macros
|
||||
macro = base64.b64decode(data["macro"])
|
||||
if macro != self.macro:
|
||||
Unlocker.unlock(self)
|
||||
self.set_macro(macro)
|
||||
self.lock()
|
||||
def restore_macro(self, macro):
|
||||
if not isinstance(macro, list):
|
||||
return
|
||||
print(macro)
|
||||
|
||||
def reset(self):
|
||||
self.usb_send(self.dev, struct.pack("B", 0xB))
|
||||
|
|
@ -376,6 +473,42 @@ class Keyboard:
|
|||
|
||||
self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_LOCK), retries=20)
|
||||
|
||||
def macro_serialize(self, macro):
|
||||
"""
|
||||
Serialize a single macro, a macro is made out of macro actions (BasicAction)
|
||||
"""
|
||||
out = b""
|
||||
for action in macro:
|
||||
out += action.serialize(self.vial_protocol)
|
||||
return out
|
||||
|
||||
def macro_deserialize(self, data):
|
||||
"""
|
||||
Deserialize a single macro
|
||||
"""
|
||||
if self.vial_protocol >= 2:
|
||||
return macro_deserialize_v2(data)
|
||||
return macro_deserialize_v1(data)
|
||||
|
||||
def macros_serialize(self, macros):
|
||||
"""
|
||||
Serialize a list of macros, the list must contain all macros (macro_count)
|
||||
"""
|
||||
if len(macros) != self.macro_count:
|
||||
raise RuntimeError("expected array with {} macros, got {} macros".format(self.macro_count, len(macros)))
|
||||
out = [self.macro_serialize(macro) for macro in macros]
|
||||
return b"\x00".join(out)
|
||||
|
||||
def macros_deserialize(self, data):
|
||||
"""
|
||||
Deserialize a list of macros
|
||||
"""
|
||||
macros = data.split(b"\x00")
|
||||
if len(macros) < self.macro_count:
|
||||
macros += [b""] * (self.macro_count - len(macros))
|
||||
macros = macros[:self.macro_count]
|
||||
return [self.macro_deserialize(x) for x in macros]
|
||||
|
||||
|
||||
class DummyKeyboard(Keyboard):
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,6 @@
|
|||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
import struct
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, Qt
|
||||
from PyQt5.QtWidgets import QLineEdit, QToolButton, QComboBox, QWidget, QSizePolicy, QSpinBox
|
||||
|
||||
from flowlayout import FlowLayout
|
||||
from keycodes import KEYCODES_BASIC, KEYCODES_ISO, KEYCODES_MEDIA
|
||||
from util import tr
|
||||
|
||||
|
||||
MACRO_SEQUENCE_KEYCODES = KEYCODES_BASIC + KEYCODES_ISO + KEYCODES_MEDIA
|
||||
KC_A = MACRO_SEQUENCE_KEYCODES[0]
|
||||
|
||||
SS_QMK_PREFIX = 1
|
||||
|
||||
SS_TAP_CODE = 1
|
||||
|
|
@ -20,118 +9,36 @@ SS_UP_CODE = 3
|
|||
SS_DELAY_CODE = 4
|
||||
|
||||
|
||||
class BasicAction(QObject):
|
||||
|
||||
changed = pyqtSignal()
|
||||
|
||||
def __init__(self, container):
|
||||
super().__init__()
|
||||
self.container = container
|
||||
class BasicAction:
|
||||
pass
|
||||
|
||||
|
||||
class ActionText(BasicAction):
|
||||
|
||||
def __init__(self, container, text=""):
|
||||
super().__init__(container)
|
||||
self.text = QLineEdit()
|
||||
self.text.setText(text)
|
||||
self.text.textChanged.connect(self.on_change)
|
||||
|
||||
def insert(self, row):
|
||||
self.container.addWidget(self.text, row, 2)
|
||||
|
||||
def remove(self):
|
||||
self.container.removeWidget(self.text)
|
||||
|
||||
def delete(self):
|
||||
self.text.deleteLater()
|
||||
def __init__(self, text=""):
|
||||
super().__init__()
|
||||
self.text = text
|
||||
|
||||
def serialize(self, vial_protocol):
|
||||
return self.text.text().encode("utf-8")
|
||||
|
||||
def on_change(self):
|
||||
self.changed.emit()
|
||||
return self.text.encode("utf-8")
|
||||
|
||||
|
||||
class ActionSequence(BasicAction):
|
||||
|
||||
def __init__(self, container, sequence=None):
|
||||
super().__init__(container)
|
||||
def __init__(self, sequence=None):
|
||||
super().__init__()
|
||||
if sequence is None:
|
||||
sequence = []
|
||||
self.sequence = sequence
|
||||
|
||||
self.btn_plus = QToolButton()
|
||||
self.btn_plus.setText("+")
|
||||
self.btn_plus.setToolButtonStyle(Qt.ToolButtonTextOnly)
|
||||
self.btn_plus.clicked.connect(self.on_add)
|
||||
|
||||
self.layout = FlowLayout()
|
||||
self.layout_container = QWidget()
|
||||
self.layout_container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum)
|
||||
self.layout_container.setLayout(self.layout)
|
||||
self.widgets = []
|
||||
self.recreate_sequence()
|
||||
|
||||
def recreate_sequence(self):
|
||||
self.layout.removeWidget(self.btn_plus)
|
||||
for w in self.widgets:
|
||||
self.layout.removeWidget(w)
|
||||
w.deleteLater()
|
||||
self.widgets.clear()
|
||||
|
||||
for item in self.sequence:
|
||||
w = QComboBox()
|
||||
w.view().setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
w.setStyleSheet("QComboBox { combobox-popup: 0; }")
|
||||
w.addItem(tr("MacroEditor", "Remove"))
|
||||
w.insertSeparator(1)
|
||||
for k in MACRO_SEQUENCE_KEYCODES:
|
||||
w.addItem(k.label.replace("\n", ""))
|
||||
w.setCurrentIndex(2 + MACRO_SEQUENCE_KEYCODES.index(item))
|
||||
w.currentIndexChanged.connect(self.on_change)
|
||||
self.layout.addWidget(w)
|
||||
self.widgets.append(w)
|
||||
self.layout.addWidget(self.btn_plus)
|
||||
|
||||
def insert(self, row):
|
||||
self.container.addWidget(self.layout_container, row, 2)
|
||||
|
||||
def remove(self):
|
||||
self.container.removeWidget(self.layout_container)
|
||||
|
||||
def delete(self):
|
||||
for w in self.widgets:
|
||||
w.deleteLater()
|
||||
self.btn_plus.deleteLater()
|
||||
self.layout_container.deleteLater()
|
||||
|
||||
def on_add(self):
|
||||
self.sequence.append(KC_A)
|
||||
self.recreate_sequence()
|
||||
self.changed.emit()
|
||||
|
||||
def on_change(self):
|
||||
for x in range(len(self.sequence)):
|
||||
index = self.widgets[x].currentIndex()
|
||||
if index == 0:
|
||||
# asked to remove this item
|
||||
del self.sequence[x]
|
||||
self.recreate_sequence()
|
||||
break
|
||||
else:
|
||||
self.sequence[x] = MACRO_SEQUENCE_KEYCODES[self.widgets[x].currentIndex() - 2]
|
||||
self.changed.emit()
|
||||
|
||||
def serialize_prefix(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def serialize(self, vial_protocol):
|
||||
if vial_protocol >= 2:
|
||||
out = b"\x01"
|
||||
else:
|
||||
out = b""
|
||||
out = b""
|
||||
for k in self.sequence:
|
||||
if vial_protocol >= 2:
|
||||
out += struct.pack("B", SS_QMK_PREFIX)
|
||||
out += self.serialize_prefix()
|
||||
out += struct.pack("B", k.code)
|
||||
return out
|
||||
|
|
@ -157,28 +64,12 @@ class ActionTap(ActionSequence):
|
|||
|
||||
class ActionDelay(BasicAction):
|
||||
|
||||
def __init__(self, container, delay=0):
|
||||
super().__init__(container)
|
||||
self.value = QSpinBox()
|
||||
self.value.setMinimum(0)
|
||||
self.value.setMaximum(64000) # up to 64s
|
||||
self.value.setValue(delay)
|
||||
self.value.valueChanged.connect(self.on_change)
|
||||
|
||||
def insert(self, row):
|
||||
self.container.addWidget(self.value, row, 2)
|
||||
|
||||
def remove(self):
|
||||
self.container.removeWidget(self.value)
|
||||
|
||||
def delete(self):
|
||||
self.value.deleteLater()
|
||||
|
||||
def on_change(self):
|
||||
self.changed.emit()
|
||||
def __init__(self, delay=0):
|
||||
super().__init__()
|
||||
self.delay = delay
|
||||
|
||||
def serialize(self, vial_protocol):
|
||||
if vial_protocol < 2:
|
||||
raise RuntimeError("ActionDelay can only be used with vial_protocol>=2")
|
||||
delay = self.value.value()
|
||||
delay = self.delay
|
||||
return struct.pack("BBBB", SS_QMK_PREFIX, SS_DELAY_CODE, (delay % 255) + 1, (delay // 255) + 1)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,160 @@
|
|||
import struct
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, Qt
|
||||
from PyQt5.QtWidgets import QLineEdit, QToolButton, QComboBox, QWidget, QSizePolicy, QSpinBox
|
||||
|
||||
from flowlayout import FlowLayout
|
||||
from keycodes import KEYCODES_BASIC, KEYCODES_ISO, KEYCODES_MEDIA
|
||||
from macro_action import ActionText, ActionSequence, ActionDown, ActionUp, ActionTap, ActionDelay
|
||||
from util import tr
|
||||
|
||||
|
||||
MACRO_SEQUENCE_KEYCODES = KEYCODES_BASIC + KEYCODES_ISO + KEYCODES_MEDIA
|
||||
KC_A = MACRO_SEQUENCE_KEYCODES[0]
|
||||
|
||||
|
||||
class BasicActionUI(QObject):
|
||||
|
||||
changed = pyqtSignal()
|
||||
actcls = None
|
||||
|
||||
def __init__(self, container, act=None):
|
||||
super().__init__()
|
||||
self.container = container
|
||||
if act is None:
|
||||
act = self.actcls()
|
||||
if not isinstance(act, self.actcls):
|
||||
raise RuntimeError("{} was initialized with {}, expecting {}".format(self, act, self.actcls))
|
||||
self.act = act
|
||||
|
||||
|
||||
class ActionTextUI(BasicActionUI):
|
||||
|
||||
actcls = ActionText
|
||||
|
||||
def __init__(self, container, act=None):
|
||||
super().__init__(container, act)
|
||||
self.text = QLineEdit()
|
||||
self.text.setText(self.act.text)
|
||||
self.text.textChanged.connect(self.on_change)
|
||||
|
||||
def insert(self, row):
|
||||
self.container.addWidget(self.text, row, 2)
|
||||
|
||||
def remove(self):
|
||||
self.container.removeWidget(self.text)
|
||||
|
||||
def delete(self):
|
||||
self.text.deleteLater()
|
||||
|
||||
def on_change(self):
|
||||
self.act.text = self.text.text()
|
||||
self.changed.emit()
|
||||
|
||||
|
||||
class ActionSequenceUI(BasicActionUI):
|
||||
|
||||
actcls = ActionSequence
|
||||
|
||||
def __init__(self, container, act=None):
|
||||
super().__init__(container, act)
|
||||
|
||||
self.btn_plus = QToolButton()
|
||||
self.btn_plus.setText("+")
|
||||
self.btn_plus.setToolButtonStyle(Qt.ToolButtonTextOnly)
|
||||
self.btn_plus.clicked.connect(self.on_add)
|
||||
|
||||
self.layout = FlowLayout()
|
||||
self.layout_container = QWidget()
|
||||
self.layout_container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum)
|
||||
self.layout_container.setLayout(self.layout)
|
||||
self.widgets = []
|
||||
self.recreate_sequence()
|
||||
|
||||
def recreate_sequence(self):
|
||||
self.layout.removeWidget(self.btn_plus)
|
||||
for w in self.widgets:
|
||||
self.layout.removeWidget(w)
|
||||
w.deleteLater()
|
||||
self.widgets.clear()
|
||||
|
||||
for item in self.act.sequence:
|
||||
w = QComboBox()
|
||||
w.view().setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
w.setStyleSheet("QComboBox { combobox-popup: 0; }")
|
||||
w.addItem(tr("MacroEditor", "Remove"))
|
||||
w.insertSeparator(1)
|
||||
for k in MACRO_SEQUENCE_KEYCODES:
|
||||
w.addItem(k.label.replace("\n", ""))
|
||||
w.setCurrentIndex(2 + MACRO_SEQUENCE_KEYCODES.index(item))
|
||||
w.currentIndexChanged.connect(self.on_change)
|
||||
self.layout.addWidget(w)
|
||||
self.widgets.append(w)
|
||||
self.layout.addWidget(self.btn_plus)
|
||||
|
||||
def insert(self, row):
|
||||
self.container.addWidget(self.layout_container, row, 2)
|
||||
|
||||
def remove(self):
|
||||
self.container.removeWidget(self.layout_container)
|
||||
|
||||
def delete(self):
|
||||
for w in self.widgets:
|
||||
w.deleteLater()
|
||||
self.btn_plus.deleteLater()
|
||||
self.layout_container.deleteLater()
|
||||
|
||||
def on_add(self):
|
||||
self.act.sequence.append(KC_A)
|
||||
self.recreate_sequence()
|
||||
self.changed.emit()
|
||||
|
||||
def on_change(self):
|
||||
for x in range(len(self.act.sequence)):
|
||||
index = self.widgets[x].currentIndex()
|
||||
if index == 0:
|
||||
# asked to remove this item
|
||||
del self.act.sequence[x]
|
||||
self.recreate_sequence()
|
||||
break
|
||||
else:
|
||||
self.act.sequence[x] = MACRO_SEQUENCE_KEYCODES[self.widgets[x].currentIndex() - 2]
|
||||
self.changed.emit()
|
||||
|
||||
|
||||
class ActionDownUI(ActionSequenceUI):
|
||||
actcls = ActionDown
|
||||
|
||||
|
||||
class ActionUpUI(ActionSequenceUI):
|
||||
actcls = ActionUp
|
||||
|
||||
|
||||
class ActionTapUI(ActionSequenceUI):
|
||||
actcls = ActionTap
|
||||
|
||||
|
||||
class ActionDelayUI(BasicActionUI):
|
||||
|
||||
actcls = ActionDelay
|
||||
|
||||
def __init__(self, container, act=None):
|
||||
super().__init__(container, act)
|
||||
self.value = QSpinBox()
|
||||
self.value.setMinimum(0)
|
||||
self.value.setMaximum(64000) # up to 64s
|
||||
self.value.setValue(self.act.delay)
|
||||
self.value.valueChanged.connect(self.on_change)
|
||||
|
||||
def insert(self, row):
|
||||
self.container.addWidget(self.value, row, 2)
|
||||
|
||||
def remove(self):
|
||||
self.container.removeWidget(self.value)
|
||||
|
||||
def delete(self):
|
||||
self.value.deleteLater()
|
||||
|
||||
def on_change(self):
|
||||
self.act.delay = self.value.value()
|
||||
self.changed.emit()
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
from PyQt5.QtCore import QObject, pyqtSignal, Qt
|
||||
from PyQt5.QtWidgets import QHBoxLayout, QToolButton, QComboBox
|
||||
|
||||
from macro_action import ActionText, ActionDown, ActionUp, ActionTap, ActionDelay
|
||||
from macro_action_ui import ActionTextUI, ActionDownUI, ActionUpUI, ActionTapUI, ActionDelayUI
|
||||
|
||||
|
||||
class MacroLine(QObject):
|
||||
|
|
@ -11,7 +11,7 @@ class MacroLine(QObject):
|
|||
changed = pyqtSignal()
|
||||
|
||||
types = ["Text", "Down", "Up", "Tap"]
|
||||
type_to_cls = [ActionText, ActionDown, ActionUp, ActionTap]
|
||||
type_to_cls = [ActionTextUI, ActionDownUI, ActionUpUI, ActionTapUI]
|
||||
|
||||
def __init__(self, parent, action):
|
||||
super().__init__()
|
||||
|
|
@ -21,7 +21,7 @@ class MacroLine(QObject):
|
|||
|
||||
if self.parent.parent.keyboard.vial_protocol >= 2:
|
||||
self.types = self.types[:] + ["Delay"]
|
||||
self.type_to_cls = self.type_to_cls[:] + [ActionDelay]
|
||||
self.type_to_cls = self.type_to_cls[:] + [ActionDelayUI]
|
||||
|
||||
self.arrows = QHBoxLayout()
|
||||
self.btn_up = QToolButton()
|
||||
|
|
@ -91,6 +91,3 @@ class MacroLine(QObject):
|
|||
|
||||
def on_change(self):
|
||||
self.changed.emit()
|
||||
|
||||
def serialize(self, vial_protocol):
|
||||
return self.action.serialize(vial_protocol)
|
||||
|
|
|
|||
|
|
@ -9,236 +9,16 @@ from basic_editor import BasicEditor
|
|||
from keycodes import Keycode
|
||||
from macro_action import ActionText, ActionTap, ActionDown, ActionUp, ActionDelay, SS_TAP_CODE, SS_DOWN_CODE, \
|
||||
SS_UP_CODE, SS_DELAY_CODE, SS_QMK_PREFIX
|
||||
from macro_action_ui import ActionTextUI, ActionUpUI, ActionDownUI, ActionTapUI, ActionDelayUI
|
||||
from macro_key import KeyString, KeyDown, KeyUp, KeyTap
|
||||
from macro_line import MacroLine
|
||||
from macro_optimizer import macro_optimize
|
||||
from macro_tab import MacroTab
|
||||
from unlocker import Unlocker
|
||||
from util import tr
|
||||
from vial_device import VialKeyboard
|
||||
|
||||
|
||||
class MacroTab(QVBoxLayout):
|
||||
|
||||
changed = pyqtSignal()
|
||||
record = pyqtSignal(object, bool)
|
||||
record_stop = pyqtSignal()
|
||||
|
||||
def __init__(self, parent, enable_recorder):
|
||||
super().__init__()
|
||||
|
||||
self.parent = parent
|
||||
|
||||
self.lines = []
|
||||
|
||||
self.container = QGridLayout()
|
||||
|
||||
menu_record = QMenu()
|
||||
menu_record.addAction(tr("MacroRecorder", "Append to current"))\
|
||||
.triggered.connect(lambda: self.record.emit(self, True))
|
||||
menu_record.addAction(tr("MacroRecorder", "Replace everything"))\
|
||||
.triggered.connect(lambda: self.record.emit(self, False))
|
||||
|
||||
self.btn_record = QPushButton(tr("MacroRecorder", "Record macro"))
|
||||
self.btn_record.setMenu(menu_record)
|
||||
if not enable_recorder:
|
||||
self.btn_record.hide()
|
||||
|
||||
self.btn_record_stop = QPushButton(tr("MacroRecorder", "Stop recording"))
|
||||
self.btn_record_stop.clicked.connect(lambda: self.record_stop.emit())
|
||||
self.btn_record_stop.hide()
|
||||
|
||||
self.btn_add = QToolButton()
|
||||
self.btn_add.setText(tr("MacroRecorder", "Add action"))
|
||||
self.btn_add.setToolButtonStyle(Qt.ToolButtonTextOnly)
|
||||
self.btn_add.clicked.connect(self.on_add)
|
||||
|
||||
self.btn_tap_enter = QToolButton()
|
||||
self.btn_tap_enter.setText(tr("MacroRecorder", "Tap Enter"))
|
||||
self.btn_tap_enter.setToolButtonStyle(Qt.ToolButtonTextOnly)
|
||||
self.btn_tap_enter.clicked.connect(self.on_tap_enter)
|
||||
|
||||
layout_buttons = QHBoxLayout()
|
||||
layout_buttons.addStretch()
|
||||
layout_buttons.addWidget(self.btn_add)
|
||||
layout_buttons.addWidget(self.btn_tap_enter)
|
||||
layout_buttons.addWidget(self.btn_record)
|
||||
layout_buttons.addWidget(self.btn_record_stop)
|
||||
|
||||
vbox = QVBoxLayout()
|
||||
vbox.addLayout(self.container)
|
||||
vbox.addStretch()
|
||||
|
||||
w = QWidget()
|
||||
w.setLayout(vbox)
|
||||
w.setObjectName("w")
|
||||
scroll = QScrollArea()
|
||||
scroll.setFrameShape(QFrame.NoFrame)
|
||||
scroll.setStyleSheet("QScrollArea { background-color:transparent; }")
|
||||
w.setStyleSheet("#w { background-color:transparent; }")
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setWidget(w)
|
||||
|
||||
self.addWidget(scroll)
|
||||
self.addLayout(layout_buttons)
|
||||
|
||||
def add_action(self, act):
|
||||
line = MacroLine(self, act)
|
||||
line.changed.connect(self.on_change)
|
||||
self.lines.append(line)
|
||||
line.insert(len(self.lines) - 1)
|
||||
self.changed.emit()
|
||||
|
||||
def on_add(self):
|
||||
self.add_action(ActionText(self.container))
|
||||
|
||||
def on_remove(self, obj):
|
||||
for line in self.lines:
|
||||
if line == obj:
|
||||
line.remove()
|
||||
line.delete()
|
||||
self.lines.remove(obj)
|
||||
for line in self.lines:
|
||||
line.remove()
|
||||
for x, line in enumerate(self.lines):
|
||||
line.insert(x)
|
||||
self.changed.emit()
|
||||
|
||||
def clear(self):
|
||||
for line in self.lines[:]:
|
||||
self.on_remove(line)
|
||||
|
||||
def on_move(self, obj, offset):
|
||||
if offset == 0:
|
||||
return
|
||||
index = self.lines.index(obj)
|
||||
if index + offset < 0 or index + offset >= len(self.lines):
|
||||
return
|
||||
other = self.lines.index(self.lines[index + offset])
|
||||
self.lines[index].remove()
|
||||
self.lines[other].remove()
|
||||
self.lines[index], self.lines[other] = self.lines[other], self.lines[index]
|
||||
self.lines[index].insert(index)
|
||||
self.lines[other].insert(other)
|
||||
self.changed.emit()
|
||||
|
||||
def serialize(self):
|
||||
out = b""
|
||||
for line in self.lines:
|
||||
out += line.serialize(self.parent.keyboard.vial_protocol)
|
||||
return out
|
||||
|
||||
def deserialize_v1(self, data):
|
||||
self.clear()
|
||||
|
||||
sequence = []
|
||||
data = bytearray(data)
|
||||
while len(data) > 0:
|
||||
if data[0] in [SS_TAP_CODE, SS_DOWN_CODE, SS_UP_CODE]:
|
||||
# append to previous *_CODE if it's the same type, otherwise create a new entry
|
||||
if len(sequence) > 0 and isinstance(sequence[-1], list) and sequence[-1][0] == data[0]:
|
||||
sequence[-1][1].append(data[1])
|
||||
else:
|
||||
sequence.append([data[0], [data[1]]])
|
||||
|
||||
data.pop(0)
|
||||
data.pop(0)
|
||||
else:
|
||||
# append to previous string if it is a string, otherwise create a new entry
|
||||
ch = chr(data[0])
|
||||
if len(sequence) > 0 and isinstance(sequence[-1], str):
|
||||
sequence[-1] += ch
|
||||
else:
|
||||
sequence.append(ch)
|
||||
data.pop(0)
|
||||
for s in sequence:
|
||||
if isinstance(s, str):
|
||||
self.add_action(ActionText(self.container, s))
|
||||
else:
|
||||
# map integer values to qmk keycodes
|
||||
keycodes = []
|
||||
for code in s[1]:
|
||||
keycode = Keycode.find_outer_keycode(code)
|
||||
if keycode:
|
||||
keycodes.append(keycode)
|
||||
cls = {SS_TAP_CODE: ActionTap, SS_DOWN_CODE: ActionDown, SS_UP_CODE: ActionUp}[s[0]]
|
||||
self.add_action(cls(self.container, keycodes))
|
||||
|
||||
def deserialize_v2(self, data):
|
||||
self.clear()
|
||||
|
||||
sequence = []
|
||||
data = bytearray(data)
|
||||
while len(data) > 0:
|
||||
if data[0] == SS_QMK_PREFIX:
|
||||
if data[1] in [SS_TAP_CODE, SS_DOWN_CODE, SS_UP_CODE]:
|
||||
# append to previous *_CODE if it's the same type, otherwise create a new entry
|
||||
if len(sequence) > 0 and isinstance(sequence[-1], list) and sequence[-1][0] == data[1]:
|
||||
sequence[-1][1].append(data[2])
|
||||
else:
|
||||
sequence.append([data[1], [data[2]]])
|
||||
|
||||
for x in range(3):
|
||||
data.pop(0)
|
||||
elif data[1] == SS_DELAY_CODE:
|
||||
# decode the delay
|
||||
delay = (data[2] - 1) + (data[3] - 1) * 255
|
||||
sequence.append([SS_DELAY_CODE, delay])
|
||||
|
||||
for x in range(4):
|
||||
data.pop(0)
|
||||
else:
|
||||
# append to previous string if it is a string, otherwise create a new entry
|
||||
ch = chr(data[0])
|
||||
if len(sequence) > 0 and isinstance(sequence[-1], str):
|
||||
sequence[-1] += ch
|
||||
else:
|
||||
sequence.append(ch)
|
||||
data.pop(0)
|
||||
for s in sequence:
|
||||
if isinstance(s, str):
|
||||
self.add_action(ActionText(self.container, s))
|
||||
else:
|
||||
args = None
|
||||
if s[0] in [SS_TAP_CODE, SS_DOWN_CODE, SS_UP_CODE]:
|
||||
# map integer values to qmk keycodes
|
||||
args = []
|
||||
for code in s[1]:
|
||||
keycode = Keycode.find_outer_keycode(code)
|
||||
if keycode:
|
||||
args.append(keycode)
|
||||
elif s[0] == SS_DELAY_CODE:
|
||||
args = s[1]
|
||||
|
||||
if args is not None:
|
||||
cls = {SS_TAP_CODE: ActionTap, SS_DOWN_CODE: ActionDown, SS_UP_CODE: ActionUp,
|
||||
SS_DELAY_CODE: ActionDelay}[s[0]]
|
||||
self.add_action(cls(self.container, args))
|
||||
|
||||
def deserialize(self, data):
|
||||
if self.parent.keyboard.vial_protocol >= 2:
|
||||
return self.deserialize_v2(data)
|
||||
else:
|
||||
return self.deserialize_v1(data)
|
||||
|
||||
def on_change(self):
|
||||
self.changed.emit()
|
||||
|
||||
def on_tap_enter(self):
|
||||
self.add_action(ActionTap(self.container, [Keycode.find_by_qmk_id("KC_ENTER")]))
|
||||
|
||||
def pre_record(self):
|
||||
self.btn_record.hide()
|
||||
self.btn_add.hide()
|
||||
self.btn_tap_enter.hide()
|
||||
self.btn_record_stop.show()
|
||||
|
||||
def post_record(self):
|
||||
self.btn_record.show()
|
||||
self.btn_add.show()
|
||||
self.btn_tap_enter.show()
|
||||
self.btn_record_stop.hide()
|
||||
|
||||
|
||||
class MacroRecorder(BasicEditor):
|
||||
|
||||
def __init__(self):
|
||||
|
|
@ -320,7 +100,7 @@ class MacroRecorder(BasicEditor):
|
|||
macros = self.keyboard.macro.split(b"\x00")
|
||||
for x, w in enumerate(self.macro_tab_w[:self.keyboard.macro_count]):
|
||||
title = "M{}".format(x)
|
||||
if macros[x] != self.macro_tabs[x].serialize():
|
||||
if macros[x] != self.keyboard.macro_serialize(self.macro_tabs[x].actions()):
|
||||
title += "*"
|
||||
self.tabs.setTabText(x, title)
|
||||
|
||||
|
|
@ -371,19 +151,25 @@ class MacroRecorder(BasicEditor):
|
|||
self.lbl_memory.setStyleSheet("QLabel { color: red; }" if memory > self.keyboard.macro_memory else "")
|
||||
self.update_tab_titles()
|
||||
|
||||
def deserialize(self, data):
|
||||
macros = data.split(b"\x00")
|
||||
for x, tab in enumerate(self.macro_tabs[:self.keyboard.macro_count]):
|
||||
macro = b"\x00"
|
||||
if len(macros) > x:
|
||||
macro = macros[x]
|
||||
tab.deserialize(macro)
|
||||
|
||||
def serialize(self):
|
||||
data = b""
|
||||
for tab in self.macro_tabs[:self.keyboard.macro_count]:
|
||||
data += tab.serialize() + b"\x00"
|
||||
return data
|
||||
macros = []
|
||||
for x, t in enumerate(self.macro_tabs[:self.keyboard.macro_count]):
|
||||
macros.append(t.actions())
|
||||
return self.keyboard.macros_serialize(macros)
|
||||
|
||||
def deserialize(self, data):
|
||||
ui_action = {
|
||||
ActionText: ActionTextUI,
|
||||
ActionUp: ActionUpUI,
|
||||
ActionDown: ActionDownUI,
|
||||
ActionTap: ActionTapUI,
|
||||
ActionDelay: ActionDelayUI,
|
||||
}
|
||||
macros = self.keyboard.macros_deserialize(data)
|
||||
for macro, tab in zip(macros, self.macro_tabs[:self.keyboard.macro_count]):
|
||||
tab.clear()
|
||||
for act in macro:
|
||||
tab.add_action(ui_action[type(act)](tab.container, act))
|
||||
|
||||
def on_revert(self):
|
||||
self.keyboard.reload_macros()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
import sys
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
from PyQt5.QtWidgets import QPushButton, QGridLayout, QHBoxLayout, QToolButton, QVBoxLayout, \
|
||||
QTabWidget, QWidget, QLabel, QMenu, QScrollArea, QFrame
|
||||
|
||||
from basic_editor import BasicEditor
|
||||
from keycodes import Keycode
|
||||
from macro_action import ActionText, ActionTap, ActionDown, ActionUp, ActionDelay, SS_TAP_CODE, SS_DOWN_CODE, \
|
||||
SS_UP_CODE, SS_DELAY_CODE, SS_QMK_PREFIX
|
||||
from macro_action_ui import ActionTextUI
|
||||
from macro_key import KeyString, KeyDown, KeyUp, KeyTap
|
||||
from macro_line import MacroLine
|
||||
from macro_optimizer import macro_optimize
|
||||
from unlocker import Unlocker
|
||||
from util import tr
|
||||
from vial_device import VialKeyboard
|
||||
|
||||
|
||||
class MacroTab(QVBoxLayout):
|
||||
|
||||
changed = pyqtSignal()
|
||||
record = pyqtSignal(object, bool)
|
||||
record_stop = pyqtSignal()
|
||||
|
||||
def __init__(self, parent, enable_recorder):
|
||||
super().__init__()
|
||||
|
||||
self.parent = parent
|
||||
|
||||
self.lines = []
|
||||
|
||||
self.container = QGridLayout()
|
||||
|
||||
menu_record = QMenu()
|
||||
menu_record.addAction(tr("MacroRecorder", "Append to current"))\
|
||||
.triggered.connect(lambda: self.record.emit(self, True))
|
||||
menu_record.addAction(tr("MacroRecorder", "Replace everything"))\
|
||||
.triggered.connect(lambda: self.record.emit(self, False))
|
||||
|
||||
self.btn_record = QPushButton(tr("MacroRecorder", "Record macro"))
|
||||
self.btn_record.setMenu(menu_record)
|
||||
if not enable_recorder:
|
||||
self.btn_record.hide()
|
||||
|
||||
self.btn_record_stop = QPushButton(tr("MacroRecorder", "Stop recording"))
|
||||
self.btn_record_stop.clicked.connect(lambda: self.record_stop.emit())
|
||||
self.btn_record_stop.hide()
|
||||
|
||||
self.btn_add = QToolButton()
|
||||
self.btn_add.setText(tr("MacroRecorder", "Add action"))
|
||||
self.btn_add.setToolButtonStyle(Qt.ToolButtonTextOnly)
|
||||
self.btn_add.clicked.connect(self.on_add)
|
||||
|
||||
self.btn_tap_enter = QToolButton()
|
||||
self.btn_tap_enter.setText(tr("MacroRecorder", "Tap Enter"))
|
||||
self.btn_tap_enter.setToolButtonStyle(Qt.ToolButtonTextOnly)
|
||||
self.btn_tap_enter.clicked.connect(self.on_tap_enter)
|
||||
|
||||
layout_buttons = QHBoxLayout()
|
||||
layout_buttons.addStretch()
|
||||
layout_buttons.addWidget(self.btn_add)
|
||||
layout_buttons.addWidget(self.btn_tap_enter)
|
||||
layout_buttons.addWidget(self.btn_record)
|
||||
layout_buttons.addWidget(self.btn_record_stop)
|
||||
|
||||
vbox = QVBoxLayout()
|
||||
vbox.addLayout(self.container)
|
||||
vbox.addStretch()
|
||||
|
||||
w = QWidget()
|
||||
w.setLayout(vbox)
|
||||
w.setObjectName("w")
|
||||
scroll = QScrollArea()
|
||||
scroll.setFrameShape(QFrame.NoFrame)
|
||||
scroll.setStyleSheet("QScrollArea { background-color:transparent; }")
|
||||
w.setStyleSheet("#w { background-color:transparent; }")
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setWidget(w)
|
||||
|
||||
self.addWidget(scroll)
|
||||
self.addLayout(layout_buttons)
|
||||
|
||||
def add_action(self, act):
|
||||
line = MacroLine(self, act)
|
||||
line.changed.connect(self.on_change)
|
||||
self.lines.append(line)
|
||||
line.insert(len(self.lines) - 1)
|
||||
self.changed.emit()
|
||||
|
||||
def on_add(self):
|
||||
self.add_action(ActionTextUI(self.container))
|
||||
|
||||
def on_remove(self, obj):
|
||||
for line in self.lines:
|
||||
if line == obj:
|
||||
line.remove()
|
||||
line.delete()
|
||||
self.lines.remove(obj)
|
||||
for line in self.lines:
|
||||
line.remove()
|
||||
for x, line in enumerate(self.lines):
|
||||
line.insert(x)
|
||||
self.changed.emit()
|
||||
|
||||
def clear(self):
|
||||
for line in self.lines[:]:
|
||||
self.on_remove(line)
|
||||
|
||||
def on_move(self, obj, offset):
|
||||
if offset == 0:
|
||||
return
|
||||
index = self.lines.index(obj)
|
||||
if index + offset < 0 or index + offset >= len(self.lines):
|
||||
return
|
||||
other = self.lines.index(self.lines[index + offset])
|
||||
self.lines[index].remove()
|
||||
self.lines[other].remove()
|
||||
self.lines[index], self.lines[other] = self.lines[other], self.lines[index]
|
||||
self.lines[index].insert(index)
|
||||
self.lines[other].insert(other)
|
||||
self.changed.emit()
|
||||
|
||||
def on_change(self):
|
||||
self.changed.emit()
|
||||
|
||||
def on_tap_enter(self):
|
||||
self.add_action(ActionTap(self.container, [Keycode.find_by_qmk_id("KC_ENTER")]))
|
||||
|
||||
def pre_record(self):
|
||||
self.btn_record.hide()
|
||||
self.btn_add.hide()
|
||||
self.btn_tap_enter.hide()
|
||||
self.btn_record_stop.show()
|
||||
|
||||
def post_record(self):
|
||||
self.btn_record.show()
|
||||
self.btn_add.show()
|
||||
self.btn_tap_enter.show()
|
||||
self.btn_record_stop.hide()
|
||||
|
||||
def actions(self):
|
||||
return [line.action.act for line in self.lines]
|
||||
Loading…
Reference in New Issue