Merge branch 'next'

main
Ilya Zhuravlev 2021-07-17 23:59:32 -04:00
commit 642aaf0f07
18 changed files with 1284 additions and 123 deletions

View File

@ -2,5 +2,5 @@
"app_name": "Vial",
"author": "xyz",
"main_module": "src/main/python/main.py",
"version": "0.0.0"
"version": "0.4"
}

View File

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

View File

@ -156,8 +156,10 @@ class FirmwareFlasher(BasicEditor):
if isinstance(self.device, VialBootloader):
self.log("Valid Vial Bootloader device at {}".format(self.device.desc["path"].decode("utf-8")))
self.chk_restore_keymap.hide()
elif isinstance(self.device, VialKeyboard):
self.log("Vial keyboard detected")
self.chk_restore_keymap.show()
def valid(self):
return isinstance(self.device, VialBootloader) or\

View File

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

View File

@ -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 unlocker import Unlocker
from util import MSG_LEN, hid_send, chunks
SUPPORTED_VIA_PROTOCOL = [-1, 9]
SUPPORTED_VIAL_PROTOCOL = [-1, 0, 1, 2, 3]
SUPPORTED_VIAL_PROTOCOL = [-1, 0, 1, 2, 3, 4]
CMD_VIA_GET_PROTOCOL_VERSION = 0x01
CMD_VIA_GET_KEYBOARD_VALUE = 0x02
@ -42,6 +41,12 @@ QMK_RGBLIGHT_EFFECT = 0x81
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_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
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 @@ class Keyboard:
self.vibl = False
self.custom_keycodes = None
self.lighting_qmk_rgblight = self.lighting_qmk_backlight = False
self.lighting_qmk_rgblight = self.lighting_qmk_backlight = self.lighting_vialrgb = False
# 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 @@ class Keyboard:
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 @@ class Keyboard:
macros = self.macro.split(b"\x00") + [b""] * self.macro_count
self.macro = b"\x00".join(macros[:self.macro_count]) + b"\x00"
def reload_rgb(self):
def reload_persistent_rgb(self):
"""
Reload RGB properties which are slow, and do not change while keyboard is plugged in
e.g. VialRGB supported effects list
"""
if "lighting" in self.definition:
self.lighting_qmk_rgblight = self.definition["lighting"] in ["qmk_rgblight", "qmk_backlight_rgblight"]
self.lighting_qmk_backlight = self.definition["lighting"] in ["qmk_backlight", "qmk_backlight_rgblight"]
self.lighting_vialrgb = self.definition["lighting"] == "vialrgb"
if self.lighting_vialrgb:
data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_LIGHTING_GET_VALUE, VIALRGB_GET_INFO),
retries=20)[2:]
self.rgb_version = data[0] | (data[1] << 8)
if self.rgb_version != 1:
raise RuntimeError("Unsupported VialRGB protocol ({}), update your Vial version to latest"
.format(self.rgb_version))
self.rgb_maximum_brightness = data[2]
self.rgb_supported_effects = {0}
max_effect = 0
while max_effect < 0xFFFF:
data = self.usb_send(self.dev, struct.pack("<BBH", CMD_VIA_LIGHTING_GET_VALUE, VIALRGB_GET_SUPPORTED,
max_effect))[2:]
for x in range(0, len(data), 2):
value = int.from_bytes(data[x:x+2], byteorder="little")
if value != 0xFFFF:
self.rgb_supported_effects.add(value)
max_effect = max(max_effect, value)
def reload_rgb(self):
if self.lighting_qmk_rgblight:
self.underglow_brightness = self.usb_send(
self.dev, struct.pack(">BB", CMD_VIA_LIGHTING_GET_VALUE, QMK_RGBLIGHT_BRIGHTNESS), retries=20)[2]
@ -369,6 +424,68 @@ class Keyboard:
self.backlight_effect = self.usb_send(
self.dev, struct.pack(">BB", CMD_VIA_LIGHTING_GET_VALUE, QMK_BACKLIGHT_EFFECT), retries=20)[2]
if self.lighting_vialrgb:
data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_LIGHTING_GET_VALUE, VIALRGB_GET_MODE),
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("<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:
self.supported_settings.add(qsid)
for qsid in self.supported_settings:
from qmk_settings import QmkSettings
if not QmkSettings.is_qsid_supported(qsid):
continue
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:
self.settings[qsid] = QmkSettings.qsid_deserialize(qsid, data[1:])
def reload_dynamic(self):
if self.vial_protocol < 4:
self.tap_dance_count = 0
self.tap_dance_entries = []
self.combo_count = 0
self.combo_entries = []
return
data = self.usb_send(self.dev, struct.pack("BBB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_DYNAMIC_ENTRY_OP,
DYNAMIC_VIAL_GET_NUMBER_OF_ENTRIES), retries=20)
self.tap_dance_count = data[0]
self.combo_count = data[1]
self.tap_dance_entries = []
for x in range(self.tap_dance_count):
data = self.usb_send(self.dev, struct.pack("BBBB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_DYNAMIC_ENTRY_OP,
DYNAMIC_VIAL_TAP_DANCE_GET, x), retries=20)
if data[0] != 0:
raise RuntimeError("failed retrieving tapdance entry {} from the device".format(x))
self.tap_dance_entries.append(struct.unpack("<HHHHH", data[1:11]))
self.combo_entries = []
for x in range(self.combo_count):
data = self.usb_send(self.dev, struct.pack("BBBB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_DYNAMIC_ENTRY_OP,
DYNAMIC_VIAL_COMBO_GET, x), retries=20)
if data[0] != 0:
raise RuntimeError("failed retrieving combo entry {} from the device".format(x))
self.combo_entries.append(struct.unpack("<HHHHH", data[1:11]))
def set_key(self, layer, row, col, code):
if code < 0:
return
@ -473,6 +590,30 @@ class Keyboard:
data["vial_protocol"] = self.vial_protocol
data["via_protocol"] = self.via_protocol
tap_dance = []
for entry in self.tap_dance_entries:
tap_dance.append((
Keycode.serialize(entry[0]),
Keycode.serialize(entry[1]),
Keycode.serialize(entry[2]),
Keycode.serialize(entry[3]),
entry[4]
))
data["tap_dance"] = tap_dance
combo = []
for entry in self.combo_entries:
combo.append((
Keycode.serialize(entry[0]),
Keycode.serialize(entry[1]),
Keycode.serialize(entry[2]),
Keycode.serialize(entry[3]),
Keycode.serialize(entry[4]),
))
data["combo"] = combo
data["settings"] = self.settings
return json.dumps(data).encode("utf-8")
def save_macro(self):
@ -503,6 +644,25 @@ class Keyboard:
self.set_layout_options(data["layout_options"])
self.restore_macros(data.get("macro"))
for x, e in enumerate(data.get("tap_dance", [])):
if x < self.tap_dance_count:
e = (Keycode.deserialize(e[0]), Keycode.deserialize(e[1]), Keycode.deserialize(e[2]),
Keycode.deserialize(e[3]), e[4])
self.tap_dance_set(x, e)
for x, e in enumerate(data.get("combo", [])):
if x < self.combo_count:
e = (Keycode.deserialize(e[0]), Keycode.deserialize(e[1]), Keycode.deserialize(e[2]),
Keycode.deserialize(e[3]), Keycode.deserialize(e[4]))
self.combo_set(x, e)
for qsid, value in data.get("settings", dict()).items():
from qmk_settings import QmkSettings
qsid = int(qsid)
if QmkSettings.is_qsid_supported(qsid):
self.qmk_settings_set(qsid, value)
def restore_macros(self, macros):
if not isinstance(macros, list):
return
@ -637,6 +797,60 @@ class Keyboard:
macros = macros[:self.macro_count]
return [self.macro_deserialize(x) for x in macros]
def qmk_settings_set(self, qsid, value):
from qmk_settings import QmkSettings
self.settings[qsid] = value
data = self.usb_send(self.dev, struct.pack("<BBH", CMD_VIA_VIAL_PREFIX, CMD_VIAL_QMK_SETTINGS_SET, qsid)
+ QmkSettings.qsid_serialize(qsid, value),
retries=20)
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))
def tap_dance_get(self, idx):
return self.tap_dance_entries[idx]
def tap_dance_set(self, idx, entry):
if self.tap_dance_entries[idx] == entry:
return
self.tap_dance_entries[idx] = entry
serialized = struct.pack("<HHHHH", *self.tap_dance_entries[idx])
self.usb_send(self.dev, struct.pack("BBBB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_DYNAMIC_ENTRY_OP,
DYNAMIC_VIAL_TAP_DANCE_SET, idx) + serialized, retries=20)
def combo_get(self, idx):
return self.combo_entries[idx]
def combo_set(self, idx, entry):
if self.combo_entries[idx] == entry:
return
self.combo_entries[idx] = entry
serialized = struct.pack("<HHHHH", *self.combo_entries[idx])
self.usb_send(self.dev, struct.pack("BBBB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_DYNAMIC_ENTRY_OP,
DYNAMIC_VIAL_COMBO_SET, idx) + serialized, retries=20)
def _vialrgb_set_mode(self):
self.usb_send(self.dev, struct.pack("BBHBBBB", CMD_VIA_LIGHTING_SET_VALUE, VIALRGB_SET_MODE,
self.rgb_mode, self.rgb_speed,
self.rgb_hsv[0], self.rgb_hsv[1], self.rgb_hsv[2]))
def set_vialrgb_brightness(self, value):
self.rgb_hsv = (self.rgb_hsv[0], self.rgb_hsv[1], value)
self._vialrgb_set_mode()
def set_vialrgb_speed(self, value):
self.rgb_speed = value
self._vialrgb_set_mode()
def set_vialrgb_mode(self, value):
self.rgb_mode = value
self._vialrgb_set_mode()
def set_vialrgb_color(self, h, s, v):
self.rgb_hsv = (h, s, v)
self._vialrgb_set_mode()
class DummyKeyboard(Keyboard):

