feat: show crop region overlay on end-frame preview

PreviewLabel widget replaces plain QLabel, draws dimmed areas outside
the crop window and blue border lines matching the crop bar position.
Updates live when portrait ratio or crop center changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 16:58:31 +02:00
parent a543c72ff5
commit 633e421a68
+81 -12
View File
@@ -22,7 +22,7 @@ from PyQt6.QtWidgets import (
QComboBox, QCheckBox, QSpinBox, QDoubleSpinBox, QComboBox, QCheckBox, QSpinBox, QDoubleSpinBox,
QMessageBox, QInputDialog, QMessageBox, QInputDialog,
) )
from PyQt6.QtCore import Qt, QObject, QThread, QTimer, QRect, pyqtSignal, QSettings from PyQt6.QtCore import Qt, QObject, QThread, QTimer, QRect, QSize, pyqtSignal, QSettings
from PyQt6.QtGui import QPainter, QColor, QPen, QPixmap, QDragEnterEvent, QDropEvent, QCursor, QFont, QKeySequence, QShortcut from PyQt6.QtGui import QPainter, QColor, QPen, QPixmap, QDragEnterEvent, QDropEvent, QCursor, QFont, QKeySequence, QShortcut
import mpv import mpv
@@ -1307,6 +1307,73 @@ class CropBarWidget(QWidget):
self.crop_changed.emit(self._crop_center) self.crop_changed.emit(self._crop_center)
class PreviewLabel(QWidget):
"""Displays a pixmap with optional crop region overlay lines."""
def __init__(self):
super().__init__()
self._pixmap: QPixmap | None = None
self._portrait_ratio: tuple[int, int] | None = None
self._source_ratio: float = 16 / 9
self._crop_center: float = 0.5
self.setMinimumSize(160, 120)
def setPixmap(self, px: QPixmap) -> None:
self._pixmap = px
self.update()
def set_crop(self, portrait_ratio: tuple[int, int] | None,
source_ratio: float, crop_center: float) -> None:
self._portrait_ratio = portrait_ratio
self._source_ratio = source_ratio
self._crop_center = crop_center
self.update()
def sizeHint(self):
if self._pixmap:
return self._pixmap.size()
return QSize(320, 240)
def paintEvent(self, event):
p = QPainter(self)
try:
w, h = self.width(), self.height()
p.fillRect(0, 0, w, h, QColor(26, 26, 26))
if self._pixmap and not self._pixmap.isNull():
# Scale pixmap to fit, centered.
scaled = self._pixmap.scaled(
w, h,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
ix = (w - scaled.width()) // 2
iy = (h - scaled.height()) // 2
p.drawPixmap(ix, iy, scaled)
# Draw crop lines if portrait mode is active.
if self._portrait_ratio is not None:
num, den = self._portrait_ratio
crop_ar = num / den
win_frac = crop_ar / self._source_ratio
if win_frac < 1.0:
iw = scaled.width()
win_px = iw * win_frac
max_x = iw - win_px
cx = ix + int(max_x * self._crop_center)
cw = int(win_px)
# Dim outside crop region.
dim = QColor(0, 0, 0, 120)
p.fillRect(ix, iy, int(cx - ix), scaled.height(), dim)
p.fillRect(cx + cw, iy, ix + iw - cx - cw, scaled.height(), dim)
# Crop border lines.
pen = QPen(QColor(100, 160, 240, 200))
pen.setWidth(1)
p.setPen(pen)
p.drawLine(cx, iy, cx, iy + scaled.height())
p.drawLine(cx + cw, iy, cx + cw, iy + scaled.height())
finally:
p.end()
class SnapPreviewWindow(QWidget): class SnapPreviewWindow(QWidget):
"""Floating preview window that snaps and docks to the main window edges.""" """Floating preview window that snaps and docks to the main window edges."""
@@ -1633,10 +1700,7 @@ class MainWindow(QMainWindow):
self._mpv = MpvWidget() self._mpv = MpvWidget()
self._mpv.file_loaded.connect(self._after_load) self._mpv.file_loaded.connect(self._after_load)
self._end_preview = QLabel() self._end_preview = PreviewLabel()
self._end_preview.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._end_preview.setStyleSheet("background: #1a1a1a;")
self._end_preview.setScaledContents(False)
self._preview_win = SnapPreviewWindow(self) self._preview_win = SnapPreviewWindow(self)
self._preview_win.setWindowTitle("End frame") self._preview_win.setWindowTitle("End frame")
@@ -2332,6 +2396,7 @@ class MainWindow(QMainWindow):
# Fall back to random overlay guides (or hide) # Fall back to random overlay guides (or hide)
self._update_rand_overlays() self._update_rand_overlays()
self._settings.setValue("portrait_ratio", text) self._settings.setValue("portrait_ratio", text)
self._update_preview_crop()
def _on_rand_toggle(self, _checked: bool = False) -> None: def _on_rand_toggle(self, _checked: bool = False) -> None:
ratio_text = self._cmb_portrait.currentText() ratio_text = self._cmb_portrait.currentText()
@@ -2381,6 +2446,7 @@ class MainWindow(QMainWindow):
self._crop_bar.set_crop_center(frac) self._crop_bar.set_crop_center(frac)
if ratio != "Off": if ratio != "Off":
self._mpv.set_crop_overlay(_RATIOS[ratio], frac) self._mpv.set_crop_overlay(_RATIOS[ratio], frac)
self._update_preview_crop()
return return
self._crop_center = frac self._crop_center = frac
self._settings.setValue("crop_center", str(self._crop_center)) self._settings.setValue("crop_center", str(self._crop_center))
@@ -2389,6 +2455,7 @@ class MainWindow(QMainWindow):
self._mpv.set_crop_overlay(_RATIOS[ratio], self._crop_center) self._mpv.set_crop_overlay(_RATIOS[ratio], self._crop_center)
else: else:
self._update_rand_overlays() self._update_rand_overlays()
self._update_preview_crop()
# --- End-frame preview --- # --- End-frame preview ---
@@ -2411,13 +2478,15 @@ class MainWindow(QMainWindow):
px = QPixmap() px = QPixmap()
px.loadFromData(png_data) px.loadFromData(png_data)
if not px.isNull(): if not px.isNull():
scaled = px.scaled( self._end_preview.setPixmap(px)
320, 240, self._update_preview_crop()
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation, def _update_preview_crop(self) -> None:
) self._end_preview.set_crop(
self._end_preview.setPixmap(scaled) self._crop_bar._portrait_ratio,
self._preview_win.adjustSize() self._crop_bar._source_ratio,
self._crop_bar._crop_center,
)
# --- Playback --- # --- Playback ---