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
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()
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()
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)))
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()
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)
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
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()
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)
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)
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)
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())
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()))
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