Merge branch 'next'
commit
642aaf0f07
|
|
@ -2,5 +2,5 @@
|
|||
"app_name": "Vial",
|
||||
"author": "xyz",
|
||||
"main_module": "src/main/python/main.py",
|
||||
"version": "0.0.0"
|
||||
"version": "0.4"
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
|
@ -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\
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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_()
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue