From eceb31a03fc35e329a75e0009651d4e5bc21ca08 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sat, 17 Oct 2020 07:38:21 -0400 Subject: [PATCH] implement support for rotated/complex keys --- src/main/python/constants.py | 3 +- src/main/python/keyboard_container.py | 49 ++----- src/main/python/keyboard_widget.py | 180 ++++++++++++++++++++++++++ src/main/python/kle_serial.py | 16 ++- 4 files changed, 205 insertions(+), 43 deletions(-) create mode 100644 src/main/python/keyboard_widget.py diff --git a/src/main/python/constants.py b/src/main/python/constants.py index 43563fe..9171830 100644 --- a/src/main/python/constants.py +++ b/src/main/python/constants.py @@ -12,5 +12,4 @@ WINDOW_WIDTH, WINDOW_HEIGHT = 1024, 768 LAYER_BTN_STYLE = "border: 1px solid black; padding: 5px" ACTIVE_LAYER_BTN_STYLE = "border: 1px solid black; padding: 5px; background-color: black; color: white" -KEY_NORMAL_STYLE = "background-color:white; border: 1px solid black" -KEY_ACTIVE_STYLE = "background-color:black; color: white; border: 1px solid black" +KEYBOARD_WIDGET_PADDING = 5 diff --git a/src/main/python/keyboard_container.py b/src/main/python/keyboard_container.py index e6cb87a..5ec9d35 100644 --- a/src/main/python/keyboard_container.py +++ b/src/main/python/keyboard_container.py @@ -6,9 +6,9 @@ from PyQt5.QtCore import pyqtSignal, Qt from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QVBoxLayout from clickable_label import ClickableLabel +from keyboard_widget import KeyboardWidget from keycodes import keycode_label, keycode_tooltip -from constants import KEY_WIDTH, KEY_SPACING, KEY_HEIGHT, LAYER_BTN_STYLE, ACTIVE_LAYER_BTN_STYLE, KEY_NORMAL_STYLE, \ - KEY_ACTIVE_STYLE +from constants import LAYER_BTN_STYLE, ACTIVE_LAYER_BTN_STYLE from util import tr @@ -28,7 +28,8 @@ class KeyboardContainer(QWidget): layout_labels_container.addStretch() # contains the actual keyboard - self.container = QWidget() + self.container = KeyboardWidget() + self.container.clicked.connect(self.on_key_clicked) layout = QVBoxLayout() layout.addLayout(layout_labels_container) @@ -39,7 +40,6 @@ class KeyboardContainer(QWidget): self.keys = [] self.layer_labels = [] self.rowcol = defaultdict(list) - self.selected_key = None self.selected_row = -1 self.selected_col = -1 self.keyboard = None @@ -75,33 +75,11 @@ class KeyboardContainer(QWidget): # prepare for fetching keymap self.rowcol = defaultdict(list) - max_w = max_h = 0 + self.container.set_keys(keyboard.keys) + for key in self.container.keys: + if key.desc.row is not None: + self.rowcol[(key.desc.row, key.desc.col)].append(key) - for key in keyboard.keys: - widget = ClickableLabel() - widget.clicked.connect(lambda w=widget: self.on_key_clicked(w)) - - if key.row is not None: - self.rowcol[(key.row, key.col)].append(widget) - - widget.setParent(self.container) - widget.setAlignment(Qt.AlignCenter) - - x = (KEY_WIDTH + KEY_SPACING) * key.x - y = (KEY_HEIGHT + KEY_SPACING) * key.y - w = (KEY_WIDTH + KEY_SPACING) * key.width - KEY_SPACING - h = (KEY_HEIGHT + KEY_SPACING) * key.height - KEY_SPACING - - widget.setFixedSize(w, h) - widget.move(x, y) - widget.show() - - max_w = max(max_w, x + w) - max_h = max(max_h, y + h) - - self.keys.append(widget) - - self.container.setFixedSize(max_w, max_h) self.current_layer = 0 self.refresh_layer_display() @@ -117,15 +95,13 @@ class KeyboardContainer(QWidget): text = keycode_label(code) tooltip = keycode_tooltip(code) for widget in widgets: - widget.setStyleSheet(KEY_NORMAL_STYLE) - if widget == self.selected_key: - widget.setStyleSheet(KEY_ACTIVE_STYLE) widget.setText(text) widget.setToolTip(tooltip) + self.container.update() def switch_layer(self, idx): + self.container.deselect() self.current_layer = idx - self.selected_key = None self.selected_row = -1 self.selected_col = -1 self.refresh_layer_display() @@ -140,11 +116,6 @@ class KeyboardContainer(QWidget): def on_key_clicked(self, widget): """ Change which key is currently selected """ - if self.selected_key == widget: - self.selected_key = None - else: - self.selected_key = widget - for (row, col), widgets in self.rowcol.items(): if widget in widgets: self.selected_row = row diff --git a/src/main/python/keyboard_widget.py b/src/main/python/keyboard_widget.py new file mode 100644 index 0000000..d95a8f4 --- /dev/null +++ b/src/main/python/keyboard_widget.py @@ -0,0 +1,180 @@ +from PyQt5.QtGui import QPainter, QColor, QPainterPath, QTransform, QBrush, QPolygonF +from PyQt5.QtWidgets import QWidget, QToolTip +from PyQt5.QtCore import Qt, QSize, QRect, QPointF, pyqtSignal, QEvent + +from constants import KEY_WIDTH, KEY_SPACING, KEY_HEIGHT, KEYBOARD_WIDGET_PADDING + + +class KeyWidget: + + def __init__(self, desc): + self.desc = desc + self.text = "" + self.tooltip = "" + + self.rotation_x = (KEY_WIDTH + KEY_SPACING) * desc.rotation_x + self.rotation_y = (KEY_HEIGHT + KEY_SPACING) * desc.rotation_y + self.rotation_angle = desc.rotation_angle + + self.x = (KEY_WIDTH + KEY_SPACING) * desc.x + self.y = (KEY_HEIGHT + KEY_SPACING) * desc.y + self.w = (KEY_WIDTH + KEY_SPACING) * desc.width - KEY_SPACING + self.h = (KEY_HEIGHT + KEY_SPACING) * desc.height - KEY_SPACING + + self.rect = QRect(self.x, self.y, self.w, self.h) + + self.has2 = desc.width2 != desc.width or desc.height2 != desc.height or desc.x2 != 0 or desc.y2 != 0 + + self.x2 = self.x + (KEY_WIDTH + KEY_SPACING) * desc.x2 + self.y2 = self.y + (KEY_WIDTH + KEY_SPACING) * desc.y2 + self.w2 = (KEY_WIDTH + KEY_SPACING) * desc.width2 - KEY_SPACING + self.h2 = (KEY_HEIGHT + KEY_SPACING) * desc.height2 - KEY_SPACING + + self.bbox = self.calculate_bbox() + self.polygon = QPolygonF(self.bbox + [self.bbox[0]]) + self.draw_path = self.calculate_draw_path() + + def calculate_bbox(self): + x1 = self.x + y1 = self.y + x2 = self.x + self.w + y2 = self.y + self.h + points = [(x1, y1), (x1, y2), (x2, y2), (x2, y1)] + bbox = [] + for p in points: + t = QTransform() + t.translate(KEYBOARD_WIDGET_PADDING, KEYBOARD_WIDGET_PADDING) + t.translate(self.rotation_x, self.rotation_y) + t.rotate(self.rotation_angle) + t.translate(-self.rotation_x, -self.rotation_y) + p = t.map(QPointF(p[0], p[1])) + bbox.append(p) + return bbox + + def calculate_draw_path(self): + path = QPainterPath() + path.addRect(int(self.x), int(self.y), int(self.w), int(self.h)) + + # second part only considered if different from first + if self.has2: + path2 = QPainterPath() + path2.addRect(int(self.x2), int(self.y2), int(self.w2), int(self.h2)) + path = path.united(path2) + + return path + + def setText(self, text): + self.text = text + + def setToolTip(self, tooltip): + self.tooltip = tooltip + + +class KeyboardWidget(QWidget): + + clicked = pyqtSignal(KeyWidget) + + def __init__(self): + super().__init__() + self.setMouseTracking(True) + + self.keys = [] + self.width = self.height = 0 + self.active_key = None + + def set_keys(self, keys): + self.keys = [] + for key in keys: + self.keys.append(KeyWidget(key)) + self.calculate_size() + self.update() + + def calculate_size(self): + max_w = max_h = 0 + + for key in self.keys: + p = key.polygon.boundingRect().bottomRight() + max_w = max(max_w, p.x()) + max_h = max(max_h, p.y()) + + self.width = max_w + 2 * KEYBOARD_WIDGET_PADDING + self.height = max_h + 2 * KEYBOARD_WIDGET_PADDING + + def paintEvent(self, event): + qp = QPainter() + qp.begin(self) + qp.setRenderHint(QPainter.Antialiasing) + + # add a little padding + qp.translate(KEYBOARD_WIDGET_PADDING, KEYBOARD_WIDGET_PADDING) + + # for regular keycaps + regular_pen = qp.pen() + regular_pen.setColor(QColor("black")) + qp.setPen(regular_pen) + + regular_brush = QBrush() + regular_brush.setColor(QColor("white")) + regular_brush.setStyle(Qt.SolidPattern) + qp.setBrush(regular_brush) + + # for currently selected keycap + active_pen = qp.pen() + active_pen.setColor(QColor("white")) + + active_brush = QBrush() + active_brush.setColor(QColor("black")) + active_brush.setStyle(Qt.SolidPattern) + + for idx, key in enumerate(self.keys): + qp.save() + qp.translate(key.rotation_x, key.rotation_y) + qp.rotate(key.rotation_angle) + qp.translate(-key.rotation_x, -key.rotation_y) + + if self.active_key == key: + qp.setPen(active_pen) + qp.setBrush(active_brush) + + # draw the keycap + qp.drawPath(key.draw_path) + + # draw the legend + qp.drawText(key.rect, Qt.AlignCenter, key.text) + + qp.restore() + + qp.end() + + def minimumSizeHint(self): + return QSize(self.width, self.height) + + def hit_test(self, pos): + for key in self.keys: + if key.polygon.containsPoint(pos, Qt.OddEvenFill): + return key + return None + + def mousePressEvent(self, ev): + prev_active_key = self.active_key + + self.active_key = self.hit_test(ev.pos()) + if self.active_key is not None: + self.clicked.emit(self.active_key) + + if prev_active_key != self.active_key: + self.update() + + def deselect(self): + if self.active_key is not None: + self.active_key = None + self.update() + + def event(self, ev): + if ev.type() == QEvent.ToolTip: + key = self.hit_test(ev.pos()) + if key is not None: + QToolTip.showText(ev.globalPos(), key.tooltip) + else: + QToolTip.hideText() + return super().event(ev) diff --git a/src/main/python/kle_serial.py b/src/main/python/kle_serial.py index 4fddc60..ca9cad7 100644 --- a/src/main/python/kle_serial.py +++ b/src/main/python/kle_serial.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later # Based on https://github.com/ijprest/kle-serial +# & see https://github.com/ijprest/kle-serial/pull/1 import json from copy import copy @@ -62,6 +63,12 @@ class Keyboard: self.keys = [] +class Cluster: + + def __init__(self): + self.x = self.y = 0 + + class Serial: labelMap = [ @@ -88,6 +95,7 @@ class Serial: def deserialize(self, rows): current = Key() + cluster = Cluster() kbd = Keyboard() align = 4 @@ -127,9 +135,13 @@ class Serial: if "r" in item: current.rotation_angle = item["r"] if "rx" in item: - current.rotation_x = item["rx"] + current.rotation_x = cluster.x = item["rx"] + current.x = cluster.x + current.y = cluster.y if "ry" in item: - current.rotation_y = item["ry"] + current.rotation_y = cluster.y = item["ry"] + current.x = cluster.x + current.y = cluster.y if "a" in item: align = item["a"] if "f" in item: