diff --git a/src/build/settings/base.json b/src/build/settings/base.json index 6c3140debf..cc697926ab 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/combos.py b/src/main/python/combos.py new file mode 100644 index 0000000000..ee179656f7 --- /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/firmware_flasher.py b/src/main/python/firmware_flasher.py index 5be3c8e8ee..a1bc133c9b 100644 --- a/src/main/python/firmware_flasher.py +++ b/src/main/python/firmware_flasher.py @@ -156,8 +156,10 @@ def rebuild(self, device): 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\ diff --git a/src/main/python/key_widget.py b/src/main/python/key_widget.py new file mode 100644 index 0000000000..607fdcfb3b --- /dev/null +++ b/src/main/python/key_widget.py @@ -0,0 +1,61 @@ +from PyQt5.QtCore import pyqtSignal + +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): + + changed = pyqtSignal() + + 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], []) + + self.anykey.connect(self.on_anykey) + + def mousePressEvent(self, ev): + super().mousePressEvent(ev) + if self.active_key is not None: + TabbedKeycodes.open_tray(self) + 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 """ + + 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): + 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/keyboard_comm.py b/src/main/python/keyboard_comm.py index 59d7c4a77e..2ddc704456 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 @@ -13,7 +12,7 @@ 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 @@ -42,6 +41,12 @@ QMK_RGBLIGHT_EFFECT_SPEED = 0x82 QMK_RGBLIGHT_COLOR = 0x83 +VIALRGB_GET_INFO = 0x40 +VIALRGB_GET_MODE = 0x41 +VIALRGB_GET_SUPPORTED = 0x42 + +VIALRGB_SET_MODE = 0x41 + CMD_VIAL_GET_KEYBOARD_ID = 0x00 CMD_VIAL_GET_SIZE = 0x01 CMD_VIAL_GET_DEFINITION = 0x02 @@ -52,6 +57,19 @@ 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 + +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 @@ -182,10 +200,17 @@ def __init__(self, dev, usb_send=hid_send): 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 + + # 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 = 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 @@ -201,7 +226,10 @@ def reload(self, sideload_json=None): self.reload_layers() self.reload_keymap() self.reload_macros() + self.reload_persistent_rgb() self.reload_rgb() + self.reload_settings() + self.reload_dynamic() def reload_layers(self): """ Get how many layers the keyboard has """ @@ -346,11 +374,38 @@ def reload_macros(self): 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, QMK_RGBLIGHT_BRIGHTNESS), retries=20)[2] @@ -369,6 +424,68 @@ def reload_rgb(self): 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), + retries=20)[2:] + 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() + self.supported_settings = set() + if self.vial_protocol < 4: + return + cur = 0 + while cur != 0xFFFF: + data = self.usb_send(self.dev, struct.pack(" 0: create_custom_user_keycodes(keyboard.custom_keycodes) diff --git a/src/main/python/keymap_editor.py b/src/main/python/keymap_editor.py index 85139a9820..ce2d328faa 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 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 @@ def __init__(self, layout_editor): 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 @@ def __init__(self, layout_editor): 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 """ @@ -115,6 +113,7 @@ def rebuild(self, device): 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()) @@ -135,11 +134,6 @@ def restore_layout(self, data): 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 +142,6 @@ def on_any_keycode(self): 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 +160,7 @@ def refresh_layer_display(self): 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() @@ -202,6 +172,9 @@ def switch_layer(self, idx): 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: @@ -245,3 +218,6 @@ def on_layout_changed(self): 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/layout_editor.py b/src/main/python/layout_editor.py index 9473dea023..bad05089ce 100644 --- a/src/main/python/layout_editor.py +++ b/src/main/python/layout_editor.py @@ -4,6 +4,7 @@ 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 @@ def rebuild(self, device): if not self.valid(): return + self.keyboard = device.keyboard + self.blockSignals(True) for choice in self.choices: @@ -122,6 +139,7 @@ def rebuild(self, device): 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 @@ def get_choice(self, index): def on_changed(self): self.changed.emit() + self.update_preview() diff --git a/src/main/python/macro_recorder.py b/src/main/python/macro_recorder.py index 83c8e5b702..fd0d3c07fc 100644 --- a/src/main/python/macro_recorder.py +++ b/src/main/python/macro_recorder.py @@ -181,6 +181,7 @@ def deserialize(self, data): 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) diff --git a/src/main/python/main.py b/src/main/python/main.py index 9d2ae19820..a1891606b3 100644 --- a/src/main/python/main.py +++ b/src/main/python/main.py @@ -64,7 +64,7 @@ def exception_hook(self, exc_type, exc_value, exc_traceback): 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 c86ccbecaf..f28fd55f80 100644 --- a/src/main/python/main_window.py +++ b/src/main/python/main_window.py @@ -9,6 +9,7 @@ import sys from urllib.request import urlopen +from combos import Combos from editor_container import EditorContainer from firmware_flasher import FirmwareFlasher from keyboard_comm import ProtocolError @@ -16,9 +17,12 @@ 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 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 @@ -27,8 +31,9 @@ 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 +61,17 @@ def __init__(self): self.keymap_editor = KeymapEditor(self.layout_editor) self.firmware_flasher = FirmwareFlasher(self) self.macro_recorder = MacroRecorder() + self.tap_dance = TapDance() + self.combos = Combos() + QmkSettings.initialize(appctx) + self.qmk_settings = QmkSettings() 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.tap_dance, "Tap Dance"), (self.combos, "Combos"), + (self.qmk_settings, "QMK Settings"), + (self.matrix_tester, "Matrix tester"), (self.firmware_flasher, "Firmware updater")] Unlocker.global_layout_editor = self.layout_editor @@ -75,7 +85,7 @@ def __init__(self): 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) @@ -85,6 +95,10 @@ def __init__(self): layout.addWidget(self.tabs) layout.addWidget(self.lbl_no_devices) layout.setAlignment(self.lbl_no_devices, Qt.AlignHCenter) + 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) @@ -181,6 +195,11 @@ def init_menu(self): 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") @@ -254,7 +273,7 @@ def rebuild(self): 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.tap_dance, self.combos, self.qmk_settings, self.matrix_tester, self.rgb_configurator]: e.rebuild(self.current_device) def refresh_tabs(self): @@ -324,7 +343,7 @@ def reboot_to_bootloader(self): 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") @@ -337,6 +356,7 @@ def set_theme(self, theme): msg.exec_() def on_tab_changed(self, index): + TabbedKeycodes.close_tray() old_tab = self.current_tab new_tab = None if index >= 0: @@ -348,3 +368,13 @@ def on_tab_changed(self, index): 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"]) + ) diff --git a/src/main/python/qmk_settings.py b/src/main/python/qmk_settings.py new file mode 100644 index 0000000000..a6c0b5f7b4 --- /dev/null +++ b/src/main/python/qmk_settings.py @@ -0,0 +1,270 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +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 + +from basic_editor import BasicEditor +from util import tr +from vial_device import VialKeyboard + + +class GenericOption(QObject): + + changed = pyqtSignal() + + def __init__(self, option, container): + super().__init__() + + 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): + return keyboard.settings.get(self.qsid) + + def delete(self): + self.lbl.hide() + self.lbl.deleteLater() + + def on_change(self): + self.changed.emit() + + +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 reload(self, keyboard): + value = super().reload(keyboard) + checked = value & (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.spinbox.valueChanged.connect(self.on_change) + self.container.addWidget(self.spinbox, self.row, 1) + + def reload(self, keyboard): + value = super().reload(keyboard) + self.spinbox.blockSignals(True) + self.spinbox.setValue(value) + self.spinbox.blockSignals(False) + + def value(self): + return self.spinbox.value() + + def delete(self): + super().delete() + self.spinbox.hide() + self.spinbox.deleteLater() + + +class QmkSettings(BasicEditor): + + def __init__(self): + super().__init__() + self.keyboard = None + + self.tabs_widget = QTabWidget() + self.addWidget(self.tabs_widget) + buttons = QHBoxLayout() + buttons.addStretch() + 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) + self.addLayout(buttons) + + self.tabs = [] + self.misc_widgets = [] + + def populate_tab(self, tab, container): + options = [] + for field in tab["fields"]: + if field["qsid"] not in self.keyboard.supported_settings: + continue + if field["type"] == "boolean": + opt = BooleanOption(field, container) + options.append(opt) + opt.changed.connect(self.on_change) + elif field["type"] == "integer": + opt = IntegerOption(field, container) + options.append(opt) + opt.changed.connect(self.on_change) + 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) + + # create new GUI + 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"]: + if field["qsid"] in self.keyboard.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.keyboard.reload_settings() + self.recreate_gui() + + for tab in self.tabs: + for field in tab: + field.reload(self.keyboard) + + self.on_change() + + def on_change(self): + changed = False + qsid_values = self.prepare_settings() + + 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) + + def rebuild(self, device): + super().rebuild(device) + if self.valid(): + self.keyboard = device.keyboard + self.reload_settings() + + 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) + self.on_change() + + 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 >= 4 + and len(self.device.keyboard.supported_settings)) + + @classmethod + def initialize(cls, appctx): + cls.qsid_fields = defaultdict(list) + 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"]: + cls.qsid_fields[field["qsid"]].append(field) + + @classmethod + def is_qsid_supported(cls, qsid): + """ Return whether this qsid is supported by the settings editor """ + return qsid in cls.qsid_fields + + @classmethod + def qsid_serialize(cls, qsid, data): + """ Serialize from internal representation into binary that can be sent to the firmware """ + fields = cls.qsid_fields[qsid] + if fields[0]["type"] == "boolean": + assert isinstance(data, int) + return data.to_bytes(1, byteorder="little") + elif fields[0]["type"] == "integer": + assert isinstance(data, int) + assert len(fields) == 1 + return data.to_bytes(fields[0]["width"], byteorder="little") + + @classmethod + def qsid_deserialize(cls, qsid, data): + """ Deserialize from binary received from firmware into internal representation """ + fields = cls.qsid_fields[qsid] + if fields[0]["type"] == "boolean": + return data[0] + elif fields[0]["type"] == "integer": + assert len(fields) == 1 + return int.from_bytes(data[0:fields[0]["width"]], byteorder="little") + else: + raise RuntimeError("unsupported field") diff --git a/src/main/python/rgb_configurator.py b/src/main/python/rgb_configurator.py index 33475b5a71..82d2b163d6 100644 --- a/src/main/python/rgb_configurator.py +++ b/src/main/python/rgb_configurator.py @@ -60,32 +60,92 @@ def __init__(self, idx, name, color_picker): ] +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 Mods"), + VialRGBEffect(4, "Gradient Up Down"), + VialRGBEffect(5, "Gradient Left Right"), + VialRGBEffect(6, "Breathing"), + VialRGBEffect(7, "Band Sat"), + VialRGBEffect(8, "Band Val"), + VialRGBEffect(9, "Band Pinwheel Sat"), + VialRGBEffect(10, "Band Pinwheel Val"), + VialRGBEffect(11, "Band Spiral Sat"), + VialRGBEffect(12, "Band Spiral Val"), + VialRGBEffect(13, "Cycle All"), + VialRGBEffect(14, "Cycle Left Right"), + VialRGBEffect(15, "Cycle Up Down"), + VialRGBEffect(16, "Rainbow Moving Chevron"), + VialRGBEffect(17, "Cycle Out In"), + VialRGBEffect(18, "Cycle Out In Dual"), + VialRGBEffect(19, "Cycle Pinwheel"), + VialRGBEffect(20, "Cycle Spiral"), + VialRGBEffect(21, "Dual Beacon"), + VialRGBEffect(22, "Rainbow Beacon"), + VialRGBEffect(23, "Rainbow Pinwheels"), + VialRGBEffect(24, "Raindrops"), + VialRGBEffect(25, "Jellybean Raindrops"), + VialRGBEffect(26, "Hue Breathing"), + VialRGBEffect(27, "Hue Pendulum"), + VialRGBEffect(28, "Hue Wave"), + VialRGBEffect(29, "Typing Heatmap"), + VialRGBEffect(30, "Digital Rain"), + VialRGBEffect(31, "Solid Reactive Simple"), + VialRGBEffect(32, "Solid Reactive"), + VialRGBEffect(33, "Solid Reactive Wide"), + VialRGBEffect(34, "Solid Reactive Multiwide"), + VialRGBEffect(35, "Solid Reactive Cross"), + VialRGBEffect(36, "Solid Reactive Multicross"), + VialRGBEffect(37, "Solid Reactive Nexus"), + VialRGBEffect(38, "Solid Reactive Multinexus"), + VialRGBEffect(39, "Splash"), + VialRGBEffect(40, "Multisplash"), + VialRGBEffect(41, "Solid Splash"), + VialRGBEffect(42, "Solid Multisplash"), +] + + class BasicHandler(QObject): update = pyqtSignal() 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.keyboard 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,33 +184,13 @@ def __init__(self, container): 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): + 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()) @@ -206,27 +246,13 @@ def __init__(self, container): 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): + if not self.valid(): + return + self.backlight_brightness.setValue(self.device.keyboard.backlight_brightness) self.backlight_breathing.setChecked(self.device.keyboard.backlight_effect == 1) @@ -240,6 +266,103 @@ def on_backlight_breathing_changed(self, checked): 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_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 + 1, 0) + self.rgb_color = ClickableLabel(" ") + self.rgb_color.clicked.connect(self.on_rgb_color) + container.addWidget(self.rgb_color, row + 1, 1) + + self.lbl_rgb_brightness = QLabel(tr("RGBConfigurator", "RGB Brightness")) + 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 + 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_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) + + 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.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.keyboard.rgb_hsv[0] / 255.0, + 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): + if not self.valid(): + return + + 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_speed.setValue(self.keyboard.rgb_speed) + 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 + + class RGBConfigurator(BasicEditor): def __init__(self): @@ -258,7 +381,9 @@ def __init__(self): 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 +398,8 @@ def on_save(self): 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: diff --git a/src/main/python/tabbed_keycodes.py b/src/main/python/tabbed_keycodes.py index 9c2d5c4594..da9e9ff6ad 100644 --- a/src/main/python/tabbed_keycodes.py +++ b/src/main/python/tabbed_keycodes.py @@ -7,10 +7,9 @@ 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 + KEYCODES_BACKLIGHT, KEYCODES_MEDIA, KEYCODES_SPECIAL, KEYCODES_SHIFTED, KEYCODES_USER, Keycode, KEYCODES_TAP_DANCE from square_button import SquareButton -from util import tr +from util import tr, KeycodeDisplay class TabbedKeycodes(QTabWidget): @@ -21,7 +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() @@ -29,6 +29,7 @@ def __init__(self, parent=None): 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 @@ def __init__(self, parent=None): (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 @@ def __init__(self, parent=None): self.addTab(tab, tr("TabbedKeycodes", label)) self.layer_keycode_buttons = [] + self.tap_dance_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): + 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,28 +96,63 @@ def create_buttons(self, layout, keycodes, wordWrap = False): 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 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: 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.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): + if self.target is not None: + self.target.on_anykey() diff --git a/src/main/python/tap_dance.py b/src/main/python/tap_dance.py new file mode 100644 index 0000000000..5afb982478 --- /dev/null +++ b/src/main/python/tap_dance.py @@ -0,0 +1,179 @@ +# 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 + +from key_widget import KeyWidget +from tabbed_keycodes import TabbedKeycodes +from util import tr +from vial_device import VialKeyboard +from basic_editor import BasicEditor + + +class TapDanceEntryUI(QObject): + + key_changed = pyqtSignal() + timing_changed = pyqtSignal() + + def __init__(self, idx): + super().__init__() + + self.idx = idx + self.container = QGridLayout() + self.populate_container() + + w = QWidget() + w.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) + w.setLayout(self.container) + l = QVBoxLayout() + l.addStretch() + l.addSpacing(10) + l.addWidget(w) + l.setAlignment(w, QtCore.Qt.AlignHCenter) + 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) + l.addStretch() + self.w2 = QWidget() + self.w2.setLayout(l) + + 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) + + def widget(self): + 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, + self.kc_on_hold.keycode, + self.kc_on_double_tap.keycode, + self.kc_on_tap_hold.keycode, + self.txt_tapping_term.value() + ) + + def on_key_changed(self): + self.key_changed.emit() + + def on_timing_changed(self): + self.timing_changed.emit() + + +class CustomTabWidget(QTabWidget): + + def mouseReleaseEvent(self, ev): + TabbedKeycodes.close_tray() + + +class TapDance(BasicEditor): + + def __init__(self): + super().__init__() + self.keyboard = None + + self.tap_dance_entries = [] + self.tap_dance_entries_available = [] + self.tabs = CustomTabWidget() + for x in range(128): + 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() + 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(self.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)) + 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() + 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 + 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() diff --git a/src/main/python/util.py b/src/main/python/util.py index 1c020ae382..47f46ed8fb 100644 --- a/src/main/python/util.py +++ b/src/main/python/util.py @@ -5,9 +5,12 @@ 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 @@ -25,6 +28,7 @@ 0xD4A36200603E3007, # vial_stm32f103_vibl 0x32F62BC2EEF2237B, # vial_atmega32u4 0x38CEA320F23046A5, # vial_stm32f072 + 0xBED2D31EC59A0BD8, # vial_stm32f401 ] @@ -147,3 +151,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() diff --git a/src/main/resources/base/qmk_settings.json b/src/main/resources/base/qmk_settings.json new file mode 100644 index 0000000000..b247093e11 --- /dev/null +++ b/src/main/resources/base/qmk_settings.json @@ -0,0 +1,65 @@ +{ + "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": "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 }, + { "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 } + ] + }, + { + "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": "Combo", + "fields": [ + { "type": "integer", "title": "Time out period for combos", "qsid": 2, "min": 0, "max": 10000, "width": 2 } + ] + }, + { + "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 } + ] + } + ] +}