Files
linuxcncAllWasm/lib/python/qtvcp/widgets/joypad.py
2026-08-20 06:17:39 +08:00

682 lines
24 KiB
Python

#!/usr/bin/env python3
# Qtvcp Joypad widget
#
# Copyright (c) 2021 Jim Sloot <persei802@gmail.com>
#
# 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() )