示例#1
0
    def paintEvent(self, e):
        """
		
		:param e: 
		:return: 
		"""
        self._is_painting = True
        super(EventsWidget, self).paintEvent(e)

        painter = QPainter()
        painter.begin(self)
        painter.setRenderHint(QPainter.Antialiasing)
        painter.setFont(QFont('Decorative', 8))

        slider_pos = self._scroll.sliderPosition()
        start = slider_pos * self._scale
        end = start + self.width() * self._scale

        for i, track in enumerate(self._tracks):
            if self._break_draw:
                break
            track.draw_periods(painter,
                               start,
                               end,
                               track_index=i,
                               left_shift=-self._scroll.sliderPosition(),
                               scale=self._scale)

        # Draw only from pixel start to end
        painter.setPen(QtCore.Qt.DashLine)
        painter.setOpacity(0.3)

        # print('Draw', start, end, self._scale, self._scroll.sliderPosition(), self.width())

        # Draw vertical lines
        for x in range(start - (start % (100 * self._scale)), end,
                       100 * self._scale):
            x2draw = (x - slider_pos * self._scale) // self._scale
            painter.drawLine(x2draw, 20, x2draw, self.height())
            string = str(x)
            boundtext = painter.boundingRect(QtCore.QRectF(), string)
            painter.drawText(x2draw - boundtext.width() / 2, 15, string)

        for index, track in enumerate(self._tracks):
            top = self.which_top(index)
            # print(top)
            painter.drawLine(0, top, self.width(), top)
            painter.drawText(10, top + 15, track.title)
        painter.setOpacity(1.0)

        self._pointer.draw(painter, left_shift=-slider_pos,
                           scale=self._scale)  # Draw the time pointer
        painter.end()

        self._break_draw = False
        self._is_painting = False
示例#2
0
 def paintEvent(self, e):
     painter = QPainter(self)
     painter.setClipping(True)
     painter.rotate(-90)
     painter.translate(-self.height(), 0)
     flags = Qt.AlignCenter
     self.text_rect = painter.boundingRect(0, 0, self.height(),
                                           self.width(), flags, self.text)
     painter.drawText(self.text_rect, flags, self.text)
     painter.end()
示例#3
0
    def paintEvent(self, event, target=None):
        if not target:
            target = self
        black, white = QColor(0, 0, 0), QColor(255, 255, 255)
        paint = QPainter(target)
        paint.setRenderHint(QPainter.Antialiasing, False)
        paint.setRenderHint(QPainter.TextAntialiasing, False)
        paint.setRenderHint(QPainter.HighQualityAntialiasing, False)
        paint.setBrush(white)
        paint.setPen(white)
        paint.drawRect(0, 0, self.size[0], self.size[1])

        pen = QPen()
        pen.setWidth(2)
        paint.setPen(pen)
        paint.setBrush(black)

        for line in self.document['lines']:
            paint.drawLine(line[0] * self.size[0], line[1] * self.size[1],
                           line[2] * self.size[0], line[3] * self.size[1])
        for text in self.document['text']:
            if not text['text']:
                continue
            flags = 0
            if text['align'] == "center":
                flags |= Qt.AlignCenter
            else:
                flags |= Qt.AlignLeft
            if text['size'] == 0:
                fits = True
                last_size = 1
                current_size = 1
                while fits:
                    last_size = current_size
                    current_size += 1
                    paint.setFont(QFont(text['font'], current_size))
                    rect = paint.boundingRect(text['x'] * self.size[0],
                                              text['y'] * self.size[1],
                                              text['width'] * self.size[0],
                                              text['height'] * self.size[1],
                                              flags, text['text'])
                    if rect.width(
                    ) >= text['width'] * self.size[0] or rect.height(
                    ) >= text['height'] * self.size[1]:
                        fits = False
                paint.setFont(QFont(text['font'], last_size))
            else:
                paint.setFont(QFont(text['font'], text['size']))
            paint.drawText(text['x'] * self.size[0], text['y'] * self.size[1],
                           text['width'] * self.size[0],
                           text['height'] * self.size[1], flags, text['text'])
        paint.end()
示例#4
0
def paint(editor):
    painter = QPainter(editor.viewport())
    font = painter.font()
    font.setPointSize(10)
    painter.setFont(font)
    #Find top and bottom blocks in viewport. We only need to paint these
    topBlock = editor.document().findBlock(
        editor.cursorForPosition(QPoint(0, 0)).position())
    bottomright = editor.viewport().contentsRect().bottomRight()
    bottomBlock = editor.document().findBlock(
        editor.cursorForPosition(bottomright).position())
    number = len(editor.getBlockRects(endBlock=topBlock)) + 1
    boundingbox = painter.boundingRect(editor.contentsRect(), Qt.AlignLeft,
                                       str(number))
    offset = editor.verticalScrollBar().value()
    heights = [
        rect.topLeft().y() - offset
        for rect in editor.getBlockRects(topBlock, bottomBlock, inclusive=True)
    ]
    for height in heights:
        boundingbox = painter.boundingRect(editor.contentsRect(), Qt.AlignLeft,
                                           str(number))
        linebarwidth = boundingbox.width() + 4
        # Set only left margin
        frameformat = editor.document().rootFrame().frameFormat()
        if frameformat.leftMargin() != linebarwidth:
            frameformat.setLeftMargin(linebarwidth)
            editor.document().rootFrame().setFrameFormat(frameformat)
        # TODO: Find why adding 5 to the height makes it correct, ie find the right way to calculate height
        # in the first place
        painter.drawText(0, height + floor((boundingbox.height() / 2)) + 5,
                         str(number))
        number += 1
    painter.setCompositionMode(painter.CompositionMode_DestinationAtop)
    painter.fillRect(
        QRect(0, 0,
              boundingbox.bottomRight().x() + 4,
              editor.contentsRect().bottomRight().y()),
        QBrush(QColor(233, 237, 245, 240)))
示例#5
0
    def _draw(self):
        if self._pixmap is not None:
            return

        self._pixmap = QPixmap(self._rect.width(), self._rect.height())
        self._pixmap.fill(Qt.white)
        painter = QPainter()
        painter.begin(self._pixmap)

        if self._pattern_id:
            pattern = self._theme.get_pattern(self._pattern_id)
            painter.drawPixmap(0, 0, pattern)

        if self.show_frame:
            frame = self._theme.get_frame()
            painter.drawPixmap(0, 0, frame)
        elif self.last_moved:
            frame = self._theme.get_last_moved()
            if frame:
                painter.drawPixmap(0, 0, frame)
        elif self.moveable:
            frame = self._theme.get_moveable()
            if frame:
                painter.drawPixmap(0, 0, frame)

        # notation
        painter.setPen(Qt.white)
        notation_rect = painter.boundingRect(2, 2, 0, 0, Qt.AlignLeft,
                                             self.notation)
        if self.notation_above:
            self._draw_piece(painter)
            if self.show_label:
                painter.fillRect(notation_rect, Qt.black)
                painter.drawText(notation_rect, Qt.AlignTop | Qt.AlignLeft,
                                 self.notation)
        else:
            if self.show_label:
                painter.drawText(notation_rect, Qt.AlignTop | Qt.AlignLeft,
                                 self.notation)
            self._draw_piece(painter)

        if self.captured:
            captured = self._theme.get_captured()
            painter.drawPixmap(0, 0, captured)

        painter.end()
示例#6
0
    def _drawText(self, painter: QPainter, text: str):
        widgetRect: QRect = self.geometry()
        font: QFont = QFontDatabase.systemFont(QFontDatabase.GeneralFont)
        font.setPointSize(18)
        painter.setFont(font)

        textRect: QRect = painter.boundingRect(widgetRect, Qt.AlignCenter,
                                               text)
        # DEBUG: ...
        # painter.setPen(QPen(QBrush(Qt.black), 1))
        # painter.drawRect(textRect)

        palette: QPalette = QApplication.instance().palette()
        painter.setPen(QPen(palette.color(QPalette.Active, QPalette.Dark), 1))
        textRect.moveTo(widgetRect.width() / 2 - textRect.width() / 2,
                        widgetRect.height() / 2 - textRect.height() / 2)
        painter.drawText(textRect, Qt.AlignCenter, text)
示例#7
0
文件: main.py 项目: nanoric/pyqtchart
 def paint(
     self,
     painter: QPainter,
     option: QStyleOptionGraphicsItem,
     widget: QWidget = None,
 ):
     painter.drawRect(self.rect)
     b = self.data.bar_data
     text = (f"open: {b.open}\n"
             f"high: {b.high}\n"
             f"low: {b.low}\n"
             f"close: {b.close}\n"
             f"date: {self.data.x}")
     rect = painter.boundingRect(0.0, 0.0, 1000.0, 1000.0, Qt.AlignLeft,
                                 text)
     painter.drawText(self.rect, Qt.AlignLeft, text)
     self.rect = rect  # fixme: scrolling will make this mismatch
