From 49948a7e767454663ea64f61d4103b43f5a20d6f Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Tue, 29 Jun 2021 19:43:21 -0400 Subject: [PATCH 01/56] qmk_settings: initial prototype --- src/main/python/keyboard_comm.py | 21 +++++++++++++ src/main/python/main_window.py | 8 +++-- src/main/python/qmk_settings.py | 52 ++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 src/main/python/qmk_settings.py diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 59d7c4a..f14afa4 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -52,6 +52,10 @@ CMD_VIAL_UNLOCK_START = 0x06 CMD_VIAL_UNLOCK_POLL = 0x07 CMD_VIAL_LOCK = 0x08 +CMD_VIAL_QMK_SETTINGS_QUERY = 0x09 +CMD_VIAL_QMK_SETTINGS_GET = 0x0A +CMD_VIAL_QMK_SETTINGS_SET = 0x0B + # how much of a macro/keymap buffer we can read/write per packet BUFFER_FETCH_CHUNK = 28 @@ -637,6 +641,23 @@ class Keyboard: macros = macros[:self.macro_count] return [self.macro_deserialize(x) for x in macros] + def qmk_settings_query(self): + raise NotImplementedError + + def qmk_settings_get(self, qsid): + data = self.usb_send(self.dev, struct.pack("= 3) # TODO(xyz): protocol bump From 2fd8a0a7d03e5b13d1234288f43e0a4310dfcd01 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Wed, 30 Jun 2021 23:20:18 -0400 Subject: [PATCH 02/56] qmk_settings: store settings as json --- src/main/python/main.py | 2 +- src/main/python/main_window.py | 5 +- src/main/python/qmk_settings.py | 108 +++++++++++++++++----- src/main/resources/base/qmk_settings.json | 48 ++++++++++ 4 files changed, 138 insertions(+), 25 deletions(-) create mode 100644 src/main/resources/base/qmk_settings.json diff --git a/src/main/python/main.py b/src/main/python/main.py index 9d2ae19..a189160 100644 --- a/src/main/python/main.py +++ b/src/main/python/main.py @@ -64,7 +64,7 @@ if __name__ == '__main__': appctxt = ApplicationContext() # 1. Instantiate ApplicationContext init_logger() qt_exception_hook = UncaughtHook() - window = MainWindow() + window = MainWindow(appctxt) window.resize(WINDOW_WIDTH, WINDOW_HEIGHT) window.show() exit_code = appctxt.app.exec_() # 2. Invoke appctxt.app.exec_() diff --git a/src/main/python/main_window.py b/src/main/python/main_window.py index 7443428..8c32fe8 100644 --- a/src/main/python/main_window.py +++ b/src/main/python/main_window.py @@ -28,8 +28,9 @@ import themes class MainWindow(QMainWindow): - def __init__(self): + def __init__(self, appctx): super().__init__() + self.appctx = appctx self.settings = QSettings("Vial", "Vial") themes.set_theme(self.get_theme()) @@ -57,7 +58,7 @@ class MainWindow(QMainWindow): self.keymap_editor = KeymapEditor(self.layout_editor) self.firmware_flasher = FirmwareFlasher(self) self.macro_recorder = MacroRecorder() - self.qmk_settings = QmkSettings() + self.qmk_settings = QmkSettings(self.appctx) self.matrix_tester = MatrixTest(self.layout_editor) self.rgb_configurator = RGBConfigurator() diff --git a/src/main/python/qmk_settings.py b/src/main/python/qmk_settings.py index c9e6720..3b0f823 100644 --- a/src/main/python/qmk_settings.py +++ b/src/main/python/qmk_settings.py @@ -1,41 +1,105 @@ # SPDX-License-Identifier: GPL-2.0-or-later +import json import struct from PyQt5 import QtCore -from PyQt5.QtWidgets import QVBoxLayout, QCheckBox, QGridLayout, QLabel, QWidget, QSizePolicy +from PyQt5.QtWidgets import QVBoxLayout, QCheckBox, QGridLayout, QLabel, QWidget, QSizePolicy, QTabWidget, QSpinBox, \ + QHBoxLayout, QPushButton from basic_editor import BasicEditor from vial_device import VialKeyboard +class GenericOption: + + def __init__(self, option, container): + self.row = container.rowCount() + self.option = option + self.container = container + + self.container.addWidget(QLabel(option["title"]), self.row, 0) + + +class BooleanOption(GenericOption): + + def __init__(self, option, container): + super().__init__(option, container) + + self.checkbox = QCheckBox() + self.container.addWidget(self.checkbox, self.row, 1) + + +class IntegerOption(GenericOption): + + def __init__(self, option ,container): + super().__init__(option, container) + + self.spinbox = QSpinBox() + self.container.addWidget(self.spinbox, self.row, 1) + + class QmkSettings(BasicEditor): - def __init__(self): + def __init__(self, appctx): super().__init__() - - w = QWidget() - w.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) - self.container = QGridLayout() - w.setLayout(self.container) - self.addWidget(w) - self.setAlignment(w, QtCore.Qt.AlignHCenter) - - self.container.addWidget(QLabel("Always send Escape if Alt is pressed"), 0, 0) - self.container.addWidget(QCheckBox(), 0, 1) - self.container.addWidget(QLabel("Always send Escape if Control is pressed"), 1, 0) - self.chk_ctrl = QCheckBox() - self.chk_ctrl.stateChanged.connect(self.on_checked) - self.container.addWidget(self.chk_ctrl, 1, 1) - self.container.addWidget(QLabel("Always send Escape if GUI is pressed"), 2, 0) - self.container.addWidget(QCheckBox(), 2, 1) - self.container.addWidget(QLabel("Always send Escape if Shift is pressed"), 3, 0) - self.container.addWidget(QCheckBox(), 3, 1) - + self.appctx = appctx self.keyboard = None + self.tabs_widget = QTabWidget() + self.addWidget(self.tabs_widget) + buttons = QHBoxLayout() + buttons.addStretch() + buttons.addWidget(QPushButton("Save")) + buttons.addWidget(QPushButton("Undo")) + buttons.addWidget(QPushButton("Reset")) + self.addLayout(buttons) + + self.tabs = [] + self.create_gui() + + @staticmethod + def populate_tab(tab, container): + options = [] + for field in tab["fields"]: + if field["type"] == "boolean": + options.append(BooleanOption(field, container)) + elif field["type"] == "integer": + options.append(IntegerOption(field, container)) + else: + raise RuntimeError("unsupported field type: {}".format(field)) + return options + + def create_gui(self): + with open(self.appctx.get_resource("qmk_settings.json"), "r") as inf: + settings = json.load(inf) + + for tab in settings["tabs"]: + w = QWidget() + w.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) + container = QGridLayout() + w.setLayout(container) + l = QVBoxLayout() + l.addWidget(w) + l.setAlignment(w, QtCore.Qt.AlignHCenter) + w2 = QWidget() + w2.setLayout(l) + self.tabs_widget.addTab(w2, tab["name"]) + self.tabs.append(self.populate_tab(tab, container)) + + # self.container.addWidget(QLabel("Always send Escape if Alt is pressed"), 0, 0) + # self.container.addWidget(QCheckBox(), 0, 1) + # self.container.addWidget(QLabel("Always send Escape if Control is pressed"), 1, 0) + # self.chk_ctrl = QCheckBox() + # self.chk_ctrl.stateChanged.connect(self.on_checked) + # self.container.addWidget(self.chk_ctrl, 1, 1) + # self.container.addWidget(QLabel("Always send Escape if GUI is pressed"), 2, 0) + # self.container.addWidget(QCheckBox(), 2, 1) + # self.container.addWidget(QLabel("Always send Escape if Shift is pressed"), 3, 0) + # self.container.addWidget(QCheckBox(), 3, 1) + def reload_settings(self): gresc = self.keyboard.qmk_settings_get(1)[0] - self.chk_ctrl.setChecked(gresc & 2) + # self.chk_ctrl.setChecked(gresc & 2) def on_checked(self, state): data = struct.pack("B", int(self.chk_ctrl.isChecked()) * 2) diff --git a/src/main/resources/base/qmk_settings.json b/src/main/resources/base/qmk_settings.json new file mode 100644 index 0000000..6244779 --- /dev/null +++ b/src/main/resources/base/qmk_settings.json @@ -0,0 +1,48 @@ +{ + "tabs": [ + { + "name": "Grave Escape", + "fields": [ + { "type": "boolean", "title": "Always send Escape if Alt is pressed", "qsid": 1, "bit": 0 }, + { "type": "boolean", "title": "Always send Escape if Control is pressed", "qsid": 1, "bit": 1 }, + { "type": "boolean", "title": "Always send Escape if GUI is pressed", "qsid": 1, "bit": 2 }, + { "type": "boolean", "title": "Always send Escape if Shift is pressed", "qsid": 1, "bit": 3 } + ] + }, + { + "name": "Debounce", + "fields": [ + { "type": "integer", "title": "Debounce time (ms)", "qsid": 2, "min": 0, "max": 1000 } + ] + }, + { + "name": "Auto Shift", + "fields": [ + { "type": "boolean", "title": "Enable", "qsid": 3, "bit": 0 }, + { "type": "boolean", "title": "Enable for modifiers", "qsid": 3, "bit": 1 }, + { "type": "integer", "title": "Timeout", "qsid": 4, "min": 0, "max": 1000 }, + { "type": "boolean", "title": "Do not Auto Shift special keys", "qsid": 3, "bit": 2 }, + { "type": "boolean", "title": "Do not Auto Shift numeric keys", "qsid": 3, "bit": 3 }, + { "type": "boolean", "title": "Do not Auto Shift alpha characters", "qsid": 3, "bit": 4 }, + { "type": "boolean", "title": "Enable keyrepeat", "qsid": 3, "bit": 5 }, + { "type": "boolean", "title": "Disable automatically keyrepeating when AUTO_SHIFT_TIMEOUT is exceeded", "qsid": 3, "bit": 6 } + ] + }, + { + "name": "One Shot Keys", + "fields": [ + { "type": "integer", "title": "Tapping this number of times holds the key until tapped once again", "qsid": 5, "min": 0, "max": 50 }, + { "type": "integer", "title": "Time (in ms) before the one shot key is released", "qsid": 6, "min": 0, "max": 100000 } + ] + }, + { + "name": "Tap-Hold", + "fields": [ + { "type": "integer", "title": "TAPPING_TERM", "qsid": 7, "min": 0, "max": 10000 }, + { "type": "boolean", "title": "PERMISSIVE_HOLD", "qsid": 8, "bit": 0 }, + { "type": "boolean", "title": "IGNORE_MOD_TAP_INTERRUPT", "qsid": 8, "bit": 1 }, + { "type": "boolean", "title": "TAPPING_FORCE_HOLD", "qsid": 8, "bit": 2 } + ] + } + ] +} From e8d448f084acc3fddcd3873ee94c41da6105c708 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Wed, 30 Jun 2021 23:42:56 -0400 Subject: [PATCH 03/56] qmk_settings: load data from keyboard --- src/main/python/qmk_settings.py | 58 +++++++++++++++-------- src/main/resources/base/qmk_settings.json | 10 ++-- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/main/python/qmk_settings.py b/src/main/python/qmk_settings.py index 3b0f823..6429079 100644 --- a/src/main/python/qmk_settings.py +++ b/src/main/python/qmk_settings.py @@ -7,6 +7,7 @@ from PyQt5.QtWidgets import QVBoxLayout, QCheckBox, QGridLayout, QLabel, QWidget QHBoxLayout, QPushButton from basic_editor import BasicEditor +from util import tr from vial_device import VialKeyboard @@ -15,28 +16,55 @@ class GenericOption: def __init__(self, option, container): self.row = container.rowCount() self.option = option + self.qsid = self.option["qsid"] self.container = container self.container.addWidget(QLabel(option["title"]), self.row, 0) + def reload(self, keyboard): + data = keyboard.qmk_settings_get(self.qsid) + if not data: + raise RuntimeError("failed to retrieve setting {} from keyboard".format(self.option)) + return data + class BooleanOption(GenericOption): def __init__(self, option, container): super().__init__(option, container) + self.qsid_bit = self.option["bit"] + self.checkbox = QCheckBox() + self.checkbox.stateChanged.connect(self.on_change) self.container.addWidget(self.checkbox, self.row, 1) + def on_change(self): + print(self.option, self.checkbox.isChecked()) + + def reload(self, keyboard): + data = super().reload(keyboard) + checked = data[0] & (1 << self.qsid_bit) + + self.checkbox.blockSignals(True) + self.checkbox.setChecked(checked != 0) + self.checkbox.blockSignals(False) + class IntegerOption(GenericOption): - def __init__(self, option ,container): + def __init__(self, option, container): super().__init__(option, container) self.spinbox = QSpinBox() + self.spinbox.setMinimum(option["min"]) + self.spinbox.setMaximum(option["max"]) self.container.addWidget(self.spinbox, self.row, 1) + def reload(self, keyboard): + data = super().reload(keyboard)[0:self.option["width"]] + self.spinbox.setValue(int.from_bytes(data, byteorder="little")) + class QmkSettings(BasicEditor): @@ -49,9 +77,11 @@ class QmkSettings(BasicEditor): self.addWidget(self.tabs_widget) buttons = QHBoxLayout() buttons.addStretch() - buttons.addWidget(QPushButton("Save")) - buttons.addWidget(QPushButton("Undo")) - buttons.addWidget(QPushButton("Reset")) + buttons.addWidget(QPushButton(tr("QmkSettings", "Save"))) + btn_undo = QPushButton(tr("QmkSettings", "Undo")) + btn_undo.clicked.connect(self.reload_settings) + buttons.addWidget(btn_undo) + buttons.addWidget(QPushButton(tr("QmkSettings", "Reset"))) self.addLayout(buttons) self.tabs = [] @@ -86,24 +116,10 @@ class QmkSettings(BasicEditor): self.tabs_widget.addTab(w2, tab["name"]) self.tabs.append(self.populate_tab(tab, container)) - # self.container.addWidget(QLabel("Always send Escape if Alt is pressed"), 0, 0) - # self.container.addWidget(QCheckBox(), 0, 1) - # self.container.addWidget(QLabel("Always send Escape if Control is pressed"), 1, 0) - # self.chk_ctrl = QCheckBox() - # self.chk_ctrl.stateChanged.connect(self.on_checked) - # self.container.addWidget(self.chk_ctrl, 1, 1) - # self.container.addWidget(QLabel("Always send Escape if GUI is pressed"), 2, 0) - # self.container.addWidget(QCheckBox(), 2, 1) - # self.container.addWidget(QLabel("Always send Escape if Shift is pressed"), 3, 0) - # self.container.addWidget(QCheckBox(), 3, 1) - def reload_settings(self): - gresc = self.keyboard.qmk_settings_get(1)[0] - # self.chk_ctrl.setChecked(gresc & 2) - - def on_checked(self, state): - data = struct.pack("B", int(self.chk_ctrl.isChecked()) * 2) - self.keyboard.qmk_settings_set(1, data) + for tab in self.tabs: + for field in tab: + field.reload(self.keyboard) def rebuild(self, device): super().rebuild(device) diff --git a/src/main/resources/base/qmk_settings.json b/src/main/resources/base/qmk_settings.json index 6244779..e39d5c5 100644 --- a/src/main/resources/base/qmk_settings.json +++ b/src/main/resources/base/qmk_settings.json @@ -12,7 +12,7 @@ { "name": "Debounce", "fields": [ - { "type": "integer", "title": "Debounce time (ms)", "qsid": 2, "min": 0, "max": 1000 } + { "type": "integer", "title": "Debounce time (ms)", "qsid": 2, "min": 0, "max": 1000, "width": 2 } ] }, { @@ -20,7 +20,7 @@ "fields": [ { "type": "boolean", "title": "Enable", "qsid": 3, "bit": 0 }, { "type": "boolean", "title": "Enable for modifiers", "qsid": 3, "bit": 1 }, - { "type": "integer", "title": "Timeout", "qsid": 4, "min": 0, "max": 1000 }, + { "type": "integer", "title": "Timeout", "qsid": 4, "min": 0, "max": 1000, "width": 2 }, { "type": "boolean", "title": "Do not Auto Shift special keys", "qsid": 3, "bit": 2 }, { "type": "boolean", "title": "Do not Auto Shift numeric keys", "qsid": 3, "bit": 3 }, { "type": "boolean", "title": "Do not Auto Shift alpha characters", "qsid": 3, "bit": 4 }, @@ -31,14 +31,14 @@ { "name": "One Shot Keys", "fields": [ - { "type": "integer", "title": "Tapping this number of times holds the key until tapped once again", "qsid": 5, "min": 0, "max": 50 }, - { "type": "integer", "title": "Time (in ms) before the one shot key is released", "qsid": 6, "min": 0, "max": 100000 } + { "type": "integer", "title": "Tapping this number of times holds the key until tapped once again", "qsid": 5, "min": 0, "max": 50, "width": 1 }, + { "type": "integer", "title": "Time (in ms) before the one shot key is released", "qsid": 6, "min": 0, "max": 60000, "width": 2 } ] }, { "name": "Tap-Hold", "fields": [ - { "type": "integer", "title": "TAPPING_TERM", "qsid": 7, "min": 0, "max": 10000 }, + { "type": "integer", "title": "TAPPING_TERM", "qsid": 7, "min": 0, "max": 10000, "width": 2 }, { "type": "boolean", "title": "PERMISSIVE_HOLD", "qsid": 8, "bit": 0 }, { "type": "boolean", "title": "IGNORE_MOD_TAP_INTERRUPT", "qsid": 8, "bit": 1 }, { "type": "boolean", "title": "TAPPING_FORCE_HOLD", "qsid": 8, "bit": 2 } From c8c85c22709fb1832079e44f8f259c0f3fdd51b1 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Wed, 30 Jun 2021 23:53:40 -0400 Subject: [PATCH 04/56] qmk_settings: send store command to keyboard --- src/main/python/qmk_settings.py | 40 +++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/main/python/qmk_settings.py b/src/main/python/qmk_settings.py index 6429079..5341c93 100644 --- a/src/main/python/qmk_settings.py +++ b/src/main/python/qmk_settings.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: GPL-2.0-or-later import json -import struct +from collections import defaultdict from PyQt5 import QtCore from PyQt5.QtWidgets import QVBoxLayout, QCheckBox, QGridLayout, QLabel, QWidget, QSizePolicy, QTabWidget, QSpinBox, \ @@ -36,12 +36,8 @@ class BooleanOption(GenericOption): self.qsid_bit = self.option["bit"] self.checkbox = QCheckBox() - self.checkbox.stateChanged.connect(self.on_change) self.container.addWidget(self.checkbox, self.row, 1) - def on_change(self): - print(self.option, self.checkbox.isChecked()) - def reload(self, keyboard): data = super().reload(keyboard) checked = data[0] & (1 << self.qsid_bit) @@ -50,6 +46,10 @@ class BooleanOption(GenericOption): self.checkbox.setChecked(checked != 0) self.checkbox.blockSignals(False) + def value(self): + checked = int(self.checkbox.isChecked()) + return checked << self.qsid_bit + class IntegerOption(GenericOption): @@ -65,6 +65,9 @@ class IntegerOption(GenericOption): data = super().reload(keyboard)[0:self.option["width"]] self.spinbox.setValue(int.from_bytes(data, byteorder="little")) + def value(self): + return self.spinbox.value().to_bytes(self.option["width"], byteorder="little") + class QmkSettings(BasicEditor): @@ -77,11 +80,15 @@ class QmkSettings(BasicEditor): self.addWidget(self.tabs_widget) buttons = QHBoxLayout() buttons.addStretch() - buttons.addWidget(QPushButton(tr("QmkSettings", "Save"))) + btn_save = QPushButton(tr("QmkSettings", "Save")) + btn_save.clicked.connect(self.save_settings) + buttons.addWidget(btn_save) btn_undo = QPushButton(tr("QmkSettings", "Undo")) btn_undo.clicked.connect(self.reload_settings) buttons.addWidget(btn_undo) - buttons.addWidget(QPushButton(tr("QmkSettings", "Reset"))) + btn_reset = QPushButton(tr("QmkSettings", "Reset")) + btn_reset.clicked.connect(self.reset_settings) + buttons.addWidget(btn_reset) self.addLayout(buttons) self.tabs = [] @@ -127,6 +134,25 @@ class QmkSettings(BasicEditor): self.keyboard = device.keyboard self.reload_settings() + def save_settings(self): + qsid_values = defaultdict(int) + for tab in self.tabs: + for field in tab: + # hack for boolean options - we pack several booleans into a single byte + if isinstance(field, BooleanOption): + qsid_values[field.qsid] |= field.value() + else: + qsid_values[field.qsid] = field.value() + + for qsid, value in qsid_values.items(): + if isinstance(value, int): + value = value.to_bytes(1, byteorder="little") + self.keyboard.qmk_settings_set(qsid, value) + + def reset_settings(self): + # TODO: implement this + raise NotImplementedError + def valid(self): return isinstance(self.device, VialKeyboard) and \ (self.device.keyboard and self.device.keyboard.vial_protocol >= 3) # TODO(xyz): protocol bump From 7129fca01f51f5c476757d5e20e7e3c3613d428e Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Thu, 1 Jul 2021 00:08:08 -0400 Subject: [PATCH 05/56] qmk_settings: reword autoshift --- src/main/resources/base/qmk_settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/base/qmk_settings.json b/src/main/resources/base/qmk_settings.json index e39d5c5..fe439ff 100644 --- a/src/main/resources/base/qmk_settings.json +++ b/src/main/resources/base/qmk_settings.json @@ -25,7 +25,7 @@ { "type": "boolean", "title": "Do not Auto Shift numeric keys", "qsid": 3, "bit": 3 }, { "type": "boolean", "title": "Do not Auto Shift alpha characters", "qsid": 3, "bit": 4 }, { "type": "boolean", "title": "Enable keyrepeat", "qsid": 3, "bit": 5 }, - { "type": "boolean", "title": "Disable automatically keyrepeating when AUTO_SHIFT_TIMEOUT is exceeded", "qsid": 3, "bit": 6 } + { "type": "boolean", "title": "Disable keyrepeat when timeout is exceeded", "qsid": 3, "bit": 6 } ] }, { From 6c90d164c047e43444316d2c0a658a91e7739077 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Thu, 1 Jul 2021 15:00:45 -0400 Subject: [PATCH 06/56] qmk_settings: implement reset command --- src/main/python/keyboard_comm.py | 4 ++++ src/main/python/qmk_settings.py | 9 ++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index f14afa4..8bf6391 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -55,6 +55,7 @@ CMD_VIAL_LOCK = 0x08 CMD_VIAL_QMK_SETTINGS_QUERY = 0x09 CMD_VIAL_QMK_SETTINGS_GET = 0x0A CMD_VIAL_QMK_SETTINGS_SET = 0x0B +CMD_VIAL_QMK_SETTINGS_RESET = 0x0C # how much of a macro/keymap buffer we can read/write per packet BUFFER_FETCH_CHUNK = 28 @@ -658,6 +659,9 @@ class Keyboard: print("resp", data.hex()) return data[0] + def qmk_settings_reset(self): + self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_QMK_SETTINGS_RESET)) + class DummyKeyboard(Keyboard): diff --git a/src/main/python/qmk_settings.py b/src/main/python/qmk_settings.py index 5341c93..039661a 100644 --- a/src/main/python/qmk_settings.py +++ b/src/main/python/qmk_settings.py @@ -4,7 +4,7 @@ from collections import defaultdict from PyQt5 import QtCore from PyQt5.QtWidgets import QVBoxLayout, QCheckBox, QGridLayout, QLabel, QWidget, QSizePolicy, QTabWidget, QSpinBox, \ - QHBoxLayout, QPushButton + QHBoxLayout, QPushButton, QMessageBox from basic_editor import BasicEditor from util import tr @@ -150,8 +150,11 @@ class QmkSettings(BasicEditor): self.keyboard.qmk_settings_set(qsid, value) def reset_settings(self): - # TODO: implement this - raise NotImplementedError + if QMessageBox.question(self.widget(), "", + tr("QmkSettings", "Reset all settings to default values?"), + QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes: + self.keyboard.qmk_settings_reset() + self.reload_settings() def valid(self): return isinstance(self.device, VialKeyboard) and \ From 3318a2c167ce64fe557ed7e9d5b62b35cd760824 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Thu, 1 Jul 2021 15:10:32 -0400 Subject: [PATCH 07/56] qmk_settings: retrieve supported settings --- src/main/python/keyboard_comm.py | 12 +++++++++++- src/main/python/qmk_settings.py | 3 +++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 8bf6391..6086b17 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -643,7 +643,17 @@ class Keyboard: return [self.macro_deserialize(x) for x in macros] def qmk_settings_query(self): - raise NotImplementedError + cur = 0 + supported_settings = [] + while cur != 0xFFFF: + data = self.usb_send(self.dev, struct.pack(" Date: Thu, 1 Jul 2021 15:34:35 -0400 Subject: [PATCH 08/56] qmk_settings: don't display unsupported settings --- src/main/python/qmk_settings.py | 54 ++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/src/main/python/qmk_settings.py b/src/main/python/qmk_settings.py index c18e466..75b39cc 100644 --- a/src/main/python/qmk_settings.py +++ b/src/main/python/qmk_settings.py @@ -19,7 +19,8 @@ class GenericOption: self.qsid = self.option["qsid"] self.container = container - self.container.addWidget(QLabel(option["title"]), self.row, 0) + self.lbl = QLabel(option["title"]) + self.container.addWidget(self.lbl, self.row, 0) def reload(self, keyboard): data = keyboard.qmk_settings_get(self.qsid) @@ -27,6 +28,10 @@ class GenericOption: raise RuntimeError("failed to retrieve setting {} from keyboard".format(self.option)) return data + def delete(self): + self.lbl.hide() + self.lbl.deleteLater() + class BooleanOption(GenericOption): @@ -50,6 +55,11 @@ class BooleanOption(GenericOption): checked = int(self.checkbox.isChecked()) return checked << self.qsid_bit + def delete(self): + super().delete() + self.checkbox.hide() + self.checkbox.deleteLater() + class IntegerOption(GenericOption): @@ -68,6 +78,11 @@ class IntegerOption(GenericOption): def value(self): return self.spinbox.value().to_bytes(self.option["width"], byteorder="little") + def delete(self): + super().delete() + self.spinbox.hide() + self.spinbox.deleteLater() + class QmkSettings(BasicEditor): @@ -91,13 +106,15 @@ class QmkSettings(BasicEditor): buttons.addWidget(btn_reset) self.addLayout(buttons) + self.supported_settings = set() self.tabs = [] - self.create_gui() + self.misc_widgets = [] - @staticmethod - def populate_tab(tab, container): + def populate_tab(self, tab, container): options = [] for field in tab["fields"]: + if field["qsid"] not in self.supported_settings: + continue if field["type"] == "boolean": options.append(BooleanOption(field, container)) elif field["type"] == "integer": @@ -106,11 +123,33 @@ class QmkSettings(BasicEditor): raise RuntimeError("unsupported field type: {}".format(field)) return options - def create_gui(self): + def recreate_gui(self): + # delete old GUI + for tab in self.tabs: + for field in tab: + field.delete() + self.tabs.clear() + for w in self.misc_widgets: + w.hide() + w.deleteLater() + self.misc_widgets.clear() + while self.tabs_widget.count() > 0: + self.tabs_widget.removeTab(0) + with open(self.appctx.get_resource("qmk_settings.json"), "r") as inf: settings = json.load(inf) + # create new GUI for tab in settings["tabs"]: + # don't bother creating tabs that would be empty - i.e. at least one qsid in a tab should be supported + use_tab = False + for field in tab["fields"]: + if field["qsid"] in self.supported_settings: + use_tab = True + break + if not use_tab: + continue + w = QWidget() w.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) container = QGridLayout() @@ -120,12 +159,13 @@ class QmkSettings(BasicEditor): l.setAlignment(w, QtCore.Qt.AlignHCenter) w2 = QWidget() w2.setLayout(l) + self.misc_widgets += [w, w2] self.tabs_widget.addTab(w2, tab["name"]) self.tabs.append(self.populate_tab(tab, container)) def reload_settings(self): - settings = self.keyboard.qmk_settings_query() - print(settings) + self.supported_settings = set(self.keyboard.qmk_settings_query()) + self.recreate_gui() for tab in self.tabs: for field in tab: From eef81070361ec1ab56066e064c9174da68f1e117 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Thu, 1 Jul 2021 20:43:58 -0400 Subject: [PATCH 09/56] qmk_settings: support mouse keys --- src/main/resources/base/qmk_settings.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/resources/base/qmk_settings.json b/src/main/resources/base/qmk_settings.json index fe439ff..bcabb3c 100644 --- a/src/main/resources/base/qmk_settings.json +++ b/src/main/resources/base/qmk_settings.json @@ -35,6 +35,20 @@ { "type": "integer", "title": "Time (in ms) before the one shot key is released", "qsid": 6, "min": 0, "max": 60000, "width": 2 } ] }, + { + "name": "Mouse keys", + "fields": [ + { "type": "integer", "title": "Delay between pressing a movement key and cursor movement", "qsid": 9, "min": 0, "max": 10000, "width": 2 }, + { "type": "integer", "title": "Time between cursor movements in milliseconds", "qsid": 10, "min": 0, "max": 10000, "width": 2 }, + { "type": "integer", "title": "Step size", "qsid": 11, "min": 0, "max": 1000, "width": 2 }, + { "type": "integer", "title": "Maximum cursor speed at which acceleration stops", "qsid": 12, "min": 0, "max": 1000, "width": 2 }, + { "type": "integer", "title": "Time until maximum cursor speed is reached", "qsid": 13, "min": 0, "max": 1000, "width": 2 }, + { "type": "integer", "title": "Delay between pressing a wheel key and wheel movement", "qsid": 14, "min": 0, "max": 10000, "width": 2 }, + { "type": "integer", "title": "Time between wheel movements", "qsid": 15, "min": 0, "max": 10000, "width": 2 }, + { "type": "integer", "title": "Maximum number of scroll steps per scroll action", "qsid": 16, "min": 0, "max": 1000, "width": 2 }, + { "type": "integer", "title": "Time until maximum scroll speed is reached", "qsid": 17, "min": 0, "max": 1000, "width": 2 } + ] + }, { "name": "Tap-Hold", "fields": [ From aa3ed9aeeeeeb874219c43337b36a840118a9c7b Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Thu, 1 Jul 2021 20:44:52 -0400 Subject: [PATCH 10/56] qmk_settings: remove debounce, tap-hold -- too much effort to implement --- src/main/resources/base/qmk_settings.json | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/main/resources/base/qmk_settings.json b/src/main/resources/base/qmk_settings.json index bcabb3c..3fd4885 100644 --- a/src/main/resources/base/qmk_settings.json +++ b/src/main/resources/base/qmk_settings.json @@ -9,12 +9,6 @@ { "type": "boolean", "title": "Always send Escape if Shift is pressed", "qsid": 1, "bit": 3 } ] }, - { - "name": "Debounce", - "fields": [ - { "type": "integer", "title": "Debounce time (ms)", "qsid": 2, "min": 0, "max": 1000, "width": 2 } - ] - }, { "name": "Auto Shift", "fields": [ @@ -48,15 +42,6 @@ { "type": "integer", "title": "Maximum number of scroll steps per scroll action", "qsid": 16, "min": 0, "max": 1000, "width": 2 }, { "type": "integer", "title": "Time until maximum scroll speed is reached", "qsid": 17, "min": 0, "max": 1000, "width": 2 } ] - }, - { - "name": "Tap-Hold", - "fields": [ - { "type": "integer", "title": "TAPPING_TERM", "qsid": 7, "min": 0, "max": 10000, "width": 2 }, - { "type": "boolean", "title": "PERMISSIVE_HOLD", "qsid": 8, "bit": 0 }, - { "type": "boolean", "title": "IGNORE_MOD_TAP_INTERRUPT", "qsid": 8, "bit": 1 }, - { "type": "boolean", "title": "TAPPING_FORCE_HOLD", "qsid": 8, "bit": 2 } - ] } ] } From 944b771ee74efad7c3fefa6e2945a00789e71698 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Fri, 2 Jul 2021 23:24:35 -0400 Subject: [PATCH 11/56] keyboard_comm: support next protocol version 4 --- src/main/python/keyboard_comm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 59d7c4a..fd1d758 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -13,7 +13,7 @@ from unlocker import Unlocker from util import MSG_LEN, hid_send, chunks SUPPORTED_VIA_PROTOCOL = [-1, 9] -SUPPORTED_VIAL_PROTOCOL = [-1, 0, 1, 2, 3] +SUPPORTED_VIAL_PROTOCOL = [-1, 0, 1, 2, 3, 4] CMD_VIA_GET_PROTOCOL_VERSION = 0x01 CMD_VIA_GET_KEYBOARD_VALUE = 0x02 From 62647e957e899b4e2f98a5d3e6f390f018326418 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Fri, 2 Jul 2021 23:26:53 -0400 Subject: [PATCH 12/56] qmk_settings: bump protocol requirement --- src/main/python/qmk_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/python/qmk_settings.py b/src/main/python/qmk_settings.py index 75b39cc..8af97f8 100644 --- a/src/main/python/qmk_settings.py +++ b/src/main/python/qmk_settings.py @@ -201,4 +201,4 @@ class QmkSettings(BasicEditor): def valid(self): return isinstance(self.device, VialKeyboard) and \ - (self.device.keyboard and self.device.keyboard.vial_protocol >= 3) # TODO(xyz): protocol bump + (self.device.keyboard and self.device.keyboard.vial_protocol >= 4) From d209f4846320c0b047d4f1c437ae2ed1325e060e Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sat, 3 Jul 2021 12:20:08 -0400 Subject: [PATCH 13/56] keyboard_comm: remove qmk_settings debug output --- src/main/python/keyboard_comm.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index a0ffb53..fdaa5d3 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -663,10 +663,8 @@ class Keyboard: return data[1:] def qmk_settings_set(self, qsid, value): - print("change setting {} to value {}".format(qsid, value.hex())) data = self.usb_send(self.dev, struct.pack(" Date: Sat, 3 Jul 2021 12:20:54 -0400 Subject: [PATCH 14/56] tap_dance: initial UI --- src/main/python/main_window.py | 7 ++-- src/main/python/tap_dance.py | 62 ++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/main/python/tap_dance.py diff --git a/src/main/python/main_window.py b/src/main/python/main_window.py index 8c32fe8..b879efb 100644 --- a/src/main/python/main_window.py +++ b/src/main/python/main_window.py @@ -18,6 +18,7 @@ from layout_editor import LayoutEditor from macro_recorder import MacroRecorder from qmk_settings import QmkSettings from rgb_configurator import RGBConfigurator +from tap_dance import TapDance from unlocker import Unlocker from util import tr, find_vial_devices, EXAMPLE_KEYBOARDS from vial_device import VialKeyboard @@ -58,12 +59,14 @@ class MainWindow(QMainWindow): self.keymap_editor = KeymapEditor(self.layout_editor) self.firmware_flasher = FirmwareFlasher(self) self.macro_recorder = MacroRecorder() + self.tap_dance = TapDance() self.qmk_settings = QmkSettings(self.appctx) self.matrix_tester = MatrixTest(self.layout_editor) self.rgb_configurator = RGBConfigurator() self.editors = [(self.keymap_editor, "Keymap"), (self.layout_editor, "Layout"), (self.macro_recorder, "Macros"), - (self.rgb_configurator, "Lighting"), (self.qmk_settings, "QMK Settings"), + (self.rgb_configurator, "Lighting"), (self.tap_dance, "Tap Dance"), + (self.qmk_settings, "QMK Settings"), (self.matrix_tester, "Matrix tester"), (self.firmware_flasher, "Firmware updater")] Unlocker.global_layout_editor = self.layout_editor @@ -257,7 +260,7 @@ class MainWindow(QMainWindow): self.current_device.keyboard.reload() for e in [self.layout_editor, self.keymap_editor, self.firmware_flasher, self.macro_recorder, - self.qmk_settings, self.matrix_tester, self.rgb_configurator]: + self.tap_dance, self.qmk_settings, self.matrix_tester, self.rgb_configurator]: e.rebuild(self.current_device) def refresh_tabs(self): diff --git a/src/main/python/tap_dance.py b/src/main/python/tap_dance.py new file mode 100644 index 0000000..7d920a3 --- /dev/null +++ b/src/main/python/tap_dance.py @@ -0,0 +1,62 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +from PyQt5 import QtCore +from PyQt5.QtWidgets import QTabWidget, QWidget, QSizePolicy, QGridLayout, QVBoxLayout, QLabel, QLineEdit, QHBoxLayout, \ + QPushButton + +from util import tr +from vial_device import VialKeyboard +from basic_editor import BasicEditor + + +class TapDance(BasicEditor): + + def __init__(self): + super().__init__() + self.keyboard = None + + self.tabs = QTabWidget() + for x in range(32): + container = QGridLayout() + + container.addWidget(QLabel("On tap"), 0, 0) + container.addWidget(QLineEdit(), 0, 1) + container.addWidget(QLabel("On hold"), 1, 0) + container.addWidget(QLineEdit(), 1, 1) + container.addWidget(QLabel("On double tap"), 2, 0) + container.addWidget(QLineEdit(), 2, 1) + container.addWidget(QLabel("On tap + hold"), 3, 0) + container.addWidget(QLineEdit(), 3, 1) + + w = QWidget() + w.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) + w.setLayout(container) + l = QVBoxLayout() + l.addStretch() + l.addWidget(w) + l.setAlignment(w, QtCore.Qt.AlignHCenter) + l.addSpacing(100) + lbl = QLabel("Use TD({}) to set up this action in the keymap.".format(x)) + l.addWidget(lbl) + l.setAlignment(lbl, QtCore.Qt.AlignHCenter) + l.addStretch() + w2 = QWidget() + w2.setLayout(l) + self.tabs.addTab(w2, str(x)) + + self.addWidget(self.tabs) + buttons = QHBoxLayout() + buttons.addStretch() + btn_save = QPushButton(tr("TapDance", "Save")) + btn_revert = QPushButton(tr("TapDance", "Revert")) + buttons.addWidget(btn_save) + buttons.addWidget(btn_revert) + self.addLayout(buttons) + + def rebuild(self, device): + super().rebuild(device) + if self.valid(): + self.keyboard = device.keyboard + + def valid(self): + return isinstance(self.device, VialKeyboard) and \ + (self.device.keyboard and self.device.keyboard.vial_protocol >= 4) From c9ff0e735a1283c3337fb01a89ff90b986cd1fb9 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sat, 3 Jul 2021 13:31:07 -0400 Subject: [PATCH 15/56] tap_dance: initial implementation --- src/main/python/keyboard_comm.py | 34 +++++++++ src/main/python/tap_dance.py | 121 +++++++++++++++++++++++-------- 2 files changed, 126 insertions(+), 29 deletions(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index fdaa5d3..a104814 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -57,6 +57,12 @@ CMD_VIAL_QMK_SETTINGS_GET = 0x0A CMD_VIAL_QMK_SETTINGS_SET = 0x0B CMD_VIAL_QMK_SETTINGS_RESET = 0x0C +CMD_VIAL_DYNAMIC_ENTRY_OP = 0x0D + +DYNAMIC_VIAL_GET_NUMBER_OF_ENTRIES = 0x00 +DYNAMIC_VIAL_TAP_DANCE_GET = 0x01 +DYNAMIC_VIAL_TAP_DANCE_SET = 0x02 + # how much of a macro/keymap buffer we can read/write per packet BUFFER_FETCH_CHUNK = 28 @@ -207,6 +213,7 @@ class Keyboard: self.reload_keymap() self.reload_macros() self.reload_rgb() + self.reload_dynamic() def reload_layers(self): """ Get how many layers the keyboard has """ @@ -374,6 +381,22 @@ class Keyboard: self.backlight_effect = self.usb_send( self.dev, struct.pack(">BB", CMD_VIA_LIGHTING_GET_VALUE, QMK_BACKLIGHT_EFFECT), retries=20)[2] + def reload_dynamic(self): + if self.vial_protocol < 4: + self.tap_dance_count = 0 + self.tap_dance_entries = [] + return + data = self.usb_send(self.dev, struct.pack("BBB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_DYNAMIC_ENTRY_OP, + DYNAMIC_VIAL_GET_NUMBER_OF_ENTRIES), retries=20) + self.tap_dance_count = data[0] + self.tap_dance_entries = [] + for x in range(self.tap_dance_count): + data = self.usb_send(self.dev, struct.pack("BBBB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_DYNAMIC_ENTRY_OP, + DYNAMIC_VIAL_TAP_DANCE_GET, x), retries=20) + if data[0] != 0: + raise RuntimeError("failed retrieving tapdance entry {} from the device".format(x)) + self.tap_dance_entries.append(struct.unpack("TD({}) to set up this action in the keymap.".format(self.idx)) + l.addWidget(lbl) + l.setAlignment(lbl, QtCore.Qt.AlignHCenter) + l.addStretch() + self.w2 = QWidget() + self.w2.setLayout(l) + + def populate_container(self): + self.container.addWidget(QLabel("On tap"), 0, 0) + self.txt_on_tap = QLineEdit() + self.container.addWidget(self.txt_on_tap, 0, 1) + self.container.addWidget(QLabel("On hold"), 1, 0) + self.txt_on_hold = QLineEdit() + self.container.addWidget(self.txt_on_hold, 1, 1) + self.container.addWidget(QLabel("On double tap"), 2, 0) + self.txt_on_double_tap = QLineEdit() + self.container.addWidget(self.txt_on_double_tap, 2, 1) + self.container.addWidget(QLabel("On tap + hold"), 3, 0) + self.txt_on_tap_hold = QLineEdit() + self.container.addWidget(self.txt_on_tap_hold, 3, 1) + self.container.addWidget(QLabel("Tapping term (ms)"), 4, 0) + self.txt_tapping_term = QSpinBox() + self.txt_tapping_term.setMinimum(0) + self.txt_tapping_term.setMaximum(10000) + self.container.addWidget(self.txt_tapping_term, 4, 1) + + def widget(self): + return self.w2 + + def load(self, data): + self.txt_on_tap.setText(str(data[0])) + self.txt_on_hold.setText(str(data[1])) + self.txt_on_double_tap.setText(str(data[2])) + self.txt_on_tap_hold.setText(str(data[3])) + self.txt_tapping_term.setValue(data[4]) + + def save(self): + return ( + int(self.txt_on_tap.text()), + int(self.txt_on_hold.text()), + int(self.txt_on_double_tap.text()), + int(self.txt_on_tap_hold.text()), + self.txt_tapping_term.value() + ) + + class TapDance(BasicEditor): def __init__(self): super().__init__() self.keyboard = None + self.tap_dance_entries = [] + self.tap_dance_entries_available = [] self.tabs = QTabWidget() - for x in range(32): - container = QGridLayout() - - container.addWidget(QLabel("On tap"), 0, 0) - container.addWidget(QLineEdit(), 0, 1) - container.addWidget(QLabel("On hold"), 1, 0) - container.addWidget(QLineEdit(), 1, 1) - container.addWidget(QLabel("On double tap"), 2, 0) - container.addWidget(QLineEdit(), 2, 1) - container.addWidget(QLabel("On tap + hold"), 3, 0) - container.addWidget(QLineEdit(), 3, 1) - - w = QWidget() - w.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) - w.setLayout(container) - l = QVBoxLayout() - l.addStretch() - l.addWidget(w) - l.setAlignment(w, QtCore.Qt.AlignHCenter) - l.addSpacing(100) - lbl = QLabel("Use TD({}) to set up this action in the keymap.".format(x)) - l.addWidget(lbl) - l.setAlignment(lbl, QtCore.Qt.AlignHCenter) - l.addStretch() - w2 = QWidget() - w2.setLayout(l) - self.tabs.addTab(w2, str(x)) + for x in range(128): + self.tap_dance_entries_available.append(TapDanceEntryUI(x)) self.addWidget(self.tabs) buttons = QHBoxLayout() buttons.addStretch() btn_save = QPushButton(tr("TapDance", "Save")) + btn_save.clicked.connect(self.on_save) btn_revert = QPushButton(tr("TapDance", "Revert")) + btn_revert.clicked.connect(self.on_revert) buttons.addWidget(btn_save) buttons.addWidget(btn_revert) self.addLayout(buttons) + def rebuild_ui(self): + while self.tabs.count() > 0: + self.tabs.removeTab(0) + self.tap_dance_entries = self.tap_dance_entries_available[:self.keyboard.tap_dance_count] + for x, e in enumerate(self.tap_dance_entries): + self.tabs.addTab(e.widget(), str(x)) + self.reload_ui() + + def reload_ui(self): + for x, e in enumerate(self.tap_dance_entries): + e.load(self.keyboard.tap_dance_get(x)) + + def on_save(self): + for x, e in enumerate(self.tap_dance_entries): + self.keyboard.tap_dance_set(x, self.tap_dance_entries[x].save()) + + def on_revert(self): + self.keyboard.reload_dynamic() + self.reload_ui() + def rebuild(self, device): super().rebuild(device) if self.valid(): self.keyboard = device.keyboard + self.rebuild_ui() def valid(self): return isinstance(self.device, VialKeyboard) and \ - (self.device.keyboard and self.device.keyboard.vial_protocol >= 4) + (self.device.keyboard and self.device.keyboard.vial_protocol >= 4 + and self.device.keyboard.tap_dance_count > 0) From cc031a5c01adfe7e9ef92def17c9484687de0670 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sat, 3 Jul 2021 13:50:34 -0400 Subject: [PATCH 16/56] keyboard_comm: save/restore tapdance --- src/main/python/keyboard_comm.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index a104814..33eb7bf 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -500,6 +500,7 @@ class Keyboard: data["macro"] = self.save_macro() data["vial_protocol"] = self.vial_protocol data["via_protocol"] = self.via_protocol + data["tap_dance"] = self.tap_dance_entries return json.dumps(data).encode("utf-8") @@ -531,6 +532,10 @@ class Keyboard: self.set_layout_options(data["layout_options"]) self.restore_macros(data.get("macro")) + for x, e in enumerate(data.get("tap_dance", [])): + if x < self.tap_dance_count: + self.tap_dance_set(x, e) + def restore_macros(self, macros): if not isinstance(macros, list): return From ec3df5bdd072d9bbb8e4790d1ee7466e2be89655 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sat, 3 Jul 2021 14:37:45 -0400 Subject: [PATCH 17/56] tap_dance: initial implementation of key selection tray --- src/main/python/key_widget.py | 25 +++++++++++++++++++++++++ src/main/python/keyboard_widget.py | 12 +++++++----- src/main/python/main_window.py | 6 ++++++ src/main/python/tabbed_keycodes.py | 18 ++++++++++++++++++ src/main/python/tap_dance.py | 14 ++++++++------ 5 files changed, 64 insertions(+), 11 deletions(-) create mode 100644 src/main/python/key_widget.py diff --git a/src/main/python/key_widget.py b/src/main/python/key_widget.py new file mode 100644 index 0000000..0e018fe --- /dev/null +++ b/src/main/python/key_widget.py @@ -0,0 +1,25 @@ +from keyboard_widget import KeyboardWidget +from kle_serial import Key +from tabbed_keycodes import TabbedKeycodes + + +class KeyWidget(KeyboardWidget): + + def __init__(self): + super().__init__(None) + + self.padding = 1 + + self.keycode = 0 + + key = Key() + key.row = key.col = 0 + key.layout_index = key.layout_option = -1 + self.set_keys([key], []) + + def mousePressEvent(self, ev): + super().mousePressEvent(ev) + if self.active_key is not None: + TabbedKeycodes.open_tray(self) + else: + TabbedKeycodes.close_tray() diff --git a/src/main/python/keyboard_widget.py b/src/main/python/keyboard_widget.py index e4493a8..5eb9450 100644 --- a/src/main/python/keyboard_widget.py +++ b/src/main/python/keyboard_widget.py @@ -4,7 +4,8 @@ from PyQt5.QtGui import QPainter, QColor, QPainterPath, QTransform, QBrush, QPol from PyQt5.QtWidgets import QWidget, QToolTip, QApplication from PyQt5.QtCore import Qt, QSize, QRect, QPointF, pyqtSignal, QEvent, QRectF -from constants import KEY_SIZE_RATIO, KEY_SPACING_RATIO, KEYBOARD_WIDGET_PADDING, KEYBOARD_WIDGET_MASK_PADDING, KEYBOARD_WIDGET_MASK_HEIGHT, KEY_ROUNDNESS +from constants import KEY_SIZE_RATIO, KEY_SPACING_RATIO, KEYBOARD_WIDGET_PADDING, KEYBOARD_WIDGET_MASK_PADDING,\ + KEYBOARD_WIDGET_MASK_HEIGHT, KEY_ROUNDNESS class KeyWidget: @@ -166,6 +167,7 @@ class KeyboardWidget(QWidget): self.enabled = True self.scale = 1 + self.padding = KEYBOARD_WIDGET_PADDING self.setMouseTracking(True) @@ -215,7 +217,7 @@ class KeyboardWidget(QWidget): # place common widgets, that is, ones which are always displayed and require no extra transforms for widget in self.common_widgets: - widget.update_position(scale_factor, -top_x + KEYBOARD_WIDGET_PADDING, -top_y + KEYBOARD_WIDGET_PADDING) + widget.update_position(scale_factor, -top_x + self.padding, -top_y + self.padding) self.widgets.append(widget) # top-left position for specific layout @@ -236,7 +238,7 @@ class KeyboardWidget(QWidget): if opt == self.layout_editor.get_choice(idx): shift_x = layout_x[idx][opt] - layout_x[idx][0] shift_y = layout_y[idx][opt] - layout_y[idx][0] - widget.update_position(scale_factor, -shift_x - top_x + KEYBOARD_WIDGET_PADDING, -shift_y - top_y + KEYBOARD_WIDGET_PADDING) + widget.update_position(scale_factor, -shift_x - top_x + self.padding, -shift_y - top_y + self.padding) self.widgets.append(widget) def update_layout(self): @@ -255,8 +257,8 @@ class KeyboardWidget(QWidget): max_w = max(max_w, p.x() * self.scale) max_h = max(max_h, p.y() * self.scale) - self.width = max_w + 2 * KEYBOARD_WIDGET_PADDING - self.height = max_h + 2 * KEYBOARD_WIDGET_PADDING + self.width = max_w + 2 * self.padding + self.height = max_h + 2 * self.padding self.update() self.updateGeometry() diff --git a/src/main/python/main_window.py b/src/main/python/main_window.py index b879efb..a30b4e3 100644 --- a/src/main/python/main_window.py +++ b/src/main/python/main_window.py @@ -18,6 +18,7 @@ from layout_editor import LayoutEditor from macro_recorder import MacroRecorder from qmk_settings import QmkSettings from rgb_configurator import RGBConfigurator +from tabbed_keycodes import TabbedKeycodes from tap_dance import TapDance from unlocker import Unlocker from util import tr, find_vial_devices, EXAMPLE_KEYBOARDS @@ -91,6 +92,10 @@ class MainWindow(QMainWindow): layout.addWidget(self.tabs) layout.addWidget(self.lbl_no_devices) layout.setAlignment(self.lbl_no_devices, Qt.AlignHCenter) + self.popup_keycodes = TabbedKeycodes() + TabbedKeycodes.set_tray(self.popup_keycodes) + layout.addWidget(self.popup_keycodes) + self.popup_keycodes.hide() w = QWidget() w.setLayout(layout) self.setCentralWidget(w) @@ -343,6 +348,7 @@ class MainWindow(QMainWindow): msg.exec_() def on_tab_changed(self, index): + TabbedKeycodes.close_tray() old_tab = self.current_tab new_tab = None if index >= 0: diff --git a/src/main/python/tabbed_keycodes.py b/src/main/python/tabbed_keycodes.py index 9c2d5c4..edb2471 100644 --- a/src/main/python/tabbed_keycodes.py +++ b/src/main/python/tabbed_keycodes.py @@ -22,6 +22,7 @@ class TabbedKeycodes(QTabWidget): super().__init__(parent) self.keymap_override = None + self.target = None self.tab_basic = QScrollArea() self.tab_iso = QScrollArea() @@ -116,3 +117,20 @@ class TabbedKeycodes(QTabWidget): label = widget.keycode.label widget.setStyleSheet("QPushButton {}") widget.setText(label.replace("&", "&&")) + + @classmethod + def set_tray(cls, tray): + cls.tray = tray + + @classmethod + def open_tray(cls, target): + cls.tray.show() + if cls.tray.target is not None and cls.tray.target != target: + cls.tray.target.deselect() + cls.tray.target = target + + @classmethod + def close_tray(cls): + if cls.tray.target is not None: + cls.tray.target.deselect() + cls.tray.hide() diff --git a/src/main/python/tap_dance.py b/src/main/python/tap_dance.py index ff33ec7..8f8088d 100644 --- a/src/main/python/tap_dance.py +++ b/src/main/python/tap_dance.py @@ -3,6 +3,7 @@ from PyQt5 import QtCore from PyQt5.QtWidgets import QTabWidget, QWidget, QSizePolicy, QGridLayout, QVBoxLayout, QLabel, QLineEdit, QHBoxLayout, \ QPushButton, QSpinBox +from key_widget import KeyWidget from util import tr from vial_device import VialKeyboard from basic_editor import BasicEditor @@ -20,10 +21,10 @@ class TapDanceEntryUI: w.setLayout(self.container) l = QVBoxLayout() l.addStretch() - l.addSpacing(100) + l.addSpacing(10) l.addWidget(w) l.setAlignment(w, QtCore.Qt.AlignHCenter) - l.addSpacing(100) + l.addSpacing(10) lbl = QLabel("Use TD({}) to set up this action in the keymap.".format(self.idx)) l.addWidget(lbl) l.setAlignment(lbl, QtCore.Qt.AlignHCenter) @@ -34,16 +35,17 @@ class TapDanceEntryUI: def populate_container(self): self.container.addWidget(QLabel("On tap"), 0, 0) self.txt_on_tap = QLineEdit() - self.container.addWidget(self.txt_on_tap, 0, 1) + # self.container.addWidget(self.txt_on_tap, 0, 1) + self.container.addWidget(KeyWidget(), 0, 1) self.container.addWidget(QLabel("On hold"), 1, 0) self.txt_on_hold = QLineEdit() - self.container.addWidget(self.txt_on_hold, 1, 1) + self.container.addWidget(KeyWidget(), 1, 1) self.container.addWidget(QLabel("On double tap"), 2, 0) self.txt_on_double_tap = QLineEdit() - self.container.addWidget(self.txt_on_double_tap, 2, 1) + self.container.addWidget(KeyWidget(), 2, 1) self.container.addWidget(QLabel("On tap + hold"), 3, 0) self.txt_on_tap_hold = QLineEdit() - self.container.addWidget(self.txt_on_tap_hold, 3, 1) + self.container.addWidget(KeyWidget(), 3, 1) self.container.addWidget(QLabel("Tapping term (ms)"), 4, 0) self.txt_tapping_term = QSpinBox() self.txt_tapping_term.setMinimum(0) From fea0dcaa9b8403d4d6c6ea8c77780337aaa0ac7d Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sat, 3 Jul 2021 14:56:41 -0400 Subject: [PATCH 18/56] move keymap override handling into global util --- src/main/python/key_widget.py | 4 +++ src/main/python/keymap_editor.py | 42 ++++-------------------- src/main/python/main_window.py | 12 +++---- src/main/python/tabbed_keycodes.py | 29 ++++++++++++----- src/main/python/util.py | 52 +++++++++++++++++++++++++++++- 5 files changed, 89 insertions(+), 50 deletions(-) diff --git a/src/main/python/key_widget.py b/src/main/python/key_widget.py index 0e018fe..b686816 100644 --- a/src/main/python/key_widget.py +++ b/src/main/python/key_widget.py @@ -23,3 +23,7 @@ class KeyWidget(KeyboardWidget): TabbedKeycodes.open_tray(self) else: TabbedKeycodes.close_tray() + + def on_keycode_changed(self, kc): + """ Unlike set_keycode, this handles setting masked keycode inside the mask """ + print(kc) diff --git a/src/main/python/keymap_editor.py b/src/main/python/keymap_editor.py index 85139a9..8f9c543 100644 --- a/src/main/python/keymap_editor.py +++ b/src/main/python/keymap_editor.py @@ -1,8 +1,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later import json -from PyQt5.QtGui import QPalette -from PyQt5.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QMessageBox, QApplication +from PyQt5.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QMessageBox from PyQt5.QtCore import Qt from any_keycode_dialog import AnyKeycodeDialog @@ -12,7 +11,7 @@ from keycodes import recreate_keyboard_keycodes, Keycode from keymaps import KEYMAPS from square_button import SquareButton from tabbed_keycodes import TabbedKeycodes -from util import tr +from util import tr, KeycodeDisplay from vial_device import VialKeyboard @@ -48,8 +47,6 @@ class KeymapEditor(BasicEditor): layout_editor.changed.connect(self.on_layout_changed) - self.keymap_override = KEYMAPS[0][1] - self.container.anykey.connect(self.on_any_keycode) self.tabbed_keycodes = TabbedKeycodes() @@ -60,6 +57,7 @@ class KeymapEditor(BasicEditor): self.addWidget(self.tabbed_keycodes) self.device = None + KeycodeDisplay.notify_keymap_override(self) def on_container_clicked(self): """ Called when a mouse click event is bubbled up to the editor's container """ @@ -135,11 +133,6 @@ class KeymapEditor(BasicEditor): self.keyboard.restore_layout(data) self.refresh_layer_display() - def set_keymap_override(self, override): - self.keymap_override = override - self.refresh_layer_display() - self.tabbed_keycodes.set_keymap_override(override) - def on_any_keycode(self): if self.container.active_key is None: return @@ -148,17 +141,6 @@ class KeymapEditor(BasicEditor): if dlg.exec_() and dlg.value >= 0: self.on_keycode_changed(dlg.value) - def code_is_overriden(self, code): - """ Check whether a country-specific keymap overrides a code """ - key = Keycode.find_outer_keycode(code) - return key is not None and key.qmk_id in self.keymap_override - - def get_label(self, code): - """ Get label for a specific keycode """ - if self.code_is_overriden(code): - return self.keymap_override[Keycode.find_outer_keycode(code).qmk_id] - return Keycode.label(code) - def code_for_widget(self, widget): if widget.desc.row is not None: return self.keyboard.layout[(self.current_layer, widget.desc.row, widget.desc.col)] @@ -177,20 +159,7 @@ class KeymapEditor(BasicEditor): for widget in self.container.widgets: code = self.code_for_widget(widget) - text = self.get_label(code) - tooltip = Keycode.tooltip(code) - mask = Keycode.is_mask(code) - mask_text = self.get_label(code & 0xFF) - if mask: - text = text.split("\n")[0] - widget.masked = mask - widget.setText(text) - widget.setMaskText(mask_text) - widget.setToolTip(tooltip) - if self.code_is_overriden(code): - widget.setColor(QApplication.palette().color(QPalette.Link)) - else: - widget.setColor(None) + KeycodeDisplay.display_keycode(widget, code) self.container.update() self.container.updateGeometry() @@ -245,3 +214,6 @@ class KeymapEditor(BasicEditor): self.refresh_layer_display() self.keyboard.set_layout_options(self.layout_editor.pack()) + + def on_keymap_override(self): + self.refresh_layer_display() diff --git a/src/main/python/main_window.py b/src/main/python/main_window.py index a30b4e3..43011e6 100644 --- a/src/main/python/main_window.py +++ b/src/main/python/main_window.py @@ -21,7 +21,7 @@ from rgb_configurator import RGBConfigurator from tabbed_keycodes import TabbedKeycodes from tap_dance import TapDance from unlocker import Unlocker -from util import tr, find_vial_devices, EXAMPLE_KEYBOARDS +from util import tr, find_vial_devices, EXAMPLE_KEYBOARDS, KeycodeDisplay from vial_device import VialKeyboard from matrix_test import MatrixTest @@ -92,10 +92,10 @@ class MainWindow(QMainWindow): layout.addWidget(self.tabs) layout.addWidget(self.lbl_no_devices) layout.setAlignment(self.lbl_no_devices, Qt.AlignHCenter) - self.popup_keycodes = TabbedKeycodes() - TabbedKeycodes.set_tray(self.popup_keycodes) - layout.addWidget(self.popup_keycodes) - self.popup_keycodes.hide() + self.tray_keycodes = TabbedKeycodes() + self.tray_keycodes.make_tray() + layout.addWidget(self.tray_keycodes) + self.tray_keycodes.hide() w = QWidget() w.setLayout(layout) self.setCentralWidget(w) @@ -335,7 +335,7 @@ class MainWindow(QMainWindow): def change_keyboard_layout(self, index): self.settings.setValue("keymap", KEYMAPS[index][0]) - self.keymap_editor.set_keymap_override(KEYMAPS[index][1]) + KeycodeDisplay.set_keymap_override(KEYMAPS[index][1]) def get_theme(self): return self.settings.value("theme", "Dark") diff --git a/src/main/python/tabbed_keycodes.py b/src/main/python/tabbed_keycodes.py index edb2471..b59eff4 100644 --- a/src/main/python/tabbed_keycodes.py +++ b/src/main/python/tabbed_keycodes.py @@ -8,9 +8,8 @@ from constants import KEYCODE_BTN_RATIO from flowlayout import FlowLayout from keycodes import KEYCODES_BASIC, KEYCODES_ISO, KEYCODES_MACRO, KEYCODES_LAYERS, KEYCODES_QUANTUM, \ KEYCODES_BACKLIGHT, KEYCODES_MEDIA, KEYCODES_SPECIAL, KEYCODES_SHIFTED, KEYCODES_USER, Keycode -from keymaps import KEYMAPS from square_button import SquareButton -from util import tr +from util import tr, KeycodeDisplay class TabbedKeycodes(QTabWidget): @@ -21,8 +20,8 @@ class TabbedKeycodes(QTabWidget): def __init__(self, parent=None): super().__init__(parent) - self.keymap_override = None self.target = None + self.is_tray = False self.tab_basic = QScrollArea() self.tab_iso = QScrollArea() @@ -74,7 +73,7 @@ class TabbedKeycodes(QTabWidget): self.layer_keycode_buttons = [] self.macro_keycode_buttons = [] self.user_keycode_buttons = [] - self.set_keymap_override(KEYMAPS[0][1]) + KeycodeDisplay.notify_keymap_override(self) def create_buttons(self, layout, keycodes, wordWrap = False): buttons = [] @@ -102,15 +101,14 @@ class TabbedKeycodes(QTabWidget): self.widgets += self.layer_keycode_buttons + self.macro_keycode_buttons + self.user_keycode_buttons self.relabel_buttons() - def set_keymap_override(self, override): - self.keymap_override = override + def on_keymap_override(self): self.relabel_buttons() def relabel_buttons(self): for widget in self.widgets: qmk_id = widget.keycode.qmk_id - if qmk_id in self.keymap_override: - label = self.keymap_override[qmk_id] + if qmk_id in KeycodeDisplay.keymap_override: + label = KeycodeDisplay.keymap_override[qmk_id] highlight_color = QApplication.palette().color(QPalette.Link).getRgb() widget.setStyleSheet("QPushButton {color: rgb"+str(highlight_color)+";}") else: @@ -133,4 +131,19 @@ class TabbedKeycodes(QTabWidget): def close_tray(cls): if cls.tray.target is not None: cls.tray.target.deselect() + cls.tray.target = None cls.tray.hide() + + def make_tray(self): + self.is_tray = True + TabbedKeycodes.set_tray(self) + + self.keycode_changed.connect(self.on_tray_keycode_changed) + self.anykey.connect(self.on_tray_anykey) + + def on_tray_keycode_changed(self, kc): + if self.target is not None: + self.target.on_keycode_changed(kc) + + def on_tray_anykey(self): + pass diff --git a/src/main/python/util.py b/src/main/python/util.py index 1c020ae..2740431 100644 --- a/src/main/python/util.py +++ b/src/main/python/util.py @@ -5,9 +5,12 @@ import time from logging.handlers import RotatingFileHandler from PyQt5.QtCore import QCoreApplication, QStandardPaths +from PyQt5.QtGui import QPalette +from PyQt5.QtWidgets import QApplication from hidproxy import hid - +from keycodes import Keycode +from keymaps import KEYMAPS tr = QCoreApplication.translate @@ -147,3 +150,50 @@ def init_logger(): handler = RotatingFileHandler(path, maxBytes=5 * 1024 * 1024, backupCount=5) handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s")) logging.getLogger().addHandler(handler) + + +class KeycodeDisplay: + + keymap_override = KEYMAPS[0][1] + clients = [] + + @classmethod + def get_label(cls, code): + """ Get label for a specific keycode """ + if cls.code_is_overriden(code): + return cls.keymap_override[Keycode.find_outer_keycode(code).qmk_id] + return Keycode.label(code) + + @classmethod + def code_is_overriden(cls, code): + """ Check whether a country-specific keymap overrides a code """ + key = Keycode.find_outer_keycode(code) + return key is not None and key.qmk_id in cls.keymap_override + + @classmethod + def display_keycode(cls, widget, code): + text = cls.get_label(code) + tooltip = Keycode.tooltip(code) + mask = Keycode.is_mask(code) + mask_text = cls.get_label(code & 0xFF) + if mask: + text = text.split("\n")[0] + widget.masked = mask + widget.setText(text) + widget.setMaskText(mask_text) + widget.setToolTip(tooltip) + if cls.code_is_overriden(code): + widget.setColor(QApplication.palette().color(QPalette.Link)) + else: + widget.setColor(None) + + @classmethod + def set_keymap_override(cls, override): + cls.keymap_override = override + for client in cls.clients: + client.on_keymap_override() + + @classmethod + def notify_keymap_override(cls, client): + cls.clients.append(client) + client.on_keymap_override() From af311b51c3fc8b8724b6eaf25fe197d26b59b0c7 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sat, 3 Jul 2021 16:16:07 -0400 Subject: [PATCH 19/56] key_widget: support changing keycodes --- src/main/python/key_widget.py | 25 +++++++++++++++++++++++-- src/main/python/tabbed_keycodes.py | 3 ++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/main/python/key_widget.py b/src/main/python/key_widget.py index b686816..86f37fd 100644 --- a/src/main/python/key_widget.py +++ b/src/main/python/key_widget.py @@ -1,6 +1,8 @@ +from any_keycode_dialog import AnyKeycodeDialog from keyboard_widget import KeyboardWidget from kle_serial import Key from tabbed_keycodes import TabbedKeycodes +from util import KeycodeDisplay class KeyWidget(KeyboardWidget): @@ -17,6 +19,8 @@ class KeyWidget(KeyboardWidget): key.layout_index = key.layout_option = -1 self.set_keys([key], []) + self.anykey.connect(self.on_anykey) + def mousePressEvent(self, ev): super().mousePressEvent(ev) if self.active_key is not None: @@ -24,6 +28,23 @@ class KeyWidget(KeyboardWidget): else: TabbedKeycodes.close_tray() - def on_keycode_changed(self, kc): + def on_keycode_changed(self, keycode): """ Unlike set_keycode, this handles setting masked keycode inside the mask """ - print(kc) + + if self.active_mask: + if keycode > 0xFF: + return + keycode = (self.keycode & 0xFF00) | keycode + self.set_keycode(keycode) + + def on_anykey(self): + if self.active_key is None: + return + dlg = AnyKeycodeDialog(self.keycode) + if dlg.exec_() and dlg.value >= 0: + self.set_keycode(dlg.value) + + def set_keycode(self, kc): + self.keycode = kc + KeycodeDisplay.display_keycode(self.widgets[0], self.keycode) + self.update() diff --git a/src/main/python/tabbed_keycodes.py b/src/main/python/tabbed_keycodes.py index b59eff4..266bec3 100644 --- a/src/main/python/tabbed_keycodes.py +++ b/src/main/python/tabbed_keycodes.py @@ -146,4 +146,5 @@ class TabbedKeycodes(QTabWidget): self.target.on_keycode_changed(kc) def on_tray_anykey(self): - pass + if self.target is not None: + self.target.on_anykey() From 028655f418b3a84ee09a952382e849f801c90995 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sat, 3 Jul 2021 16:27:22 -0400 Subject: [PATCH 20/56] tap_dance: switch to new key widgets --- src/main/python/tap_dance.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/main/python/tap_dance.py b/src/main/python/tap_dance.py index 8f8088d..3e9d4ac 100644 --- a/src/main/python/tap_dance.py +++ b/src/main/python/tap_dance.py @@ -34,18 +34,17 @@ class TapDanceEntryUI: def populate_container(self): self.container.addWidget(QLabel("On tap"), 0, 0) - self.txt_on_tap = QLineEdit() - # self.container.addWidget(self.txt_on_tap, 0, 1) - self.container.addWidget(KeyWidget(), 0, 1) + self.kc_on_tap = KeyWidget() + self.container.addWidget(self.kc_on_tap, 0, 1) self.container.addWidget(QLabel("On hold"), 1, 0) - self.txt_on_hold = QLineEdit() - self.container.addWidget(KeyWidget(), 1, 1) + self.kc_on_hold = KeyWidget() + self.container.addWidget(self.kc_on_hold, 1, 1) self.container.addWidget(QLabel("On double tap"), 2, 0) - self.txt_on_double_tap = QLineEdit() - self.container.addWidget(KeyWidget(), 2, 1) + self.kc_on_double_tap = KeyWidget() + self.container.addWidget(self.kc_on_double_tap, 2, 1) self.container.addWidget(QLabel("On tap + hold"), 3, 0) - self.txt_on_tap_hold = QLineEdit() - self.container.addWidget(KeyWidget(), 3, 1) + self.kc_on_tap_hold = KeyWidget() + self.container.addWidget(self.kc_on_tap_hold, 3, 1) self.container.addWidget(QLabel("Tapping term (ms)"), 4, 0) self.txt_tapping_term = QSpinBox() self.txt_tapping_term.setMinimum(0) @@ -56,18 +55,18 @@ class TapDanceEntryUI: return self.w2 def load(self, data): - self.txt_on_tap.setText(str(data[0])) - self.txt_on_hold.setText(str(data[1])) - self.txt_on_double_tap.setText(str(data[2])) - self.txt_on_tap_hold.setText(str(data[3])) + self.kc_on_tap.set_keycode(data[0]) + self.kc_on_hold.set_keycode(data[1]) + self.kc_on_double_tap.set_keycode(data[2]) + self.kc_on_tap_hold.set_keycode(data[3]) self.txt_tapping_term.setValue(data[4]) def save(self): return ( - int(self.txt_on_tap.text()), - int(self.txt_on_hold.text()), - int(self.txt_on_double_tap.text()), - int(self.txt_on_tap_hold.text()), + self.kc_on_tap.keycode, + self.kc_on_hold.keycode, + self.kc_on_double_tap.keycode, + self.kc_on_tap_hold.keycode, self.txt_tapping_term.value() ) From 8f4734ab4056870703296775bfe49f00e8aa8de3 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sat, 3 Jul 2021 16:53:24 -0400 Subject: [PATCH 21/56] tap_dance: track state of tab change --- src/main/python/key_widget.py | 8 +++++ src/main/python/tap_dance.py | 56 +++++++++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/main/python/key_widget.py b/src/main/python/key_widget.py index 86f37fd..bf7fd6b 100644 --- a/src/main/python/key_widget.py +++ b/src/main/python/key_widget.py @@ -1,3 +1,5 @@ +from PyQt5.QtCore import pyqtSignal + from any_keycode_dialog import AnyKeycodeDialog from keyboard_widget import KeyboardWidget from kle_serial import Key @@ -7,6 +9,8 @@ from util import KeycodeDisplay class KeyWidget(KeyboardWidget): + changed = pyqtSignal() + def __init__(self): super().__init__(None) @@ -45,6 +49,10 @@ class KeyWidget(KeyboardWidget): self.set_keycode(dlg.value) def set_keycode(self, kc): + if kc == self.keycode: + return self.keycode = kc KeycodeDisplay.display_keycode(self.widgets[0], self.keycode) self.update() + + self.changed.emit() diff --git a/src/main/python/tap_dance.py b/src/main/python/tap_dance.py index 3e9d4ac..cf950b3 100644 --- a/src/main/python/tap_dance.py +++ b/src/main/python/tap_dance.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: GPL-2.0-or-later from PyQt5 import QtCore +from PyQt5.QtCore import pyqtSignal, QObject from PyQt5.QtWidgets import QTabWidget, QWidget, QSizePolicy, QGridLayout, QVBoxLayout, QLabel, QLineEdit, QHBoxLayout, \ QPushButton, QSpinBox @@ -9,9 +10,14 @@ from vial_device import VialKeyboard from basic_editor import BasicEditor -class TapDanceEntryUI: +class TapDanceEntryUI(QObject): + + key_changed = pyqtSignal() + timing_changed = pyqtSignal() def __init__(self, idx): + super().__init__() + self.idx = idx self.container = QGridLayout() self.populate_container() @@ -35,18 +41,23 @@ class TapDanceEntryUI: def populate_container(self): self.container.addWidget(QLabel("On tap"), 0, 0) self.kc_on_tap = KeyWidget() + self.kc_on_tap.changed.connect(self.on_key_changed) self.container.addWidget(self.kc_on_tap, 0, 1) self.container.addWidget(QLabel("On hold"), 1, 0) self.kc_on_hold = KeyWidget() + self.kc_on_hold.changed.connect(self.on_key_changed) self.container.addWidget(self.kc_on_hold, 1, 1) self.container.addWidget(QLabel("On double tap"), 2, 0) self.kc_on_double_tap = KeyWidget() + self.kc_on_double_tap.changed.connect(self.on_key_changed) self.container.addWidget(self.kc_on_double_tap, 2, 1) self.container.addWidget(QLabel("On tap + hold"), 3, 0) self.kc_on_tap_hold = KeyWidget() + self.kc_on_tap_hold.changed.connect(self.on_key_changed) self.container.addWidget(self.kc_on_tap_hold, 3, 1) self.container.addWidget(QLabel("Tapping term (ms)"), 4, 0) self.txt_tapping_term = QSpinBox() + self.txt_tapping_term.valueChanged.connect(self.on_timing_changed) self.txt_tapping_term.setMinimum(0) self.txt_tapping_term.setMaximum(10000) self.container.addWidget(self.txt_tapping_term, 4, 1) @@ -55,12 +66,19 @@ class TapDanceEntryUI: return self.w2 def load(self, data): + objs = [self.kc_on_tap, self.kc_on_hold, self.kc_on_double_tap, self.kc_on_tap_hold, self.txt_tapping_term] + for o in objs: + o.blockSignals(True) + self.kc_on_tap.set_keycode(data[0]) self.kc_on_hold.set_keycode(data[1]) self.kc_on_double_tap.set_keycode(data[2]) self.kc_on_tap_hold.set_keycode(data[3]) self.txt_tapping_term.setValue(data[4]) + for o in objs: + o.blockSignals(False) + def save(self): return ( self.kc_on_tap.keycode, @@ -70,6 +88,12 @@ class TapDanceEntryUI: self.txt_tapping_term.value() ) + def on_key_changed(self): + self.key_changed.emit() + + def on_timing_changed(self): + self.timing_changed.emit() + class TapDance(BasicEditor): @@ -81,16 +105,19 @@ class TapDance(BasicEditor): self.tap_dance_entries_available = [] self.tabs = QTabWidget() for x in range(128): - self.tap_dance_entries_available.append(TapDanceEntryUI(x)) + entry = TapDanceEntryUI(x) + entry.key_changed.connect(self.on_key_changed) + entry.timing_changed.connect(self.on_timing_changed) + self.tap_dance_entries_available.append(entry) self.addWidget(self.tabs) buttons = QHBoxLayout() buttons.addStretch() - btn_save = QPushButton(tr("TapDance", "Save")) - btn_save.clicked.connect(self.on_save) + self.btn_save = QPushButton(tr("TapDance", "Save")) + self.btn_save.clicked.connect(self.on_save) btn_revert = QPushButton(tr("TapDance", "Revert")) btn_revert.clicked.connect(self.on_revert) - buttons.addWidget(btn_save) + buttons.addWidget(self.btn_save) buttons.addWidget(btn_revert) self.addLayout(buttons) @@ -105,10 +132,12 @@ class TapDance(BasicEditor): def reload_ui(self): for x, e in enumerate(self.tap_dance_entries): e.load(self.keyboard.tap_dance_get(x)) + self.update_modified_state() def on_save(self): for x, e in enumerate(self.tap_dance_entries): self.keyboard.tap_dance_set(x, self.tap_dance_entries[x].save()) + self.update_modified_state() def on_revert(self): self.keyboard.reload_dynamic() @@ -124,3 +153,20 @@ class TapDance(BasicEditor): return isinstance(self.device, VialKeyboard) and \ (self.device.keyboard and self.device.keyboard.vial_protocol >= 4 and self.device.keyboard.tap_dance_count > 0) + + def on_key_changed(self): + self.on_save() + + def update_modified_state(self): + """ Update indication of which tabs are modified, and keep Save button enabled only if it's needed """ + has_changes = False + for x, e in enumerate(self.tap_dance_entries): + if self.tap_dance_entries[x].save() != self.keyboard.tap_dance_get(x): + has_changes = True + self.tabs.setTabText(x, "{}*".format(x)) + else: + self.tabs.setTabText(x, str(x)) + self.btn_save.setEnabled(has_changes) + + def on_timing_changed(self): + self.update_modified_state() From 838182494802f897b2d8c192b7ff6404a602ae55 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sat, 3 Jul 2021 20:06:26 -0400 Subject: [PATCH 22/56] tap_dance: hide keycode tray when empty space is clicked --- src/main/python/key_widget.py | 3 +++ src/main/python/tap_dance.py | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/python/key_widget.py b/src/main/python/key_widget.py index bf7fd6b..607fdcf 100644 --- a/src/main/python/key_widget.py +++ b/src/main/python/key_widget.py @@ -32,6 +32,9 @@ class KeyWidget(KeyboardWidget): else: TabbedKeycodes.close_tray() + def mouseReleaseEvent(self, ev): + ev.accept() + def on_keycode_changed(self, keycode): """ Unlike set_keycode, this handles setting masked keycode inside the mask """ diff --git a/src/main/python/tap_dance.py b/src/main/python/tap_dance.py index cf950b3..5afb982 100644 --- a/src/main/python/tap_dance.py +++ b/src/main/python/tap_dance.py @@ -5,6 +5,7 @@ from PyQt5.QtWidgets import QTabWidget, QWidget, QSizePolicy, QGridLayout, QVBox QPushButton, QSpinBox from key_widget import KeyWidget +from tabbed_keycodes import TabbedKeycodes from util import tr from vial_device import VialKeyboard from basic_editor import BasicEditor @@ -95,6 +96,12 @@ class TapDanceEntryUI(QObject): self.timing_changed.emit() +class CustomTabWidget(QTabWidget): + + def mouseReleaseEvent(self, ev): + TabbedKeycodes.close_tray() + + class TapDance(BasicEditor): def __init__(self): @@ -103,7 +110,7 @@ class TapDance(BasicEditor): self.tap_dance_entries = [] self.tap_dance_entries_available = [] - self.tabs = QTabWidget() + self.tabs = CustomTabWidget() for x in range(128): entry = TapDanceEntryUI(x) entry.key_changed.connect(self.on_key_changed) From 5bb644615597b4e0ec3d370fb74755d31ef458ab Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sat, 3 Jul 2021 22:25:45 -0400 Subject: [PATCH 23/56] tabbed_keycodes: show tap dance in the UI --- src/main/python/keycodes.py | 10 ++++++++-- src/main/python/tabbed_keycodes.py | 20 ++++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/main/python/keycodes.py b/src/main/python/keycodes.py index f0d550f..d965bec 100644 --- a/src/main/python/keycodes.py +++ b/src/main/python/keycodes.py @@ -524,6 +524,8 @@ KEYCODES_MEDIA = [ K(132, "KC_LSCR", "Locking\nScroll", "Locking Scroll Lock", alias=["KC_LOCKING_SCROLL"]), ] +KEYCODES_TAP_DANCE = [] + KEYCODES_USER = [] KEYCODES_MACRO = [] @@ -544,8 +546,8 @@ def recreate_keycodes(): KEYCODES.clear() KEYCODES.extend(KEYCODES_SPECIAL + KEYCODES_BASIC + KEYCODES_SHIFTED + KEYCODES_ISO + KEYCODES_LAYERS + - KEYCODES_QUANTUM + KEYCODES_BACKLIGHT + KEYCODES_MEDIA + KEYCODES_MACRO + KEYCODES_USER + - KEYCODES_HIDDEN) + KEYCODES_QUANTUM + KEYCODES_BACKLIGHT + KEYCODES_MEDIA + KEYCODES_TAP_DANCE + KEYCODES_MACRO + + KEYCODES_USER + KEYCODES_HIDDEN) def create_user_keycodes(): @@ -608,6 +610,10 @@ def recreate_keyboard_keycodes(keyboard): lbl = "M{}".format(x) KEYCODES_MACRO.append(Keycode(0x5F12 + x, lbl, lbl)) + for x in range(keyboard.tap_dance_count): + lbl = "TD({})".format(x) + KEYCODES_TAP_DANCE.append(Keycode(QK_TAP_DANCE | x, lbl, lbl)) + # Check if custom keycodes are defined in keyboard, and if so add them to user keycodes if keyboard.custom_keycodes is not None and len(keyboard.custom_keycodes) > 0: create_custom_user_keycodes(keyboard.custom_keycodes) diff --git a/src/main/python/tabbed_keycodes.py b/src/main/python/tabbed_keycodes.py index 266bec3..da9e9ff 100644 --- a/src/main/python/tabbed_keycodes.py +++ b/src/main/python/tabbed_keycodes.py @@ -7,7 +7,7 @@ from PyQt5.QtGui import QPalette from constants import KEYCODE_BTN_RATIO from flowlayout import FlowLayout from keycodes import KEYCODES_BASIC, KEYCODES_ISO, KEYCODES_MACRO, KEYCODES_LAYERS, KEYCODES_QUANTUM, \ - KEYCODES_BACKLIGHT, KEYCODES_MEDIA, KEYCODES_SPECIAL, KEYCODES_SHIFTED, KEYCODES_USER, Keycode + KEYCODES_BACKLIGHT, KEYCODES_MEDIA, KEYCODES_SPECIAL, KEYCODES_SHIFTED, KEYCODES_USER, Keycode, KEYCODES_TAP_DANCE from square_button import SquareButton from util import tr, KeycodeDisplay @@ -29,6 +29,7 @@ class TabbedKeycodes(QTabWidget): self.tab_quantum = QScrollArea() self.tab_backlight = QScrollArea() self.tab_media = QScrollArea() + self.tab_tap_dance = QScrollArea() self.tab_user = QScrollArea() self.tab_macro = QScrollArea() @@ -41,12 +42,15 @@ class TabbedKeycodes(QTabWidget): (self.tab_quantum, "Quantum", KEYCODES_QUANTUM), (self.tab_backlight, "Backlight", KEYCODES_BACKLIGHT), (self.tab_media, "App, Media and Mouse", KEYCODES_MEDIA), + (self.tab_tap_dance, "Tap Dance", KEYCODES_TAP_DANCE), (self.tab_user, "User", KEYCODES_USER), (self.tab_macro, "Macro", KEYCODES_MACRO), ]: layout = FlowLayout() if tab == self.tab_layers: self.layout_layers = layout + elif tab == self.tab_tap_dance: + self.layout_tap_dance = layout elif tab == self.tab_macro: self.layout_macro = layout elif tab == self.tab_user: @@ -71,16 +75,17 @@ class TabbedKeycodes(QTabWidget): self.addTab(tab, tr("TabbedKeycodes", label)) self.layer_keycode_buttons = [] + self.tap_dance_keycode_buttons = [] self.macro_keycode_buttons = [] self.user_keycode_buttons = [] KeycodeDisplay.notify_keymap_override(self) - def create_buttons(self, layout, keycodes, wordWrap = False): + def create_buttons(self, layout, keycodes, word_wrap=False): buttons = [] for keycode in keycodes: btn = SquareButton() - btn.setWordWrap(wordWrap) + btn.setWordWrap(word_wrap) btn.setRelSize(KEYCODE_BTN_RATIO) btn.setToolTip(Keycode.tooltip(keycode.code)) btn.clicked.connect(lambda st, k=keycode: self.keycode_changed.emit(k.code)) @@ -91,14 +96,17 @@ class TabbedKeycodes(QTabWidget): return buttons def recreate_keycode_buttons(self): - for btn in self.layer_keycode_buttons + self.macro_keycode_buttons + self.user_keycode_buttons: + for btn in self.layer_keycode_buttons + self.tap_dance_keycode_buttons + self.macro_keycode_buttons \ + + self.user_keycode_buttons: self.widgets.remove(btn) btn.hide() btn.deleteLater() self.layer_keycode_buttons = self.create_buttons(self.layout_layers, KEYCODES_LAYERS) + self.tap_dance_keycode_buttons = self.create_buttons(self.layout_tap_dance, KEYCODES_TAP_DANCE) self.macro_keycode_buttons = self.create_buttons(self.layout_macro, KEYCODES_MACRO) - self.user_keycode_buttons = self.create_buttons(self.layout_user, KEYCODES_USER, wordWrap=True) - self.widgets += self.layer_keycode_buttons + self.macro_keycode_buttons + self.user_keycode_buttons + self.user_keycode_buttons = self.create_buttons(self.layout_user, KEYCODES_USER, word_wrap=True) + self.widgets += self.layer_keycode_buttons + self.tap_dance_keycode_buttons + \ + self.macro_keycode_buttons + self.user_keycode_buttons self.relabel_buttons() def on_keymap_override(self): From 57386d41dc3940154647b41045009ee0a4972aa3 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sat, 3 Jul 2021 22:28:05 -0400 Subject: [PATCH 24/56] keymap_editor: also recreate keycodes in the tray TabbedKeycodes --- src/main/python/keymap_editor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/python/keymap_editor.py b/src/main/python/keymap_editor.py index 8f9c543..f075e46 100644 --- a/src/main/python/keymap_editor.py +++ b/src/main/python/keymap_editor.py @@ -113,6 +113,7 @@ class KeymapEditor(BasicEditor): recreate_keyboard_keycodes(self.keyboard) self.tabbed_keycodes.recreate_keycode_buttons() + TabbedKeycodes.tray.recreate_keycode_buttons() self.refresh_layer_display() self.container.setEnabled(self.valid()) From 3ea387dee971a833e61105fa8c3ce937625aad05 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sun, 4 Jul 2021 10:13:16 -0400 Subject: [PATCH 25/56] combos: add initial --- src/main/python/combos.py | 118 +++++++++++++++++++++++++++++++ src/main/python/keyboard_comm.py | 26 +++++++ src/main/python/main_window.py | 6 +- 3 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 src/main/python/combos.py diff --git a/src/main/python/combos.py b/src/main/python/combos.py new file mode 100644 index 0000000..ee17965 --- /dev/null +++ b/src/main/python/combos.py @@ -0,0 +1,118 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +from PyQt5 import QtCore +from PyQt5.QtCore import pyqtSignal, QObject +from PyQt5.QtWidgets import QTabWidget, QWidget, QSizePolicy, QGridLayout, QVBoxLayout, QLabel + +from key_widget import KeyWidget +from tabbed_keycodes import TabbedKeycodes +from vial_device import VialKeyboard +from basic_editor import BasicEditor + + +class ComboEntryUI(QObject): + + key_changed = pyqtSignal() + + def __init__(self, idx): + super().__init__() + + self.idx = idx + self.container = QGridLayout() + self.kc_inputs = [] + self.populate_container() + + w = QWidget() + w.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) + w.setLayout(self.container) + l = QVBoxLayout() + l.addWidget(w) + l.setAlignment(w, QtCore.Qt.AlignHCenter) + self.w2 = QWidget() + self.w2.setLayout(l) + + def populate_container(self): + for x in range(4): + kc_widget = KeyWidget() + kc_widget.changed.connect(self.on_key_changed) + self.container.addWidget(QLabel("Key {}".format(x + 1)), x, 0) + self.container.addWidget(kc_widget, x, 1) + self.kc_inputs.append(kc_widget) + + self.kc_output = KeyWidget() + self.kc_output.changed.connect(self.on_key_changed) + self.container.addWidget(QLabel("Output key"), 4, 0) + self.container.addWidget(self.kc_output, 4, 1) + + def widget(self): + return self.w2 + + def load(self, data): + objs = self.kc_inputs + [self.kc_output] + for o in objs: + o.blockSignals(True) + + for x in range(4): + self.kc_inputs[x].set_keycode(data[x]) + self.kc_output.set_keycode(data[4]) + + for o in objs: + o.blockSignals(False) + + def save(self): + return ( + self.kc_inputs[0].keycode, + self.kc_inputs[1].keycode, + self.kc_inputs[2].keycode, + self.kc_inputs[3].keycode, + self.kc_output.keycode + ) + + def on_key_changed(self): + self.key_changed.emit() + + +class CustomTabWidget(QTabWidget): + + def mouseReleaseEvent(self, ev): + TabbedKeycodes.close_tray() + + +class Combos(BasicEditor): + + def __init__(self): + super().__init__() + self.keyboard = None + + self.combo_entries = [] + self.combo_entries_available = [] + self.tabs = CustomTabWidget() + for x in range(128): + entry = ComboEntryUI(x) + entry.key_changed.connect(self.on_key_changed) + self.combo_entries_available.append(entry) + + self.addWidget(self.tabs) + + def rebuild_ui(self): + while self.tabs.count() > 0: + self.tabs.removeTab(0) + self.combo_entries = self.combo_entries_available[:self.keyboard.combo_count] + for x, e in enumerate(self.combo_entries): + self.tabs.addTab(e.widget(), str(x + 1)) + for x, e in enumerate(self.combo_entries): + e.load(self.keyboard.combo_get(x)) + + def rebuild(self, device): + super().rebuild(device) + if self.valid(): + self.keyboard = device.keyboard + self.rebuild_ui() + + def valid(self): + return isinstance(self.device, VialKeyboard) and \ + (self.device.keyboard and self.device.keyboard.vial_protocol >= 4 + and self.device.keyboard.combo_count > 0) + + def on_key_changed(self): + for x, e in enumerate(self.combo_entries): + self.keyboard.combo_set(x, self.combo_entries[x].save()) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 33eb7bf..784fdc3 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -62,6 +62,8 @@ CMD_VIAL_DYNAMIC_ENTRY_OP = 0x0D DYNAMIC_VIAL_GET_NUMBER_OF_ENTRIES = 0x00 DYNAMIC_VIAL_TAP_DANCE_GET = 0x01 DYNAMIC_VIAL_TAP_DANCE_SET = 0x02 +DYNAMIC_VIAL_COMBO_GET = 0x03 +DYNAMIC_VIAL_COMBO_SET = 0x04 # how much of a macro/keymap buffer we can read/write per packet BUFFER_FETCH_CHUNK = 28 @@ -385,10 +387,14 @@ class Keyboard: if self.vial_protocol < 4: self.tap_dance_count = 0 self.tap_dance_entries = [] + self.combo_count = 0 + self.combo_entries = [] return data = self.usb_send(self.dev, struct.pack("BBB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_DYNAMIC_ENTRY_OP, DYNAMIC_VIAL_GET_NUMBER_OF_ENTRIES), retries=20) self.tap_dance_count = data[0] + self.combo_count = data[1] + self.tap_dance_entries = [] for x in range(self.tap_dance_count): data = self.usb_send(self.dev, struct.pack("BBBB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_DYNAMIC_ENTRY_OP, @@ -397,6 +403,15 @@ class Keyboard: raise RuntimeError("failed retrieving tapdance entry {} from the device".format(x)) self.tap_dance_entries.append(struct.unpack(" Date: Sun, 4 Jul 2021 18:58:46 -0400 Subject: [PATCH 26/56] keyboard_comm: store/restore combos --- src/main/python/keyboard_comm.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 784fdc3..29c3280 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -515,7 +515,9 @@ class Keyboard: data["macro"] = self.save_macro() data["vial_protocol"] = self.vial_protocol data["via_protocol"] = self.via_protocol + # TODO: should store/restore serialized keycodes for these two data["tap_dance"] = self.tap_dance_entries + data["combo"] = self.combo_entries return json.dumps(data).encode("utf-8") @@ -550,6 +552,9 @@ class Keyboard: for x, e in enumerate(data.get("tap_dance", [])): if x < self.tap_dance_count: self.tap_dance_set(x, e) + for x, e in enumerate(data.get("combo", [])): + if x < self.combo_count: + self.combo_set(x, e) def restore_macros(self, macros): if not isinstance(macros, list): From 5a02b74d41d0c804f962f64fd900b82ca1da2c00 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sun, 4 Jul 2021 19:05:02 -0400 Subject: [PATCH 27/56] qmk_settings: support combo settings --- src/main/resources/base/qmk_settings.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/resources/base/qmk_settings.json b/src/main/resources/base/qmk_settings.json index 3fd4885..7b03e14 100644 --- a/src/main/resources/base/qmk_settings.json +++ b/src/main/resources/base/qmk_settings.json @@ -22,6 +22,12 @@ { "type": "boolean", "title": "Disable keyrepeat when timeout is exceeded", "qsid": 3, "bit": 6 } ] }, + { + "name": "Combo", + "fields": [ + { "type": "integer", "title": "Time out period for combos", "qsid": 2, "min": 0, "max": 10000, "width": 2 } + ] + }, { "name": "One Shot Keys", "fields": [ From 62c4d416692bf1f83a4bdef721c3d12029d262dd Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sun, 4 Jul 2021 22:54:10 -0400 Subject: [PATCH 28/56] main_window: add "about vial" menu --- src/build/settings/base.json | 2 +- src/main/python/main_window.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/build/settings/base.json b/src/build/settings/base.json index 6c3140d..cc69792 100644 --- a/src/build/settings/base.json +++ b/src/build/settings/base.json @@ -2,5 +2,5 @@ "app_name": "Vial", "author": "xyz", "main_module": "src/main/python/main.py", - "version": "0.0.0" + "version": "0.4" } \ No newline at end of file diff --git a/src/main/python/main_window.py b/src/main/python/main_window.py index 678bcc4..64da576 100644 --- a/src/main/python/main_window.py +++ b/src/main/python/main_window.py @@ -194,6 +194,11 @@ class MainWindow(QMainWindow): if theme_group.checkedAction() is None: theme_group.actions()[0].setChecked(True) + about_vial_act = QAction(tr("MenuAbout", "About Vial..."), self) + about_vial_act.triggered.connect(self.about_vial) + self.about_menu = self.menuBar().addMenu(tr("Menu", "About")) + self.about_menu.addAction(about_vial_act) + def on_layout_load(self): dialog = QFileDialog() dialog.setDefaultSuffix("vil") @@ -362,3 +367,13 @@ class MainWindow(QMainWindow): new_tab.editor.activate() self.current_tab = new_tab + + def about_vial(self): + QMessageBox.about( + self, + "About Vial", + 'Vial {}