View File

@ -4,7 +4,8 @@ from PyQt5.QtGui import QPainter, QColor, QPainterPath, QTransform, QBrush, QPol
from PyQt5.QtWidgets import QWidget, QToolTip, QApplication
from PyQt5.QtCore import Qt, QSize, QRect, QPointF, pyqtSignal, QEvent, QRectF
from constants import KEY_SIZE_RATIO, KEY_SPACING_RATIO, KEYBOARD_WIDGET_PADDING, KEYBOARD_WIDGET_MASK_PADDING, KEYBOARD_WIDGET_MASK_HEIGHT, KEY_ROUNDNESS
from constants import KEY_SIZE_RATIO, KEY_SPACING_RATIO, KEYBOARD_WIDGET_PADDING, KEYBOARD_WIDGET_MASK_PADDING,\
KEYBOARD_WIDGET_MASK_HEIGHT, KEY_ROUNDNESS
class KeyWidget:
@ -166,6 +167,7 @@ class KeyboardWidget(QWidget):
self.enabled = True
self.scale = 1
self.padding = KEYBOARD_WIDGET_PADDING
self.setMouseTracking(True)
@ -215,7 +217,7 @@ class KeyboardWidget(QWidget):
# place common widgets, that is, ones which are always displayed and require no extra transforms
for widget in self.common_widgets:
widget.update_position(scale_factor, -top_x + KEYBOARD_WIDGET_PADDING, -top_y + KEYBOARD_WIDGET_PADDING)
widget.update_position(scale_factor, -top_x + self.padding, -top_y + self.padding)
self.widgets.append(widget)
# top-left position for specific layout
@ -236,7 +238,7 @@ class KeyboardWidget(QWidget):
if opt == self.layout_editor.get_choice(idx):
shift_x = layout_x[idx][opt] - layout_x[idx][0]
shift_y = layout_y[idx][opt] - layout_y[idx][0]
widget.update_position(scale_factor, -shift_x - top_x + KEYBOARD_WIDGET_PADDING, -shift_y - top_y + KEYBOARD_WIDGET_PADDING)
widget.update_position(scale_factor, -shift_x - top_x + self.padding, -shift_y - top_y + self.padding)
self.widgets.append(widget)
def update_layout(self):
@ -255,8 +257,8 @@ class KeyboardWidget(QWidget):
max_w = max(max_w, p.x() * self.scale)
max_h = max(max_h, p.y() * self.scale)
self.width = max_w + 2 * KEYBOARD_WIDGET_PADDING
self.height = max_h + 2 * KEYBOARD_WIDGET_PADDING
self.width = max_w + 2 * self.padding
self.height = max_h + 2 * self.padding
self.update()
self.updateGeometry()

