"""This module contains all the buttons used. A base class AnimBox handles the drawing and animation, inherited by all the buttons. """ import math from PySide2.QtCore import (QAbstractAnimation, Qt, QRectF, QLineF, QPropertyAnimation, Property, Signal) from PySide2.QtGui import QPen, QColor from PySide2.QtWidgets import (QGraphicsObject) from .textbox import AnimatedText class AnimBox(QGraphicsObject): """A Box that draws an outline when hover over. Attributes ---------- hoverEnter: Signal Emitted when the mouse hover into the box hoverExit: Signal Emitted when the mouse hover out of the box """ hoverEnter = Signal() hoverExit = Signal() def __init__(self, x, y, width, height, parent=None): """Prepares the box and animation Parameters ---------- x: float x position of the top-left corner of the box y: float y position of the top-left corner of the box width: float Width of the box height: float Height of the box parent: object Passed into QGraphicsObject init method """ super().__init__(parent=parent) self.x = x self.y = y self.width = width self.height = height self.circumference = 2*(width+height) self.default_pen = QPen() self.default_pen.setColor(Qt.white) self.outline_pen = QPen() self.outline_pen.setColor(Qt.white) self.outline_pen.setWidth(5) self.detected = False # Whether the mouse hover over the box self.btn_rect = QRectF(self.x, self.y, self.width, self.height) self.left = QLineF() self.down = QLineF() self.right = QLineF() self.up = QLineF() self.line_order = [self.up, self.right, self.down, self.left] self.accepted_buttons = Qt.LeftButton self.set_freeze(False) self.length = 0 self.anim = QPropertyAnimation(self, b'length') self.anim.setStartValue(0) for t in range(1, 10): self.anim.setKeyValueAt(t / 10, self.logistic_func(t / 10)) self.anim.setEndValue(self.circumference) def set_freeze(self, freeze): """Set whether the box should accept the mouse events Parameters ---------- freeze: bool True to stop the box from accepting mouse inputs, False otherwise """ if freeze: self.setAcceptedMouseButtons(Qt.NoButton) self.setAcceptHoverEvents(False) else: self.setAcceptedMouseButtons(self.accepted_buttons) self.setAcceptHoverEvents(True) def toggle_anim(self, toggling): """Toggle the highlight animation to be play forward or backward Parameters ---------- toggling: bool True for forward, False for backwards """ if toggling: self.anim.setDirection(QAbstractAnimation.Forward) else: self.anim.setDirection(QAbstractAnimation.Backward) self.anim.start() def logistic_func(self, x): """The logistic function that determines the animation motion Parameters ---------- x: list or numpy array Values to be feed into the function Returns ------- list or numpy array Values of the logistic function corresponding to the input range """ return self.circumference / (1+math.exp(-(x-0.5)*18)) def boundingRect(self): """Reimplemented from QGraphicsObject. """ return QRectF(self.x-5, self.y-5, self.width+10, self.height+10) def paint(self, painter, style, widget=None): """Reimplemented from QGraphicsObject. Draws the Box and the highlights. """ painter.setPen(self.outline_pen) for line in self.line_order: if line.length() > 1: painter.drawLine(line) painter.setPen(self.default_pen) painter.drawRect(self.btn_rect) @Property(float) def length(self): """float: The length of the highlight to be drawn. When set, the length of the outlines are determined """ return self._length @length.setter def length(self, value): self._length = value remaining_length = value if remaining_length >= 2 * self.width + self.height: length_to_draw = remaining_length - (2 * self.width + self.height) remaining_length -= length_to_draw else: length_to_draw = 0 self.line_order[3].setLine(self.x, self.y + self.height, self.x, self.y + self.height - length_to_draw) if remaining_length >= self.width + self.height: length_to_draw = remaining_length - (self.width + self.height) remaining_length -= length_to_draw else: length_to_draw = 0 self.line_order[2].setLine(self.x + self.width, self.y + self.height, self.x + self.width - length_to_draw, self.y + self.height) if remaining_length >= self.width: length_to_draw = remaining_length - self.width remaining_length -= length_to_draw else: length_to_draw = 0 self.line_order[1].setLine(self.x + self.width, self.y, self.x + self.width, self.y + length_to_draw) self.line_order[0].setLine(self.x, self.y, self.x + remaining_length, self.y) self.update() def hoverEnterEvent(self, event): """Reimplemented hoverEnterEvent. Detect the mouse and toggle the animation """ if ~self.detected: self.hoverEnter.emit() self.detected = True self.toggle_anim(True) super().hoverEnterEvent(event) def hoverLeaveEvent(self, event): """Reimplemented hoverLeaveEvent. Detect the mouse leaving and reverse the animation """ if self.detected: self.hoverExit.emit() self.detected = False self.toggle_anim(False) super().hoverLeaveEvent(event) class RingButton(AnimBox): """Button specific to the Number Ring. Contains the function to be transparent Attributes ---------- buttonClicked: Signal(str) Emitted when it is clicked. Sends the text of the button """ buttonClicked = Signal(str, int) # Initialisation def __init__(self, x, y, width, height, text, parent=None): """Set the text and transparency Parameters ---------- text: str Text of the button The remaining parameters are passed into AnimBox init method """ super().__init__(x, y, width, height, parent=parent) self.text = text self.transparent = False self.accepted_buttons = self.accepted_buttons | Qt.RightButton def set_transparent(self, state): """Make the button transparent Parameters ---------- state: bool True for transparent, False otherwise """ self.transparent = state col = self.default_pen.color() if state: col.setAlphaF(0.2) else: col.setAlphaF(1) self.default_pen.setColor(col) self.update() # Reimplemented paint def paint(self, painter, style, widget=None): """Reimplement from AnimBox. Calls for AnimBox paint event first, then draw its background and text. """ super().paint(painter, style, widget) painter.setPen(self.default_pen) if self.transparent: painter.fillRect(self.btn_rect, QColor(255, 255, 255, 0.1)) else: painter.fillRect(self.btn_rect, Qt.black) painter.drawText(self.boundingRect(), Qt.AlignCenter, self.text) def mousePressEvent(self, event): """Reimplemented from QGraphicsObject. Receive the click event, then reverse its animation and emit buttonClicked signal """ event.accept() self.toggle_anim(False) self.buttonClicked.emit(self.text, event.button()) class MenuButton(AnimBox): """Button used in menu. Contains animated text. Attributes ---------- buttonClicked: Signal(str) Emitted when it is clicked. Sends the text of the button """ buttonClicked = Signal(str) def __init__(self, x, y, width, height, text, parent=None): """Set the text and create AnimatedText Parameters ---------- text: str Text of the button The remaining parameters are passed into AnimBox init method """ super().__init__(x, y, width, height, parent=parent) self.text = text self.animText = AnimatedText(text, parent=self) def paint(self, painter, style, widget=None): """Reimplement from AnimBox. Calls for AnimBox paint event first, then draw its background. """ super().paint(painter, style, widget) painter.fillRect(self.btn_rect, Qt.black) def mousePressEvent(self, event): """Reimplemented from QGraphicsObject. Receive the click event, then reverse its animation and emit buttonClicked signal """ event.accept() self.toggle_anim(False) self.buttonClicked.emit(self.text)