' + 'Licensed under the terms of the
GNU General Public License (version 2 or later)

' + 'https://get.vial.today/' + .format(self.appctx.build_settings["version"]) + ) From 1ce89e49e534ea3d5a95c8bfd73139326a5e59bd Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sun, 4 Jul 2021 23:00:26 -0400 Subject: [PATCH 29/56] firmware_flasher: hide layout restore checkbox when in bootloader mode because we cannot restore layouts without having a keyboard --- src/main/python/firmware_flasher.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/python/firmware_flasher.py b/src/main/python/firmware_flasher.py index 5be3c8e..a1bc133 100644 --- a/src/main/python/firmware_flasher.py +++ b/src/main/python/firmware_flasher.py @@ -156,8 +156,10 @@ class FirmwareFlasher(BasicEditor): if isinstance(self.device, VialBootloader): self.log("Valid Vial Bootloader device at {}".format(self.device.desc["path"].decode("utf-8"))) + self.chk_restore_keymap.hide() elif isinstance(self.device, VialKeyboard): self.log("Vial keyboard detected") + self.chk_restore_keymap.show() def valid(self): return isinstance(self.device, VialBootloader) or\ From 02043c8c119996e37e8cea2fc9106c02c42dfe84 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Mon, 5 Jul 2021 23:42:03 -0400 Subject: [PATCH 30/56] keyboard_comm: add cache to bring settings api in line with others --- src/main/python/keyboard_comm.py | 43 ++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 29c3280..ec709bf 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -215,6 +215,7 @@ class Keyboard: self.reload_keymap() self.reload_macros() self.reload_rgb() + self.reload_settings() self.reload_dynamic() def reload_layers(self): @@ -383,6 +384,29 @@ class Keyboard: self.backlight_effect = self.usb_send( self.dev, struct.pack(">BB", CMD_VIA_LIGHTING_GET_VALUE, QMK_BACKLIGHT_EFFECT), retries=20)[2] + def reload_settings(self): + self.settings = dict() + self.supported_settings = set() + if self.vial_protocol < 4: + return + cur = 0 + while cur != 0xFFFF: + data = self.usb_send(self.dev, struct.pack(" Date: Mon, 5 Jul 2021 23:46:27 -0400 Subject: [PATCH 31/56] qmk_settings: hide the tab if settings unsupported --- src/main/python/keyboard_comm.py | 3 --- src/main/python/qmk_settings.py | 9 ++++----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index ec709bf..5186dd8 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -714,9 +714,6 @@ class Keyboard: macros = macros[:self.macro_count] return [self.macro_deserialize(x) for x in macros] - def qmk_settings_query(self): - return self.supported_settings - def qmk_settings_get(self, qsid): return self.settings[qsid] diff --git a/src/main/python/qmk_settings.py b/src/main/python/qmk_settings.py index 8af97f8..2094c3a 100644 --- a/src/main/python/qmk_settings.py +++ b/src/main/python/qmk_settings.py @@ -106,14 +106,13 @@ class QmkSettings(BasicEditor): buttons.addWidget(btn_reset) self.addLayout(buttons) - self.supported_settings = set() self.tabs = [] self.misc_widgets = [] def populate_tab(self, tab, container): options = [] for field in tab["fields"]: - if field["qsid"] not in self.supported_settings: + if field["qsid"] not in self.keyboard.supported_settings: continue if field["type"] == "boolean": options.append(BooleanOption(field, container)) @@ -144,7 +143,7 @@ class QmkSettings(BasicEditor): # don't bother creating tabs that would be empty - i.e. at least one qsid in a tab should be supported use_tab = False for field in tab["fields"]: - if field["qsid"] in self.supported_settings: + if field["qsid"] in self.keyboard.supported_settings: use_tab = True break if not use_tab: @@ -164,7 +163,6 @@ class QmkSettings(BasicEditor): self.tabs.append(self.populate_tab(tab, container)) def reload_settings(self): - self.supported_settings = set(self.keyboard.qmk_settings_query()) self.recreate_gui() for tab in self.tabs: @@ -201,4 +199,5 @@ class QmkSettings(BasicEditor): def valid(self): return isinstance(self.device, VialKeyboard) and \ - (self.device.keyboard and self.device.keyboard.vial_protocol >= 4) + (self.device.keyboard and self.device.keyboard.vial_protocol >= 4 + and len(self.device.keyboard.supported_settings)) From adcb9116c0538038a520d0530e7171d035f0f7ce Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Mon, 5 Jul 2021 23:54:43 -0400 Subject: [PATCH 32/56] qmk_settings: take care of setting width when caching --- src/main/python/keyboard_comm.py | 12 +++++++----- src/main/python/main_window.py | 3 ++- src/main/python/qmk_settings.py | 33 ++++++++++++++++++++++---------- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 5186dd8..5420e9f 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -400,12 +400,17 @@ class Keyboard: self.supported_settings.add(qsid) for qsid in self.supported_settings: + from qmk_settings import QmkSettings + width = QmkSettings.setting_width(qsid) + if width is None: + continue + data = self.usb_send(self.dev, struct.pack(" 0: self.tabs_widget.removeTab(0) - with open(self.appctx.get_resource("qmk_settings.json"), "r") as inf: - settings = json.load(inf) - # create new GUI - for tab in settings["tabs"]: + for tab in self.settings_defs["tabs"]: # don't bother creating tabs that would be empty - i.e. at least one qsid in a tab should be supported use_tab = False for field in tab["fields"]: @@ -163,6 +156,7 @@ class QmkSettings(BasicEditor): self.tabs.append(self.populate_tab(tab, container)) def reload_settings(self): + self.keyboard.reload_settings() self.recreate_gui() for tab in self.tabs: @@ -201,3 +195,22 @@ class QmkSettings(BasicEditor): return isinstance(self.device, VialKeyboard) and \ (self.device.keyboard and self.device.keyboard.vial_protocol >= 4 and len(self.device.keyboard.supported_settings)) + + @classmethod + def initialize(cls, appctx): + cls.settings_widths = dict() + with open(appctx.get_resource("qmk_settings.json"), "r") as inf: + cls.settings_defs = json.load(inf) + for tab in cls.settings_defs["tabs"]: + for field in tab["fields"]: + if field["type"] == "boolean": + width = 1 + elif field["type"] == "integer": + width = field["width"] + else: + raise RuntimeError("unsupported field type: {}".format(field)) + cls.settings_widths[field["qsid"]] = width + + @classmethod + def setting_width(cls, qsid): + return cls.settings_widths.get(qsid) From 827f4711da288f51be5f7e0e4c0bec8b9f025aca Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Tue, 6 Jul 2021 00:40:33 -0400 Subject: [PATCH 33/56] qmk_settings: refactor to keep integers internally --- src/main/python/keyboard_comm.py | 14 ++++---- src/main/python/qmk_settings.py | 57 +++++++++++++++++++------------- 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 5420e9f..0d875ac 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -401,16 +401,14 @@ class Keyboard: for qsid in self.supported_settings: from qmk_settings import QmkSettings - width = QmkSettings.setting_width(qsid) - if width is None: + + if not QmkSettings.is_qsid_supported(qsid): continue data = self.usb_send(self.dev, struct.pack(" Date: Tue, 6 Jul 2021 00:46:55 -0400 Subject: [PATCH 34/56] keyboard_comm: save/restore qmk settings across updates --- src/main/python/keyboard_comm.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 0d875ac..5ef5bf1 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -1,5 +1,4 @@ # SPDX-License-Identifier: GPL-2.0-or-later -import base64 import struct import json import lzma @@ -545,6 +544,7 @@ class Keyboard: # TODO: should store/restore serialized keycodes for these two data["tap_dance"] = self.tap_dance_entries data["combo"] = self.combo_entries + data["settings"] = self.settings return json.dumps(data).encode("utf-8") @@ -583,6 +583,13 @@ class Keyboard: if x < self.combo_count: self.combo_set(x, e) + for qsid, value in data.get("settings", dict()).items(): + from qmk_settings import QmkSettings + + qsid = int(qsid) + if QmkSettings.is_qsid_supported(qsid): + self.qmk_settings_set(qsid, value) + def restore_macros(self, macros): if not isinstance(macros, list): return From c7e782d23ce54d2cc31322463dfd04a395d39905 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Tue, 6 Jul 2021 00:52:00 -0400 Subject: [PATCH 35/56] keyboard_comm: store/restore serialized keycodes for combos and tapdance --- src/main/python/keyboard_comm.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 5ef5bf1..0eaf7ff 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -541,9 +541,29 @@ class Keyboard: data["macro"] = self.save_macro() data["vial_protocol"] = self.vial_protocol data["via_protocol"] = self.via_protocol - # TODO: should store/restore serialized keycodes for these two - data["tap_dance"] = self.tap_dance_entries - data["combo"] = self.combo_entries + + tap_dance = [] + for entry in self.tap_dance_entries: + tap_dance.append(( + Keycode.serialize(entry[0]), + Keycode.serialize(entry[1]), + Keycode.serialize(entry[2]), + Keycode.serialize(entry[3]), + entry[4] + )) + data["tap_dance"] = tap_dance + + combo = [] + for entry in self.combo_entries: + combo.append(( + Keycode.serialize(entry[0]), + Keycode.serialize(entry[1]), + Keycode.serialize(entry[2]), + Keycode.serialize(entry[3]), + Keycode.serialize(entry[4]), + )) + data["combo"] = combo + data["settings"] = self.settings return json.dumps(data).encode("utf-8") @@ -578,9 +598,14 @@ class Keyboard: for x, e in enumerate(data.get("tap_dance", [])): if x < self.tap_dance_count: + e = (Keycode.deserialize(e[0]), Keycode.deserialize(e[1]), Keycode.deserialize(e[2]), + Keycode.deserialize(e[3]), e[4]) self.tap_dance_set(x, e) + for x, e in enumerate(data.get("combo", [])): if x < self.combo_count: + e = (Keycode.deserialize(e[0]), Keycode.deserialize(e[1]), Keycode.deserialize(e[2]), + Keycode.deserialize(e[3]), Keycode.deserialize(e[4])) self.combo_set(x, e) for qsid, value in data.get("settings", dict()).items(): From 13f652107627316e89f32075439d84f65d1c0eea Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Tue, 6 Jul 2021 01:05:31 -0400 Subject: [PATCH 36/56] qmk_settings: disable save/undo buttons unless there are changes --- src/main/python/qmk_settings.py | 54 ++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/src/main/python/qmk_settings.py b/src/main/python/qmk_settings.py index f0ac6c0..c970c5a 100644 --- a/src/main/python/qmk_settings.py +++ b/src/main/python/qmk_settings.py @@ -3,6 +3,7 @@ import json from collections import defaultdict from PyQt5 import QtCore +from PyQt5.QtCore import pyqtSignal, QObject from PyQt5.QtWidgets import QVBoxLayout, QCheckBox, QGridLayout, QLabel, QWidget, QSizePolicy, QTabWidget, QSpinBox, \ QHBoxLayout, QPushButton, QMessageBox @@ -11,9 +12,13 @@ from util import tr from vial_device import VialKeyboard -class GenericOption: +class GenericOption(QObject): + + changed = pyqtSignal() def __init__(self, option, container): + super().__init__() + self.row = container.rowCount() self.option = option self.qsid = self.option["qsid"] @@ -29,6 +34,9 @@ class GenericOption: self.lbl.hide() self.lbl.deleteLater() + def on_change(self): + self.changed.emit() + class BooleanOption(GenericOption): @@ -38,6 +46,7 @@ class BooleanOption(GenericOption): self.qsid_bit = self.option["bit"] self.checkbox = QCheckBox() + self.checkbox.stateChanged.connect(self.on_change) self.container.addWidget(self.checkbox, self.row, 1) def reload(self, keyboard): @@ -66,10 +75,14 @@ class IntegerOption(GenericOption): self.spinbox = QSpinBox() self.spinbox.setMinimum(option["min"]) self.spinbox.setMaximum(option["max"]) + self.spinbox.valueChanged.connect(self.on_change) self.container.addWidget(self.spinbox, self.row, 1) def reload(self, keyboard): - self.spinbox.setValue(super().reload(keyboard)) + value = super().reload(keyboard) + self.spinbox.blockSignals(True) + self.spinbox.setValue(value) + self.spinbox.blockSignals(False) def value(self): return self.spinbox.value() @@ -90,12 +103,12 @@ class QmkSettings(BasicEditor): self.addWidget(self.tabs_widget) buttons = QHBoxLayout() buttons.addStretch() - btn_save = QPushButton(tr("QmkSettings", "Save")) - btn_save.clicked.connect(self.save_settings) - buttons.addWidget(btn_save) - btn_undo = QPushButton(tr("QmkSettings", "Undo")) - btn_undo.clicked.connect(self.reload_settings) - buttons.addWidget(btn_undo) + self.btn_save = QPushButton(tr("QmkSettings", "Save")) + self.btn_save.clicked.connect(self.save_settings) + buttons.addWidget(self.btn_save) + self.btn_undo = QPushButton(tr("QmkSettings", "Undo")) + self.btn_undo.clicked.connect(self.reload_settings) + buttons.addWidget(self.btn_undo) btn_reset = QPushButton(tr("QmkSettings", "Reset")) btn_reset.clicked.connect(self.reset_settings) buttons.addWidget(btn_reset) @@ -110,9 +123,13 @@ class QmkSettings(BasicEditor): if field["qsid"] not in self.keyboard.supported_settings: continue if field["type"] == "boolean": - options.append(BooleanOption(field, container)) + opt = BooleanOption(field, container) + options.append(opt) + opt.changed.connect(self.on_change) elif field["type"] == "integer": - options.append(IntegerOption(field, container)) + opt = IntegerOption(field, container) + options.append(opt) + opt.changed.connect(self.on_change) else: raise RuntimeError("unsupported field type: {}".format(field)) return options @@ -162,17 +179,32 @@ class QmkSettings(BasicEditor): for field in tab: field.reload(self.keyboard) + self.on_change() + + def on_change(self): + changed = False + qsid_values = self.prepare_settings() + for qsid, value in qsid_values.items(): + if self.keyboard.settings[qsid] != value: + changed = True + self.btn_save.setEnabled(changed) + self.btn_undo.setEnabled(changed) + def rebuild(self, device): super().rebuild(device) if self.valid(): self.keyboard = device.keyboard self.reload_settings() - def save_settings(self): + def prepare_settings(self): qsid_values = defaultdict(int) for tab in self.tabs: for field in tab: qsid_values[field.qsid] |= field.value() + return qsid_values + + def save_settings(self): + qsid_values = self.prepare_settings() for qsid, value in qsid_values.items(): self.keyboard.qmk_settings_set(qsid, value) From 8c7c748c810bb0f9e3b2fac930c8cca1b8e2dcfe Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Tue, 6 Jul 2021 01:09:50 -0400 Subject: [PATCH 37/56] qmk_settings: indicate which tabs have changed values --- src/main/python/qmk_settings.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/main/python/qmk_settings.py b/src/main/python/qmk_settings.py index c970c5a..a6c0b5f 100644 --- a/src/main/python/qmk_settings.py +++ b/src/main/python/qmk_settings.py @@ -184,9 +184,19 @@ class QmkSettings(BasicEditor): def on_change(self): changed = False qsid_values = self.prepare_settings() - for qsid, value in qsid_values.items(): - if self.keyboard.settings[qsid] != value: - changed = True + + for x, tab in enumerate(self.tabs): + tab_changed = False + for opt in tab: + if qsid_values[opt.qsid] != self.keyboard.settings[opt.qsid]: + changed = True + tab_changed = True + title = self.tabs_widget.tabText(x).rstrip("*") + if tab_changed: + self.tabs_widget.setTabText(x, title + "*") + else: + self.tabs_widget.setTabText(x, title) + self.btn_save.setEnabled(changed) self.btn_undo.setEnabled(changed) @@ -207,6 +217,7 @@ class QmkSettings(BasicEditor): qsid_values = self.prepare_settings() for qsid, value in qsid_values.items(): self.keyboard.qmk_settings_set(qsid, value) + self.on_change() def reset_settings(self): if QMessageBox.question(self.widget(), "", From 3d63e8bc1668acc744a7de06c4fde8d5cf7a70a9 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Thu, 8 Jul 2021 18:24:24 -0400 Subject: [PATCH 38/56] qmk_settings: add tap/hold settings --- src/main/resources/base/qmk_settings.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/resources/base/qmk_settings.json b/src/main/resources/base/qmk_settings.json index 7b03e14..545b297 100644 --- a/src/main/resources/base/qmk_settings.json +++ b/src/main/resources/base/qmk_settings.json @@ -9,6 +9,16 @@ { "type": "boolean", "title": "Always send Escape if Shift is pressed", "qsid": 1, "bit": 3 } ] }, + { + "name": "Tap-Hold", + "fields": [ + { "type": "integer", "title": "Tapping Term", "qsid": 7, "min": 0, "max": 10000, "width": 2 }, + { "type": "boolean", "title": "Permissive Hold", "qsid": 8, "bit": 0 }, + { "type": "boolean", "title": "Ignore Mod Tap Interrupt", "qsid": 8, "bit": 1 }, + { "type": "boolean", "title": "Tapping Force Hold", "qsid": 8, "bit": 2 }, + { "type": "boolean", "title": "Retro Tapping", "qsid": 8, "bit": 3 } + ] + }, { "name": "Auto Shift", "fields": [ From baf67139c6da81c1a9400afe7c42e91d865f4c7d Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Thu, 8 Jul 2021 18:24:35 -0400 Subject: [PATCH 39/56] macro_recorder: fix change tracking when reverting --- src/main/python/macro_recorder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/python/macro_recorder.py b/src/main/python/macro_recorder.py index 83c8e5b..fd0d3c0 100644 --- a/src/main/python/macro_recorder.py +++ b/src/main/python/macro_recorder.py @@ -181,6 +181,7 @@ class MacroRecorder(BasicEditor): def on_revert(self): self.keyboard.reload_macros() self.deserialize(self.keyboard.macro) + self.on_change() def on_save(self): Unlocker.unlock(self.device.keyboard) From 9b9301163718f6755c0cccab420825c52f63261f Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Thu, 8 Jul 2021 19:27:40 -0400 Subject: [PATCH 40/56] vialrgb: initial --- src/main/python/keyboard_comm.py | 25 +++++++++++++- src/main/python/rgb_configurator.py | 51 +++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 0eaf7ff..d1ee87d 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -41,6 +41,10 @@ QMK_RGBLIGHT_EFFECT = 0x81 QMK_RGBLIGHT_EFFECT_SPEED = 0x82 QMK_RGBLIGHT_COLOR = 0x83 +VIALRGB_GET_MODE = 0x40 + +VIALRGB_SET_MODE = 0x40 + CMD_VIAL_GET_KEYBOARD_ID = 0x00 CMD_VIAL_GET_SIZE = 0x01 CMD_VIAL_GET_DEFINITION = 0x02 @@ -194,10 +198,13 @@ class Keyboard: self.vibl = False self.custom_keycodes = None - self.lighting_qmk_rgblight = self.lighting_qmk_backlight = False + self.lighting_qmk_rgblight = self.lighting_qmk_backlight = self.lighting_vialrgb = False self.underglow_brightness = self.underglow_effect = self.underglow_effect_speed = -1 self.backlight_brightness = self.backlight_effect = -1 self.underglow_color = (0, 0) + # vialrgb + self.rgb_mode = self.rgb_speed = -1 + self.rgb_hsv = (0, 0, 0) self.via_protocol = self.vial_protocol = self.keyboard_id = -1 @@ -364,6 +371,7 @@ class Keyboard: if "lighting" in self.definition: self.lighting_qmk_rgblight = self.definition["lighting"] in ["qmk_rgblight", "qmk_backlight_rgblight"] self.lighting_qmk_backlight = self.definition["lighting"] in ["qmk_backlight", "qmk_backlight_rgblight"] + self.lighting_vialrgb = self.definition["lighting"] == "vialrgb" if self.lighting_qmk_rgblight: self.underglow_brightness = self.usb_send( @@ -383,6 +391,12 @@ class Keyboard: self.backlight_effect = self.usb_send( self.dev, struct.pack(">BB", CMD_VIA_LIGHTING_GET_VALUE, QMK_BACKLIGHT_EFFECT), retries=20)[2] + if self.lighting_vialrgb: + data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_LIGHTING_GET_VALUE, VIALRGB_GET_MODE))[2:] + self.rgb_mode = data[0] + self.rgb_speed = data[1] + self.rgb_hsv = (data[2], data[3], data[4]) + def reload_settings(self): self.settings = dict() self.supported_settings = set() @@ -782,6 +796,15 @@ class Keyboard: self.usb_send(self.dev, struct.pack("BBBB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_DYNAMIC_ENTRY_OP, DYNAMIC_VIAL_COMBO_SET, idx) + serialized, retries=20) + def _vialrgb_set_mode(self): + self.usb_send(self.dev, struct.pack("BBBBBBB", CMD_VIA_LIGHTING_SET_VALUE, VIALRGB_SET_MODE, + self.rgb_mode, self.rgb_speed, + self.rgb_hsv[0], self.rgb_hsv[1], self.rgb_hsv[2])) + + def set_vialrgb_brightness(self, value): + self.rgb_hsv = (self.rgb_hsv[0], self.rgb_hsv[1], value) + self._vialrgb_set_mode() + class DummyKeyboard(Keyboard): diff --git a/src/main/python/rgb_configurator.py b/src/main/python/rgb_configurator.py index 33475b5..a747e12 100644 --- a/src/main/python/rgb_configurator.py +++ b/src/main/python/rgb_configurator.py @@ -240,6 +240,50 @@ class QmkBacklightHandler(BasicHandler): self.device.keyboard.set_qmk_backlight_effect(int(checked)) +class VialRGBHandler(BasicHandler): + + def __init__(self, container): + super().__init__(container) + + row = container.rowCount() + + self.lbl_rgb_brightness = QLabel(tr("RGBConfigurator", "RGB Brightness")) + container.addWidget(self.lbl_rgb_brightness, row + 1, 0) + self.rgb_brightness = QSlider(QtCore.Qt.Horizontal) + self.rgb_brightness.setMinimum(0) + self.rgb_brightness.setMaximum(255) + self.rgb_brightness.valueChanged.connect(self.on_rgb_brightness_changed) + container.addWidget(self.rgb_brightness, row + 1, 1) + + self.widgets = [self.lbl_rgb_brightness, self.rgb_brightness] + + def on_rgb_brightness_changed(self, value): + self.device.keyboard.set_vialrgb_brightness(value) + + def show(self): + for w in self.widgets: + w.show() + + def hide(self): + for w in self.widgets: + w.hide() + + def block_signals(self): + for w in self.widgets: + w.blockSignals(True) + + def unblock_signals(self): + for w in self.widgets: + w.blockSignals(False) + + def update_from_keyboard(self): + print("hsv", self.device.keyboard.rgb_hsv) + self.rgb_brightness.setValue(self.device.keyboard.rgb_hsv[2]) + + def valid(self): + return isinstance(self.device, VialKeyboard) and self.device.keyboard.lighting_vialrgb + + class RGBConfigurator(BasicEditor): def __init__(self): @@ -258,7 +302,9 @@ class RGBConfigurator(BasicEditor): self.handler_backlight.update.connect(self.update_from_keyboard) self.handler_rgblight = QmkRgblightHandler(self.container) self.handler_rgblight.update.connect(self.update_from_keyboard) - self.handlers = [self.handler_backlight, self.handler_rgblight] + self.handler_vialrgb = VialRGBHandler(self.container) + self.handler_vialrgb.update.connect(self.update_from_keyboard) + self.handlers = [self.handler_backlight, self.handler_rgblight, self.handler_vialrgb] self.addStretch() buttons = QHBoxLayout() @@ -273,7 +319,8 @@ class RGBConfigurator(BasicEditor): def valid(self): return isinstance(self.device, VialKeyboard) and \ - (self.device.keyboard.lighting_qmk_rgblight or self.device.keyboard.lighting_qmk_backlight) + (self.device.keyboard.lighting_qmk_rgblight or self.device.keyboard.lighting_qmk_backlight + or self.device.keyboard.lighting_vialrgb) def block_signals(self): for h in self.handlers: From 6efebe82bc64f24b9ab47b5c238647f5ea614c68 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Thu, 8 Jul 2021 19:35:27 -0400 Subject: [PATCH 41/56] rgb_configurator: reduce code duplication --- src/main/python/rgb_configurator.py | 80 ++++++----------------------- 1 file changed, 15 insertions(+), 65 deletions(-) diff --git a/src/main/python/rgb_configurator.py b/src/main/python/rgb_configurator.py index a747e12..b963622 100644 --- a/src/main/python/rgb_configurator.py +++ b/src/main/python/rgb_configurator.py @@ -66,26 +66,32 @@ class BasicHandler(QObject): def __init__(self, container): super().__init__() - self.device = None + self.device = self.keyboard = None + self.widgets = [] def set_device(self, device): self.device = device if self.valid(): + self.keyboard = self.device self.show() else: self.hide() def show(self): - raise NotImplementedError + for w in self.widgets: + w.show() def hide(self): - raise NotImplementedError + for w in self.widgets: + w.hide() def block_signals(self): - raise NotImplementedError + for w in self.widgets: + w.blockSignals(True) def unblock_signals(self): - raise NotImplementedError + for w in self.widgets: + w.blockSignals(False) def update_from_keyboard(self): raise NotImplementedError @@ -124,31 +130,8 @@ class QmkRgblightHandler(BasicHandler): self.underglow_effect.currentIndexChanged.connect(self.on_underglow_effect_changed) - def show(self): - self.lbl_underglow_effect.show() - self.underglow_effect.show() - self.lbl_underglow_brightness.show() - self.underglow_brightness.show() - self.lbl_underglow_color.show() - self.underglow_color.show() - - def hide(self): - self.lbl_underglow_effect.hide() - self.underglow_effect.hide() - self.lbl_underglow_brightness.hide() - self.underglow_brightness.hide() - self.lbl_underglow_color.hide() - self.underglow_color.hide() - - def block_signals(self): - self.underglow_brightness.blockSignals(True) - self.underglow_effect.blockSignals(True) - self.underglow_color.blockSignals(True) - - def unblock_signals(self): - self.underglow_brightness.blockSignals(False) - self.underglow_effect.blockSignals(False) - self.underglow_color.blockSignals(False) + self.widgets = [self.lbl_underglow_effect, self.underglow_effect, self.lbl_underglow_brightness, + self.underglow_brightness, self.lbl_underglow_color, self.underglow_color] def update_from_keyboard(self): self.underglow_brightness.setValue(self.device.keyboard.underglow_brightness) @@ -206,25 +189,8 @@ class QmkBacklightHandler(BasicHandler): self.backlight_breathing.stateChanged.connect(self.on_backlight_breathing_changed) container.addWidget(self.backlight_breathing, row + 1, 1) - def show(self): - self.lbl_backlight_brightness.show() - self.backlight_brightness.show() - self.lbl_backlight_breathing.show() - self.backlight_breathing.show() - - def hide(self): - self.lbl_backlight_brightness.hide() - self.backlight_brightness.hide() - self.lbl_backlight_breathing.hide() - self.backlight_breathing.hide() - - def block_signals(self): - self.backlight_brightness.blockSignals(True) - self.backlight_breathing.blockSignals(True) - - def unblock_signals(self): - self.backlight_brightness.blockSignals(False) - self.backlight_breathing.blockSignals(False) + self.widgets = [self.lbl_backlight_brightness, self.backlight_brightness, self.lbl_backlight_breathing, + self.backlight_breathing] def update_from_keyboard(self): self.backlight_brightness.setValue(self.device.keyboard.backlight_brightness) @@ -260,22 +226,6 @@ class VialRGBHandler(BasicHandler): def on_rgb_brightness_changed(self, value): self.device.keyboard.set_vialrgb_brightness(value) - def show(self): - for w in self.widgets: - w.show() - - def hide(self): - for w in self.widgets: - w.hide() - - def block_signals(self): - for w in self.widgets: - w.blockSignals(True) - - def unblock_signals(self): - for w in self.widgets: - w.blockSignals(False) - def update_from_keyboard(self): print("hsv", self.device.keyboard.rgb_hsv) self.rgb_brightness.setValue(self.device.keyboard.rgb_hsv[2]) From 5cf8a67ba71bf19959e3cb8a929a20367912a8a5 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Thu, 8 Jul 2021 20:12:28 -0400 Subject: [PATCH 42/56] rgb_configurator/vialrgb: set color --- src/main/python/keyboard_comm.py | 8 ++++++ src/main/python/rgb_configurator.py | 44 ++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index d1ee87d..6684a03 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -805,6 +805,14 @@ class Keyboard: self.rgb_hsv = (self.rgb_hsv[0], self.rgb_hsv[1], value) self._vialrgb_set_mode() + def set_vialrgb_mode(self, value): + self.rgb_mode = value + self._vialrgb_set_mode() + + def set_vialrgb_color(self, h, s, v): + self.rgb_hsv = (h, s, v) + self._vialrgb_set_mode() + class DummyKeyboard(Keyboard): diff --git a/src/main/python/rgb_configurator.py b/src/main/python/rgb_configurator.py index b963622..33847b2 100644 --- a/src/main/python/rgb_configurator.py +++ b/src/main/python/rgb_configurator.py @@ -72,7 +72,7 @@ class BasicHandler(QObject): def set_device(self, device): self.device = device if self.valid(): - self.keyboard = self.device + self.keyboard = self.device.keyboard self.show() else: self.hide() @@ -213,6 +213,22 @@ class VialRGBHandler(BasicHandler): row = container.rowCount() + self.lbl_rgb_effect = QLabel(tr("RGBConfigurator", "RGB Effect")) + container.addWidget(self.lbl_rgb_effect, row, 0) + self.rgb_effect = QComboBox() + self.rgb_effect.addItem("0") + self.rgb_effect.addItem("1") + self.rgb_effect.addItem("2") + self.rgb_effect.addItem("3") + self.rgb_effect.currentIndexChanged.connect(self.on_rgb_effect_changed) + container.addWidget(self.rgb_effect, row, 1) + + self.lbl_rgb_color = QLabel(tr("RGBConfigurator", "RGB Color")) + container.addWidget(self.lbl_rgb_color, row + 2, 0) + self.rgb_color = ClickableLabel(" ") + self.rgb_color.clicked.connect(self.on_rgb_color) + container.addWidget(self.rgb_color, row + 2, 1) + self.lbl_rgb_brightness = QLabel(tr("RGBConfigurator", "RGB Brightness")) container.addWidget(self.lbl_rgb_brightness, row + 1, 0) self.rgb_brightness = QSlider(QtCore.Qt.Horizontal) @@ -221,14 +237,34 @@ class VialRGBHandler(BasicHandler): self.rgb_brightness.valueChanged.connect(self.on_rgb_brightness_changed) container.addWidget(self.rgb_brightness, row + 1, 1) - self.widgets = [self.lbl_rgb_brightness, self.rgb_brightness] + self.widgets = [self.lbl_rgb_effect, self.rgb_effect, self.lbl_rgb_brightness, self.rgb_brightness, + self.lbl_rgb_color, self.rgb_color] def on_rgb_brightness_changed(self, value): - self.device.keyboard.set_vialrgb_brightness(value) + self.keyboard.set_vialrgb_brightness(value) + + def on_rgb_effect_changed(self, value): + self.keyboard.set_vialrgb_mode(value) + + def on_rgb_color(self): + color = QColorDialog.getColor(self.current_color()) + if not color.isValid(): + return + self.rgb_color.setStyleSheet("QWidget { background-color: %s}" % color.name()) + h, s, v, a = color.getHsvF() + if h < 0: + h = 0 + self.device.keyboard.set_vialrgb_color(int(255 * h), int(255 * s), int(255 * v)) + self.update.emit() + + def current_color(self): + return QColor.fromHsvF(self.device.keyboard.rgb_hsv[0] / 255.0, + self.device.keyboard.rgb_hsv[1] / 255.0, + 1.0) def update_from_keyboard(self): - print("hsv", self.device.keyboard.rgb_hsv) self.rgb_brightness.setValue(self.device.keyboard.rgb_hsv[2]) + self.rgb_color.setStyleSheet("QWidget { background-color: %s}" % self.current_color().name()) def valid(self): return isinstance(self.device, VialKeyboard) and self.device.keyboard.lighting_vialrgb From 0f46237047d09c5d6ba44949f1171728b207ec06 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Thu, 8 Jul 2021 20:20:11 -0400 Subject: [PATCH 43/56] vialrgb: retrieve protocol version, max brightness --- src/main/python/keyboard_comm.py | 22 +++++++++++++++++----- src/main/python/rgb_configurator.py | 3 ++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 6684a03..ec03495 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -41,9 +41,10 @@ QMK_RGBLIGHT_EFFECT = 0x81 QMK_RGBLIGHT_EFFECT_SPEED = 0x82 QMK_RGBLIGHT_COLOR = 0x83 -VIALRGB_GET_MODE = 0x40 +VIALRGB_GET_INFO = 0x40 +VIALRGB_GET_MODE = 0x41 -VIALRGB_SET_MODE = 0x40 +VIALRGB_SET_MODE = 0x41 CMD_VIAL_GET_KEYBOARD_ID = 0x00 CMD_VIAL_GET_SIZE = 0x01 @@ -199,11 +200,14 @@ class Keyboard: self.custom_keycodes = None self.lighting_qmk_rgblight = self.lighting_qmk_backlight = self.lighting_vialrgb = False + + # underglow self.underglow_brightness = self.underglow_effect = self.underglow_effect_speed = -1 - self.backlight_brightness = self.backlight_effect = -1 self.underglow_color = (0, 0) + # backlight + self.backlight_brightness = self.backlight_effect = -1 # vialrgb - self.rgb_mode = self.rgb_speed = -1 + self.rgb_mode = self.rgb_speed = self.rgb_version = self.rgb_maximum_brightness = -1 self.rgb_hsv = (0, 0, 0) self.via_protocol = self.vial_protocol = self.keyboard_id = -1 @@ -392,7 +396,15 @@ class Keyboard: self.dev, struct.pack(">BB", CMD_VIA_LIGHTING_GET_VALUE, QMK_BACKLIGHT_EFFECT), retries=20)[2] if self.lighting_vialrgb: - data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_LIGHTING_GET_VALUE, VIALRGB_GET_MODE))[2:] + data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_LIGHTING_GET_VALUE, VIALRGB_GET_INFO), + retries=20)[2:] + self.rgb_version = data[0] | (data[1] << 8) + if self.rgb_version != 1: + raise RuntimeError("Unsupported VialRGB protocol, update your Vial version to latest") + self.rgb_maximum_brightness = data[2] + + data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_LIGHTING_GET_VALUE, VIALRGB_GET_MODE), + retries=20)[2:] self.rgb_mode = data[0] self.rgb_speed = data[1] self.rgb_hsv = (data[2], data[3], data[4]) diff --git a/src/main/python/rgb_configurator.py b/src/main/python/rgb_configurator.py index 33847b2..6ed110d 100644 --- a/src/main/python/rgb_configurator.py +++ b/src/main/python/rgb_configurator.py @@ -263,7 +263,8 @@ class VialRGBHandler(BasicHandler): 1.0) def update_from_keyboard(self): - self.rgb_brightness.setValue(self.device.keyboard.rgb_hsv[2]) + self.rgb_brightness.setMaximum(self.keyboard.rgb_maximum_brightness) + self.rgb_brightness.setValue(self.keyboard.rgb_hsv[2]) self.rgb_color.setStyleSheet("QWidget { background-color: %s}" % self.current_color().name()) def valid(self): From da984c578f5a79476ba1390e9580e517d65f6a33 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Thu, 8 Jul 2021 20:24:46 -0400 Subject: [PATCH 44/56] vialrgb: log protocol version on failure --- src/main/python/keyboard_comm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index ec03495..d1a421e 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -400,7 +400,8 @@ class Keyboard: retries=20)[2:] self.rgb_version = data[0] | (data[1] << 8) if self.rgb_version != 1: - raise RuntimeError("Unsupported VialRGB protocol, update your Vial version to latest") + raise RuntimeError("Unsupported VialRGB protocol ({}), update your Vial version to latest" + .format(self.rgb_version)) self.rgb_maximum_brightness = data[2] data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_LIGHTING_GET_VALUE, VIALRGB_GET_MODE), From ceae2be4a47202d1b9fde75138440df24ad84a7e Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Thu, 8 Jul 2021 20:26:53 -0400 Subject: [PATCH 45/56] vialrgb: don't touch v when changing color --- src/main/python/rgb_configurator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/python/rgb_configurator.py b/src/main/python/rgb_configurator.py index 6ed110d..572f4c3 100644 --- a/src/main/python/rgb_configurator.py +++ b/src/main/python/rgb_configurator.py @@ -254,12 +254,12 @@ class VialRGBHandler(BasicHandler): h, s, v, a = color.getHsvF() if h < 0: h = 0 - self.device.keyboard.set_vialrgb_color(int(255 * h), int(255 * s), int(255 * v)) + self.keyboard.set_vialrgb_color(int(255 * h), int(255 * s), self.keyboard.rgb_hsv[2]) self.update.emit() def current_color(self): - return QColor.fromHsvF(self.device.keyboard.rgb_hsv[0] / 255.0, - self.device.keyboard.rgb_hsv[1] / 255.0, + return QColor.fromHsvF(self.keyboard.rgb_hsv[0] / 255.0, + self.keyboard.rgb_hsv[1] / 255.0, 1.0) def update_from_keyboard(self): From 30d4731cfd4c61ef7d2f1e512bab376a8a1e5737 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Thu, 8 Jul 2021 22:39:17 -0400 Subject: [PATCH 46/56] vialrgb: set effects using vialrgb id --- src/main/python/keyboard_comm.py | 38 ++++++++++++++++++++++------- src/main/python/rgb_configurator.py | 36 +++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index d1a421e..921c562 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -43,6 +43,7 @@ QMK_RGBLIGHT_COLOR = 0x83 VIALRGB_GET_INFO = 0x40 VIALRGB_GET_MODE = 0x41 +VIALRGB_GET_SUPPORTED = 0x42 VIALRGB_SET_MODE = 0x41 @@ -209,6 +210,7 @@ class Keyboard: # vialrgb self.rgb_mode = self.rgb_speed = self.rgb_version = self.rgb_maximum_brightness = -1 self.rgb_hsv = (0, 0, 0) + self.rgb_supported_effects = set() self.via_protocol = self.vial_protocol = self.keyboard_id = -1 @@ -224,6 +226,7 @@ class Keyboard: self.reload_layers() self.reload_keymap() self.reload_macros() + self.reload_persistent_rgb() self.reload_rgb() self.reload_settings() self.reload_dynamic() @@ -371,12 +374,37 @@ class Keyboard: macros = self.macro.split(b"\x00") + [b""] * self.macro_count self.macro = b"\x00".join(macros[:self.macro_count]) + b"\x00" - def reload_rgb(self): + def reload_persistent_rgb(self): + """ + Reload RGB properties which are slow, and do not change while keyboard is plugged in + e.g. VialRGB supported effects list + """ + if "lighting" in self.definition: self.lighting_qmk_rgblight = self.definition["lighting"] in ["qmk_rgblight", "qmk_backlight_rgblight"] self.lighting_qmk_backlight = self.definition["lighting"] in ["qmk_backlight", "qmk_backlight_rgblight"] self.lighting_vialrgb = self.definition["lighting"] == "vialrgb" + if self.lighting_vialrgb: + data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_LIGHTING_GET_VALUE, VIALRGB_GET_INFO), + retries=20)[2:] + self.rgb_version = data[0] | (data[1] << 8) + if self.rgb_version != 1: + raise RuntimeError("Unsupported VialRGB protocol ({}), update your Vial version to latest" + .format(self.rgb_version)) + self.rgb_maximum_brightness = data[2] + + self.rgb_supported_effects = {0} + max_effect = 0 + while max_effect < 0xFFFF: + data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_LIGHTING_GET_VALUE, VIALRGB_GET_SUPPORTED))[2:] + for x in range(0, len(data), 2): + value = int.from_bytes(data[x:x+2], byteorder="little") + if value != 0xFFFF: + self.rgb_supported_effects.add(value) + max_effect = max(max_effect, value) + + def reload_rgb(self): if self.lighting_qmk_rgblight: self.underglow_brightness = self.usb_send( self.dev, struct.pack(">BB", CMD_VIA_LIGHTING_GET_VALUE, QMK_RGBLIGHT_BRIGHTNESS), retries=20)[2] @@ -396,14 +424,6 @@ class Keyboard: self.dev, struct.pack(">BB", CMD_VIA_LIGHTING_GET_VALUE, QMK_BACKLIGHT_EFFECT), retries=20)[2] if self.lighting_vialrgb: - data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_LIGHTING_GET_VALUE, VIALRGB_GET_INFO), - retries=20)[2:] - self.rgb_version = data[0] | (data[1] << 8) - if self.rgb_version != 1: - raise RuntimeError("Unsupported VialRGB protocol ({}), update your Vial version to latest" - .format(self.rgb_version)) - self.rgb_maximum_brightness = data[2] - data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_LIGHTING_GET_VALUE, VIALRGB_GET_MODE), retries=20)[2:] self.rgb_mode = data[0] diff --git a/src/main/python/rgb_configurator.py b/src/main/python/rgb_configurator.py index 572f4c3..3be7c77 100644 --- a/src/main/python/rgb_configurator.py +++ b/src/main/python/rgb_configurator.py @@ -60,6 +60,21 @@ QMK_RGBLIGHT_EFFECTS = [ ] +class VialRGBEffect: + + def __init__(self, idx, name): + self.idx = idx + self.name = name + + +VIALRGB_EFFECTS = [ + VialRGBEffect(0, "Disable"), + VialRGBEffect(1, "Direct Control"), + VialRGBEffect(2, "Solid Color"), + VialRGBEffect(3, "Alphas and Mods"), +] + + class BasicHandler(QObject): update = pyqtSignal() @@ -240,11 +255,13 @@ class VialRGBHandler(BasicHandler): self.widgets = [self.lbl_rgb_effect, self.rgb_effect, self.lbl_rgb_brightness, self.rgb_brightness, self.lbl_rgb_color, self.rgb_color] + self.effects = [] + def on_rgb_brightness_changed(self, value): self.keyboard.set_vialrgb_brightness(value) - def on_rgb_effect_changed(self, value): - self.keyboard.set_vialrgb_mode(value) + def on_rgb_effect_changed(self, index): + self.keyboard.set_vialrgb_mode(self.effects[index].idx) def on_rgb_color(self): color = QColorDialog.getColor(self.current_color()) @@ -262,7 +279,22 @@ class VialRGBHandler(BasicHandler): self.keyboard.rgb_hsv[1] / 255.0, 1.0) + def rebuild_effects(self): + self.effects = [] + for effect in VIALRGB_EFFECTS: + if effect.idx in self.keyboard.rgb_supported_effects: + self.effects.append(effect) + + self.rgb_effect.clear() + for effect in self.effects: + self.rgb_effect.addItem(effect.name) + def update_from_keyboard(self): + self.rebuild_effects() + for x, effect in enumerate(self.effects): + if effect.idx == self.keyboard.rgb_mode: + self.rgb_effect.setCurrentIndex(x) + break self.rgb_brightness.setMaximum(self.keyboard.rgb_maximum_brightness) self.rgb_brightness.setValue(self.keyboard.rgb_hsv[2]) self.rgb_color.setStyleSheet("QWidget { background-color: %s}" % self.current_color().name()) From deb0882630567aae5bdb2ccc78f8dbe418cdb9cf Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Thu, 8 Jul 2021 23:02:47 -0400 Subject: [PATCH 47/56] qmk_settings expose TAP_CODE_DELAY and TAP_HOLD_CAPS_DELAY --- src/main/resources/base/qmk_settings.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/resources/base/qmk_settings.json b/src/main/resources/base/qmk_settings.json index 545b297..b247093 100644 --- a/src/main/resources/base/qmk_settings.json +++ b/src/main/resources/base/qmk_settings.json @@ -16,7 +16,9 @@ { "type": "boolean", "title": "Permissive Hold", "qsid": 8, "bit": 0 }, { "type": "boolean", "title": "Ignore Mod Tap Interrupt", "qsid": 8, "bit": 1 }, { "type": "boolean", "title": "Tapping Force Hold", "qsid": 8, "bit": 2 }, - { "type": "boolean", "title": "Retro Tapping", "qsid": 8, "bit": 3 } + { "type": "boolean", "title": "Retro Tapping", "qsid": 8, "bit": 3 }, + { "type": "integer", "title": "Tap Code Delay", "qsid": 18, "min": 0, "max": 1000, "width": 2 }, + { "type": "integer", "title": "Tap Hold Caps Delay", "qsid": 19, "min": 0, "max": 1000, "width": 2 } ] }, { From b2dd82d3399c1982dad9bb6ae0584ce90bdb22c4 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Fri, 9 Jul 2021 00:01:11 -0400 Subject: [PATCH 48/56] layout_editor: add keyboard preview image --- src/main/python/layout_editor.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/main/python/layout_editor.py b/src/main/python/layout_editor.py index 9473dea..bad0508 100644 --- a/src/main/python/layout_editor.py +++ b/src/main/python/layout_editor.py @@ -4,6 +4,7 @@ from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QLabel, QCheckBox, QComboBox, QGridLayout, QWidget, QSizePolicy from basic_editor import BasicEditor +from keyboard_widget import KeyboardWidget from vial_device import VialKeyboard @@ -87,18 +88,32 @@ class LayoutEditor(BasicEditor): def __init__(self, parent=None): super().__init__(parent) - self.device = None + self.device = self.keyboard = None self.choices = [] self.widgets = [] + self.addStretch() + self.keyboard_preview = KeyboardWidget(self) + self.keyboard_preview.set_enabled(False) + self.keyboard_preview.set_scale(0.7) + self.addWidget(self.keyboard_preview) + self.setAlignment(self.keyboard_preview, QtCore.Qt.AlignHCenter) + w = QWidget() w.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) self.container = QGridLayout() w.setLayout(self.container) self.addWidget(w) self.setAlignment(w, QtCore.Qt.AlignHCenter) + self.addStretch() + + def update_preview(self): + self.keyboard_preview.set_keys(self.keyboard.keys, self.keyboard.encoders) + self.keyboard_preview.update_layout() + self.keyboard_preview.update() + self.keyboard_preview.updateGeometry() def rebuild(self, device): super().rebuild(device) @@ -106,6 +121,8 @@ class LayoutEditor(BasicEditor): if not self.valid(): return + self.keyboard = device.keyboard + self.blockSignals(True) for choice in self.choices: @@ -122,6 +139,7 @@ class LayoutEditor(BasicEditor): self.unpack(self.device.keyboard.layout_options) self.blockSignals(False) + self.update_preview() def valid(self): return isinstance(self.device, VialKeyboard) and self.device.keyboard.layout_labels @@ -149,3 +167,4 @@ class LayoutEditor(BasicEditor): def on_changed(self): self.changed.emit() + self.update_preview() From 6bfd4bc3ec11adf6fd892964a46d4d8b06f28400 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Fri, 9 Jul 2021 17:54:15 -0400 Subject: [PATCH 49/56] vialrgb: add all supported effects --- src/main/python/keyboard_comm.py | 3 ++- src/main/python/rgb_configurator.py | 41 ++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 921c562..f370d98 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -397,7 +397,8 @@ class Keyboard: self.rgb_supported_effects = {0} max_effect = 0 while max_effect < 0xFFFF: - data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_LIGHTING_GET_VALUE, VIALRGB_GET_SUPPORTED))[2:] + data = self.usb_send(self.dev, struct.pack(" Date: Fri, 9 Jul 2021 18:29:49 -0400 Subject: [PATCH 50/56] vialrgb: switch to 16-bit mode --- src/main/python/keyboard_comm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index f370d98..9f0be9c 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -427,9 +427,9 @@ class Keyboard: if self.lighting_vialrgb: data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_LIGHTING_GET_VALUE, VIALRGB_GET_MODE), retries=20)[2:] - self.rgb_mode = data[0] - self.rgb_speed = data[1] - self.rgb_hsv = (data[2], data[3], data[4]) + self.rgb_mode = int.from_bytes(data[0:2], byteorder="little") + self.rgb_speed = data[2] + self.rgb_hsv = (data[3], data[4], data[5]) def reload_settings(self): self.settings = dict() @@ -831,7 +831,7 @@ class Keyboard: DYNAMIC_VIAL_COMBO_SET, idx) + serialized, retries=20) def _vialrgb_set_mode(self): - self.usb_send(self.dev, struct.pack("BBBBBBB", CMD_VIA_LIGHTING_SET_VALUE, VIALRGB_SET_MODE, + self.usb_send(self.dev, struct.pack("BBHBBBB", CMD_VIA_LIGHTING_SET_VALUE, VIALRGB_SET_MODE, self.rgb_mode, self.rgb_speed, self.rgb_hsv[0], self.rgb_hsv[1], self.rgb_hsv[2])) From 0759f76fd5b1ee9745299355604e0cc5ff1be54d Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Fri, 9 Jul 2021 18:38:08 -0400 Subject: [PATCH 51/56] vialrgb: add rgb speed --- src/main/python/keyboard_comm.py | 4 ++++ src/main/python/rgb_configurator.py | 22 +++++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 9f0be9c..2ddc704 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -839,6 +839,10 @@ class Keyboard: self.rgb_hsv = (self.rgb_hsv[0], self.rgb_hsv[1], value) self._vialrgb_set_mode() + def set_vialrgb_speed(self, value): + self.rgb_speed = value + self._vialrgb_set_mode() + def set_vialrgb_mode(self, value): self.rgb_mode = value self._vialrgb_set_mode() diff --git a/src/main/python/rgb_configurator.py b/src/main/python/rgb_configurator.py index d3a8de4..d50cf53 100644 --- a/src/main/python/rgb_configurator.py +++ b/src/main/python/rgb_configurator.py @@ -278,27 +278,38 @@ class VialRGBHandler(BasicHandler): container.addWidget(self.rgb_effect, row, 1) self.lbl_rgb_color = QLabel(tr("RGBConfigurator", "RGB Color")) - container.addWidget(self.lbl_rgb_color, row + 2, 0) + container.addWidget(self.lbl_rgb_color, row + 1, 0) self.rgb_color = ClickableLabel(" ") self.rgb_color.clicked.connect(self.on_rgb_color) - container.addWidget(self.rgb_color, row + 2, 1) + container.addWidget(self.rgb_color, row + 1, 1) self.lbl_rgb_brightness = QLabel(tr("RGBConfigurator", "RGB Brightness")) - container.addWidget(self.lbl_rgb_brightness, row + 1, 0) + container.addWidget(self.lbl_rgb_brightness, row + 2, 0) self.rgb_brightness = QSlider(QtCore.Qt.Horizontal) self.rgb_brightness.setMinimum(0) self.rgb_brightness.setMaximum(255) self.rgb_brightness.valueChanged.connect(self.on_rgb_brightness_changed) - container.addWidget(self.rgb_brightness, row + 1, 1) + container.addWidget(self.rgb_brightness, row + 2, 1) + + self.lbl_rgb_speed = QLabel(tr("RGBConfigurator", "RGB Speed")) + container.addWidget(self.lbl_rgb_speed, row + 3, 0) + self.rgb_speed = QSlider(QtCore.Qt.Horizontal) + self.rgb_speed.setMinimum(0) + self.rgb_speed.setMaximum(255) + self.rgb_speed.valueChanged.connect(self.on_rgb_speed_changed) + container.addWidget(self.rgb_speed, row + 3, 1) self.widgets = [self.lbl_rgb_effect, self.rgb_effect, self.lbl_rgb_brightness, self.rgb_brightness, - self.lbl_rgb_color, self.rgb_color] + self.lbl_rgb_color, self.rgb_color, self.lbl_rgb_speed, self.rgb_speed] self.effects = [] def on_rgb_brightness_changed(self, value): self.keyboard.set_vialrgb_brightness(value) + def on_rgb_speed_changed(self, value): + self.keyboard.set_vialrgb_speed(value) + def on_rgb_effect_changed(self, index): self.keyboard.set_vialrgb_mode(self.effects[index].idx) @@ -336,6 +347,7 @@ class VialRGBHandler(BasicHandler): break self.rgb_brightness.setMaximum(self.keyboard.rgb_maximum_brightness) self.rgb_brightness.setValue(self.keyboard.rgb_hsv[2]) + self.rgb_speed.setValue(self.keyboard.rgb_speed) self.rgb_color.setStyleSheet("QWidget { background-color: %s}" % self.current_color().name()) def valid(self): From 27b5dc99f3cd9ae21693828c48b9aa482fdc5df1 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Fri, 9 Jul 2021 22:34:12 -0400 Subject: [PATCH 52/56] keymap_editor: don't crash when setting a key and nothing is selected --- src/main/python/keymap_editor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/python/keymap_editor.py b/src/main/python/keymap_editor.py index f075e46..ce2d328 100644 --- a/src/main/python/keymap_editor.py +++ b/src/main/python/keymap_editor.py @@ -172,6 +172,9 @@ class KeymapEditor(BasicEditor): def set_key(self, keycode): """ Change currently selected key to provided keycode """ + if self.container.active_key is None: + return + if isinstance(self.container.active_key, EncoderWidget): self.set_key_encoder(keycode) else: From 33a191569136c110f98070218fed62ca2032cd50 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sun, 11 Jul 2021 23:44:31 -0400 Subject: [PATCH 53/56] util: add example keyboard uid for vial_example/vial_stm32f401 --- src/main/python/util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/python/util.py b/src/main/python/util.py index 2740431..47f46ed 100644 --- a/src/main/python/util.py +++ b/src/main/python/util.py @@ -28,6 +28,7 @@ EXAMPLE_KEYBOARDS = [ 0xD4A36200603E3007, # vial_stm32f103_vibl 0x32F62BC2EEF2237B, # vial_atmega32u4 0x38CEA320F23046A5, # vial_stm32f072 + 0xBED2D31EC59A0BD8, # vial_stm32f401 ] From 1e3dfa7c88c55fed4ee6b01b40b422aeea7d8e5d Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Tue, 13 Jul 2021 00:27:01 -0400 Subject: [PATCH 54/56] rgb_configurator: fix crash when refreshing --- src/main/python/rgb_configurator.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/python/rgb_configurator.py b/src/main/python/rgb_configurator.py index d50cf53..82d2b16 100644 --- a/src/main/python/rgb_configurator.py +++ b/src/main/python/rgb_configurator.py @@ -188,6 +188,9 @@ class QmkRgblightHandler(BasicHandler): self.underglow_brightness, self.lbl_underglow_color, self.underglow_color] def update_from_keyboard(self): + if not self.valid(): + return + self.underglow_brightness.setValue(self.device.keyboard.underglow_brightness) self.underglow_effect.setCurrentIndex(self.device.keyboard.underglow_effect) self.underglow_color.setStyleSheet("QWidget { background-color: %s}" % self.current_color().name()) @@ -247,6 +250,9 @@ class QmkBacklightHandler(BasicHandler): self.backlight_breathing] def update_from_keyboard(self): + if not self.valid(): + return + self.backlight_brightness.setValue(self.device.keyboard.backlight_brightness) self.backlight_breathing.setChecked(self.device.keyboard.backlight_effect == 1) @@ -340,6 +346,9 @@ class VialRGBHandler(BasicHandler): self.rgb_effect.addItem(effect.name) def update_from_keyboard(self): + if not self.valid(): + return + self.rebuild_effects() for x, effect in enumerate(self.effects): if effect.idx == self.keyboard.rgb_mode: From 7206da4ebd975b6c004939a01c5c5afbfe0fc4bd Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Tue, 13 Jul 2021 02:09:25 -0400 Subject: [PATCH 55/56] keycodes: clear tapdance before recreating --- src/main/python/keycodes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/python/keycodes.py b/src/main/python/keycodes.py index d965bec..5936985 100644 --- a/src/main/python/keycodes.py +++ b/src/main/python/keycodes.py @@ -610,6 +610,7 @@ def recreate_keyboard_keycodes(keyboard): lbl = "M{}".format(x) KEYCODES_MACRO.append(Keycode(0x5F12 + x, lbl, lbl)) + KEYCODES_TAP_DANCE.clear() for x in range(keyboard.tap_dance_count): lbl = "TD({})".format(x) KEYCODES_TAP_DANCE.append(Keycode(QK_TAP_DANCE | x, lbl, lbl)) From 6bb4fa07c583bc0258ffdb740ecc1b6ad8133eab Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sat, 17 Jul 2021 15:48:52 -0400 Subject: [PATCH 56/56] main_window: update URL for linux udev --- src/main/python/main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/python/main_window.py b/src/main/python/main_window.py index 76047fd..f28fd55 100644 --- a/src/main/python/main_window.py +++ b/src/main/python/main_window.py @@ -85,7 +85,7 @@ class MainWindow(QMainWindow): if sys.platform.startswith("linux"): no_devices += '

On Linux you need to set up a custom udev rule for keyboards to be detected. ' \ 'Follow the instructions linked below:
' \ - 'https://get.vial.today/getting-started/linux-udev.html' + 'https://get.vial.today/manual/linux-udev.html' self.lbl_no_devices = QLabel(tr("MainWindow", no_devices)) self.lbl_no_devices.setTextFormat(Qt.RichText) self.lbl_no_devices.setAlignment(Qt.AlignCenter)