View File

@ -524,6 +524,8 @@ KEYCODES_MEDIA = [
K(132, "KC_LSCR", "Locking\nScroll", "Locking Scroll Lock", alias=["KC_LOCKING_SCROLL"]),
]
KEYCODES_TAP_DANCE = []
KEYCODES_USER = []
KEYCODES_MACRO = []
@ -544,8 +546,8 @@ def recreate_keycodes():
KEYCODES.clear()
KEYCODES.extend(KEYCODES_SPECIAL + KEYCODES_BASIC + KEYCODES_SHIFTED + KEYCODES_ISO + KEYCODES_LAYERS +
KEYCODES_QUANTUM + KEYCODES_BACKLIGHT + KEYCODES_MEDIA + KEYCODES_MACRO + KEYCODES_USER +
KEYCODES_HIDDEN)
KEYCODES_QUANTUM + KEYCODES_BACKLIGHT + KEYCODES_MEDIA + KEYCODES_TAP_DANCE + KEYCODES_MACRO +
KEYCODES_USER + KEYCODES_HIDDEN)
def create_user_keycodes():
@ -608,6 +610,11 @@ def recreate_keyboard_keycodes(keyboard):
lbl = "M{}".format(x)
KEYCODES_MACRO.append(Keycode(0x5F12 + x, lbl, lbl))
KEYCODES_TAP_DANCE.clear()
for x in range(keyboard.tap_dance_count):
lbl = "TD({})".format(x)
KEYCODES_TAP_DANCE.append(Keycode(QK_TAP_DANCE | x, lbl, lbl))
# Check if custom keycodes are defined in keyboard, and if so add them to user keycodes
if keyboard.custom_keycodes is not None and len(keyboard.custom_keycodes) > 0:
create_custom_user_keycodes(keyboard.custom_keycodes)

