Files
HVideoTool/hvideotool/ui/marker_slider.py
T

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)