from __future__ import annotations from app.qt_compat import QPainter, QPointF, QRectF, Qt, Signal, QWidget, event_posf from app.config.models import SegmentConfig, TileConfig from app.core.geometry import segment_led_positions, segment_side from app.core.types import PreviewFrame from .preview_layout import compute_preview_layout, segment_display_rect, tile_aspect_ratio from .preview_modes import ( PREVIEW_MODE_LEDS, PREVIEW_MODE_TECHNICAL, PREVIEW_MODE_TILE, normalize_preview_mode, preview_mode_flags, ) from .preview_painter import paint_empty_preview, paint_preview_scene class PreviewWidget(QWidget): tileClicked = Signal(str) def __init__( self, controller, preview_mode: str = PREVIEW_MODE_TILE, scene_role: str = "live", technical_preview: bool | None = None, parent: QWidget | None = None, ) -> None: super().__init__(parent) self.controller = controller self.scene_role = "next" if str(scene_role).strip().lower() == "next" else "live" if technical_preview is not None: preview_mode = PREVIEW_MODE_TECHNICAL if technical_preview else PREVIEW_MODE_TILE self.preview_mode = normalize_preview_mode(preview_mode) self.technical_preview = self.preview_mode == PREVIEW_MODE_TECHNICAL self.current_frame: PreviewFrame | None = self.controller.preview_frame_for(self.scene_role) self._tile_rects: dict[str, QRectF] = {} self.setMinimumSize(640, 360) self.setMouseTracking(True) if self.scene_role == "next": self.controller.next_frame_ready.connect(self._on_frame_ready) else: self.controller.frame_ready.connect(self._on_frame_ready) self.controller.config_changed.connect(self.update) self.controller.state_changed.connect(self.update) def set_preview_mode(self, mode: str) -> None: self.preview_mode = normalize_preview_mode(mode) self.technical_preview = self.preview_mode == PREVIEW_MODE_TECHNICAL self.update() def set_technical_preview(self, enabled: bool) -> None: self.set_preview_mode(PREVIEW_MODE_TECHNICAL if enabled else PREVIEW_MODE_TILE) def _mode_flags(self) -> dict[str, bool]: return preview_mode_flags(self.preview_mode) def _on_frame_ready(self, frame: PreviewFrame) -> None: self.current_frame = frame self.update() def mousePressEvent(self, event) -> None: point = event_posf(event) for tile_id, rect in self._tile_rects.items(): if rect.contains(point): self.tileClicked.emit(tile_id) break super().mousePressEvent(event) def paintEvent(self, event) -> None: painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing, True) painter.setRenderHint(QPainter.TextAntialiasing, True) frame = self.current_frame if frame is None or not self.controller.config.tiles: paint_empty_preview(painter, QRectF(self.rect())) return layout = compute_preview_layout(QRectF(self.rect()), self.controller.config) self._tile_rects = layout.tile_rects paint_preview_scene( painter, config=self.controller.config, frame=frame, preview_mode=self.preview_mode, selected_tile_id=self.controller.selected_tile_id, target_rect=QRectF(self.rect()), layout=layout, ) def _compute_layout(self) -> tuple[QRectF, dict[str, QRectF]]: layout = compute_preview_layout(QRectF(self.rect()), self.controller.config) return layout.canvas_rect, layout.tile_rects def _tile_aspect_ratio(self) -> float: return tile_aspect_ratio(self.controller.config) def _segment_display_rect(self, tile: TileConfig, rect: QRectF) -> QRectF: return segment_display_rect(tile, rect) def _segment_side(self, tile: TileConfig, segment: SegmentConfig) -> str | None: return segment_side(tile, segment) def _segment_points_for_side(self, tile: TileConfig, segment: SegmentConfig, rect: QRectF) -> list[QPointF]: inset = max(2.0, min(rect.width(), rect.height()) * 0.02) insets = ( inset / max(1.0, rect.width()), inset / max(1.0, rect.height()), ) return [ QPointF(rect.left() + x_pos * rect.width(), rect.top() + y_pos * rect.height()) for x_pos, y_pos in segment_led_positions(tile, segment, insets=insets) ]