vial/src/main/python/keyboard_widget.py

376 lines
13 KiB
Python
Raw Normal View History

from collections import defaultdict
from PyQt5.QtGui import QPainter, QColor, QPainterPath, QTransform, QBrush, QPolygonF, QPalette
from PyQt5.QtWidgets import QWidget, QToolTip, QApplication
2020-10-18 00:06:00 -04:00
from PyQt5.QtCore import Qt, QSize, QRect, QPointF, pyqtSignal, QEvent, QRectF
2021-02-28 11:44:14 -05:00
from constants import KEY_SIZE_RATIO, KEY_SPACING_RATIO, KEYBOARD_WIDGET_PADDING, KEYBOARD_WIDGET_MASK_PADDING, KEYBOARD_WIDGET_MASK_HEIGHT
class KeyWidget:
def __init__(self, desc, scale, shift_x=0, shift_y=0):
2020-12-29 14:47:16 -05:00
self.active = False
self.masked = False
self.desc = desc
self.text = ""
2020-10-18 00:06:00 -04:00
self.mask_text = ""
self.tooltip = ""
self.color = None
self.scale = 0
self.rotation_angle = desc.rotation_angle
self.has2 = desc.width2 != desc.width or desc.height2 != desc.height or desc.x2 != 0 or desc.y2 != 0
self.update_position(scale, shift_x, shift_y)
def update_position(self, scale, shift_x=0, shift_y=0):
if self.scale != scale or self.shift_x != shift_x or self.shift_y != shift_y:
self.scale = scale
size = self.scale * (KEY_SIZE_RATIO + KEY_SPACING_RATIO)
spacing = self.scale * KEY_SPACING_RATIO
self.rotation_x = size * self.desc.rotation_x
self.rotation_y = size * self.desc.rotation_y
self.shift_x = shift_x
self.shift_y = shift_y
self.x = size * self.desc.x
self.y = size * self.desc.y
self.w = size * self.desc.width - spacing
self.h = size * self.desc.height - spacing
self.rect = QRect(self.x, self.y, self.w, self.h)
self.x2 = self.x + size * self.desc.x2
self.y2 = self.y + size * self.desc.y2
self.w2 = size * self.desc.width2 - spacing
self.h2 = size * self.desc.height2 - spacing
2021-02-28 11:44:14 -05:00
self.bbox = self.calculate_bbox(self.rect)
self.polygon = QPolygonF(self.bbox + [self.bbox[0]])
self.draw_path = self.calculate_draw_path()
self.draw_path2 = self.calculate_draw_path2()
# calculate areas where the inner keycode will be located
# nonmask = outer (e.g. Rsft_T)
# mask = inner (e.g. KC_A)
2021-02-28 11:44:14 -05:00
self.nonmask_rect = QRectF(int(self.x), int(self.y), int(self.w), int(self.h * (1 - KEYBOARD_WIDGET_MASK_HEIGHT)))
self.mask_rect = QRectF(int(self.x + KEYBOARD_WIDGET_MASK_PADDING),
int(self.y) + int(self.h * (1 - KEYBOARD_WIDGET_MASK_HEIGHT)),
int(self.w - 2 * KEYBOARD_WIDGET_MASK_PADDING),
int(self.h * KEYBOARD_WIDGET_MASK_HEIGHT - KEYBOARD_WIDGET_MASK_PADDING) + 1)
self.mask_bbox = self.calculate_bbox(self.mask_rect)
self.mask_polygon = QPolygonF(self.mask_bbox + [self.mask_bbox[0]])
2020-10-18 00:06:00 -04:00
def calculate_bbox(self, rect):
x1 = rect.topLeft().x()
y1 = rect.topLeft().y()
x2 = rect.bottomRight().x()
y2 = rect.bottomRight().y()
points = [(x1, y1), (x1, y2), (x2, y2), (x2, y1)]
bbox = []
for p in points:
t = QTransform()
t.translate(self.shift_x, self.shift_y)
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
2020-12-06 19:24:34 -05:00
def calculate_draw_path2(self):
return QPainterPath()
def setText(self, text):
self.text = text
2020-10-18 00:06:00 -04:00
def setMaskText(self, text):
self.mask_text = text
def setToolTip(self, tooltip):
self.tooltip = tooltip
2020-12-29 14:47:16 -05:00
def setActive(self, active):
self.active = active
def setColor(self, color):
self.color = color
2020-12-06 19:24:34 -05:00
class EncoderWidget(KeyWidget):
def calculate_draw_path(self):
path = QPainterPath()
path.addEllipse(int(self.x), int(self.y), int(self.w), int(self.h))
return path
def calculate_draw_path2(self):
path = QPainterPath()
if self.desc.encoder_dir == 0:
path.moveTo(int(self.x), int(self.y + self.h / 2))
path.lineTo(int(self.x - self.w / 5), int(self.y + self.h / 3))
path.moveTo(int(self.x), int(self.y + self.h / 2))
path.lineTo(int(self.x + self.w / 5), int(self.y + self.h / 3))
else:
path.moveTo(int(self.x), int(self.y + self.h / 2))
path.lineTo(int(self.x - self.w / 5), int(self.y + self.h - self.h / 3))
path.moveTo(int(self.x), int(self.y + self.h / 2))
path.lineTo(int(self.x + self.w / 5), int(self.y + self.h - self.h / 3))
return path
class KeyboardWidget(QWidget):
clicked = pyqtSignal()
anykey = pyqtSignal()
def __init__(self, layout_editor):
super().__init__()
2020-12-29 14:47:16 -05:00
self.enabled = True
self.scale = 1
self.setMouseTracking(True)
self.layout_editor = layout_editor
# widgets common for all layouts
self.common_widgets = []
# layout-specific widgets
self.widgets_for_layout = []
# widgets in current layout
self.widgets = []
self.width = self.height = 0
self.active_key = None
2020-10-18 00:06:00 -04:00
self.active_mask = False
2020-12-06 19:24:34 -05:00
def set_keys(self, keys, encoders):
self.common_widgets = []
self.widgets_for_layout = []
self.add_keys([(x, KeyWidget) for x in keys] + [(x, EncoderWidget) for x in encoders])
self.update_layout()
def add_keys(self, keys):
scale_factor = self.fontMetrics().height()
for key, cls in keys:
if key.layout_index == -1:
self.common_widgets.append(cls(key, scale_factor))
else:
self.widgets_for_layout.append(cls(key, scale_factor))
def place_widgets(self):
top_x = top_y = 1e6
scale_factor = self.fontMetrics().height()
self.widgets = []
# find the global top-left position, all the keys will be shifted to the left/up by that position
for widget in self.common_widgets:
widget.update_position(scale_factor)
p = widget.polygon.boundingRect().topLeft()
top_x = min(top_x, p.x())
top_y = min(top_y, p.y())
# 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)
self.widgets.append(widget)
# top-left position for specific layout
layout_x = defaultdict(lambda: defaultdict(lambda: 1e6))
layout_y = defaultdict(lambda: defaultdict(lambda: 1e6))
# determine top-left position for every layout option
for widget in self.widgets_for_layout:
widget.update_position(scale_factor)
idx, opt = widget.desc.layout_index, widget.desc.layout_option
p = widget.polygon.boundingRect().topLeft()
layout_x[idx][opt] = min(layout_x[idx][opt], p.x())
layout_y[idx][opt] = min(layout_y[idx][opt], p.y())
# obtain widgets for all layout options now that we know how to shift them
for widget in self.widgets_for_layout:
idx, opt = widget.desc.layout_index, widget.desc.layout_option
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)
self.widgets.append(widget)
def update_layout(self):
""" Updates self.widgets for the currently active layout """
# determine widgets for current layout
self.place_widgets()
2020-12-29 07:37:24 -05:00
self.widgets = list(filter(lambda w: not w.desc.decal, self.widgets))
self.widgets.sort(key=lambda w: (w.y, w.x))
# determine maximum width and height of container
max_w = max_h = 0
for key in self.widgets:
p = key.polygon.boundingRect().bottomRight()
2020-12-29 14:47:16 -05:00
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.update()
self.updateGeometry()
def paintEvent(self, event):
qp = QPainter()
qp.begin(self)
qp.setRenderHint(QPainter.Antialiasing)
# for regular keycaps
regular_pen = qp.pen()
regular_pen.setColor(QApplication.palette().color(QPalette.ButtonText))
qp.setPen(regular_pen)
regular_brush = QBrush()
regular_brush.setColor(QApplication.palette().color(QPalette.Button))
regular_brush.setStyle(Qt.SolidPattern)
qp.setBrush(regular_brush)
# for currently selected keycap
active_pen = qp.pen()
active_pen.setColor(QApplication.palette().color(QPalette.HighlightedText))
active_brush = QBrush()
active_brush.setColor(QApplication.palette().color(QPalette.Highlight))
active_brush.setStyle(Qt.SolidPattern)
2020-10-18 00:06:00 -04:00
mask_font = qp.font()
mask_font.setPointSize(mask_font.pointSize() * 0.8)
for idx, key in enumerate(self.widgets):
qp.save()
2020-12-29 14:47:16 -05:00
qp.scale(self.scale, self.scale)
qp.translate(key.shift_x, key.shift_y)
qp.translate(key.rotation_x, key.rotation_y)
qp.rotate(key.rotation_angle)
qp.translate(-key.rotation_x, -key.rotation_y)
active = key.active or (self.active_key == key and not self.active_mask)
if active:
qp.setPen(active_pen)
qp.setBrush(active_brush)
# draw the keycap
qp.drawPath(key.draw_path)
2020-12-06 19:24:34 -05:00
qp.strokePath(key.draw_path2, regular_pen)
2020-10-18 00:06:00 -04:00
# 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)
2020-10-18 00:06:00 -04:00
qp.drawText(key.nonmask_rect, Qt.AlignCenter, key.text)
qp.restore()
2020-10-18 00:06:00 -04:00
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)
2020-10-18 00:06:00 -04:00
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)
2020-10-18 00:06:00 -04:00
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):
2020-10-18 00:06:00 -04:00
""" Returns key, hit_masked_part """
for key in self.widgets:
2020-10-18 00:06:00 -04:00
if key.masked and key.mask_polygon.containsPoint(pos, Qt.OddEvenFill):
return key, True
if key.polygon.containsPoint(pos, Qt.OddEvenFill):
2020-10-18 00:06:00 -04:00
return key, False
2020-10-18 00:06:00 -04:00
return None, False
2020-10-18 00:06:00 -04:00
def mousePressEvent(self, ev):
2020-12-29 14:47:16 -05:00
if not self.enabled:
return
2020-10-18 00:06:00 -04:00
self.active_key, self.active_mask = self.hit_test(ev.pos())
if self.active_key is not None:
self.clicked.emit()
2020-10-18 00:06:00 -04:00
self.update()
def resizeEvent(self, ev):
self.update_layout()
def select_next(self):
""" Selects next key based on their order in the keymap """
keys_looped = self.widgets + [self.widgets[0]]
for x, key in enumerate(keys_looped):
if key == self.active_key:
self.active_key = keys_looped[x + 1]
self.active_mask = False
self.clicked.emit()
return
def deselect(self):
if self.active_key is not None:
self.active_key = None
self.update()
def event(self, ev):
2020-12-29 14:47:16 -05:00
if not self.enabled:
super().event(ev)
if ev.type() == QEvent.ToolTip:
2020-10-18 00:06:00 -04:00
key = self.hit_test(ev.pos())[0]
if key is not None:
QToolTip.showText(ev.globalPos(), key.tooltip)
else:
QToolTip.hideText()
elif ev.type() == QEvent.LayoutRequest:
self.update_layout()
elif ev.type() == QEvent.MouseButtonDblClick and self.active_key:
self.anykey.emit()
return super().event(ev)
2020-12-29 14:47:16 -05:00
def set_enabled(self, val):
self.enabled = val
def set_scale(self, scale):
self.scale = scale