View File

@ -1,8 +1,7 @@
# SPDX-License-Identifier: GPL-2.0-or-later
import json
from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QMessageBox, QApplication
from PyQt5.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QMessageBox
from PyQt5.QtCore import Qt
from any_keycode_dialog import AnyKeycodeDialog
@ -12,7 +11,7 @@ from keycodes import recreate_keyboard_keycodes, Keycode
from keymaps import KEYMAPS
from square_button import SquareButton
from tabbed_keycodes import TabbedKeycodes
from util import tr
from util import tr, KeycodeDisplay
from vial_device import VialKeyboard
@ -48,8 +47,6 @@ class KeymapEditor(BasicEditor):
layout_editor.changed.connect(self.on_layout_changed)
self.keymap_override = KEYMAPS[0][1]
self.container.anykey.connect(self.on_any_keycode)
self.tabbed_keycodes = TabbedKeycodes()
@ -60,6 +57,7 @@ class KeymapEditor(BasicEditor):
self.addWidget(self.tabbed_keycodes)
self.device = None
KeycodeDisplay.notify_keymap_override(self)
def on_container_clicked(self):
""" Called when a mouse click event is bubbled up to the editor's container """
@ -115,6 +113,7 @@ class KeymapEditor(BasicEditor):
recreate_keyboard_keycodes(self.keyboard)
self.tabbed_keycodes.recreate_keycode_buttons()
TabbedKeycodes.tray.recreate_keycode_buttons()
self.refresh_layer_display()
self.container.setEnabled(self.valid())
@ -135,11 +134,6 @@ class KeymapEditor(BasicEditor):
self.keyboard.restore_layout(data)
self.refresh_layer_display()
def set_keymap_override(self, override):
self.keymap_override = override
self.refresh_layer_display()
self.tabbed_keycodes.set_keymap_override(override)
def on_any_keycode(self):
if self.container.active_key is None:
return
@ -148,17 +142,6 @@ class KeymapEditor(BasicEditor):
if dlg.exec_() and dlg.value >= 0:
self.on_keycode_changed(dlg.value)
def code_is_overriden(self, code):
""" Check whether a country-specific keymap overrides a code """
key = Keycode.find_outer_keycode(code)
return key is not None and key.qmk_id in self.keymap_override
def get_label(self, code):
""" Get label for a specific keycode """
if self.code_is_overriden(code):
return self.keymap_override[Keycode.find_outer_keycode(code).qmk_id]
return Keycode.label(code)
def code_for_widget(self, widget):
if widget.desc.row is not None:
return self.keyboard.layout[(self.current_layer, widget.desc.row, widget.desc.col)]
@ -177,20 +160,7 @@ class KeymapEditor(BasicEditor):
for widget in self.container.widgets:
code = self.code_for_widget(widget)
text = self.get_label(code)
tooltip = Keycode.tooltip(code)
mask = Keycode.is_mask(code)
mask_text = self.get_label(code & 0xFF)
if mask:
text = text.split("\n")[0]
widget.masked = mask
widget.setText(text)
widget.setMaskText(mask_text)
widget.setToolTip(tooltip)
if self.code_is_overriden(code):
widget.setColor(QApplication.palette().color(QPalette.Link))
else:
widget.setColor(None)
KeycodeDisplay.display_keycode(widget, code)
self.container.update()
self.container.updateGeometry()
@ -202,6 +172,9 @@ class KeymapEditor(BasicEditor):
def set_key(self, keycode):
""" Change currently selected key to provided keycode """
if self.container.active_key is None:
return
if isinstance(self.container.active_key, EncoderWidget):
self.set_key_encoder(keycode)
else:
@ -245,3 +218,6 @@ class KeymapEditor(BasicEditor):
self.refresh_layer_display()
self.keyboard.set_layout_options(self.layout_editor.pack())
def on_keymap_override(self):
self.refresh_layer_display()

