diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index e32f709..61dd50c 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -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): diff --git a/src/main/python/macro_action.py b/src/main/python/macro_action.py index 9c493c3..c5bd2d5 100644 --- a/src/main/python/macro_action.py +++ b/src/main/python/macro_action.py @@ -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) diff --git a/src/main/python/macro_action_ui.py b/src/main/python/macro_action_ui.py new file mode 100644 index 0000000..bb87bef --- /dev/null +++ b/src/main/python/macro_action_ui.py @@ -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() diff --git a/src/main/python/macro_line.py b/src/main/python/macro_line.py index 134bc2c..70f3afc 100644 --- a/src/main/python/macro_line.py +++ b/src/main/python/macro_line.py @@ -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) diff --git a/src/main/python/macro_recorder.py b/src/main/python/macro_recorder.py index cc5435c..2954de4 100644 --- a/src/main/python/macro_recorder.py +++ b/src/main/python/macro_recorder.py @@ -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() diff --git a/src/main/python/macro_tab.py b/src/main/python/macro_tab.py new file mode 100644 index 0000000..8575854 --- /dev/null +++ b/src/main/python/macro_tab.py @@ -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]