from __future__ import annotations from app.qt_compat import ( QColor, QFont, QLinearGradient, QPainter, QPainterPath, QPen, QRectF, Qt, Signal, QWidget, event_posf, ) from app.config.device_assignment import tile_matches_device from app.config.models import InfinityMirrorConfig from app.network.wled import DiscoveredWledDevice, normalize_mac_address from app.ui.preview_layout import compute_preview_layout class MappingAssignmentPreview(QWidget): tileClicked = Signal(str) def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) self._config = InfinityMirrorConfig() self._discovered_devices: list[DiscoveredWledDevice] = [] self._active_device: DiscoveredWledDevice | None = None self._selected_tile_id: str | None = None self._tile_rects: dict[str, QRectF] = {} self.setMinimumSize(520, 360) self.setMouseTracking(True) def set_assignment_state( self, config: InfinityMirrorConfig, discovered_devices: list[DiscoveredWledDevice], *, active_device: DiscoveredWledDevice | None = None, selected_tile_id: str | None = None, ) -> None: self._config = config self._discovered_devices = list(discovered_devices) self._active_device = active_device self._selected_tile_id = selected_tile_id 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: # type: ignore[override] painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing, True) painter.setRenderHint(QPainter.TextAntialiasing, True) rect = QRectF(self.rect()) background = QLinearGradient(rect.topLeft(), rect.bottomRight()) background.setColorAt(0.0, QColor("#12161D")) background.setColorAt(1.0, QColor("#1B2230")) painter.fillRect(rect, background) if not self._config.tiles: painter.setPen(QColor("#A8B3C7")) painter.drawText(rect, Qt.AlignCenter, "Open a mapping to assign WLED devices.") return layout = compute_preview_layout(rect, self._config) self._tile_rects = layout.tile_rects self._draw_canvas_shell(painter, layout.canvas_rect) discovered_ips = {device.ip_address for device in self._discovered_devices} discovered_macs = {normalize_mac_address(device.mac_address) for device in self._discovered_devices if device.mac_address} active_tile_id = None if self._active_device is not None: for tile in self._config.sorted_tiles(): if tile_matches_device(tile, self._active_device): active_tile_id = tile.tile_id break for tile in self._config.sorted_tiles(): tile_rect = layout.tile_rects[tile.tile_id] tile_assigned = bool(tile.controller_ip.strip() or tile.controller_mac.strip()) tile_is_active = active_tile_id == tile.tile_id tile_is_selected = self._selected_tile_id == tile.tile_id if tile_is_active: fill_color = QColor("#1D4E89") outline_color = QColor("#90CAF9") subtitle = self._active_device.instance_name or self._active_device.ip_address status = "Active Device" elif not tile_assigned: fill_color = QColor("#2A2F3A") outline_color = QColor("#4A5568") subtitle = "Unmapped" status = "Click to assign" elif (tile.controller_mac and normalize_mac_address(tile.controller_mac) in discovered_macs) or tile.controller_ip.strip() in discovered_ips: fill_color = QColor("#1D5A45") outline_color = QColor("#81E6D9") subtitle = tile.controller_name or tile.controller_host or tile.controller_ip status = "Mapped" else: fill_color = QColor("#6B4F1D") outline_color = QColor("#F6AD55") subtitle = tile.controller_name or tile.controller_host or tile.controller_ip status = "Assigned, not seen" self._draw_tile( painter, rect=tile_rect, tile_id=tile.tile_id, row=tile.row, col=tile.col, subtitle=subtitle, status=status, fill_color=fill_color, outline_color=outline_color, selected=tile_is_selected, ) def _draw_canvas_shell(self, painter: QPainter, rect: QRectF) -> None: path = QPainterPath() path.addRoundedRect(rect, 10.0, 10.0) painter.fillPath(path, QColor("#202632")) painter.setPen(QPen(QColor("#334155"), 1.0)) painter.drawPath(path) def _draw_tile( self, painter: QPainter, *, rect: QRectF, tile_id: str, row: int, col: int, subtitle: str, status: str, fill_color: QColor, outline_color: QColor, selected: bool, ) -> None: base = min(rect.width(), rect.height()) rounding = max(4.0, base * 0.045) tile_path = QPainterPath() tile_path.addRoundedRect(rect, rounding, rounding) painter.fillPath(tile_path, fill_color) highlight = QLinearGradient(rect.topLeft(), rect.bottomLeft()) highlight.setColorAt(0.0, QColor(255, 255, 255, 24)) highlight.setColorAt(0.18, QColor(255, 255, 255, 8)) highlight.setColorAt(1.0, QColor(0, 0, 0, 0)) painter.fillPath(tile_path, highlight) painter.setPen(QPen(outline_color, 1.3)) painter.drawPath(tile_path) if selected: painter.setPen(QPen(QColor("#E2E8F0"), 2.1)) painter.drawRoundedRect(rect.adjusted(-3, -3, 3, 3), rounding + 2.0, rounding + 2.0) padding_x = max(12.0, rect.width() * 0.08) padding_top = max(10.0, rect.height() * 0.08) padding_bottom = max(12.0, rect.height() * 0.08) title_font = QFont() title_font.setPointSizeF(max(13.0, base * 0.1)) title_font.setWeight(QFont.DemiBold) painter.setFont(title_font) painter.setPen(QColor("#F8FAFC")) painter.drawText( rect.adjusted(padding_x, padding_top, -padding_x, -rect.height() * 0.56), Qt.AlignLeft | Qt.AlignTop | Qt.TextWordWrap, tile_id, ) meta_font = QFont(title_font) meta_font.setPointSizeF(max(9.2, base * 0.06)) meta_font.setWeight(QFont.Normal) painter.setFont(meta_font) painter.setPen(QColor(235, 244, 249, 175)) painter.drawText( rect.adjusted(padding_x, rect.height() * 0.48, -padding_x, -padding_bottom), Qt.AlignLeft | Qt.AlignBottom | Qt.TextWordWrap, f"{subtitle}\n{status} | R{row} C{col}", )