View File

@ -4,6 +4,7 @@ from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QLabel, QCheckBox, QComboBox, QGridLayout, QWidget, QSizePolicy
from basic_editor import BasicEditor
from keyboard_widget import KeyboardWidget
from vial_device import VialKeyboard
@ -87,18 +88,32 @@ class LayoutEditor(BasicEditor):
def __init__(self, parent=None):
super().__init__(parent)
self.device = None
self.device = self.keyboard = None
self.choices = []
self.widgets = []
self.addStretch()
self.keyboard_preview = KeyboardWidget(self)
self.keyboard_preview.set_enabled(False)
self.keyboard_preview.set_scale(0.7)
self.addWidget(self.keyboard_preview)
self.setAlignment(self.keyboard_preview, QtCore.Qt.AlignHCenter)
w = QWidget()
w.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
self.container = QGridLayout()
w.setLayout(self.container)
self.addWidget(w)
self.setAlignment(w, QtCore.Qt.AlignHCenter)
self.addStretch()
def update_preview(self):
self.keyboard_preview.set_keys(self.keyboard.keys, self.keyboard.encoders)
self.keyboard_preview.update_layout()
self.keyboard_preview.update()
self.keyboard_preview.updateGeometry()
def rebuild(self, device):
super().rebuild(device)
@ -106,6 +121,8 @@ class LayoutEditor(BasicEditor):
if not self.valid():
return
self.keyboard = device.keyboard
self.blockSignals(True)
for choice in self.choices:
@ -122,6 +139,7 @@ class LayoutEditor(BasicEditor):
self.unpack(self.device.keyboard.layout_options)
self.blockSignals(False)
self.update_preview()
def valid(self):
return isinstance(self.device, VialKeyboard) and self.device.keyboard.layout_labels
@ -149,3 +167,4 @@ class LayoutEditor(BasicEditor):
def on_changed(self):
self.changed.emit()
self.update_preview()

View File

@ -181,6 +181,7 @@ class MacroRecorder(BasicEditor):
def on_revert(self):
self.keyboard.reload_macros()
self.deserialize(self.keyboard.macro)
self.on_change()
def on_save(self):
Unlocker.unlock(self.device.keyboard)

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

@ -9,6 +9,7 @@ import os
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 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 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 @@ 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 +61,17 @@ class MainWindow(QMainWindow):
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 @@ class MainWindow(QMainWindow):
if sys.platform.startswith("linux"):
no_devices += '<br><br>On Linux you need to set up a custom udev rule for keyboards to be detected. ' \
'Follow the instructions linked below:<br>' \
'<a href="https://get.vial.today/getting-started/linux-udev.html">https://get.vial.today/getting-started/linux-udev.html</a>'
'<a href="https://get.vial.today/manual/linux-udev.html">https://get.vial.today/manual/linux-udev.html</a>'
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 @@ class MainWindow(QMainWindow):
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 @@ class MainWindow(QMainWindow):
if theme_group.checkedAction() is None:
theme_group.actions()[0].setChecked(True)
about_vial_act = QAction(tr("MenuAbout", "About Vial..."), self)
about_vial_act.triggered.connect(self.about_vial)
self.about_menu = self.menuBar().addMenu(tr("Menu", "About"))
self.about_menu.addAction(about_vial_act)
def on_layout_load(self):
dialog = QFileDialog()
dialog.setDefaultSuffix("vil")
@ -254,7 +273,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.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 @@ class MainWindow(QMainWindow):
def change_keyboard_layout(self, index):
self.settings.setValue("keymap", KEYMAPS[index][0])
self.keymap_editor.set_keymap_override(KEYMAPS[index][1])
KeycodeDisplay.set_keymap_override(KEYMAPS[index][1])
def get_theme(self):
return self.settings.value("theme", "Dark")
@ -337,6 +356,7 @@ class MainWindow(QMainWindow):
msg.exec_()
def on_tab_changed(self, index):
TabbedKeycodes.close_tray()
old_tab = self.current_tab
new_tab = None
if index >= 0:
@ -348,3 +368,13 @@ class MainWindow(QMainWindow):
new_tab.editor.activate()
self.current_tab = new_tab
def about_vial(self):
QMessageBox.about(
self,
"About Vial",
'Vial {}<br><br>'
'Licensed under the terms of the<br>GNU General Public License (version 2 or later)<br><br>'
'<a href="https://get.vial.today/">https://get.vial.today/</a>'
.format(self.appctx.build_settings["version"])
)

