""" LFMRIWindow - main application window for the unified LF-MRI GUI. Single nav-bar design: tab buttons on the left. The native QMenuBar and QTabWidget tab-bar are both hidden; a QToolBar provides a unified, flat navigation strip. """ from __future__ import annotations import os from PySide6.QtCore import Qt, QSize from src import i18n from PySide6.QtWidgets import ( QApplication, QButtonGroup, QFrame, QLabel, QMainWindow, QPushButton, QSizePolicy, QStatusBar, QTabWidget, QToolBar, QWidget, ) from src.tabs.fid_tab import FidTab from src.tabs.scanner_tab import ScannerTab from src.tabs.scanning_tab import ScanningTab from src.tabs.seq_interp_tab import SeqInterpTab from src.tabs.spectroscopy_tab import SpectroscopyTab _TAB_NAV_KEYS = [ "tab_nav_sequence", "tab_nav_scanner", "tab_nav_fid", "tab_nav_scanning", "tab_nav_spectro", ] _NAV_BG = "#0f0f1e" _NAV_H = 38 _TAB_BTN_CSS = """ QPushButton { background: transparent; color: #7777aa; border: none; border-bottom: 2px solid transparent; padding: 0px 20px; font-size: 12px; min-height: 36px; } QPushButton:checked { color: #ffffff; border-bottom: 2px solid #f0c040; } QPushButton:hover:!checked { color: #aaaacc; background: #17172e; } """ _LANG_BTN_CSS = f""" QPushButton {{ background: transparent; color: #555577; border: 1px solid transparent; border-radius: 3px; padding: 0px 7px; font-size: 11px; font-weight: bold; min-height: 22px; max-height: 22px; }} QPushButton:checked {{ color: #f0c040; border-color: #f0c040; }} QPushButton:hover:!checked {{ color: #aaaacc; }} """ _NAV_TOOLBAR_CSS = f""" QToolBar {{ background: {_NAV_BG}; border: none; border-bottom: 1px solid #1e1e38; spacing: 0px; padding: 0px; }} """ class LFMRIWindow(QMainWindow): """Unified LF-MRI application window.""" def __init__( self, hw_config_path: str | None = None, output_dir: str | None = None, seq_file: str | None = None, orchestrator_url: str = "http://localhost:1717", seq_interp_url: str = "http://localhost:7475", spectroscopy_url: str = "http://localhost:8002", ) -> None: super().__init__() self.setWindowTitle("LF-MRI System") self.setMinimumSize(960, 640) self._hw_config_path = hw_config_path self._output_dir = output_dir self._seq_tab = SeqInterpTab( hw_config_path=hw_config_path, output_dir=output_dir, seq_interp_url=seq_interp_url, ) self._scanner_tab = ScannerTab( hw_config_path=hw_config_path, orchestrator_url=orchestrator_url, seq_interp_url=seq_interp_url, spectroscopy_url=spectroscopy_url, ) self._fid_tab = FidTab( hw_config_path=hw_config_path, output_dir=output_dir, ) self._scanning_tab = ScanningTab() self._scanning_tab.set_orchestrator_url(orchestrator_url) self._spectroscopy_tab = SpectroscopyTab(spectroscopy_url=spectroscopy_url) self._tabs = QTabWidget() self._tabs.tabBar().hide() self._tabs.setDocumentMode(True) self._tabs.addTab(self._seq_tab, i18n.tr(_TAB_NAV_KEYS[0])) self._tabs.addTab(self._scanner_tab, i18n.tr(_TAB_NAV_KEYS[1])) self._tabs.addTab(self._fid_tab, i18n.tr(_TAB_NAV_KEYS[2])) self._tabs.addTab(self._scanning_tab, i18n.tr(_TAB_NAV_KEYS[3])) self._tabs.addTab(self._spectroscopy_tab, i18n.tr(_TAB_NAV_KEYS[4])) self._tabs.currentChanged.connect(self._on_tab_changed) self.setCentralWidget(self._tabs) self._fid_tab.fid_seq_generated.connect(self._on_fid_generated) self._seq_tab.ready_for_scan.connect(self._on_ready_for_scan) self._scanning_tab.scan_job_started.connect(self._scanner_tab.attach_job) self._scanner_tab.scan_result_ready.connect(self._spectroscopy_tab.receive_scan_data) self.menuBar().hide() self._build_nav_bar() self._build_status_bar() self._size_and_center() if seq_file and os.path.isfile(seq_file): self._seq_tab.load_seq_file(os.path.abspath(seq_file)) def _build_nav_bar(self) -> None: tb = QToolBar("Navigation", self) tb.setMovable(False) tb.setFloatable(False) tb.setIconSize(QSize(0, 0)) tb.setStyleSheet(_NAV_TOOLBAR_CSS) tb.setFixedHeight(_NAV_H) self.addToolBar(Qt.TopToolBarArea, tb) lbl = QLabel(" LF-MRI ") lbl.setStyleSheet( f"color: #444466; font-weight: bold; font-size: 11px; " f"background: {_NAV_BG}; padding: 0 4px;" ) tb.addWidget(lbl) sep = _VSep(tb) tb.addWidget(sep) self._nav_btn_group = QButtonGroup(self) self._nav_btn_group.setExclusive(True) self._nav_tab_buttons: list[QPushButton] = [] for i, key in enumerate(_TAB_NAV_KEYS): btn = QPushButton(i18n.tr(key)) btn.setCheckable(True) btn.setStyleSheet(_TAB_BTN_CSS) btn.setFixedHeight(_NAV_H) btn.setCursor(Qt.PointingHandCursor) self._nav_btn_group.addButton(btn, i) tb.addWidget(btn) self._nav_tab_buttons.append(btn) btn.clicked.connect(lambda _checked, idx=i: self._switch_tab(idx)) self._nav_tab_buttons[0].setChecked(True) spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) spacer.setStyleSheet(f"background: {_NAV_BG};") tb.addWidget(spacer) # Language toggle (EN / RU) self._lang_btn_group = QButtonGroup(self) self._lang_btn_group.setExclusive(True) self._btn_lang_en = QPushButton("EN") self._btn_lang_en.setCheckable(True) self._btn_lang_en.setChecked(True) self._btn_lang_en.setStyleSheet(_LANG_BTN_CSS) self._btn_lang_en.setCursor(Qt.PointingHandCursor) self._btn_lang_ru = QPushButton("RU") self._btn_lang_ru.setCheckable(True) self._btn_lang_ru.setStyleSheet(_LANG_BTN_CSS) self._btn_lang_ru.setCursor(Qt.PointingHandCursor) self._lang_btn_group.addButton(self._btn_lang_en) self._lang_btn_group.addButton(self._btn_lang_ru) self._btn_lang_en.clicked.connect(lambda: self._on_language_change("en")) self._btn_lang_ru.clicked.connect(lambda: self._on_language_change("ru")) tb.addWidget(self._btn_lang_en) tb.addWidget(self._btn_lang_ru) def _switch_tab(self, index: int) -> None: self._tabs.setCurrentIndex(index) self._nav_tab_buttons[index].setChecked(True) def _build_status_bar(self) -> None: sb = QStatusBar() sb.setStyleSheet( "QStatusBar { background: #0c0c1a; color: #555577; font-size: 11px; }" ) self.setStatusBar(sb) sb.showMessage(f"{i18n.tr('active_tab')}: {i18n.tr(_TAB_NAV_KEYS[0])}") def _size_and_center(self) -> None: screen = QApplication.primaryScreen() if screen is not None: ag = screen.availableGeometry() w = min(1600, max(960, int(ag.width() * 0.92))) h = min(940, max(640, int(ag.height() * 0.90))) self.resize(w, h) self.move( ag.x() + (ag.width() - w) // 2, ag.y() + (ag.height() - h) // 2, ) else: self.resize(1440, 860) def _on_tab_changed(self, index: int) -> None: key = _TAB_NAV_KEYS[index] if 0 <= index < len(_TAB_NAV_KEYS) else "-" self.statusBar().showMessage(f"{i18n.tr('active_tab')}: {i18n.tr(key)}") if 0 <= index < len(self._nav_tab_buttons): self._nav_tab_buttons[index].setChecked(True) def _on_language_change(self, lang: str) -> None: i18n.set_language(lang) self.retranslate_ui() def retranslate_ui(self) -> None: for i, key in enumerate(_TAB_NAV_KEYS): self._nav_tab_buttons[i].setText(i18n.tr(key)) cur = self._tabs.currentIndex() key = _TAB_NAV_KEYS[cur] if 0 <= cur < len(_TAB_NAV_KEYS) else "-" self.statusBar().showMessage(f"{i18n.tr('active_tab')}: {i18n.tr(key)}") for tab in ( self._seq_tab, self._scanner_tab, self._fid_tab, self._scanning_tab, self._spectroscopy_tab, ): if hasattr(tab, "retranslate_ui"): tab.retranslate_ui() def _on_fid_generated(self, path: str) -> None: self._seq_tab.load_seq_file(path) self._switch_tab(0) def _on_ready_for_scan(self, info: dict) -> None: self._scanner_tab.apply_seq_info(info) self._scanning_tab.apply_seq_info(info) self._switch_tab(1) class _VSep(QFrame): """Thin vertical separator for the nav toolbar.""" def __init__(self, parent=None) -> None: super().__init__(parent) self.setFrameShape(QFrame.VLine) self.setFixedWidth(1) self.setStyleSheet("background: #1e1e38; border: none;")