"""A horizontal QSlider that paints marks at frames where censorship was found. Used as the navigation scrubber under the image: the file list / `_results` cache is projected onto the groove as small vertical ticks, so you can see at a glance where the detected regions are along the whole sequence. """ from __future__ import annotations from collections.abc import Iterable from PySide6.QtCore import Qt from PySide6.QtGui import QColor, QPainter from PySide6.QtWidgets import QSlider, QStyle, QStyleOptionSlider class MarkerSlider(QSlider): def __init__(self, orientation=Qt.Horizontal, parent=None) -> None: super().__init__(orientation, parent) self._marks: set[int] = set() # Bright cyan stands out against both the dark track and the orange fill. self._mark_color = QColor(0, 220, 255) def set_marks(self, marks: Iterable[int]) -> None: marks = set(marks) if marks != self._marks: self._marks = marks self.update() def paintEvent(self, event) -> None: super().paintEvent(event) if not self._marks or self.maximum() <= self.minimum(): return opt = QStyleOptionSlider() self.initStyleOption(opt) groove = self.style().subControlRect( QStyle.CC_Slider, opt, QStyle.SC_SliderGroove, self ) handle = self.style().subControlRect( QStyle.CC_Slider, opt, QStyle.SC_SliderHandle, self ) span = groove.width() - handle.width() if span <= 0: return lo, hi = self.minimum(), self.maximum() half = handle.width() // 2 # Span (almost) the full widget height so marks read over the fill/handle. top = self.rect().top() + 1 bottom = self.rect().bottom() - 1 painter = QPainter(self) pen = painter.pen() pen.setColor(self._mark_color) pen.setWidth(2) painter.setPen(pen) # Many frames can collapse onto the same pixel column — dedupe to keep # repaint cheap on big folders (tens of thousands of frames). seen_x: set[int] = set() for m in self._marks: pos = QStyle.sliderPositionFromValue(lo, hi, m, span, opt.upsideDown) x = groove.x() + half + pos if x not in seen_x: seen_x.add(x) painter.drawLine(x, top, x, bottom) painter.end()