90 lines
3.6 KiB
Python
90 lines
3.6 KiB
Python
"""A horizontal QSlider that paints marks at frames along the sequence.
|
|
|
|
Used as the navigation scrubber under the image. Two independent layers are
|
|
projected onto the groove as small vertical ticks, so you can see at a glance how
|
|
the whole sequence is processed:
|
|
|
|
* **cyan** (upper half) — frames where censorship was *detected* (`_results`);
|
|
* **green** (lower half) — frames that have been *restored* (``restored/``).
|
|
"""
|
|
|
|
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() # detected (censorship found)
|
|
self._restored_marks: set[int] = set() # restored (un-censored)
|
|
# Bright cyan stands out against both the dark track and the orange fill.
|
|
self._mark_color = QColor(0, 220, 255)
|
|
self._restored_color = QColor(80, 230, 120) # green = restored
|
|
|
|
def set_marks(self, marks: Iterable[int]) -> None:
|
|
"""Frames with detections (painted cyan, upper half)."""
|
|
marks = set(marks)
|
|
if marks != self._marks:
|
|
self._marks = marks
|
|
self.update()
|
|
|
|
def set_restored_marks(self, marks: Iterable[int]) -> None:
|
|
"""Frames that have been restored (painted green, lower half)."""
|
|
marks = set(marks)
|
|
if marks != self._restored_marks:
|
|
self._restored_marks = marks
|
|
self.update()
|
|
|
|
def paintEvent(self, event) -> None:
|
|
super().paintEvent(event)
|
|
if (not self._marks and not self._restored_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
|
|
top = self.rect().top() + 1
|
|
bottom = self.rect().bottom() - 1
|
|
mid = (top + bottom) // 2
|
|
|
|
painter = QPainter(self)
|
|
# Detections in the upper half, restored in the lower half, so a frame that's
|
|
# both detected and restored shows both ticks instead of one hiding the other.
|
|
self._paint_layer(painter, self._marks, self._mark_color, top, mid,
|
|
groove.x(), half, span, lo, hi, opt.upsideDown)
|
|
self._paint_layer(painter, self._restored_marks, self._restored_color, mid, bottom,
|
|
groove.x(), half, span, lo, hi, opt.upsideDown)
|
|
painter.end()
|
|
|
|
def _paint_layer(self, painter, marks, color, y0, y1, gx, half, span, lo, hi, upside) -> None:
|
|
if not marks:
|
|
return
|
|
pen = painter.pen()
|
|
pen.setColor(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 marks:
|
|
pos = QStyle.sliderPositionFromValue(lo, hi, m, span, upside)
|
|
x = gx + half + pos
|
|
if x not in seen_x:
|
|
seen_x.add(x)
|
|
painter.drawLine(x, y0, x, y1)
|