From 49dc6d21abb21ad7cf673f284fbc7bcfd38c5d13 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Fri, 1 Jan 2021 06:27:48 -0500 Subject: [PATCH] add support for language-specific layouts --- src/main/python/keyboard_container.py | 28 +++++++++++++++-- src/main/python/keyboard_widget.py | 15 ++++++++- src/main/python/keycodes.py | 6 ++++ src/main/python/keymap/french.py | 30 ++++++++++++++++++ src/main/python/keymap/german.py | 25 +++++++++++++++ src/main/python/keymap/russian.py | 44 +++++++++++++++++++++++++++ src/main/python/keymap_editor.py | 4 +++ src/main/python/keymaps.py | 15 +++++++++ src/main/python/main_window.py | 17 ++++++++++- src/main/python/tabbed_keycodes.py | 24 +++++++++++++-- 10 files changed, 201 insertions(+), 7 deletions(-) create mode 100644 src/main/python/keymap/french.py create mode 100644 src/main/python/keymap/german.py create mode 100644 src/main/python/keymap/russian.py create mode 100644 src/main/python/keymaps.py diff --git a/src/main/python/keyboard_container.py b/src/main/python/keyboard_container.py index 441a366..09ce656 100644 --- a/src/main/python/keyboard_container.py +++ b/src/main/python/keyboard_container.py @@ -5,8 +5,9 @@ from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QVBoxLayout from clickable_label import ClickableLabel from keyboard_widget import KeyboardWidget, EncoderWidget -from keycodes import keycode_label, keycode_tooltip, keycode_is_mask +from keycodes import keycode_label, keycode_tooltip, keycode_is_mask, find_keycode from constants import LAYER_BTN_STYLE, ACTIVE_LAYER_BTN_STYLE +from keymaps import KEYMAPS from util import tr @@ -41,6 +42,8 @@ class KeyboardContainer(QWidget): layout_editor.changed.connect(self.on_layout_changed) + self.keymap_override = KEYMAPS[0][1] + def rebuild_layers(self): # delete old layer labels for label in self.layer_labels: @@ -67,6 +70,17 @@ class KeyboardContainer(QWidget): self.current_layer = 0 self.on_layout_changed() + def code_is_overriden(self, code): + """ Check whether a country-specific keymap overrides a code """ + key = find_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[find_keycode(code).qmk_id] + return keycode_label(code) + def refresh_layer_display(self): """ Refresh text on key widgets to display data corresponding to current layer """ @@ -82,16 +96,20 @@ class KeyboardContainer(QWidget): else: code = self.keyboard.encoder_layout[(self.current_layer, widget.desc.encoder_idx, widget.desc.encoder_dir)] - text = keycode_label(code) + text = self.get_label(code) tooltip = keycode_tooltip(code) mask = keycode_is_mask(code) - mask_text = keycode_label(code & 0xFF) + 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(Qt.blue) + else: + widget.setColor(None) self.container.update() self.container.updateGeometry() @@ -153,3 +171,7 @@ class KeyboardContainer(QWidget): self.refresh_layer_display() self.keyboard.set_layout_options(self.layout_editor.pack()) + + def set_keymap_override(self, override): + self.keymap_override = override + self.refresh_layer_display() diff --git a/src/main/python/keyboard_widget.py b/src/main/python/keyboard_widget.py index 933bb52..3622aa1 100644 --- a/src/main/python/keyboard_widget.py +++ b/src/main/python/keyboard_widget.py @@ -16,6 +16,7 @@ class KeyWidget: self.text = "" self.mask_text = "" self.tooltip = "" + self.color = None self.rotation_x = (KEY_WIDTH + KEY_SPACING) * desc.rotation_x self.rotation_y = (KEY_HEIGHT + KEY_SPACING) * desc.rotation_y @@ -93,6 +94,9 @@ class KeyWidget: def setActive(self, active): self.active = active + def setColor(self, color): + self.color = color + class EncoderWidget(KeyWidget): @@ -248,7 +252,8 @@ class KeyboardWidget(QWidget): qp.rotate(key.rotation_angle) qp.translate(-key.rotation_x, -key.rotation_y) - if key.active or (self.active_key == key and not self.active_mask): + active = key.active or (self.active_key == key and not self.active_mask) + if active: qp.setPen(active_pen) qp.setBrush(active_brush) @@ -259,16 +264,24 @@ class KeyboardWidget(QWidget): # if this is a mask key, draw the inner key if key.masked: qp.setFont(mask_font) + qp.save() + if key.color is not None and not active: + qp.setPen(key.color) qp.drawText(key.nonmask_rect, Qt.AlignCenter, key.text) + qp.restore() if self.active_key == key and self.active_mask: qp.setPen(active_pen) qp.setBrush(active_brush) qp.drawRect(key.mask_rect) + if key.color is not None and not active: + qp.setPen(key.color) qp.drawText(key.mask_rect, Qt.AlignCenter, key.mask_text) else: # draw the legend + if key.color is not None and not active: + qp.setPen(key.color) qp.drawText(key.rect, Qt.AlignCenter, key.text) qp.restore() diff --git a/src/main/python/keycodes.py b/src/main/python/keycodes.py index 8b14766..d36edc2 100644 --- a/src/main/python/keycodes.py +++ b/src/main/python/keycodes.py @@ -6,10 +6,12 @@ class Keycode: masked_keycodes = set() recorder_alias_to_keycode = dict() + qmk_id_to_keycode = dict() def __init__(self, code, qmk_id, label, tooltip=None, masked=False, printable=None, recorder_alias=None): self.code = code self.qmk_id = qmk_id + self.qmk_id_to_keycode[qmk_id] = self self.label = label self.tooltip = tooltip # whether this keycode requires another sub-keycode @@ -31,6 +33,10 @@ class Keycode: def find_by_recorder_alias(cls, alias): return cls.recorder_alias_to_keycode.get(alias) + @classmethod + def find_by_qmk_id(cls, qmk_id): + return cls.qmk_id_to_keycode.get(qmk_id) + K = Keycode diff --git a/src/main/python/keymap/french.py b/src/main/python/keymap/french.py new file mode 100644 index 0000000..14498a1 --- /dev/null +++ b/src/main/python/keymap/french.py @@ -0,0 +1,30 @@ +# coding: utf-8 + +keymap = { + "KC_GRAVE": "²", + "KC_1": "1\n&", + "KC_2": "2\né", + "KC_3": '3\n"', + "KC_4": "4\n'", + "KC_5": "5\n(", + "KC_6": "6\n-", + "KC_7": "7\nè", + "KC_8": "8\n_", + "KC_9": "9\nç", + "KC_0": "0\nà", + "KC_MINUS": "°\n)", + "KC_Q": "A", + "KC_W": "Z", + "KC_LBRACKET": "¨\n^", + "KC_RBRACKET": "£\n$", + "KC_A": "Q", + "KC_SCOLON": "M", + "KC_QUOTE": "%\nù", + "KC_NONUS_HASH": "µ\n*", + "KC_NONUS_BSLASH": ">\n<", + "KC_Z": "W", + "KC_M": "?\n,", + "KC_COMMA": ".\n;", + "KC_DOT": "/\n:", + "KC_SLASH": "§\n!", +} diff --git a/src/main/python/keymap/german.py b/src/main/python/keymap/german.py new file mode 100644 index 0000000..3213965 --- /dev/null +++ b/src/main/python/keymap/german.py @@ -0,0 +1,25 @@ +# coding: utf-8 + +keymap = { + "KC_GRAVE": "°\n^", + "KC_2": '"\n2', + "KC_3": "§\n3", + "KC_6": "&\n6", + "KC_7": "/\n7", + "KC_8": "(\n8", + "KC_9": ")\n9", + "KC_0": "=\n0", + "KC_MINUS": "?\nß", + "KC_EQUAL": "`\n´", + "KC_Y": "Z", + "KC_LBRACKET": "Ü", + "KC_RBRACKET": "*\n+", + "KC_SCOLON": "Ö", + "KC_QUOTE": "Ä", + "KC_NONUS_HASH": "'\n#", + "KC_NONUS_BSLASH": ">\n<", + "KC_Z": "Y", + "KC_COMMA": ";\n,", + "KC_DOT": ":\n.", + "KC_SLASH": "_\n-", +} diff --git a/src/main/python/keymap/russian.py b/src/main/python/keymap/russian.py new file mode 100644 index 0000000..15d11dc --- /dev/null +++ b/src/main/python/keymap/russian.py @@ -0,0 +1,44 @@ +# coding: utf-8 + +keymap = { + "KC_GRAVE": "Ё", + "KC_2": '"\n2', + "KC_3": "№\n3", + "KC_4": ";\n4", + "KC_6": ":\n6", + "KC_7": "?\n7", + "KC_Q": "Q\nЙ", + "KC_W": "W\nЦ", + "KC_E": "E\nУ", + "KC_R": "R\nК", + "KC_T": "T\nЕ", + "KC_Y": "Y\nН", + "KC_U": "U\nГ", + "KC_I": "I\nШ", + "KC_O": "O\nЩ", + "KC_P": "P\nЗ", + "KC_LBRACKET": "{\n[ Х", + "KC_RBRACKET": "}\n] Ъ", + "KC_BSLASH": "| /\n\\", + "KC_A": "A\nФ", + "KC_S": "S\nЫ", + "KC_D": "D\nВ", + "KC_F": "F\nА", + "KC_G": "G\nП", + "KC_H": "H\nР", + "KC_J": "J\nО", + "KC_K": "K\nЛ", + "KC_L": "L\nД", + "KC_SCOLON": ":\n; Ж", + "KC_QUOTE": "\"\n' Э", + "KC_Z": "Z\nЯ", + "KC_X": "X\nЧ", + "KC_C": "C\nС", + "KC_V": "V\nМ", + "KC_B": "B\nИ", + "KC_N": "N\nТ", + "KC_M": "M\nЬ", + "KC_COMMA": "<\n, Б", + "KC_DOT": ">\n. Ю", + "KC_SLASH": "? ,\n/ ." +} diff --git a/src/main/python/keymap_editor.py b/src/main/python/keymap_editor.py index 3cf1feb..4484b1b 100644 --- a/src/main/python/keymap_editor.py +++ b/src/main/python/keymap_editor.py @@ -41,3 +41,7 @@ class KeymapEditor(BasicEditor): def restore_layout(self, data): self.keyboard_container.restore_layout(data) + + def set_keymap_override(self, override): + self.keyboard_container.set_keymap_override(override) + self.tabbed_keycodes.set_keymap_override(override) diff --git a/src/main/python/keymaps.py b/src/main/python/keymaps.py new file mode 100644 index 0000000..6ab268f --- /dev/null +++ b/src/main/python/keymaps.py @@ -0,0 +1,15 @@ +from keycodes import Keycode +from keymap import french, german, russian + +KEYMAPS = [ + ("QWERTY", dict()), + ("French (AZERTY)", french.keymap), + ("German (QWERTZ)", german.keymap), + ("Russian (ЙЦУКЕН)", russian.keymap), +] + +# make sure that qmk IDs we used are all correct +for name, keymap in KEYMAPS: + for qmk_id in keymap.keys(): + if Keycode.find_by_qmk_id(qmk_id) is None: + raise RuntimeError("Misconfigured - cannot find QMK keycode {}".format(qmk_id)) diff --git a/src/main/python/main_window.py b/src/main/python/main_window.py index 6293ad6..9a50055 100644 --- a/src/main/python/main_window.py +++ b/src/main/python/main_window.py @@ -2,12 +2,13 @@ from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QWidget, QComboBox, QToolButton, QHBoxLayout, QVBoxLayout, QMainWindow, QAction, qApp, \ - QFileDialog, QDialog, QTabWidget + QFileDialog, QDialog, QTabWidget, QActionGroup import json from firmware_flasher import FirmwareFlasher from keymap_editor import KeymapEditor +from keymaps import KEYMAPS from layout_editor import LayoutEditor from macro_recorder import MacroRecorder from unlocker import Unlocker @@ -94,6 +95,17 @@ class MainWindow(QMainWindow): keyboard_reset_act = QAction(tr("MenuSecurity", "Reboot to bootloader"), self) keyboard_reset_act.triggered.connect(self.reboot_to_bootloader) + keyboard_layout_menu = self.menuBar().addMenu(tr("Menu", "Keyboard layout")) + keymap_group = QActionGroup(self) + for idx, keymap in enumerate(KEYMAPS): + act = QAction(tr("KeyboardLayout", keymap[0]), self) + act.triggered.connect(lambda checked, x=idx: self.change_keyboard_layout(x)) + act.setCheckable(True) + if idx == 0: + act.setChecked(True) + keymap_group.addAction(act) + keyboard_layout_menu.addAction(act) + self.security_menu = self.menuBar().addMenu(tr("Menu", "Security")) self.security_menu.addAction(keyboard_unlock_act) self.security_menu.addAction(keyboard_lock_act) @@ -194,3 +206,6 @@ class MainWindow(QMainWindow): if isinstance(self.current_device, VialKeyboard): self.unlocker.perform_unlock(self.current_device.keyboard) self.current_device.keyboard.reset() + + def change_keyboard_layout(self, index): + self.keymap_editor.set_keymap_override(KEYMAPS[index][1]) diff --git a/src/main/python/tabbed_keycodes.py b/src/main/python/tabbed_keycodes.py index 0292b21..fe08a51 100644 --- a/src/main/python/tabbed_keycodes.py +++ b/src/main/python/tabbed_keycodes.py @@ -7,6 +7,7 @@ from constants import KEYCODE_BTN_WIDTH, KEYCODE_BTN_HEIGHT from flowlayout import FlowLayout from keycodes import keycode_tooltip, KEYCODES_BASIC, KEYCODES_ISO, KEYCODES_MACRO, KEYCODES_LAYERS, KEYCODES_QUANTUM, \ KEYCODES_BACKLIGHT, KEYCODES_MEDIA +from keymaps import KEYMAPS from util import tr @@ -17,6 +18,8 @@ class TabbedKeycodes(QTabWidget): def __init__(self, parent=None): super().__init__(parent) + self.keymap_override = None + self.tab_basic = QScrollArea() self.tab_iso = QScrollArea() self.tab_layers = QScrollArea() @@ -25,6 +28,8 @@ class TabbedKeycodes(QTabWidget): self.tab_media = QScrollArea() self.tab_macro = QScrollArea() + self.widgets = [] + for (tab, label, keycodes) in [ (self.tab_basic, "Basic", KEYCODES_BASIC), (self.tab_iso, "ISO/JIS", KEYCODES_ISO), @@ -40,7 +45,7 @@ class TabbedKeycodes(QTabWidget): elif tab == self.tab_macro: self.layout_macro = layout - self.create_buttons(layout, keycodes) + self.widgets += self.create_buttons(layout, keycodes) tab.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) tab.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) @@ -53,15 +58,17 @@ class TabbedKeycodes(QTabWidget): self.layer_keycode_buttons = [] self.macro_keycode_buttons = [] + self.set_keymap_override(KEYMAPS[0][1]) def create_buttons(self, layout, keycodes): buttons = [] for keycode in keycodes: - btn = QPushButton(keycode.label.replace("&", "&&")) + btn = QPushButton() btn.setFixedSize(KEYCODE_BTN_WIDTH, KEYCODE_BTN_HEIGHT) btn.setToolTip(keycode_tooltip(keycode.code)) btn.clicked.connect(lambda st, k=keycode: self.keycode_changed.emit(k.code)) + btn.keycode = keycode layout.addWidget(btn) buttons.append(btn) @@ -73,3 +80,16 @@ class TabbedKeycodes(QTabWidget): btn.deleteLater() self.layer_keycode_buttons = self.create_buttons(self.layout_layers, KEYCODES_LAYERS) self.macro_keycode_buttons = self.create_buttons(self.layout_macro, KEYCODES_MACRO) + + def set_keymap_override(self, override): + self.keymap_override = override + + for widget in self.widgets: + qmk_id = widget.keycode.qmk_id + if qmk_id in self.keymap_override: + label = self.keymap_override[qmk_id] + widget.setStyleSheet("QPushButton {color: blue;}") + else: + label = widget.keycode.label + widget.setStyleSheet("QPushButton {}") + widget.setText(label.replace("&", "&&"))