Refactor macros, decouple UI and logic

main
Ilya Zhuravlev 2021-03-24 00:16:19 -04:00
parent 2f982c7e38
commit db69e1cd8d
6 changed files with 488 additions and 377 deletions

View File

@ -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):

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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]