Merge branch 'qmk-settings2' into next

main
Ilya Zhuravlev 2021-07-02 23:24:50 -04:00
commit 63a15af1ed
5 changed files with 294 additions and 5 deletions

View File

@ -52,6 +52,11 @@ 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
CMD_VIAL_QMK_SETTINGS_RESET = 0x0C
# how much of a macro/keymap buffer we can read/write per packet
BUFFER_FETCH_CHUNK = 28
@ -637,6 +642,36 @@ class Keyboard:
macros = macros[:self.macro_count]
return [self.macro_deserialize(x) for x in macros]
def qmk_settings_query(self):
cur = 0
supported_settings = []
while cur != 0xFFFF:
data = self.usb_send(self.dev, struct.pack("<BBH", CMD_VIA_VIAL_PREFIX, CMD_VIAL_QMK_SETTINGS_QUERY, cur),
retries=20)
for x in range(0, len(data), 2):
qsid = int.from_bytes(data[x:x+2], byteorder="little")
cur = max(cur, qsid)
if qsid != 0xFFFF:
supported_settings.append(qsid)
return supported_settings
def qmk_settings_get(self, qsid):
data = self.usb_send(self.dev, struct.pack("<BBH", CMD_VIA_VIAL_PREFIX, CMD_VIAL_QMK_SETTINGS_GET, qsid),
retries=20)
if data[0] != 0:
return b""
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("<BBH", CMD_VIA_VIAL_PREFIX, CMD_VIAL_QMK_SETTINGS_SET, qsid) + value,
retries=20)
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):

View File

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

View File

@ -16,6 +16,7 @@ from keymap_editor import KeymapEditor
from keymaps import KEYMAPS
from layout_editor import LayoutEditor
from macro_recorder import MacroRecorder
from qmk_settings import QmkSettings
from rgb_configurator import RGBConfigurator
from unlocker import Unlocker
from util import tr, find_vial_devices, EXAMPLE_KEYBOARDS
@ -27,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())
@ -56,12 +58,13 @@ class MainWindow(QMainWindow):
self.keymap_editor = KeymapEditor(self.layout_editor)
self.firmware_flasher = FirmwareFlasher(self)
self.macro_recorder = MacroRecorder()
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.matrix_tester, "Matrix tester"),
(self.firmware_flasher, "Firmware updater")]
(self.rgb_configurator, "Lighting"), (self.qmk_settings, "QMK Settings"),
(self.matrix_tester, "Matrix tester"), (self.firmware_flasher, "Firmware updater")]
Unlocker.global_layout_editor = self.layout_editor
@ -254,7 +257,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.matrix_tester, self.rgb_configurator]:
self.qmk_settings, self.matrix_tester, self.rgb_configurator]:
e.rebuild(self.current_device)
def refresh_tabs(self):

View File

@ -0,0 +1,204 @@
# SPDX-License-Identifier: GPL-2.0-or-later
import json
from collections import defaultdict
from PyQt5 import QtCore
from PyQt5.QtWidgets import QVBoxLayout, QCheckBox, QGridLayout, QLabel, QWidget, QSizePolicy, QTabWidget, QSpinBox, \
QHBoxLayout, QPushButton, QMessageBox
from basic_editor import BasicEditor
from util import tr
from vial_device import VialKeyboard
class GenericOption:
def __init__(self, option, container):
self.row = container.rowCount()
self.option = option
self.qsid = self.option["qsid"]
self.container = container
self.lbl = QLabel(option["title"])
self.container.addWidget(self.lbl, 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
def delete(self):
self.lbl.hide()
self.lbl.deleteLater()
class BooleanOption(GenericOption):
def __init__(self, option, container):
super().__init__(option, container)
self.qsid_bit = self.option["bit"]
self.checkbox = QCheckBox()
self.container.addWidget(self.checkbox, self.row, 1)
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)
def value(self):
checked = int(self.checkbox.isChecked())
return checked << self.qsid_bit
def delete(self):
super().delete()
self.checkbox.hide()
self.checkbox.deleteLater()
class IntegerOption(GenericOption):
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"))
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):
def __init__(self, appctx):
super().__init__()
self.appctx = appctx
self.keyboard = None
self.tabs_widget = QTabWidget()
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)
btn_reset = QPushButton(tr("QmkSettings", "Reset"))
btn_reset.clicked.connect(self.reset_settings)
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:
continue
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 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()
w.setLayout(container)
l = QVBoxLayout()
l.addWidget(w)
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):
self.supported_settings = set(self.keyboard.qmk_settings_query())
self.recreate_gui()
for tab in self.tabs:
for field in tab:
field.reload(self.keyboard)
def rebuild(self, device):
super().rebuild(device)
if self.valid():
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):
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 \
(self.device.keyboard and self.device.keyboard.vial_protocol >= 3) # TODO(xyz): protocol bump

View File

@ -0,0 +1,47 @@
{
"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": "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, "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 },
{ "type": "boolean", "title": "Enable keyrepeat", "qsid": 3, "bit": 5 },
{ "type": "boolean", "title": "Disable keyrepeat when 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, "width": 1 },
{ "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 }
]
}
]
}