示例#8
0
    def paintEvent(self, event):
        par = self.parent()
        size = min(self.spinner_size, par.width(), par.height())
        dot_count = 5
        dot_size = int(size / dot_count) * 1.5

        r = par.rect()
        spinner_rect = QRect(r.width() / 2 - size / 2,
                             r.height() / 2 - size / 2, size, size)

        painter = QPainter()
        painter.begin(self)
        painter.setPen(QPen(Qt.NoPen))

        for i in range(dot_count):
            if self.counter % dot_count == i:
                painter.setBrush(QBrush(QColor(0, 0, 0)))
                d_size = dot_size * 1.1
            else:
                painter.setBrush(QBrush(QColor(200, 200, 200)))
                d_size = dot_size

            r = size / 2 - dot_size / 2
            x = r * math.cos(2 * math.pi * i / dot_count)
            y = r * math.sin(2 * math.pi * i / dot_count)
            x_center = spinner_rect.left(
            ) + spinner_rect.width() / 2 - dot_size / 2
            y_center = spinner_rect.top(
            ) + spinner_rect.height() / 2 - dot_size / 2
            painter.drawEllipse(x_center + x, y_center + y, d_size, d_size)

        if self.message:
            painter.setPen(QPen(Qt.black))
            if self.font_size:
                f = painter.font()
                f.setPointSize(self.font_size)
                painter.setFont(f)
            spinner_rect.setTop(spinner_rect.bottom() + 3)
            spinner_rect.setHeight(painter.fontMetrics().height() * 1.5)
            r = painter.boundingRect(spinner_rect,
                                     Qt.AlignHCenter | Qt.AlignTop,
                                     self.message)
            painter.drawText(r.bottomLeft(), self.message)

        painter.end()
示例#9
0
文件: axis.py 项目: nanoric/pyqtchart
    def draw_x(self, config: "DrawConfig", painter: QPainter):
        drawing_cache = config.drawing_cache
        text_top = (
            drawing_cache.plot_area.bottom() + 1 + self.axis.label_spacing_to_plot_area
        )

        for text_info in self.data_source:  # type: TextLabelInfo
            ui_x = drawing_cache.drawer_x_to_ui(text_info.value)
            text = text_info.text
            rect = painter.boundingRect(0, 0, 1000, 1000, TEXT_FLAG, text)
            text_width = rect.height()

            align = text_info.align
            if align is Alignment.BEFORE:
                pos = QRectF(ui_x - text_width, text_top, rect.width(), rect.height())
            elif align is Alignment.MID:
                pos = QRectF(
                    ui_x - text_width / 2, text_top, rect.width(), rect.height()
                )
            else:
                pos = QRectF(ui_x, text_top, rect.width(), rect.height())
            painter.drawText(pos, text)
示例#10
0
文件: axis.py 项目: nanoric/pyqtchart
    def draw_y(self, config: "DrawConfig", painter: QPainter):
        drawing_cache = config.drawing_cache

        label_right = (
            drawing_cache.plot_area.left() - 1 - self.axis.label_spacing_to_plot_area
        )

        for text_info in self.data_source:  # type: TextLabelInfo
            ui_y = drawing_cache.drawer_y_to_ui(text_info.value)
            text = text_info.text
            rect: QRectF = painter.boundingRect(0, 0, 1000, 1000, TEXT_FLAG, text)
            label_width = rect.width()
            label_height = rect.height()

            align = text_info.align
            if align is Alignment.BEFORE:
                pos = QRectF(
                    label_right - label_width,
                    ui_y + label_height,
                    rect.width(),
                    rect.height(),
                )
            elif align is Alignment.MID:
                pos = QRectF(
                    label_right - label_width,
                    ui_y - label_height / 2,
                    rect.width(),
                    rect.height(),
                )
            else:
                pos = QRectF(
                    label_right - label_width,
                    ui_y - label_height,
                    rect.width(),
                    rect.height(),
                )
            painter.drawText(pos, text)
示例#11
0
 def paintEvent(self, evt):
     """
     Protected method handling paint events.
     
     @param evt paint event (QPaintEvent)
     """
     painter = QPainter(self)
     
     if self.__time < self.__length:
         pal = QToolTip.palette()
         textBackgroundColor = pal.color(QPalette.Active, QPalette.Base)
         if self.__toggle:
             textColor = pal.color(QPalette.Active, QPalette.Text)
         else:
             textColor = pal.color(QPalette.Active, QPalette.Base)
         painter.setPen(textColor)
         painter.setBrush(textBackgroundColor)
         helpText = self.tr("Snapshot will be taken in %n seconds", "",
                            self.__length - self.__time)
         textRect = painter.boundingRect(
             self.rect().adjusted(2, 2, -2, -2),
             Qt.AlignHCenter | Qt.TextSingleLine, helpText)
         painter.drawText(textRect, Qt.AlignHCenter | Qt.TextSingleLine,
                          helpText)
示例#12
0
 def paintEvent(self, evt):
     """
     Protected method handling paint events.
     
     @param evt paint event (QPaintEvent)
     """
     if self.__grabbing:     # grabWindow() should just get the background
         return
     
     painter = QPainter(self)
     pal = QPalette(QToolTip.palette())
     font = QToolTip.font()
     
     handleColor = pal.color(QPalette.Active, QPalette.Highlight)
     handleColor.setAlpha(160)
     overlayColor = QColor(0, 0, 0, 160)
     textColor = pal.color(QPalette.Active, QPalette.Text)
     textBackgroundColor = pal.color(QPalette.Active, QPalette.Base)
     painter.drawPixmap(0, 0, self.__pixmap)
     painter.setFont(font)
     
     r = QRect(self.__selection)
     if not self.__selection.isNull():
         grey = QRegion(self.rect())
         if self.__mode == SnapshotRegionGrabber.Ellipse:
             reg = QRegion(r, QRegion.Ellipse)
         else:
             reg = QRegion(r)
         grey = grey.subtracted(reg)
         painter.setClipRegion(grey)
         painter.setPen(Qt.NoPen)
         painter.setBrush(overlayColor)
         painter.drawRect(self.rect())
         painter.setClipRect(self.rect())
         drawRect(painter, r, handleColor)
     
     if self.__showHelp:
         painter.setPen(textColor)
         painter.setBrush(textBackgroundColor)
         self.__helpTextRect = painter.boundingRect(
             self.rect().adjusted(2, 2, -2, -2),
             Qt.TextWordWrap, self.__helpText).translated(
             -self.__desktop.x(), -self.__desktop.y())
         self.__helpTextRect.adjust(-2, -2, 4, 2)
         drawRect(painter, self.__helpTextRect, textColor,
                  textBackgroundColor)
         painter.drawText(
             self.__helpTextRect.adjusted(3, 3, -3, -3),
             Qt.TextWordWrap, self.__helpText)
     
     if self.__selection.isNull():
         return
     
     # The grabbed region is everything which is covered by the drawn
     # rectangles (border included). This means that there is no 0px
     # selection, since a 0px wide rectangle will always be drawn as a line.
     txt = "{0:n}, {1:n} ({2:n} x {3:n})".format(
         self.__selection.x(), self.__selection.y(),
         self.__selection.width(), self.__selection.height())
     textRect = painter.boundingRect(self.rect(), Qt.AlignLeft, txt)
     boundingRect = textRect.adjusted(-4, 0, 0, 0)
     
     if textRect.width() < r.width() - 2 * self.__handleSize and \
        textRect.height() < r.height() - 2 * self.__handleSize and \
        r.width() > 100 and \
        r.height() > 100:
         # center, unsuitable for small selections
         boundingRect.moveCenter(r.center())
         textRect.moveCenter(r.center())
     elif r.y() - 3 > textRect.height() and \
             r.x() + textRect.width() < self.rect().width():
         # on top, left aligned
         boundingRect.moveBottomLeft(QPoint(r.x(), r.y() - 3))
         textRect.moveBottomLeft(QPoint(r.x() + 2, r.y() - 3))
     elif r.x() - 3 > textRect.width():
         # left, top aligned
         boundingRect.moveTopRight(QPoint(r.x() - 3, r.y()))
         textRect.moveTopRight(QPoint(r.x() - 5, r.y()))
     elif r.bottom() + 3 + textRect.height() < self.rect().bottom() and \
             r.right() > textRect.width():
         # at bottom, right aligned
         boundingRect.moveTopRight(QPoint(r.right(), r.bottom() + 3))
         textRect.moveTopRight(QPoint(r.right() - 2, r.bottom() + 3))
     elif r.right() + textRect.width() + 3 < self.rect().width():
         # right, bottom aligned
         boundingRect.moveBottomLeft(QPoint(r.right() + 3, r.bottom()))
         textRect.moveBottomLeft(QPoint(r.right() + 5, r.bottom()))
     
     # If the above didn't catch it, you are running on a very
     # tiny screen...
     drawRect(painter, boundingRect, textColor, textBackgroundColor)
     painter.drawText(textRect, Qt.AlignHCenter, txt)
     
     if (r.height() > self.__handleSize * 2 and
         r.width() > self.__handleSize * 2) or \
        not self.__mouseDown:
         self.__updateHandles()
         painter.setPen(Qt.NoPen)
         painter.setBrush(handleColor)
         painter.setClipRegion(
             self.__handleMask(SnapshotRegionGrabber.StrokeMask))
         painter.drawRect(self.rect())
         handleColor.setAlpha(60)
         painter.setBrush(handleColor)
         painter.setClipRegion(
             self.__handleMask(SnapshotRegionGrabber.FillMask))
         painter.drawRect(self.rect())
