Files
Compass/ui/main_window.py
2026-07-03 12:23:41 -04:00

196 lines
6.9 KiB
Python

"""
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