Merge branch 'qmk-settings2' into next
commit
63a15af1ed
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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_()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue