From 633e421a689f2c9acbe394a11cb28a7357acf1ae Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Mon, 13 Apr 2026 16:58:31 +0200 Subject: [PATCH] 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 --- main.py | 93 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 81 insertions(+), 12 deletions(-) diff --git a/main.py b/main.py index 3df3a5c..2264314 100755 --- a/main.py +++ b/main.py @@ -22,7 +22,7 @@ from PyQt6.QtWidgets import ( QComboBox, QCheckBox, QSpinBox, QDoubleSpinBox, 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 import mpv @@ -1307,6 +1307,73 @@ class CropBarWidget(QWidget): 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): """Floating preview window that snaps and docks to the main window edges.""" @@ -1633,10 +1700,7 @@ class MainWindow(QMainWindow): self._mpv = MpvWidget() self._mpv.file_loaded.connect(self._after_load) - self._end_preview = QLabel() - self._end_preview.setAlignment(Qt.AlignmentFlag.AlignCenter) - self._end_preview.setStyleSheet("background: #1a1a1a;") - self._end_preview.setScaledContents(False) + self._end_preview = PreviewLabel() self._preview_win = SnapPreviewWindow(self) self._preview_win.setWindowTitle("End frame") @@ -2332,6 +2396,7 @@ class MainWindow(QMainWindow): # Fall back to random overlay guides (or hide) self._update_rand_overlays() self._settings.setValue("portrait_ratio", text) + self._update_preview_crop() def _on_rand_toggle(self, _checked: bool = False) -> None: ratio_text = self._cmb_portrait.currentText() @@ -2381,6 +2446,7 @@ class MainWindow(QMainWindow): self._crop_bar.set_crop_center(frac) if ratio != "Off": self._mpv.set_crop_overlay(_RATIOS[ratio], frac) + self._update_preview_crop() return self._crop_center = frac 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) else: self._update_rand_overlays() + self._update_preview_crop() # --- End-frame preview --- @@ -2411,13 +2478,15 @@ class MainWindow(QMainWindow): px = QPixmap() px.loadFromData(png_data) if not px.isNull(): - scaled = px.scaled( - 320, 240, - Qt.AspectRatioMode.KeepAspectRatio, - Qt.TransformationMode.SmoothTransformation, - ) - self._end_preview.setPixmap(scaled) - self._preview_win.adjustSize() + self._end_preview.setPixmap(px) + self._update_preview_crop() + + def _update_preview_crop(self) -> None: + self._end_preview.set_crop( + self._crop_bar._portrait_ratio, + self._crop_bar._source_ratio, + self._crop_bar._crop_center, + ) # --- Playback ---