""" Main window — responsive full-screen layout. Left: compass rose | Right: info panel """ import math from pathlib import Path from PyQt5.QtWidgets import (QMainWindow, QWidget, QHBoxLayout, QDialog, QVBoxLayout, QListWidget, QPushButton, QLabel, QApplication) from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QColor, QIcon import config from core.nmea_parser import NavData, parse from core.serial_reader import SerialReader from ui.compass_widget import CompassWidget from ui.info_panel import InfoPanel import ui.styles as S class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle('AR Compass — Marine') _logo = Path(__file__).parent.parent / 'assets' / 'images' / 'ar_logo_full.png' if _logo.exists(): self.setWindowIcon(QIcon(str(_logo))) self.showFullScreen() self._nav = NavData() self._smooth = 0.0 self._readers: list[SerialReader] = [] self._night = False self._hdg_mode = 'M' # 'M' magnetic, 'T' true self._build_ui() self._start_serial() self._start_timer() # ── UI ────────────────────────────────────────────────────────────────── def _build_ui(self): central = QWidget() self.setCentralWidget(central) central.setStyleSheet(f'background: {S.BG.name()};') hl = QHBoxLayout(central) hl.setContentsMargins(0, 0, 0, 0) hl.setSpacing(0) self.compass = CompassWidget() self.panel = InfoPanel() # Divider div = QWidget() div.setFixedWidth(2) div.setStyleSheet(f'background: {S.DIVIDER.name()};') # 60 / 40 split hl.addWidget(self.compass, stretch=60) hl.addWidget(div) hl.addWidget(self.panel, stretch=40) self.panel.night_toggled.connect(self._on_night) self.panel.port_requested.connect(self._show_port_dialog) self.panel.hdg_mode_changed.connect(self._on_hdg_mode) # ── Timer — animation & UI refresh ────────────────────────────────────── def _start_timer(self): self._timer = QTimer(self) self._timer.timeout.connect(self._tick) self._timer.start(config.UI_REFRESH_MS) def _tick(self): # Select heading source based on mode if self._hdg_mode == 'T': hdg = self._nav.hdg_true_calc else: hdg = self._nav.hdg_mag if hdg is not None: diff = ((hdg - self._smooth + 180) % 360) - 180 self._smooth = (self._smooth + diff * config.HEADING_SMOOTHING) % 360 self.compass.set_heading(self._smooth) self.compass.set_attitude( self._nav.pitch or 0.0, self._nav.roll or 0.0, ) self.panel.update_data(self._nav) # ── Serial ─────────────────────────────────────────────────────────────── def _start_serial(self): for cfg in config.SERIAL_PORTS: r = SerialReader(cfg['port'], cfg['baud'], self) r.sentence.connect(self._on_sentence) r.start() self._readers.append(r) def _on_sentence(self, line: str): parse(line, self._nav) # ── Night mode ─────────────────────────────────────────────────────────── def _on_hdg_mode(self, mode: str): self._hdg_mode = mode self.compass.set_hdg_mode(mode) def _on_night(self, on: bool): self._night = on self.compass.set_night(on) bg = S.N_BG.name() if on else S.BG.name() self.centralWidget().setStyleSheet(f'background: {bg};') # ── Port dialog ────────────────────────────────────────────────────────── def _show_port_dialog(self): dlg = PortDialog(self) if dlg.exec_() == QDialog.Accepted: port, baud = dlg.selected() if port: self._stop_serial() config.SERIAL_PORTS = [{'port': port, 'baud': baud, 'name': 'Selected'}] self._start_serial() def _stop_serial(self): for r in self._readers: r.stop() self._readers.clear() # ── Keyboard shortcuts ─────────────────────────────────────────────────── def keyPressEvent(self, event): if event.key() == Qt.Key_Escape: self.close() elif event.key() == Qt.Key_F: if self.isFullScreen(): self.showNormal() else: self.showFullScreen() def closeEvent(self, event): self._stop_serial() super().closeEvent(event) class PortDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle('Select Serial Port') self.setMinimumSize(400, 300) self.setStyleSheet(f""" QDialog {{ background: {S.PANEL_BG.name()}; color: {S.WHITE.name()}; }} QListWidget {{ background: {S.BEZEL_DARK.name()}; color: {S.WHITE.name()}; border: 1px solid {S.GOLD_DIM.name()}; font-size: 14px; }} QListWidget::item:selected {{ background: {S.GOLD_DIM.name()}; }} QPushButton {{ background: {S.BEZEL_MID.name()}; color: {S.GOLD.name()}; border: 1px solid {S.GOLD.name()}; padding: 8px; border-radius: 4px; }} """) vl = QVBoxLayout(self) vl.addWidget(QLabel('Available serial ports:')) self._list = QListWidget() for p in SerialReader.available_ports(): self._list.addItem(p) vl.addWidget(self._list) hl = QHBoxLayout() ok = QPushButton('Connect 4800 baud') ok38 = QPushButton('Connect 38400 baud') cancel = QPushButton('Cancel') ok.clicked.connect(lambda: self._accept(4800)) ok38.clicked.connect(lambda: self._accept(38400)) cancel.clicked.connect(self.reject) hl.addWidget(ok) hl.addWidget(ok38) hl.addWidget(cancel) vl.addLayout(hl) self._port = None self._baud = 4800 def _accept(self, baud): items = self._list.selectedItems() if items: self._port = items[0].text() self._baud = baud self.accept() def selected(self): return self._port, self._baud