View File

@ -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")

View File

@ -60,32 +60,92 @@ QMK_RGBLIGHT_EFFECTS = [
]
class VialRGBEffect:
def __init__(self, idx, name):
self.idx = idx
self.name = name
VIALRGB_EFFECTS = [
VialRGBEffect(0, "Disable"),
VialRGBEffect(1, "Direct Control"),
VialRGBEffect(2, "Solid Color"),
VialRGBEffect(3, "Alphas 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 @@ class QmkRgblightHandler(BasicHandler):
self.underglow_effect.currentIndexChanged.connect(self.on_underglow_effect_changed)
def show(self):
self.lbl_underglow_effect.show()
self.underglow_effect.show()
self.lbl_underglow_brightness.show()
self.underglow_brightness.show()
self.lbl_underglow_color.show()
self.underglow_color.show()
def hide(self):
self.lbl_underglow_effect.hide()
self.underglow_effect.hide()
self.lbl_underglow_brightness.hide()
self.underglow_brightness.hide()
self.lbl_underglow_color.hide()
self.underglow_color.hide()
def block_signals(self):
self.underglow_brightness.blockSignals(True)
self.underglow_effect.blockSignals(True)
self.underglow_color.blockSignals(True)
def unblock_signals(self):
self.underglow_brightness.blockSignals(False)
self.underglow_effect.blockSignals(False)
self.underglow_color.blockSignals(False)
self.widgets = [self.lbl_underglow_effect, self.underglow_effect, self.lbl_underglow_brightness,
self.underglow_brightness, self.lbl_underglow_color, self.underglow_color]
def update_from_keyboard(self):
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 @@ class QmkBacklightHandler(BasicHandler):
self.backlight_breathing.stateChanged.connect(self.on_backlight_breathing_changed)
container.addWidget(self.backlight_breathing, row + 1, 1)
def show(self):
self.lbl_backlight_brightness.show()
self.backlight_brightness.show()
self.lbl_backlight_breathing.show()
self.backlight_breathing.show()
def hide(self):
self.lbl_backlight_brightness.hide()
self.backlight_brightness.hide()
self.lbl_backlight_breathing.hide()
self.backlight_breathing.hide()
def block_signals(self):
self.backlight_brightness.blockSignals(True)
self.backlight_breathing.blockSignals(True)
def unblock_signals(self):
self.backlight_brightness.blockSignals(False)
self.backlight_breathing.blockSignals(False)
self.widgets = [self.lbl_backlight_brightness, self.backlight_brightness, self.lbl_backlight_breathing,
self.backlight_breathing]
def update_from_keyboard(self):
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 @@ class QmkBacklightHandler(BasicHandler):
self.device.keyboard.set_qmk_backlight_effect(int(checked))
class VialRGBHandler(BasicHandler):
def __init__(self, container):
super().__init__(container)
row = container.rowCount()
self.lbl_rgb_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 @@ class RGBConfigurator(BasicEditor):
self.handler_backlight.update.connect(self.update_from_keyboard)
self.handler_rgblight = QmkRgblightHandler(self.container)
self.handler_rgblight.update.connect(self.update_from_keyboard)
self.handlers = [self.handler_backlight, self.handler_rgblight]
self.handler_vialrgb = VialRGBHandler(self.container)
self.handler_vialrgb.update.connect(self.update_from_keyboard)
self.handlers = [self.handler_backlight, self.handler_rgblight, self.handler_vialrgb]
self.addStretch()
buttons = QHBoxLayout()
@ -273,7 +398,8 @@ class RGBConfigurator(BasicEditor):
def valid(self):
return isinstance(self.device, VialKeyboard) and \
(self.device.keyboard.lighting_qmk_rgblight or self.device.keyboard.lighting_qmk_backlight)
(self.device.keyboard.lighting_qmk_rgblight or self.device.keyboard.lighting_qmk_backlight
or self.device.keyboard.lighting_vialrgb)
def block_signals(self):
for h in self.handlers:

View File

@ -7,10 +7,9 @@ from PyQt5.QtGui import QPalette
from constants import KEYCODE_BTN_RATIO
from flowlayout import FlowLayout
from keycodes import KEYCODES_BASIC, KEYCODES_ISO, KEYCODES_MACRO, KEYCODES_LAYERS, KEYCODES_QUANTUM, \
KEYCODES_BACKLIGHT, KEYCODES_MEDIA, KEYCODES_SPECIAL, KEYCODES_SHIFTED, KEYCODES_USER, Keycode
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 @@ class TabbedKeycodes(QTabWidget):
self.tab_quantum = QScrollArea()
self.tab_backlight = QScrollArea()
self.tab_media = QScrollArea()
self.tab_tap_dance = QScrollArea()
self.tab_user = QScrollArea()
self.tab_macro = QScrollArea()
@ -41,12 +42,15 @@ class TabbedKeycodes(QTabWidget):
(self.tab_quantum, "Quantum", KEYCODES_QUANTUM),
(self.tab_backlight, "Backlight", KEYCODES_BACKLIGHT),
(self.tab_media, "App, Media and Mouse", KEYCODES_MEDIA),
(self.tab_tap_dance, "Tap Dance", KEYCODES_TAP_DANCE),
(self.tab_user, "User", KEYCODES_USER),
(self.tab_macro, "Macro", KEYCODES_MACRO),
]:
layout = FlowLayout()
if tab == self.tab_layers:
self.layout_layers = layout
elif tab == self.tab_tap_dance:
self.layout_tap_dance = layout
elif tab == self.tab_macro:
self.layout_macro = layout
elif tab == self.tab_user:
@ -71,16 +75,17 @@ class TabbedKeycodes(QTabWidget):
self.addTab(tab, tr("TabbedKeycodes", label))
self.layer_keycode_buttons = []
self.tap_dance_keycode_buttons = []
self.macro_keycode_buttons = []
self.user_keycode_buttons = []
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 @@ class TabbedKeycodes(QTabWidget):
return buttons
def recreate_keycode_buttons(self):
for btn in self.layer_keycode_buttons + self.macro_keycode_buttons + self.user_keycode_buttons:
for btn in self.layer_keycode_buttons + self.tap_dance_keycode_buttons + self.macro_keycode_buttons \
+ self.user_keycode_buttons:
self.widgets.remove(btn)
btn.hide()
btn.deleteLater()
self.layer_keycode_buttons = self.create_buttons(self.layout_layers, KEYCODES_LAYERS)
self.tap_dance_keycode_buttons = self.create_buttons(self.layout_tap_dance, KEYCODES_TAP_DANCE)
self.macro_keycode_buttons = self.create_buttons(self.layout_macro, KEYCODES_MACRO)
self.user_keycode_buttons = self.create_buttons(self.layout_user, KEYCODES_USER, wordWrap=True)
self.widgets += self.layer_keycode_buttons + self.macro_keycode_buttons + self.user_keycode_buttons
self.user_keycode_buttons = self.create_buttons(self.layout_user, KEYCODES_USER, word_wrap=True)
self.widgets += self.layer_keycode_buttons + self.tap_dance_keycode_buttons + \
self.macro_keycode_buttons + self.user_keycode_buttons
self.relabel_buttons()
def 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()

View File

@ -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 <code>TD({})</code> 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()

View File

@ -5,9 +5,12 @@ import time
from logging.handlers import RotatingFileHandler
from PyQt5.QtCore import QCoreApplication, QStandardPaths
from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import QApplication
from hidproxy import hid
from keycodes import Keycode
from keymaps import KEYMAPS
tr = QCoreApplication.translate
@ -25,6 +28,7 @@ EXAMPLE_KEYBOARDS = [
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()

View File

@ -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 }
]
}
]
}