#!/usr/bin/env python3 # Qtvcp Joypad widget # # Copyright (c) 2021 Jim Sloot # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. ############################################################################### import sys import enum from qtpy import QtCore, QtGui, QtWidgets from qtpy.QtCore import Qt, QPoint, QPointF, QRect, QRectF, QSize, QSizeF, QEvent try: from qtpy.QtCore import QEnum except ImportError: try: from qtpy.QtCore import Q_ENUM as QEnum except ImportError: # before Qt 5.10 from qtpy.QtCore import Q_ENUMS as QEnum from qtpy.QtGui import QPainter, QPainterPath, QPen, QBrush, QColor, QFont, QPixmap, QRadialGradient import hal from qtvcp.widgets.widget_baseclass import _HalWidgetBase class IndicatorPosition(enum.IntEnum): NONE = 0 LEFT = 1 RIGHT = 2 CENTER = 3 TOP = 4 BOTTOM = 5 LEFTRIGHT = 6 TOPBOTTOM = 7 class JoyPad(QtWidgets.QWidget): IndicatorPosition = IndicatorPosition QEnum(IndicatorPosition) joy_btn_pressed = QtCore.Signal(str) joy_btn_released = QtCore.Signal(str) joy_l_pressed = QtCore.Signal(bool) joy_l_released = QtCore.Signal(bool) joy_r_pressed = QtCore.Signal(bool) joy_r_released = QtCore.Signal(bool) joy_c_pressed = QtCore.Signal(bool) joy_c_released = QtCore.Signal(bool) joy_t_pressed = QtCore.Signal(bool) joy_t_released = QtCore.Signal(bool) joy_b_pressed = QtCore.Signal(bool) joy_b_released = QtCore.Signal(bool) def __init__(self, parent=None): super(JoyPad, self).__init__(parent) self.rect1 = QRectF() self.rect2 = QRectF() self.left_image = None self.right_image = None self.top_image = None self.bottom_image = None self.center_image = None self._dummyPixmap = QtGui.QPixmap() self._font = QFont('Lato Heavy', 20) self._text_color = QColor('white') self._textL = '' self._textR = '' self._textC = '' self._textT = '' self._textB = '' self.colorState = False self._true_color = QColor('lawngreen') self._false_color = QColor('gray') self.highlight_color = self._false_color self._highlightPosition = IndicatorPosition.NONE self.highlight_left = False self.highlight_right = False self.highlight_top = False self.highlight_bottom = False self.highlight_center = False self.last_active_btn = None self.setMouseTracking(True) self.setToolTipDuration(2000) self.installEventFilter(self) self.btn_names = {'L': 'left', 'R': 'right', 'T': 'top', 'B': 'bottom', 'C': 'center'} self.tooltips = {'L': '', 'R': '', 'T': '', 'B': '', 'C': ''} self.axis_list = ('X', 'Y', 'Z', 'A') def eventFilter(self, obj, event): if obj is self and self.isEnabled(): if event.type() == QEvent.MouseButtonPress: if event.button() == Qt.RightButton: event.ignore() else: pos = event.localPos() active_btn = self.get_active_btn(pos) self.last_active_btn = active_btn if active_btn is not None: self._pressedOutput(active_btn) elif event.type() == QEvent.MouseButtonRelease: if event.button() == Qt.RightButton: event.ignore() elif self.last_active_btn is not None: self._releasedOutput(self.last_active_btn) elif event.type() == QEvent.MouseMove: pos = event.pos() active_btn = self.get_active_btn(pos) if active_btn is not None: self.setToolTip(self.tooltips[active_btn]) return super(JoyPad, self).eventFilter(obj, event) def _pressedOutput(self, btncode): self.joy_btn_pressed.emit(btncode) self['joy_{}_pressed'.format(btncode.lower())].emit(True) def _releasedOutput(self, btncode): self.joy_btn_released.emit(btncode) self['joy_{}_released'.format(btncode.lower())].emit(False) def get_active_btn(self, pos): if self.center_path.contains(pos): return 'C' elif self.left_path.contains(pos): return 'L' elif self.right_path.contains(pos): return 'R' elif self.bottom_path.contains(pos): return 'B' elif self.top_path.contains(pos): return 'T' return None def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(painter.Antialiasing) w = min(event.rect().width(), event.rect().height()) self.rect1.setSize(QSizeF(w * 0.4, w * 0.4)) self.rect2.setSize(QSizeF(w * 0.9, w * 0.9)) self.create_paths(painter, event) self.draw_painter_paths(painter, event) self.draw_icons(painter, event) self.draw_highlight(painter, event) painter.end() def create_paths(self, qp, event): self.left_path = QPainterPath() self.right_path = QPainterPath() self.bottom_path = QPainterPath() self.top_path = QPainterPath() self.center_path = QPainterPath() center = event.rect().center() self.rect1.moveCenter(center) self.rect2.moveCenter(center) left_start = QPointF(self.rect1.topLeft()) right_start = QPointF(self.rect1.bottomRight()) bottom_start = QPointF(self.rect1.bottomLeft()) top_start = QPointF(self.rect1.topRight()) path = (self.right_path, self.top_path, self.left_path, self.bottom_path) start = (right_start, top_start, left_start, bottom_start) angle = -45 for i in range(4): path[i].moveTo(start[i]) path[i].arcTo(self.rect1, angle, 90) path[i].arcTo(self.rect2, angle + 90, -90) path[i].closeSubpath() angle += 90 cap = QRectF() cap.setSize(QSizeF(self.rect1.width()*0.8, self.rect1.height()*0.8)) cap.moveCenter(center) self.center_path.addEllipse(cap) def draw_painter_paths(self, qp, event): w = min(event.rect().width(), event.rect().height()) center = event.rect().center() fp = QPoint(int(center.x() - w/4), int(center.y() - w/4)) bg = QRadialGradient(center, w/2, fp) bg.setColorAt(0, QColor(180, 180, 180)) bg.setColorAt(1, QColor(40, 40, 40)) qp.setBrush(QBrush(bg)) qp.setPen(QPen(QColor(Qt.black), 4)) qp.drawPath(self.left_path) qp.drawPath(self.right_path) qp.drawPath(self.top_path) qp.drawPath(self.bottom_path) qp.drawPath(self.center_path) def draw_icons(self, qp, event): rect = QRect() rect.setSize(QSize(int(self.rect1.width() * 0.4), int(self.rect1.height() * 0.4))) center = event.rect().center() qp.setPen(QPen(self._text_color, 2)) qp.setFont(self._font) # left button rect.moveCenter(QPoint(int(center.x() - self.rect2.width()/3), center.y())) if isinstance(self.left_image, QPixmap): pix = self.left_image qp.drawPixmap(rect, pix, pix.rect()) elif isinstance(self.left_image, str): qp.drawText(rect, Qt.AlignCenter, self.left_image) # right button rect.moveCenter(QPoint(int(center.x() + self.rect2.width()/3), center.y())) if isinstance(self.right_image, QPixmap): pix = self.right_image qp.drawPixmap(rect, pix, pix.rect()) elif isinstance(self.right_image, str): qp.drawText(rect, Qt.AlignCenter, self.right_image) # bottom button rect.moveCenter(QPoint(center.x(), int(center.y() + self.rect2.width()/3))) if isinstance(self.bottom_image, QPixmap): pix = self.bottom_image qp.drawPixmap(rect, pix, pix.rect()) elif isinstance(self.bottom_image, str): qp.drawText(rect, Qt.AlignCenter, self.bottom_image) # top button rect.moveCenter(QPoint(center.x(), int(center.y() - self.rect2.width()/3))) if isinstance(self.top_image, QPixmap): pix = self.top_image qp.drawPixmap(rect, pix, pix.rect()) elif isinstance(self.top_image, str): qp.drawText(rect, Qt.AlignCenter, self.top_image) # center button rect.moveCenter(QPoint(center.x(), center.y())) if isinstance(self.center_image, QPixmap): pix = self.center_image qp.drawPixmap(rect, pix, pix.rect()) elif isinstance(self.center_image, str): qp.drawText(rect, Qt.AlignCenter, self.center_image) def draw_highlight(self, qp, event): rect = QRectF() rect.setSize(self.rect1.size() * 0.9) center = event.rect().center() rect.moveCenter(center) pen_width = self.rect1.width() * 0.08 qp.setPen(QPen(self.highlight_color, pen_width, cap = Qt.FlatCap)) if self.highlight_center is True: qp.drawArc(rect, 0, 5760) else: if self.highlight_right is True: qp.drawArc(rect, -45 * 16, 90 * 16) if self.highlight_left is True: qp.drawArc(rect, 135 * 16, 90 * 16) if self.highlight_top is True: qp.drawArc(rect, 45 * 16, 90 * 16) if self.highlight_bottom is True: qp.drawArc(rect, 225 * 16, 90 * 16) def reset_highlight(self): self.highlight_left = False self.highlight_right = False self.highlight_top = False self.highlight_bottom = False self.highlight_center = False def set_highlight(self, btn, state=True): if type(btn) == int: if btn == IndicatorPosition.LEFT: btn ='L' elif btn == IndicatorPosition.RIGHT: btn ='R' elif btn == IndicatorPosition.CENTER: btn ='C' elif btn == IndicatorPosition.TOP: btn ='T' elif btn == IndicatorPosition.BOTTOM: btn ='B' elif btn == IndicatorPosition.LEFTRIGHT: btn ='X' elif btn == IndicatorPosition.TOPBOTTOM: btn ='Z' elif btn == IndicatorPosition.NONE: self.reset_highlight() return else: print ('undefined position:{}'.format(btn)) return if btn not in self.axis_list and btn not in self.btn_names.keys(): return if btn == 'X' or btn == 'A': self.highlight_left = state self.highlight_right = state elif btn == 'Y' or btn == 'Z': self.highlight_top = state self.highlight_bottom = state else: name = self.btn_names[btn] self['highlight_' + name] = state self.update() def set_button_icon(self, btn, path): self.set_icon(btn, 'image', path) def set_button_text(self, btn, text): self.set_icon(btn, 'text', text) def set_icon(self, btn, kind, data): if btn not in self.btn_names.keys(): return name = self.btn_names[btn] if kind == 'image': if data is None: self[name + "_image"] = None else: self[name + "_image"] = QPixmap(data) elif kind == 'text': self[name + "_image"] = data else: return self.update() def set_tooltip(self, btn, tip): if btn in self.btn_names.keys(): self.tooltips[btn] = tip def setLight(self, data): if data: self.highlight_color = self._true_color else: self.highlight_color = self._false_color self.update() def set_HighlightPosition(self, position): self._highlightPosition = position self.reset_highlight() self.set_highlight(position, True) self.update() def get_HighlightPosition(self): return self._highlightPosition def reset_HighlightPosition(self): self._highlightPosition = IndicatorPosition.NONE self.reset_highlight() self.update() highlightPosition = QtCore.Property(IndicatorPosition, get_HighlightPosition, set_HighlightPosition, reset_HighlightPosition) @QtCore.Slot() def set_colorStateTrue(self): self.setLight(True) @QtCore.Slot() def set_colorStateFalse(self): self.setLight(False) @QtCore.Slot(bool) def set_colorState(self, state): self.colorState = bool(state) self.setLight(state) def get_colorState(self): return self.colorState def reset_colorState(self): self.colorState = False self.setLight(False) setColorState = QtCore.Property(bool, get_colorState, set_colorState, reset_colorState) def setLeftImagePath(self, data): if data.isNull(): data = None self.set_icon('L', 'image', data) def getLeftImagePath(self): if isinstance(self.left_image, QPixmap): self.left_image else: return self._dummyPixmap def resetLeftImagePath(self): pass def setRightImagePath(self, data): if data.isNull(): data = None self.set_icon('R', 'image', data) def getRightImagePath(self): return self._dummyPixmap def resetRightImagePath(self): pass def setCenterImagePath(self, data): if data.isNull(): data = None self.set_icon('C', 'image', data) def getCenterImagePath(self): return self._dummyPixmap def resetCenterImagePath(self): pass def setTopImagePath(self, data): if data.isNull(): data = None self.set_icon('T', 'image', data) def getTopImagePath(self): return self._dummyPixmap def resetTopImagePath(self): pass def setBottomImagePath(self, data): if data.isNull(): data = None self.set_icon('B', 'image', data) def getBottomImagePath(self): return self._dummyPixmap def resetBottomImagePath(self): pass left_image_path = QtCore.Property(QPixmap, getLeftImagePath, setLeftImagePath, resetLeftImagePath) right_image_path = QtCore.Property(QPixmap, getRightImagePath, setRightImagePath, resetRightImagePath) center_image_path = QtCore.Property(QPixmap, getCenterImagePath, setCenterImagePath, resetCenterImagePath) top_image_path = QtCore.Property(QPixmap, getTopImagePath, setTopImagePath, resetTopImagePath) bottom_image_path = QtCore.Property(QPixmap, getBottomImagePath, setBottomImagePath, resetBottomImagePath) def getFont(self): return self._font def setFont(self, value): self._font = value def resetFont(self): self._font = QFont('Lato Heavy', 20) button_font = QtCore.Property(QFont, getFont, setFont, resetFont) def setLeftText(self, data): self._textL = data if not data.strip(): data = None self.set_icon('L', 'text', data) def getLeftText(self): return self._textL def resetLeftText(self): self._textL = '' self.set_icon('L', 'text', '') def setRightText(self, data): self._textR = data if not data.strip(): data = None self.set_icon('R', 'text', data) def getRightText(self): return self._textR def resetRightText(self): self._textR = '' self.set_icon('R', 'text', '') def setCenterText(self, data): self._textC = data if not data.strip(): data = None self.set_icon('C', 'text', data) def getCenterText(self): return self._textC def resetCenterText(self): self._textC = '' self.set_icon('C', 'text', '') def setTopText(self, data): self._textT = data if not data.strip(): data = None self.set_icon('T', 'text', data) def getTopText(self): return self._textT def resetTopText(self): self._textT = '' self.set_icon('T', 'text', '') def setBottomText(self, data): self._textB = data if not data.strip(): data = None self.set_icon('B', 'text', data) def getBottomText(self): return self._textB def resetBottomText(self): self._textB = '' self.set_icon('B', 'text', '') left_text = QtCore.Property(str, getLeftText, setLeftText, resetLeftText) right_text = QtCore.Property(str, getRightText, setRightText, resetRightText) center_text = QtCore.Property(str, getCenterText, setCenterText, resetCenterText) top_text = QtCore.Property(str, getTopText, setTopText, resetTopText) bottom_text = QtCore.Property(str, getBottomText, setBottomText, resetBottomText) @QtCore.Slot(QColor) def set_true_color(self, color): self._true_color = color self.setLight(self.colorState) @QtCore.Slot(str) def set_true_color(self, color): self._true_color = QColor(color) self.setLight(self.colorState) def get_true_color(self): return self._true_color def reset_true_color(self): self._true_color = QColor('lawngreen') self.setLight(self.colorState) @QtCore.Slot(QColor) def set_false_color(self, color): self._false_color = color self.setLight(self.colorState) @QtCore.Slot(str) def set_false_color(self, color): self._false_color = QColor(color) self.setLight(self.colorState) def get_false_color(self): return self._false_color def reset_false_color(self): self._false_color = QColor('gray') self.setLight(self.colorState) def set_text_color(self, color): self._text_color = QColor(color) self.update() def get_text_color(self): return self._text_color def reset_text_color(self): self._text_color = QColor('white') self.update() true_color = QtCore.Property(QColor, get_true_color, set_true_color, reset_true_color) false_color = QtCore.Property(QColor, get_false_color, set_false_color, reset_false_color) text_color = QtCore.Property(QColor, get_text_color, set_text_color, reset_text_color) @QtCore.Slot(str) def btn_pressed(self, btn): print("Button pressed", btn) @QtCore.Slot(str) def btn_released(self, btn): print("Button released", btn) # required code for object indexing def __getitem__(self, item): return getattr(self, item) def __setitem__(self, item, value): return setattr(self, item, value) class HALPinType(enum.IntEnum): NONE = 0 BIT = hal.HAL_BIT S32 = hal.HAL_S32 FLOAT = hal.HAL_FLOAT class HALPad(JoyPad, _HalWidgetBase): HALPinType = HALPinType QEnum(HALPinType) # older version of pyqt5 need this as well as QEnum NONE = 0 BIT = hal.HAL_BIT S32 = hal.HAL_S32 FLOAT = hal.HAL_FLOAT def __init__(self, parent=None): super(HALPad, self).__init__(parent) self._pin_name = '' self._pin_type = HALPinType.BIT self._trueOutputR = 1.0 self._trueOutputL = 1.0 self._trueOutputC = 1.0 self._trueOutputT = 1.0 self._trueOutputB = 1.0 self._falseOutput = 0.0 self._true_color = QColor('lawngreen') self._false_color = QColor('red') # we want center active for an indicator light self.setProperty('highlightPosition',IndicatorPosition.CENTER) self.setLight(False) def _hal_init(self): if self._pin_name == '': pname = self.HAL_NAME_ else: pname = self._pin_name if not self._pin_type == HALPinType.NONE: ptype = self._pin_type self.halPinR = self.HAL_GCOMP_.newpin(pname + '.right', ptype, hal.HAL_OUT) self.halPinL = self.HAL_GCOMP_.newpin(pname + '.left', ptype, hal.HAL_OUT) self.halPinT = self.HAL_GCOMP_.newpin(pname + '.top', ptype, hal.HAL_OUT) self.halPinB = self.HAL_GCOMP_.newpin(pname + '.bottom', ptype, hal.HAL_OUT) self.halPinC = self.HAL_GCOMP_.newpin(pname + '.center', ptype, hal.HAL_OUT) if not self._highlightPosition == IndicatorPosition.NONE: self.halPinLightCenter = self.HAL_GCOMP_.newpin(pname + '.light.center', hal.HAL_BIT, hal.HAL_IN) self.halPinLightCenter.value_changed.connect(lambda data: self.setLight(data)) def _pressedOutput(self, btncode): self.joy_btn_pressed.emit(btncode) self['joy_{}_pressed'.format(btncode.lower())].emit(True) if self._pin_type == HALPinType.BIT: data = True elif self._pin_type == HALPinType.FLOAT: data = float(self['_trueOutput{}'.format(btncode)]) elif self._pin_type == HALPinType.S32: data = int(self['_trueOutput{}'.format(btncode)]) else: return try: self['halPin{}'.format(btncode)].set(data) except: pass def _releasedOutput(self, btncode): self.joy_btn_released.emit(btncode) self['joy_{}_released'.format(btncode.lower())].emit(False) if self._pin_type == HALPinType.BIT: data = False elif self._pin_type == HALPinType.FLOAT: data = float(self._falseOutput) elif self._pin_type == HALPinType.S32: data = int(self._falseOutput) else: return try: self['halPin{}'.format(btncode)].set(data) except: pass def setPinTrueOutput(self, btn, data): btncode = self.btn_names[btn.lower()] self['_trueOutput{}'.format(btncode)] = data ######################################################################### # This is how designer can interact with our widget properties. # designer will show the Property properties in the editor # it will use the get set and reset calls to do those actions ######################################################################## def set_pin_name(self, value): self._pin_name = value def get_pin_name(self): return self._pin_name def reset_pin_name(self): self._pin_name = '' def set_pin_type(self, value): self._pin_type = value print(value) def get_pin_type(self): return self._pin_type def reset_pin_type(self): self._pin_type = HALPinType.BIT # designer will show these properties in this order: pin_type = QtCore.Property(HALPinType, get_pin_type, set_pin_type, reset_pin_type) pin_name = QtCore.Property(str, get_pin_name, set_pin_name, reset_pin_name) ############################# # Testing # ############################# if __name__ == "__main__": import sys from qtpy.QtWidgets import QWidget app = QtWidgets.QApplication(sys.argv) w = QWidget() w.setGeometry(100, 100, 600, 400) w.setWindowTitle('JoyPad') joy = JoyPad() joy.set_icon('L', 'text', 'X-') joy.set_icon('R', 'text', 'X+') joy.set_icon('T', 'text', 'Y+') joy.set_icon('B', 'text', 'Y-') joy.set_icon('C', 'image', 'stop.png') joy.set_tooltip('T', 'This is the top button') joy.set_tooltip('C', 'This is the center button') joy.set_false_color('red') joy.set_highlight('T', True) joy.joy_btn_pressed.connect(joy.btn_pressed) joy.joy_btn_released.connect(joy.btn_released) layout = QtWidgets.QVBoxLayout() layout.addWidget(joy) w.setLayout(layout) w.show() sys.exit( app.exec() )