示例#13
0
    def paintOn(self, device):
        painter = QPainter(device)

        self.font.setPointSize(self.fontSize.value())
        self.font.setBold(self.boldBox.isChecked())
        painter.setFont(self.font)

        self.color.setAlpha(self.textOpacity.value())
        painter.setPen(QPen(self.color))

        text = self.textEdit.toPlainText()

        box = painter.boundingRect(device.rect(), Qt.AlignCenter, text)

        transform = QTransform()
        center = box.center()
        transform.translate(center.x(), center.y())
        transform.rotate(self.textAngle.value())
        transform.translate(-center.x(), -center.y())

        def paintWith(tileform):
            combined = tileform * transform
            if not combined.mapRect(box).intersects(device.rect()):
                return False

            painter.setTransform(combined)
            painter.drawText(box, Qt.AlignCenter, text)
            return True

        def paintTile(x, y):
            tile = QTransform()

            stagger = 0
            if y % 2 and self.staggeredBox.isChecked():
                stagger = 0.5

            tile.translate(
                (stagger + x) * (box.width() + metrics.horizontalAdvance(" ") *
                                 self.widthSpacing.value() / 100),
                y * (box.height() +
                     metrics.lineSpacing() * self.heightSpacing.value() / 100),
            )
            return paintWith(tile)

        # paint on center
        paintWith(QTransform())

        if self.tilingBox.isChecked():
            # paint tiles with increasing distance
            # xxxxx
            # x...x
            # x.o.x
            # x...x
            # xxxxx

            metrics = QFontMetricsF(self.font)

            for radius in range(1, 1000):
                has_painted = False

                for x in range(-radius, radius + 1):
                    has_painted = paintTile(x, -radius) or has_painted
                    has_painted = paintTile(x, radius) or has_painted
                for y in range(-radius + 1, radius):
                    has_painted = paintTile(-radius, y) or has_painted
                    has_painted = paintTile(radius, y) or has_painted

                if not has_painted:
                    # everything was out of bounds, so will the next radius
                    break

        painter.end()
 def paintEvent(self, evt):
     """
     Protected method handling paint events.
     
     @param evt paint event (QPaintEvent)
     """
     if self.__grabbing:     # grabWindow() should just get the background
         return
     
     painter = QPainter(self)
     pal = QPalette(QToolTip.palette())
     font = QToolTip.font()
     
     handleColor = pal.color(QPalette.Active, QPalette.Highlight)
     handleColor.setAlpha(160)
     overlayColor = QColor(0, 0, 0, 160)
     textColor = pal.color(QPalette.Active, QPalette.Text)
     textBackgroundColor = pal.color(QPalette.Active, QPalette.Base)
     painter.drawPixmap(0, 0, self.__pixmap)
     painter.setFont(font)
     
     pol = QPolygon(self.__selection)
     if not self.__selection.boundingRect().isNull():
         # Draw outline around selection.
         # Important: the 1px-wide outline is *also* part of the
         # captured free-region
         pen = QPen(handleColor, 1, Qt.SolidLine, Qt.SquareCap,
                    Qt.BevelJoin)
         painter.setPen(pen)
         painter.drawPolygon(pol)
         
         # Draw the grey area around the selection.
         grey = QRegion(self.rect())
         grey = grey - QRegion(pol)
         painter.setClipRegion(grey)
         painter.setPen(Qt.NoPen)
         painter.setBrush(overlayColor)
         painter.drawRect(self.rect())
         painter.setClipRect(self.rect())
         drawPolygon(painter, pol, handleColor)
     
     if self.__showHelp:
         painter.setPen(textColor)
         painter.setBrush(textBackgroundColor)
         self.__helpTextRect = painter.boundingRect(
             self.rect().adjusted(2, 2, -2, -2),
             Qt.TextWordWrap, self.__helpText).translated(
             -self.__desktop.x(), -self.__desktop.y())
         self.__helpTextRect.adjust(-2, -2, 4, 2)
         drawPolygon(painter, self.__helpTextRect, textColor,
                     textBackgroundColor)
         painter.drawText(
             self.__helpTextRect.adjusted(3, 3, -3, -3),
             Qt.TextWordWrap, self.__helpText)
     
     if self.__selection.isEmpty():
         return
     
     # The grabbed region is everything which is covered by the drawn
     # rectangles (border included). This means that there is no 0px
     # selection, since a 0px wide rectangle will always be drawn as a line.
     boundingRect = self.__selection.boundingRect()
     txt = "{0}, {1} ({2} x {3})".format(
         self.__locale.toString(boundingRect.x()),
         self.__locale.toString(boundingRect.y()),
         self.__locale.toString(boundingRect.width()),
         self.__locale.toString(boundingRect.height())
     )
     textRect = painter.boundingRect(self.rect(), Qt.AlignLeft, txt)
     boundingRect = textRect.adjusted(-4, 0, 0, 0)
     
     polBoundingRect = pol.boundingRect()
     if (textRect.width() <
         polBoundingRect.width() - 2 * self.__handleSize) and \
        (textRect.height() <
         polBoundingRect.height() - 2 * self.__handleSize) and \
        polBoundingRect.width() > 100 and \
        polBoundingRect.height() > 100:
         # center, unsuitable for small selections
         boundingRect.moveCenter(polBoundingRect.center())
         textRect.moveCenter(polBoundingRect.center())
     elif polBoundingRect.y() - 3 > textRect.height() and \
             polBoundingRect.x() + textRect.width() < self.rect().width():
         # on top, left aligned
         boundingRect.moveBottomLeft(
             QPoint(polBoundingRect.x(), polBoundingRect.y() - 3))
         textRect.moveBottomLeft(
             QPoint(polBoundingRect.x() + 2, polBoundingRect.y() - 3))
     elif polBoundingRect.x() - 3 > textRect.width():
         # left, top aligned
         boundingRect.moveTopRight(
             QPoint(polBoundingRect.x() - 3, polBoundingRect.y()))
         textRect.moveTopRight(
             QPoint(polBoundingRect.x() - 5, polBoundingRect.y()))
     elif (polBoundingRect.bottom() + 3 + textRect.height() <
           self.rect().bottom()) and \
             polBoundingRect.right() > textRect.width():
         # at bottom, right aligned
         boundingRect.moveTopRight(
             QPoint(polBoundingRect.right(), polBoundingRect.bottom() + 3))
         textRect.moveTopRight(
             QPoint(polBoundingRect.right() - 2,
                    polBoundingRect.bottom() + 3))
     elif polBoundingRect.right() + textRect.width() + 3 < \
             self.rect().width():
         # right, bottom aligned
         boundingRect.moveBottomLeft(
             QPoint(polBoundingRect.right() + 3, polBoundingRect.bottom()))
         textRect.moveBottomLeft(
             QPoint(polBoundingRect.right() + 5, polBoundingRect.bottom()))
     
     # If the above didn't catch it, you are running on a very
     # tiny screen...
     drawPolygon(painter, boundingRect, textColor, textBackgroundColor)
     painter.drawText(textRect, Qt.AlignHCenter, txt)
     
     if (polBoundingRect.height() > self.__handleSize * 2 and
         polBoundingRect.width() > self.__handleSize * 2) or \
        not self.__mouseDown:
         painter.setBrush(Qt.transparent)
         painter.setClipRegion(QRegion(pol))
         painter.drawPolygon(QPolygon(self.rect()))
