Merge branch 'qmk-settings2' into next
commit
63a15af1ed
|
|
@ -52,6 +52,11 @@ CMD_VIAL_UNLOCK_START = 0x06
|
||||||
CMD_VIAL_UNLOCK_POLL = 0x07
|
CMD_VIAL_UNLOCK_POLL = 0x07
|
||||||
CMD_VIAL_LOCK = 0x08
|
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
|
# how much of a macro/keymap buffer we can read/write per packet
|
||||||
BUFFER_FETCH_CHUNK = 28
|
BUFFER_FETCH_CHUNK = 28
|
||||||
|
|
||||||
|
|
@ -637,6 +642,36 @@ class Keyboard:
|
||||||
macros = macros[:self.macro_count]
|
macros = macros[:self.macro_count]
|
||||||
return [self.macro_deserialize(x) for x in macros]
|
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):
|
class DummyKeyboard(Keyboard):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ if __name__ == '__main__':
|
||||||
appctxt = ApplicationContext() # 1. Instantiate ApplicationContext
|
appctxt = ApplicationContext() # 1. Instantiate ApplicationContext
|
||||||
init_logger()
|
init_logger()
|
||||||
qt_exception_hook = UncaughtHook()
|
qt_exception_hook = UncaughtHook()
|
||||||
window = MainWindow()
|
window = MainWindow(appctxt)
|
||||||
window.resize(WINDOW_WIDTH, WINDOW_HEIGHT)
|
window.resize(WINDOW_WIDTH, WINDOW_HEIGHT)
|
||||||
window.show()
|
window.show()
|
||||||
exit_code = appctxt.app.exec_() # 2. Invoke appctxt.app.exec_()
|
exit_code = appctxt.app.exec_() # 2. Invoke appctxt.app.exec_()
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ from keymap_editor import KeymapEditor
|
||||||
from keymaps import KEYMAPS
|
from keymaps import KEYMAPS
|
||||||
from layout_editor import LayoutEditor
|
from layout_editor import LayoutEditor
|
||||||
from macro_recorder import MacroRecorder
|
from macro_recorder import MacroRecorder
|
||||||
|
from qmk_settings import QmkSettings
|
||||||
from rgb_configurator import RGBConfigurator
|
from rgb_configurator import RGBConfigurator
|
||||||
from unlocker import Unlocker
|
from unlocker import Unlocker
|
||||||
from util import tr, find_vial_devices, EXAMPLE_KEYBOARDS
|
from util import tr, find_vial_devices, EXAMPLE_KEYBOARDS
|
||||||
|
|
@ -27,8 +28,9 @@ import themes
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, appctx):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.appctx = appctx
|
||||||
|
|
||||||
self.settings = QSettings("Vial", "Vial")
|
self.settings = QSettings("Vial", "Vial")
|
||||||
themes.set_theme(self.get_theme())
|
themes.set_theme(self.get_theme())
|
||||||
|
|
@ -56,12 +58,13 @@ class MainWindow(QMainWindow):
|
||||||
self.keymap_editor = KeymapEditor(self.layout_editor)
|
self.keymap_editor = KeymapEditor(self.layout_editor)
|
||||||
self.firmware_flasher = FirmwareFlasher(self)
|
self.firmware_flasher = FirmwareFlasher(self)
|
||||||
self.macro_recorder = MacroRecorder()
|
self.macro_recorder = MacroRecorder()
|
||||||
|
self.qmk_settings = QmkSettings(self.appctx)
|
||||||
self.matrix_tester = MatrixTest(self.layout_editor)
|
self.matrix_tester = MatrixTest(self.layout_editor)
|
||||||
self.rgb_configurator = RGBConfigurator()
|
self.rgb_configurator = RGBConfigurator()
|
||||||
|
|
||||||
self.editors = [(self.keymap_editor, "Keymap"), (self.layout_editor, "Layout"), (self.macro_recorder, "Macros"),
|
self.editors = [(self.keymap_editor, "Keymap"), (self.layout_editor, "Layout"), (self.macro_recorder, "Macros"),
|
||||||
(self.rgb_configurator, "Lighting"), (self.matrix_tester, "Matrix tester"),
|
(self.rgb_configurator, "Lighting"), (self.qmk_settings, "QMK Settings"),
|
||||||
(self.firmware_flasher, "Firmware updater")]
|
(self.matrix_tester, "Matrix tester"), (self.firmware_flasher, "Firmware updater")]
|
||||||
|
|
||||||
Unlocker.global_layout_editor = self.layout_editor
|
Unlocker.global_layout_editor = self.layout_editor
|
||||||
|
|
||||||
|
|
@ -254,7 +257,7 @@ class MainWindow(QMainWindow):
|
||||||
self.current_device.keyboard.reload()
|
self.current_device.keyboard.reload()
|
||||||
|
|
||||||
for e in [self.layout_editor, self.keymap_editor, self.firmware_flasher, self.macro_recorder,
|
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)
|
e.rebuild(self.current_device)
|
||||||
|
|
||||||
def refresh_tabs(self):
|
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