示例#15
0
class Renderer(QFrame):
    update_time_signal = pyqtSignal(int)
    pause_signal = pyqtSignal()

    def __init__(self, beatmap, replays, events, start_speed, paint_info, \
        statistic_functions):
        super().__init__()
        self.setMinimumSize(GAMEPLAY_WIDTH + GAMEPLAY_PADDING_WIDTH * 2,
            GAMEPLAY_HEIGHT + GAMEPLAY_PADDING_HEIGHT * 2)
        self.beatmap = beatmap
        # list of timestamps to highlight the frames of in a different color
        self.events = events
        # whether to show some information about each player and their cursors
        self.should_paint_info = paint_info
        # functions to display info for in the visualizer
        self.statistic_functions = statistic_functions
        # whether we should paint the frametime graph
        self.paint_frametime = False
        self.painter = QPainter()
        self.scale = 1
        self.x_offset = 0
        self.y_offset = 0
        # a map of QRect to Player, where the rectangle is the location of the
        # player's info on the screen. Updated every frame (even though it's
        # currently static except for the width, it may differ from frame to
        # frame in the future)
        self.player_info_positions = {}
        # players that have been disabled by the users and we don't want to
        # draw cursor movements for
        self.disabled_players = []

        self.setMouseTracking(True)

        # hitobjs currently on screen
        self.hitobjs_to_draw = []
        # we actually care about more than just the hitobjs on screen when
        # drawing error bar markers, so keep track of those hitobjs here.
        # this will (should) be a superset of ``hitobjs_to_draw``
        self.hitobjs_to_draw_hits_for = []
        # and we care about *less* than the hitobjs in hitobjs_to_draw when
        # drawing judgment indicators, since these disappear sooner. This will
        # be a subset of ``hitobjs_to_draw``.
        # TODO clean up this code, these lists shouldn't exist, instead hitobjs
        # should be marked with ``draw_judgment_indicators_for`` attributes
        # or something
        self.hitobjs_to_draw_judgment_indicators_for = []

        self.use_hr = any(Mod.HR in replay.mods for replay in replays)
        self.use_ez = any(Mod.EZ in replay.mods for replay in replays)
        if beatmap:
            self.hit_objects = beatmap.hit_objects(hard_rock=self.use_hr,
                easy=self.use_ez)
            self.playback_end = self.get_hit_endtime(self.hit_objects[-1])

            ar = beatmap.ar(hard_rock=self.use_hr, easy=self.use_ez)
            od = beatmap.od(hard_rock=self.use_hr, easy=self.use_ez)
            cs = beatmap.cs(hard_rock=self.use_hr, easy=self.use_ez)
            # see https://osu.ppy.sh/help/wiki/Beatmapping/Approach_rate
            if ar <= 5:
                self.preempt = 1200 + 600 * (5 - ar) / 5
                self.fade_in = 800 + 400 * (5 - ar) / 5
            else:
                self.preempt = 1200 - 750 * (ar - 5) / 5
                self.fade_in = 800 - 500 * (ar - 5) / 5

            (hitwindow_50, hitwindow_100, hitwindow_300) = hitwindows(od)
            self.hitwindow_50 = hitwindow_50
            self.hitwindow_100 = hitwindow_100
            self.hitwindow_300 = hitwindow_300

            # how much to scale our error bar by from its 'standard' size, where
            # each ms of error is a pixel.
            self.error_bar_width_factor = ERROR_BAR_WIDTH / (hitwindow_50 * 2)

            self.hitcircle_radius = hitradius(cs)
            # loading stuff
            self.is_loading = True
            self.num_hitobjects = len(self.hit_objects)
            # not fully accurate, but good enough
            self.num_sliders = self.num_hitobjects
            self.sliders_current = 0
            self.thread = threading.Thread(target=self.process_sliders)
            self.thread.start()
            self.has_beatmap = True
        else:
            self.playback_end = 0
            self.is_loading = False
            self.has_beatmap = False

        # if this is nonnull, when we finish loading sliders we will seek to
        # this position. Set in ``seek_to`` if it is called when we're loading
        self.seek_to_when_loaded = None
        # whether the previous frame was a loading frame or not, used to
        # determine when we came out of a loading state
        self.previously_loading = False

        # replay stuff
        self.num_replays = len(replays)
        self.players = []
        for i, replay in enumerate(replays):
            color = QColor().fromHslF(i / self.num_replays, 0.75, 0.5)
            player = Player(replay=replay, pen=QPen(color))
            self.players.append(player)

        self.playback_start = 0
        if self.num_replays > 0:
            self.playback_start = min(min(player.t) for player in self.players)
            self.playback_end = max(max(player.t) for player in self.players)

        # always start at 0, unless our playback_start is negative (meaning we
        # have negative frames)
        self.playback_start = min(self.playback_start, 0)

        # if our hitobjs are hard_rock versions, flip any player *without* hr
        # so they match other hr players.
        if self.use_hr:
            for player in self.players:
                if Mod.HardRock not in player.mods:
                    for d in player.xy:
                        d[1] = 384 - d[1]

        # clock stuff
        self.clock = Timer(start_speed, self.playback_start)
        self.paused = False
        self.play_direction = 1

        # render stuff
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.next_frame_from_timer)
        # 62 fps (1000ms / 60frames but the result can only be a integer)
        self.timer.start(1000/60)

        # black background
        pal = QPalette()
        pal.setColor(QPalette.Background, Qt.black)
        self.setAutoFillBackground(True)
        self.setPalette(pal)

        # Settings that are changeable from the control's setting button.
        # If `True`, don't draw crosses, and draw the line in grey if the user
        # was not pressing any keys in the start frame of that line.
        self.raw_view = False
        self.draw_hitobjects = True
        self.draw_approach_circles = True
        # how many frames for each replay to draw on screen at a time
        self.num_frames_on_screen = 15
        self.only_color_keydowns = False
        self.should_draw_hit_error_bar = True
        # TODO expose this as a setting somewhere? it's not toggleable anywhere
        # currently
        self.should_draw_judgment_indicators = True

        self.next_frame()

        self.hitobj_to_judgments = {}
        cg = KeylessCircleguard()
        self.can_access_judgments = self.num_replays == 1 and cg.map_available(replays[0])
        if self.can_access_judgments:
            r = replays[0]

            self.judgments = cg.judgments(r, beatmap=self.beatmap)
            # associate each hitobject with a judgment. Only hitobjs which are
            # spinners won't be associated in this mapping (since core doesn't
            # generate a judgment for them yet)
            for judgment in self.judgments:
                # use the hitobj time as our key. This will work fine for ranked
                # maps (no two hitobjs can be placed at the same time) but may
                # break for aspire, loved, or crazy graveyarded maps. Needs
                # testing.
                # A way around this is to simply store the slider hitobj in
                # circlecore's hitobjs, or convert core to using slider's
                # hitobjs again (or make them subclasses, or some such)
                self.hitobj_to_judgments[judgment.hitobject.t] = judgment


    def resizeEvent(self, event):
        width = event.size().width() - GAMEPLAY_PADDING_WIDTH * 2
        height = event.size().height() - GAMEPLAY_PADDING_HEIGHT * 2
        y_scale = height / GAMEPLAY_HEIGHT
        x_scale = width / GAMEPLAY_WIDTH
        if GAMEPLAY_WIDTH * y_scale > width:
            self.scale = x_scale
            self.y_offset = (height - GAMEPLAY_HEIGHT * x_scale) / 2
            self.x_offset = 0
        else:
            self.scale = y_scale
            self.y_offset = 0
            self.x_offset = (width - GAMEPLAY_WIDTH * y_scale) / 2

    def _x(self, position):
        return (self.x_offset + GAMEPLAY_PADDING_WIDTH +
            self.scaled_number(position))

    def _y(self, position):
        return (self.y_offset + GAMEPLAY_PADDING_HEIGHT +
            self.scaled_number(position))

    def scaled_point(self, x, y):
        return QPointF(self._x(x), self._y(y))

    def scaled_number(self, n):
        return n * self.scale

    def next_frame_from_timer(self):
        """
        Has the same effect as next_frame except if paused, where it returns.
        This is to allow the back/forward buttons to advance frame by frame
        while still paused (as they connect directly to next and previous
        frame), while still pausing the automatic timer advancement.
        """
        if self.paused:
            # ignore our paused state if we're still loading sliders, or else if
            # we pause before the sliders are done loading we'll deadlock
            # ourselves
            if self.is_loading:
                self.update()
                return
            # if we wanted to seek to somewhere while we were loaded, and we
            # have just come out of a loading state, ignore paused and seek to
            # that position
            if self.previously_loading and self.seek_to_when_loaded:
                self.seek_to(self.seek_to_when_loaded)
                self.previously_loading = False
            return
        self.next_frame()

    def next_frame(self, stepping_backwards=False):
        """
        Prepares the next frame.

        If we have just set our current time to be less than what it was the
        previous time next_frame was called, pass stepping_backwards=True so
        the correct frame can be chosen when searching the frame list.
        """
        # just update the frame if currently loading
        if self.is_loading:
            self.previously_loading = True
            self.update()
            return

        current_time = self.clock.get_time()
        # if we're at the end of the track or are at the beginning of the track
        # (and thus are reversing), pause and dont update
        if current_time > self.playback_end or current_time < self.playback_start:
            self.pause_signal.emit()
            return

        # This is the solution to the issue of stepping forward/backwards
        # getting stuck on certain frames - we can fix it for stepping forward
        # by always preferring the right side when searching our array, but when
        # stepping backwards we need to prefer the left side instead.
        side = "left" if stepping_backwards else "right"
        for player in self.players:
            player.end_pos = np.searchsorted(player.t, current_time, side)
            # for some reason side=right and side=left differ by 1 even when
            # the array has no duplicates, so only account for that in the
            # right side case
            if side == "right":
                player.end_pos -= 1

            player.start_pos = 0
            if player.end_pos >= self.num_frames_on_screen:
                player.start_pos = player.end_pos - self.num_frames_on_screen

        if self.has_beatmap:
            self.get_hitobjects()
        self.update_time_signal.emit(current_time)
        self.update()

    def get_hitobjects(self):
        # get currently visible hitobjects
        current_time = self.clock.get_time()
        found_all = False
        # TODO optimize this by tracking our current hitobj index, this iterates
        # through half the hitobjects of the map on average (O(1) best case and
        # O(n) worst case) which can't be good for performance
        index = 0
        self.hitobjs_to_draw = []
        self.hitobjs_to_draw_hits_for = []
        self.hitobjs_to_draw_judgment_indicators_for = []
        while not found_all:
            current_hitobj = self.hit_objects[index]
            hit_t = current_hitobj.time.total_seconds() * 1000
            if isinstance(current_hitobj, (Slider, Spinner)):
                hit_end = self.get_hit_endtime(current_hitobj) + self.fade_in
            else:
                hit_end = hit_t + self.hitwindow_50 + self.fade_in
            if hit_t > current_time - JUDGMENT_INDICATOR_THRESHOLD:
                self.hitobjs_to_draw_judgment_indicators_for.append(current_hitobj)
            if hit_t > current_time - ERROR_BAR_HIT_THRESHOLD:
                self.hitobjs_to_draw_hits_for.append(current_hitobj)
            if hit_t - self.preempt < current_time < hit_end:
                self.hitobjs_to_draw.append(current_hitobj)

            elif hit_t > current_time:
                found_all = True
            if index == self.num_hitobjects - 1:
                found_all = True
            index += 1

    def paintEvent(self, _event):
        """
        Called whenever self.update() is called
        """
        self.painter.begin(self)
        self.painter.setRenderHint(QPainter.TextAntialiasing, True)
        self.painter.setRenderHint(QPainter.Antialiasing, True)
        self.painter.setPen(PEN_WHITE)
        _pen = self.painter.pen()
        # loading screen
        if self.is_loading:
            if self.thread.is_alive():
                self.draw_loading_screen()
                self.painter.end()
                return
            else:
                self.is_loading = False
                self.clock.reset()
                self.painter.end()
                return
        # beatmap
        if self.has_beatmap:
            self.paint_beatmap()
        # cursors
        for player in self.players:
            self.paint_cursor(player)
        # other info
        self.painter.setPen(_pen)
        self.paint_border()
        if self.should_paint_info:
            self.paint_info()
        if self.paint_frametime:
            self.paint_frametime_graph()
        self.painter.end()

    def paint_border(self):
        PEN_WHITE.setWidth(self.scaled_number(1))
        self.painter.setPen(PEN_WHITE)
        self.painter.setOpacity(0.25)
        self.painter.drawRect(QRectF(self.scaled_point(0, 0),
            self.scaled_point(GAMEPLAY_WIDTH, GAMEPLAY_HEIGHT)))

    def paint_cursor(self, player):
        """
        Draws a cursor.

        Arguments:
            Player player: player to draw the cursor of.
        """
        # don't draw anything if the player is disabled
        if player in self.disabled_players:
            return
        alpha_step = 1 / self.num_frames_on_screen
        pen = player.pen
        width = WIDTH_LINE_RAW_VIEW if self.raw_view else WIDTH_LINE
        pen.setWidth(self.scaled_number(width))
        PEN_HIGHLIGHT.setWidth(self.scaled_number(width))
        self.painter.setPen(pen)
        highlighted_pen = False
        for i in range(player.start_pos, player.end_pos):
            highlight = any((player.t[i + 1] in self.events, player.t[i] in
                self.events))
            if highlight and not highlighted_pen:
                self.painter.setPen(PEN_HIGHLIGHT)
                highlighted_pen = True
            elif not highlight and highlighted_pen:
                self.painter.setPen(pen)
                highlighted_pen = False
            grey_out = False
            # only grey out lines if we're in raw view (crosses are greyed out
            # instead in the normal view)
            if self.raw_view:
                # grey out if we don't have a keypress at the start
                if not bool(player.k[i]):
                    grey_out = True
                # grey out if we're only coloring keydowns and this is not a
                # keydown
                if self.only_color_keydowns and not bool(player.keydowns[i]):
                    grey_out = True
            self.draw_line((i - player.start_pos) * alpha_step, player.xy[i],
                    player.xy[i + 1], grey_out=grey_out)
        pen.setWidth(self.scaled_number(WIDTH_CROSS))
        self.painter.setPen(pen)
        for i in range(player.start_pos, player.end_pos+1):
            alpha = (i - player.start_pos) * alpha_step
            xy = player.xy[i]
            k = player.k[i]
            t = player.t[i]
            highlight = t in self.events
            # grey out only if no keys are held by default
            grey_out = not bool(k)
            # but override if we're only coloring keydowns and this is not a
            # keydown
            if self.only_color_keydowns and not bool(player.keydowns[i]):
                grey_out = True
            self.draw_cross(alpha, xy, grey_out=grey_out, highlight=highlight)
        # reset alpha
        self.painter.setOpacity(1)

    def paint_beatmap(self):
        # draw playfield judgment indicators (yellow/green/blue circles under
        # hitobjs) before drawing hitobjs so they don't cover hitobjs
        # (though to be honest it doesn't make much of a difference either way)

        if self.should_draw_judgment_indicators and self.can_access_judgments:
            for hitobj in self.hitobjs_to_draw_hits_for:
                if isinstance(hitobj, Spinner):
                    continue

                judgment = self.hitobj_to_judgments[self.get_hit_time(hitobj)]

                if judgment.type is JudgmentType.Miss:
                    # misses don't have an intrinsic event time, so just use
                    # their hitobj's time
                    t = judgment.hitobject.time
                else:
                    t = judgment.time

                # don't draw hits that haven't happened yet
                if t <= self.clock.get_time():
                    self.draw_judgment_indicator(hitobj, judgment)
            self.painter.setBrush(BRUSH_BLANK)

        for hitobj in self.hitobjs_to_draw[::-1]:
            self.draw_hitobject(hitobj)

        # only draw hit error bars if there's only one replay
        if self.should_draw_hit_error_bar and self.can_access_judgments:
            self.draw_hit_error_bar()

            for hitobj in self.hitobjs_to_draw_hits_for:
                # core doesn't calculate judgmnets for spinners yet, TODO
                # implement this when core does
                if isinstance(hitobj, Spinner):
                    continue

                judgment = self.hitobj_to_judgments[self.get_hit_time(hitobj)]
                # don't draw any judgment bars for misses
                if judgment.type is JudgmentType.Miss:
                    continue
                # don't draw hits that haven't happened yet
                if judgment.t <= self.clock.get_time():
                    self.draw_hit(hitobj, judgment)

    def paint_info(self):
        """
        Draws various info about the replays in the upper left corner.
        """
        # our current y coordinate for drawing info. Modified throughout this
        # function
        y = 15

        PEN_WHITE.setWidth(1)
        self.painter.setPen(PEN_WHITE)
        self.painter.setOpacity(1)
        ms = round(self.clock.get_time())
        text = f"{ms}"
        self.painter.drawText(5, y, text)
        # we don't use a monospaced font, so our ms text may vary by as much as
        # 10 pixels in width (possibly more, haven't checked rigorously). If
        # we just drew our minute:seconds text directly after it, the position
        # of that text would constantly flicker left and right, since the ms
        # text (and thus its width) changes every single frame. To fix this,
        # only increment widths in multiples of 10. (This will not fix the issue
        # if the text width happens to hover exactly around a multiple of 10,
        # but there's not much to be done in that case).
        text_width = self.painter.boundingRect(5, y, 0, 0, 0, text).width()
        if text_width < 50:
            x = 50
        elif text_width < 60:
            x = 60
        elif text_width < 70:
            x = 70
        elif text_width < 80:
            x = 80
        else:
            # something crazy is going on, give up and just use text_width
            x = text_width

        # now some dirty code to deal with negattive times
        minutes = int(ms / (1000 * 60))
        seconds = ms // 1000
        seconds_negative = seconds < 0
        # pytohn modulo returns positive even when ``seconds_total`` is negative
        seconds = seconds % 60
        if seconds_negative:
            # ``seconds`` can be 0 and 59 but not 60, so use 59 instead of 60
            seconds = 59 - seconds
        sign = ""
        if minutes < 0 or seconds_negative:
            sign = "-"
            minutes = abs(minutes)
            seconds = abs(seconds)

        self.painter.drawText(5 + 4 + x, y,
            f"ms ({sign}{minutes:01}:{seconds:02})")

        self.player_info_positions = {}
        if self.num_replays > 0:
            for player in self.players:
                def _set_opacity(opacity):
                    if player in self.disabled_players:
                        opacity /= 2.4
                    self.painter.setOpacity(opacity)

                y += 13
                pen = player.pen
                self.painter.setPen(PEN_BLANK)
                self.painter.setBrush(QBrush(pen.color()))
                keys = Key(int(player.k[player.end_pos]))
                _set_opacity(1 if Key.M1 in keys else 0.3)
                self.painter.drawRect(5, y - 9, 10, 10)
                _set_opacity(1 if Key.M2 in keys else 0.3)
                self.painter.drawRect(18, y - 9, 10, 10)
                _set_opacity(1)
                self.painter.setPen(pen)
                info_text = (f"{player.username} {player.mods.short_name()}: "
                    f"{player.xy[player.end_pos][0]:.2f}, "
                    f"{player.xy[player.end_pos][1]:.2f}")
                self.painter.drawText(31, y, info_text)
                # not sure why we need to do ``y - 9`` instead of 9 here,
                # our ``drawText`` call is perfectly happy to accept ``y`` but
                # we need to pass ``y - 9`` to our ``drawRect`` calls...maybe 9
                # was a manually determined number that causes the text to align
                # with the drawn boxes?
                info_pos = self.painter.boundingRect(5, y - 9, 0, 0, 0,
                    info_text)
                info_pos = Rect(info_pos.x(), info_pos.y(), info_pos.width(),
                    info_pos.height())
                # unfortunately the rects overlap if we don't make this manual
                # adjustment; would like to figure out why but this works for
                # now.
                info_pos.height -= 3
                # our bounding rect starts at 5 but the text starts at 31, so
                # we need to increase the width by the difference to account
                info_pos.width += 31 - 5
                self.player_info_positions[info_pos] = player

            self.painter.setOpacity(1)
            self.painter.setPen(PEN_WHITE)
            if self.num_replays == 2:
                try:
                    y += 13
                    p1 = self.players[0]
                    p2 = self.players[1]
                    distance = math.sqrt(((p1.xy[p1.end_pos][0] - p2.xy[p2.end_pos][0]) ** 2) +
                                         ((p1.xy[p1.end_pos][1] - p2.xy[p2.end_pos][1]) ** 2))
                    self.painter.drawText(5, y, f"{int(distance)}px apart")
                except IndexError:
                    # we may only have data from one cursor at the moment
                    pass

            if self.num_replays == 1 and self.has_beatmap:
                y += 13
                player = self.players[0]
                current_t = timedelta(milliseconds=int(self.clock.get_time()))
                closest_hitobj = self.beatmap.closest_hitobject(current_t)
                if self.use_hr:
                    closest_hitobj = closest_hitobj.hard_rock
                distance = self.distance_between(player.xy[player.end_pos],
                    closest_hitobj)

                # show "x px inside hitobj" instead of a negative distance
                inside = False
                if distance < 0:
                    inside = True
                    distance = abs(distance)

                inside_from = "inside" if inside else "from"
                text = f"{distance:0.2f}px {inside_from} closest hitobj"
                self.painter.drawText(5, y, text)

            for function in self.statistic_functions:
                y += 13
                xys = [player.xy for player in self.players]
                indices = [player.end_pos for player in self.players]
                result = function(xys, indices)
                self.painter.drawText(5, y, f"{function.__name__}: {result}")


    def draw_line(self, alpha, start, end, grey_out=False):
        """
        Draws a line at the given alpha level from the start point to the end
        point.

        Arguments:
            Float alpha: The alpha level (from 0.0 to 1.0) to set the line to.
            List start: The X&Y position of the start of the line.
            List end: The X&Y position of the end of the line.
            Boolean grey_out: Whether to grey out the line or not.
        """
        if grey_out:
            prev_pen = self.painter.pen()
            PEN_GREY_INACTIVE.setWidth(self.scaled_number(WIDTH_LINE_RAW_VIEW))
            self.painter.setPen(PEN_GREY_INACTIVE)

        self.painter.setOpacity(alpha)
        self.painter.drawLine(self.scaled_point(start[0], start[1]),
            self.scaled_point(end[0], end[1]))

        if self.raw_view and grey_out:
            self.painter.setPen(prev_pen)

    def draw_cross(self, alpha, point, grey_out, highlight):
        """
        Draws a cross.

        Args:
           Float alpha: The alpha level from 0.0-1.0 to set the cross to.
           List point: The X and Y position of the cross.
           Boolean grey_out: Whether to grey out the cross or not.
           Boolean highlight: Whether to highlight the cross or not. This takes
               precedence over ``grey_out`` if both are set.
        """
        # crosses can clutter the screen sometimes, don't draw them if raw view
        # is on
        if self.raw_view:
            return
        prev_pen = None
        if highlight:
            prev_pen = self.painter.pen()
            PEN_HIGHLIGHT.setWidth(self.scaled_number(WIDTH_CROSS))
            self.painter.setPen(PEN_HIGHLIGHT)
        elif grey_out:
            prev_pen = self.painter.pen()
            PEN_GREY_INACTIVE.setWidth(self.scaled_number(WIDTH_CROSS))
            self.painter.setPen(PEN_GREY_INACTIVE)
        half_width = LENGTH_CROSS/2
        x = point[0]
        y = point[1]
        x1 = x + half_width
        x2 = x - half_width
        y1 = y + half_width
        y2 = y - half_width

        self.draw_line(alpha, [x1, y1], [x2, y2])
        self.draw_line(alpha, [x2, y1], [x1, y2])
        if grey_out or highlight:
            self.painter.setPen(prev_pen)


    def draw_hitobject(self, hitobj):
        """
        Calls the corresponding function to draw ``hitobj``.
        """
        if not self.draw_hitobjects:
            return
        if isinstance(hitobj, Circle):
            self.draw_hitcircle(hitobj)
            self.draw_approachcircle(hitobj)
        if isinstance(hitobj, Slider):
            self.draw_slider(hitobj)
        if isinstance(hitobj, Spinner):
            self.draw_spinner(hitobj)

    def draw_hitcircle(self, hitobj):
        """
        Draws a circle hitobject.
        """
        current_time = self.clock.get_time()
        fade_out = max(0, ((current_time - self.get_hit_time(hitobj)) / self.hitwindow_50))
        opacity = min(1, ((current_time - (self.get_hit_time(hitobj) - self.preempt)) / self.fade_in))
        opacity = max(0, min(1, opacity-fade_out))
        p = hitobj.position

        # the pen width grows outwards and inwards equally (preferring outwards
        # if the width is odd I think), so we need to tell it to start drawing
        # half of the pen's width away from the radius for the final circle to
        # have radius `self.hitcircle_radius`.
        r = self.scaled_number(self.hitcircle_radius - WIDTH_CIRCLE_BORDER / 2)

        # normal white hitobj
        pen = PEN_WHITE
        brush = BRUSH_GRAY

        if self.can_access_judgments:
            judgment = self.hitobj_to_judgments[self.get_hit_time(hitobj)]
            if judgment.type is JudgmentType.Miss:
                # hitobj was missed, tint red
                pen = PEN_RED_TINT
                brush = BRUSH_GRAY_RED_TINT

        pen.setWidth(self.scaled_number(WIDTH_CIRCLE_BORDER))
        self.painter.setPen(pen)
        self.painter.setOpacity(opacity)
        self.painter.setBrush(brush)

        self.painter.drawEllipse(self.scaled_point(p.x, p.y), r, r)
        self.painter.setBrush(BRUSH_BLANK)

    def draw_spinner(self, hitobj):
        """
        Draws a spinner hitobject.
        """
        current_time = self.clock.get_time()
        if self.get_hit_endtime(hitobj) - current_time < 0:
            return
        radius = GAMEPLAY_HEIGHT / 2
        fade_out = max(0, ((current_time - self.get_hit_endtime(hitobj)) / self.hitwindow_50))
        opacity = min(1, ((current_time - (self.get_hit_time(hitobj) - self.preempt)) / self.fade_in))
        opacity = max(0, min(1, opacity-fade_out))
        scale = min(1, (self.get_hit_endtime(hitobj) - current_time) / (self.get_hit_endtime(hitobj) - self.get_hit_time(hitobj)))
        radius = radius * scale

        PEN_WHITE.setWidth(self.scaled_number(WIDTH_CIRCLE_BORDER / 2))
        self.painter.setPen(PEN_WHITE)
        self.painter.setOpacity(opacity)
        self.painter.drawEllipse(self.scaled_point(GAMEPLAY_WIDTH / 2, GAMEPLAY_HEIGHT / 2),
            self.scaled_number(radius), self.scaled_number(radius))

    def draw_approachcircle(self, hitobj):
        """
        Draws the approach circle of a circle hitobject.
        """
        if not self.draw_approach_circles:
            return
        current_time = self.clock.get_time()
        if self.get_hit_time(hitobj) - current_time < 0:
            return
        opacity = min(1, ((current_time - (self.get_hit_time(hitobj) - self.preempt)) / self.fade_in))
        opacity = max(0, min(1, opacity))
        scale = max(1, ((self.get_hit_time(hitobj) - current_time) / self.preempt) * 3 + 1)
        p = hitobj.position
        r = self.scaled_number(self.hitcircle_radius * scale)

        pen = PEN_WHITE

        if self.can_access_judgments:
            judgment = self.hitobj_to_judgments[self.get_hit_time(hitobj)]
            if judgment.type is JudgmentType.Miss:
                # hitobj was missed, tint red
                pen = PEN_RED_TINT

        pen.setWidth(self.scaled_number(WIDTH_CIRCLE_BORDER / 2))
        self.painter.setPen(pen)
        self.painter.setOpacity(opacity)
        self.painter.drawEllipse(self.scaled_point(p.x, p.y), r, r)

    def draw_slider(self, hitobj):
        """
        Draws sliderbody, hitcircle, approachcircle if needed
        """
        self.draw_sliderbody(hitobj)
        self.draw_hitcircle(hitobj)
        self.draw_approachcircle(hitobj)

    def draw_sliderbody(self, hitobj):
        """
        Draws the sliderbody of a slider using a QpainterPath.
        """

        current_time = self.clock.get_time()
        fade_out = max(0, ((current_time - self.get_hit_endtime(hitobj)) / self.hitwindow_50))
        opacity = min(1, ((current_time - (self.get_hit_time(hitobj) - self.preempt)) / self.fade_in))
        opacity = max(0, min(1, opacity-fade_out)) * 0.75
        p = hitobj.position

        PEN_GRAY.setWidth(self.scaled_number(self.hitcircle_radius * 2))
        PEN_GRAY.setCapStyle(Qt.RoundCap)
        PEN_GRAY.setJoinStyle(Qt.RoundJoin)
        self.painter.setPen(PEN_GRAY)
        self.painter.setOpacity(opacity)

        sliderbody = QPainterPath()
        sliderbody.moveTo(self.scaled_point(p.x, p.y))
        for i in hitobj.slider_body:
            sliderbody.lineTo(self.scaled_point(i.x, i.y))
        self.painter.drawPath(sliderbody)

    def draw_hit_error_bar(self):
        mid_x = GAMEPLAY_WIDTH / 2
        y = GAMEPLAY_HEIGHT - ERROR_BAR_HIT_HEIGHT

        # draw the center white bar
        self.painter.setPen(PEN_WHITE)
        pen = self.painter.pen()
        pen.setWidth(ERROR_BAR_HIT_WIDTH)
        self.painter.setPen(pen)
        self.draw_line(1, [mid_x, y - ERROR_BAR_HIT_HEIGHT],
            [mid_x, y + ERROR_BAR_HIT_HEIGHT])

        # draw the three error zones as slightly transparent
        self.painter.setOpacity(0.65)
        self.painter.setPen(PEN_BLANK)

        hw300 = self.hitwindow_300 * self.error_bar_width_factor
        hw100 = self.hitwindow_100 * self.error_bar_width_factor
        hw50 = self.hitwindow_50 * self.error_bar_width_factor

        self.painter.setBrush(BRUSH_BLUE)
        p1 = self.scaled_point(mid_x - hw300, y - ERROR_BAR_HEIGHT)
        p2 = self.scaled_point(mid_x + hw300, y + ERROR_BAR_HEIGHT)
        self.painter.drawRect(QRectF(p1, p2))

        # draw two rects to avoid overlapping with hitwindow_300 in the center
        self.painter.setBrush(BRUSH_GREEN)
        p1 = self.scaled_point(mid_x - hw100, y - ERROR_BAR_HEIGHT)
        p2 = self.scaled_point(mid_x - hw300, y + ERROR_BAR_HEIGHT)
        self.painter.drawRect(QRectF(p1, p2))
        p1 = self.scaled_point(mid_x + hw300, y - ERROR_BAR_HEIGHT)
        p2 = self.scaled_point(mid_x + hw100, y + ERROR_BAR_HEIGHT)
        self.painter.drawRect(QRectF(p1, p2))

        self.painter.setBrush(BRUSH_YELLOW)
        p1 = self.scaled_point(mid_x - hw50, y - ERROR_BAR_HEIGHT)
        p2 = self.scaled_point(mid_x - hw100, y + ERROR_BAR_HEIGHT)
        self.painter.drawRect(QRectF(p1, p2))
        p1 = self.scaled_point(mid_x + hw100, y - ERROR_BAR_HEIGHT)
        p2 = self.scaled_point(mid_x + hw50, y + ERROR_BAR_HEIGHT)
        self.painter.drawRect(QRectF(p1, p2))

        self.painter.setBrush(BRUSH_BLANK)
        self.painter.setOpacity(1)

    def draw_hit(self, hitobj, hit):
        # TODO: avoid duplication in these constants between this and
        # `draw_hit_error_bar` - maybe just extract to globals?
        mid_x = GAMEPLAY_WIDTH / 2
        y = GAMEPLAY_HEIGHT - ERROR_BAR_HIT_HEIGHT

        if hit.type is JudgmentType.Hit300:
            pen = PEN_BLUE
        elif hit.type is JudgmentType.Hit100:
            pen = PEN_GREEN
        elif hit.type is JudgmentType.Hit50:
            pen = PEN_YELLOW

        self.painter.setPen(pen)
        pen = self.painter.pen()
        pen.setWidth(ERROR_BAR_HIT_WIDTH)
        self.painter.setPen(pen)

        current_time = self.clock.get_time()

        # positive is a late hit, negative is an early hit
        error = (hit.t - self.get_hit_time(hitobj)) * self.error_bar_width_factor
        start = [mid_x + error, y - ERROR_BAR_HIT_HEIGHT]
        end = [mid_x + error, y + ERROR_BAR_HIT_HEIGHT]

        time_passed = current_time - hit.t
        # how long in ms to display hits for before they disappear
        time_passed = min(time_passed, ERROR_BAR_HIT_THRESHOLD)
        # draw most recent hits as more visible (higher alpha) and old hits
        # as less visible (lower alpha)
        x_interp = [0, ERROR_BAR_HIT_THRESHOLD]
        y_interp = [1, 0]
        f = interpolate.interp1d(x_interp, y_interp)
        alpha = f(time_passed)
        self.draw_line(alpha, start, end)

    def draw_judgment_indicator(self, hitobj, judgment):
        if judgment.type is JudgmentType.Hit300:
            # don't draw anything for 300s
            return

        if judgment.type is JudgmentType.Miss:
            brush = BRUSH_JUDGMENT_MISS
            judgment_t = judgment.hitobject.time
        else:
            judgment_t = judgment.time
        if judgment.type is JudgmentType.Hit50:
            brush = BRUSH_JUDGMENT_50
        if judgment.type is JudgmentType.Hit100:
            brush = BRUSH_JUDGMENT_100

        self.painter.setPen(PEN_BLANK)
        self.painter.setBrush(brush)

        current_time = self.clock.get_time()

        time_passed = current_time - judgment_t
        time_passed = min(time_passed, JUDGMENT_INDICATOR_THRESHOLD)
        x_interp = [0, JUDGMENT_INDICATOR_THRESHOLD]
        y_interp = [1, 0]
        f = interpolate.interp1d(x_interp, y_interp)
        alpha = f(time_passed)

        self.painter.setOpacity(alpha)

        p = hitobj.position
        r = self.scaled_number(JUDGMENT_INDICATOR_RADIUS)
        self.painter.drawEllipse(self.scaled_point(p.x, p.y), r, r)

    def draw_progressbar(self, percentage):
        loading_bg = QPainterPath()
        loading_bar = QPainterPath()
        c = self.painter.pen().color()

        _pen = self.painter.pen()
        _pen.setWidth(5)
        _pen.setCapStyle(Qt.RoundCap)
        _pen.setJoinStyle(Qt.RoundJoin)
        _pen.setColor(QColor(c.red(), c.green(), c.blue(), 25))
        self.painter.setPen(_pen)

        loading_bg.moveTo(self.width()/2 - 75, self.height() / 2)
        loading_bg.lineTo(self.width()/2 - 75 + 150, self.height() / 2)

        loading_bar.moveTo(self.width() / 2 - 75, self.height() / 2)
        loading_bar.lineTo(self.width() / 2 - 75 + percentage * 1.5,
            self.height() / 2)

        self.painter.drawPath(loading_bg)
        _pen.setColor(QColor(c.red(), c.green(), c.blue(), 255))
        self.painter.setPen(_pen)
        self.painter.drawPath(loading_bar)

    def draw_loading_screen(self):
        x = self.width() / 2 - 75
        y = self.height() / 2 - 10
        self.painter.drawText(x, y, "Calculating Sliders, please wait...")
        progress = int((self.sliders_current / self.num_sliders) * 100)
        self.draw_progressbar(progress)

    def process_sliders(self):
        for i, hitobj in enumerate(self.hit_objects):
            self.sliders_current = i
            if isinstance(hitobj, Slider):
                steps = max(2, int((self.get_hit_endtime(hitobj) - self.get_hit_time(hitobj)) / SLIDER_TICKRATE))
                hitobj.slider_body = [hitobj.curve(i / steps) for i in range(steps + 1)]

    def search_nearest_frame(self, reverse=False):
        """
        Args
            Boolean reverse: whether to search backwards or forwards through
                time
        """
        if not reverse:
            next_frames = []
            for player in self.players:
                pos = player.end_pos + 1
                # stay at the end of the replay, avoid index error
                if pos == len(player.xy):
                    pos -= 1
                next_frames.append(player.t[pos])
            # if we're only visualizing a beatmap and there's no replays, and
            # someone tries to advance or retreat frames, min() / max() will
            # crash because next_frames is empty, so avoid this.
            if not next_frames:
                return
            self.seek_to(min(next_frames))
        else:
            prev_frames = []
            for player in self.players:
                pos = player.end_pos - 1
                # stay at the beginning of the replay, don't wrap around to end
                if pos == -1:
                    pos += 1
                prev_frames.append(player.t[pos])
            if not prev_frames:
                return
            self.seek_to(max(prev_frames), seeking_backwards=True)

    def seek_to(self, position, seeking_backwards=False):
        """
        Seeks to position if the change is bigger than ± 10.
        Also calls next_frame() so the correct frame is displayed.

        Args:
            Integer position: position to seek to in ms
            Boolean seeking_backwards: Whether we're seeking to a time before
                our current time.
        """
        self.clock.time_counter = position
        # if we want to seek somewhere while we're loading sliders, we store
        # that position so we can seek to it when loaded
        if self.is_loading:
            self.seek_to_when_loaded = position
        if self.paused:
            self.next_frame(stepping_backwards=seeking_backwards)

    def wheelEvent(self, event):
        # from the qt docs on pixelDelta: "This value is provided on platforms
        # that support high-resolution pixel-based delta values, such as macOS".
        # Since not every OS provides pixelDelta, we should use it if possible
        # but fall back to angleDelta. From my testing (sample size 1)
        # pixelDelta will have both x and y as zero if it's unsupported.
        if event.pixelDelta().x() == 0 and event.pixelDelta().y() == 0:
            # check both x and y to support users scrolling either vertically or
            # horizontally to move the timeline, just respect whichever is
            # greatest for that event.
            # this /5 is an arbitrary value to slow down scrolling to what
            # feels reasonable. TODO expose as a setting to the user ("scrolling
            # sensitivity")
            delta = max(event.angleDelta().x(), event.angleDelta().y(), key=abs) / 5
        else:
            delta = max(event.angleDelta().x(), event.angleDelta().y(), key=abs)

        self.seek_to(self.clock.time_counter + delta)

    def mouseMoveEvent(self, event):
        any_inside = False
        for rect in self.player_info_positions:
            qrect = rect.toQRect()
            if qrect.contains(event.pos()):
                any_inside = True
                self.setCursor(QCursor(Qt.PointingHandCursor))
        if not any_inside:
            self.setCursor(QCursor(Qt.ArrowCursor))
        return super().mouseMoveEvent(event)

    def mousePressEvent(self, event):
        for rect in self.player_info_positions:
            qrect = rect.toQRect()
            if qrect.contains(event.pos()):
                player = self.player_info_positions[rect]
                # toggle its membership in disabled_players, so users can click
                # a second time to re-enable a player
                if player in self.disabled_players:
                    self.disabled_players.remove(player)
                else:
                    self.disabled_players.append(player)
                self.update()
        return super().mousePressEvent(event)

    def get_hit_endtime(self, hitobj):
        if isinstance(hitobj, Circle):
            return self.get_hit_time(hitobj)
        t = hitobj.end_time.total_seconds() * 1000
        return int(round(t))

    def get_hit_time(self, hitobj):
        t = hitobj.time.total_seconds() * 1000
        # Due to floating point errors, ``t`` could actually be something
        # like ``129824.99999999999`` or ``128705.00000000001``, so round to the
        # nearest int.
        return int(round(t))

    def pause(self):
        """
        Set paused flag and pauses the clock.
        """
        self.paused = True
        self.clock.pause()

    def resume(self):
        """
        Removes paused flag and resumes the clock.
        """
        self.paused = False
        self.clock.resume()

    def toggle_frametime(self):
        self.paint_frametime = not self.paint_frametime

    def distance_between(self, point, hitobject):
        """
        The shortest distance between the given point and hitobject.
        """
        # TODO use numpy for these calculations
        x1 = point[0]
        y1 = point[1]
        x2 = hitobject.position.x
        y2 = hitobject.position.y
        r = self.hitcircle_radius
        return math.sqrt((((x2 - x1) ** 2) + (y2 - y1) ** 2)) - r

    def raw_view_changed(self, new_state):
        self.raw_view = new_state
        # redraw everything for the new raw view
        self.update()

    def only_color_keydowns_changed(self, new_state):
        self.only_color_keydowns = new_state
        self.update()

    def hitobjects_changed(self, new_state):
        self.draw_hitobjects = new_state
        self.update()

    def approach_circles_changed(self, new_state):
        self.draw_approach_circles = new_state
        self.update()

    def num_frames_changed(self, new_value):
        self.num_frames_on_screen = new_value
        self.update()

    def draw_hit_error_bar_changed(self, new_value):
        self.should_draw_hit_error_bar = new_value
        self.update()

    def circle_size_mod_changed(self, new_value):
        if not self.has_beatmap:
            # cs doesn't matter to us if we don't have a beatmap (and we don't
            # have the attributes necessary to compute it anyway)
            return
        use_hr = new_value == "HR"
        use_ez = new_value == "EZ"
        cs = self.beatmap.cs(hard_rock=use_hr, easy=use_ez)
        self.hitcircle_radius = hitradius(cs)
        self.update()
    def paintEvent(self, evt):
        """
        Protected method handling paint events.
        
        @param evt paint event (QPaintEvent)
        """
        if self.__grabbing:  # grabWindow() should just get the background
            return

        painter = QPainter(self)
        pal = QPalette(QToolTip.palette())
        font = QToolTip.font()

        handleColor = pal.color(QPalette.Active, QPalette.Highlight)
        handleColor.setAlpha(160)
        overlayColor = QColor(0, 0, 0, 160)
        textColor = pal.color(QPalette.Active, QPalette.Text)
        textBackgroundColor = pal.color(QPalette.Active, QPalette.Base)
        painter.drawPixmap(0, 0, self.__pixmap)
        painter.setFont(font)

        r = QRect(self.__selection)
        if not self.__selection.isNull():
            grey = QRegion(self.rect())
            if self.__mode == SnapshotRegionGrabber.Ellipse:
                reg = QRegion(r, QRegion.Ellipse)
            else:
                reg = QRegion(r)
            grey = grey.subtracted(reg)
            painter.setClipRegion(grey)
            painter.setPen(Qt.NoPen)
            painter.setBrush(overlayColor)
            painter.drawRect(self.rect())
            painter.setClipRect(self.rect())
            drawRect(painter, r, handleColor)

        if self.__showHelp:
            painter.setPen(textColor)
            painter.setBrush(textBackgroundColor)
            self.__helpTextRect = painter.boundingRect(
                self.rect().adjusted(2, 2, -2, -2), Qt.TextWordWrap,
                self.__helpText).translated(-self.__desktop.x(),
                                            -self.__desktop.y())
            self.__helpTextRect.adjust(-2, -2, 4, 2)
            drawRect(painter, self.__helpTextRect, textColor,
                     textBackgroundColor)
            painter.drawText(self.__helpTextRect.adjusted(3, 3, -3, -3),
                             Qt.TextWordWrap, self.__helpText)

        if self.__selection.isNull():
            return

        # The grabbed region is everything which is covered by the drawn
        # rectangles (border included). This means that there is no 0px
        # selection, since a 0px wide rectangle will always be drawn as a line.
        txt = "{0}, {1} ({2} x {3})".format(
            self.__locale.toString(self.__selection.x()),
            self.__locale.toString(self.__selection.y()),
            self.__locale.toString(self.__selection.width()),
            self.__locale.toString(self.__selection.height()))
        textRect = painter.boundingRect(self.rect(), Qt.AlignLeft, txt)
        boundingRect = textRect.adjusted(-4, 0, 0, 0)

        if textRect.width() < r.width() - 2 * self.__handleSize and \
           textRect.height() < r.height() - 2 * self.__handleSize and \
           r.width() > 100 and \
           r.height() > 100:
            # center, unsuitable for small selections
            boundingRect.moveCenter(r.center())
            textRect.moveCenter(r.center())
        elif r.y() - 3 > textRect.height() and \
                r.x() + textRect.width() < self.rect().width():
            # on top, left aligned
            boundingRect.moveBottomLeft(QPoint(r.x(), r.y() - 3))
            textRect.moveBottomLeft(QPoint(r.x() + 2, r.y() - 3))
        elif r.x() - 3 > textRect.width():
            # left, top aligned
            boundingRect.moveTopRight(QPoint(r.x() - 3, r.y()))
            textRect.moveTopRight(QPoint(r.x() - 5, r.y()))
        elif r.bottom() + 3 + textRect.height() < self.rect().bottom() and \
                r.right() > textRect.width():
            # at bottom, right aligned
            boundingRect.moveTopRight(QPoint(r.right(), r.bottom() + 3))
            textRect.moveTopRight(QPoint(r.right() - 2, r.bottom() + 3))
        elif r.right() + textRect.width() + 3 < self.rect().width():
            # right, bottom aligned
            boundingRect.moveBottomLeft(QPoint(r.right() + 3, r.bottom()))
            textRect.moveBottomLeft(QPoint(r.right() + 5, r.bottom()))

        # If the above didn't catch it, you are running on a very
        # tiny screen...
        drawRect(painter, boundingRect, textColor, textBackgroundColor)
        painter.drawText(textRect, Qt.AlignHCenter, txt)

        if (r.height() > self.__handleSize * 2 and
            r.width() > self.__handleSize * 2) or \
           not self.__mouseDown:
            self.__updateHandles()
            painter.setPen(Qt.NoPen)
            painter.setBrush(handleColor)
            painter.setClipRegion(
                self.__handleMask(SnapshotRegionGrabber.StrokeMask))
            painter.drawRect(self.rect())
            handleColor.setAlpha(60)
            painter.setBrush(handleColor)
            painter.setClipRegion(
                self.__handleMask(SnapshotRegionGrabber.FillMask))
            painter.drawRect(self.rect())
    def paintEvent(self, e):
        # call the base implementation to paint normal interface
        QWidget.paintEvent(self, e)
        draw = QPainter()
        draw.begin(self)

        h = self.height() - 1
        diff = (self._higher - self._lower) * self.scale

        try:
            self._step = float(self.width()) / float(diff)
        except ZeroDivisionError:
            self._step = 0
        x_start = (self._minVal - self._lower) * self._step * self.scale
        x_end = (self._maxVal - self._lower) * self._step * self.scale

        draw.setOpacity(1.0)
        draw.setBrush(QtCore.Qt.NoBrush)
        draw.setPen(QColor(200, 200, 255))

        for i in range(int(self.width() / 5),
                       int(self.width() - self.width() / 5) + 1,
                       int(self.width() / 5)):
            draw.drawLine(i, 0, i, h)

        draw.setBrush(QColor(238, 238, 238))
        draw.setPen(QColor(238, 238, 238))
        draw.drawRoundedRect(0, 2, self.width(), h - 4, 3, 3)

        draw.setBrush(QColor(33, 133, 208))
        draw.setPen(QColor(33, 133, 208))
        draw.drawRoundedRect(int(round(x_start)), 2,
                             int(round(x_end - x_start)), h - 4, 3, 3)
        # draw.setOpacity(1.0)
        draw.setFont(QFont('Decorative', 8))
        draw.setPen(QColor(80, 80, 80))

        str(self._maxVal) if self._use_float else str(int(round(self._maxVal)))

        boundtext = draw.boundingRect(
            QtCore.QRectF(),
            str(self._higher) if self._use_float else str(
                int(round(self._higher))))
        draw.drawText(
            self.width() - boundtext.width(), 14,
            str(self._higher) if self._use_float else str(
                int(round(self._higher))))
        draw.drawText(
            0, 14,
            str(self._lower) if self._use_float else str(
                int(round(self._lower))))

        draw.setPen(QColor(255, 255, 255))
        boundtext = draw.boundingRect(
            QtCore.QRectF(),
            str(self._minVal) if self._use_float else str(
                int(round(self._minVal))))
        draw.drawText(
            x_start + 2, 14,
            str(self._minVal) if self._use_float else str(
                int(round(self._minVal))))
        boundtext = draw.boundingRect(
            QtCore.QRectF(),
            str(self._maxVal) if self._use_float else str(
                int(round(self._maxVal))))
        draw.drawText(
            x_end - boundtext.width(), 14,
            str(self._maxVal) if self._use_float else str(
                int(round(self._maxVal))))

        draw.end()
def textSize(qp: QPainter, text: str):
    rect: QRect = QRect()
    rect = qp.boundingRect(rect, Qt.PlainText, text)
    textWidth = rect.width()
    textHeight = rect.height()
    return textWidth, textHeight