app_window.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. """
  2. LFMRIWindow - main application window for the unified LF-MRI GUI.
  3. Single nav-bar design: tab buttons on the left.
  4. The native QMenuBar and QTabWidget tab-bar are both hidden; a QToolBar
  5. provides a unified, flat navigation strip.
  6. """
  7. from __future__ import annotations
  8. import os
  9. from PySide6.QtCore import Qt, QSize
  10. from src import i18n
  11. from PySide6.QtWidgets import (
  12. QApplication,
  13. QButtonGroup,
  14. QFrame,
  15. QLabel,
  16. QMainWindow,
  17. QPushButton,
  18. QSizePolicy,
  19. QStatusBar,
  20. QTabWidget,
  21. QToolBar,
  22. QWidget,
  23. )
  24. from src.tabs.fid_tab import FidTab
  25. from src.tabs.scanner_tab import ScannerTab
  26. from src.tabs.scanning_tab import ScanningTab
  27. from src.tabs.seq_interp_tab import SeqInterpTab
  28. from src.tabs.spectroscopy_tab import SpectroscopyTab
  29. _TAB_NAV_KEYS = [
  30. "tab_nav_sequence",
  31. "tab_nav_scanner",
  32. "tab_nav_fid",
  33. "tab_nav_scanning",
  34. "tab_nav_spectro",
  35. ]
  36. _NAV_BG = "#0f0f1e"
  37. _NAV_H = 38
  38. _TAB_BTN_CSS = """
  39. QPushButton {
  40. background: transparent;
  41. color: #7777aa;
  42. border: none;
  43. border-bottom: 2px solid transparent;
  44. padding: 0px 20px;
  45. font-size: 12px;
  46. min-height: 36px;
  47. }
  48. QPushButton:checked {
  49. color: #ffffff;
  50. border-bottom: 2px solid #f0c040;
  51. }
  52. QPushButton:hover:!checked {
  53. color: #aaaacc;
  54. background: #17172e;
  55. }
  56. """
  57. _LANG_BTN_CSS = f"""
  58. QPushButton {{
  59. background: transparent;
  60. color: #555577;
  61. border: 1px solid transparent;
  62. border-radius: 3px;
  63. padding: 0px 7px;
  64. font-size: 11px;
  65. font-weight: bold;
  66. min-height: 22px;
  67. max-height: 22px;
  68. }}
  69. QPushButton:checked {{
  70. color: #f0c040;
  71. border-color: #f0c040;
  72. }}
  73. QPushButton:hover:!checked {{
  74. color: #aaaacc;
  75. }}
  76. """
  77. _NAV_TOOLBAR_CSS = f"""
  78. QToolBar {{
  79. background: {_NAV_BG};
  80. border: none;
  81. border-bottom: 1px solid #1e1e38;
  82. spacing: 0px;
  83. padding: 0px;
  84. }}
  85. """
  86. class LFMRIWindow(QMainWindow):
  87. """Unified LF-MRI application window."""
  88. def __init__(
  89. self,
  90. hw_config_path: str | None = None,
  91. output_dir: str | None = None,
  92. seq_file: str | None = None,
  93. orchestrator_url: str = "http://localhost:1717",
  94. seq_interp_url: str = "http://localhost:7475",
  95. spectroscopy_url: str = "http://localhost:8002",
  96. ) -> None:
  97. super().__init__()
  98. self.setWindowTitle("LF-MRI System")
  99. self.setMinimumSize(960, 640)
  100. self._hw_config_path = hw_config_path
  101. self._output_dir = output_dir
  102. self._seq_tab = SeqInterpTab(
  103. hw_config_path=hw_config_path,
  104. output_dir=output_dir,
  105. seq_interp_url=seq_interp_url,
  106. )
  107. self._scanner_tab = ScannerTab(
  108. hw_config_path=hw_config_path,
  109. orchestrator_url=orchestrator_url,
  110. seq_interp_url=seq_interp_url,
  111. spectroscopy_url=spectroscopy_url,
  112. )
  113. self._fid_tab = FidTab(
  114. hw_config_path=hw_config_path,
  115. output_dir=output_dir,
  116. )
  117. self._scanning_tab = ScanningTab()
  118. self._scanning_tab.set_orchestrator_url(orchestrator_url)
  119. self._spectroscopy_tab = SpectroscopyTab(spectroscopy_url=spectroscopy_url)
  120. self._tabs = QTabWidget()
  121. self._tabs.tabBar().hide()
  122. self._tabs.setDocumentMode(True)
  123. self._tabs.addTab(self._seq_tab, i18n.tr(_TAB_NAV_KEYS[0]))
  124. self._tabs.addTab(self._scanner_tab, i18n.tr(_TAB_NAV_KEYS[1]))
  125. self._tabs.addTab(self._fid_tab, i18n.tr(_TAB_NAV_KEYS[2]))
  126. self._tabs.addTab(self._scanning_tab, i18n.tr(_TAB_NAV_KEYS[3]))
  127. self._tabs.addTab(self._spectroscopy_tab, i18n.tr(_TAB_NAV_KEYS[4]))
  128. self._tabs.currentChanged.connect(self._on_tab_changed)
  129. self.setCentralWidget(self._tabs)
  130. self._fid_tab.fid_seq_generated.connect(self._on_fid_generated)
  131. self._seq_tab.ready_for_scan.connect(self._on_ready_for_scan)
  132. self._scanning_tab.scan_job_started.connect(self._scanner_tab.attach_job)
  133. self._scanner_tab.scan_result_ready.connect(self._spectroscopy_tab.receive_scan_data)
  134. self.menuBar().hide()
  135. self._build_nav_bar()
  136. self._build_status_bar()
  137. self._size_and_center()
  138. if seq_file and os.path.isfile(seq_file):
  139. self._seq_tab.load_seq_file(os.path.abspath(seq_file))
  140. def _build_nav_bar(self) -> None:
  141. tb = QToolBar("Navigation", self)
  142. tb.setMovable(False)
  143. tb.setFloatable(False)
  144. tb.setIconSize(QSize(0, 0))
  145. tb.setStyleSheet(_NAV_TOOLBAR_CSS)
  146. tb.setFixedHeight(_NAV_H)
  147. self.addToolBar(Qt.TopToolBarArea, tb)
  148. lbl = QLabel(" LF-MRI ")
  149. lbl.setStyleSheet(
  150. f"color: #444466; font-weight: bold; font-size: 11px; "
  151. f"background: {_NAV_BG}; padding: 0 4px;"
  152. )
  153. tb.addWidget(lbl)
  154. sep = _VSep(tb)
  155. tb.addWidget(sep)
  156. self._nav_btn_group = QButtonGroup(self)
  157. self._nav_btn_group.setExclusive(True)
  158. self._nav_tab_buttons: list[QPushButton] = []
  159. for i, key in enumerate(_TAB_NAV_KEYS):
  160. btn = QPushButton(i18n.tr(key))
  161. btn.setCheckable(True)
  162. btn.setStyleSheet(_TAB_BTN_CSS)
  163. btn.setFixedHeight(_NAV_H)
  164. btn.setCursor(Qt.PointingHandCursor)
  165. self._nav_btn_group.addButton(btn, i)
  166. tb.addWidget(btn)
  167. self._nav_tab_buttons.append(btn)
  168. btn.clicked.connect(lambda _checked, idx=i: self._switch_tab(idx))
  169. self._nav_tab_buttons[0].setChecked(True)
  170. spacer = QWidget()
  171. spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
  172. spacer.setStyleSheet(f"background: {_NAV_BG};")
  173. tb.addWidget(spacer)
  174. # Language toggle (EN / RU)
  175. self._lang_btn_group = QButtonGroup(self)
  176. self._lang_btn_group.setExclusive(True)
  177. self._btn_lang_en = QPushButton("EN")
  178. self._btn_lang_en.setCheckable(True)
  179. self._btn_lang_en.setChecked(True)
  180. self._btn_lang_en.setStyleSheet(_LANG_BTN_CSS)
  181. self._btn_lang_en.setCursor(Qt.PointingHandCursor)
  182. self._btn_lang_ru = QPushButton("RU")
  183. self._btn_lang_ru.setCheckable(True)
  184. self._btn_lang_ru.setStyleSheet(_LANG_BTN_CSS)
  185. self._btn_lang_ru.setCursor(Qt.PointingHandCursor)
  186. self._lang_btn_group.addButton(self._btn_lang_en)
  187. self._lang_btn_group.addButton(self._btn_lang_ru)
  188. self._btn_lang_en.clicked.connect(lambda: self._on_language_change("en"))
  189. self._btn_lang_ru.clicked.connect(lambda: self._on_language_change("ru"))
  190. tb.addWidget(self._btn_lang_en)
  191. tb.addWidget(self._btn_lang_ru)
  192. def _switch_tab(self, index: int) -> None:
  193. self._tabs.setCurrentIndex(index)
  194. self._nav_tab_buttons[index].setChecked(True)
  195. def _build_status_bar(self) -> None:
  196. sb = QStatusBar()
  197. sb.setStyleSheet(
  198. "QStatusBar { background: #0c0c1a; color: #555577; font-size: 11px; }"
  199. )
  200. self.setStatusBar(sb)
  201. sb.showMessage(f"{i18n.tr('active_tab')}: {i18n.tr(_TAB_NAV_KEYS[0])}")
  202. def _size_and_center(self) -> None:
  203. screen = QApplication.primaryScreen()
  204. if screen is not None:
  205. ag = screen.availableGeometry()
  206. w = min(1600, max(960, int(ag.width() * 0.92)))
  207. h = min(940, max(640, int(ag.height() * 0.90)))
  208. self.resize(w, h)
  209. self.move(
  210. ag.x() + (ag.width() - w) // 2,
  211. ag.y() + (ag.height() - h) // 2,
  212. )
  213. else:
  214. self.resize(1440, 860)
  215. def _on_tab_changed(self, index: int) -> None:
  216. key = _TAB_NAV_KEYS[index] if 0 <= index < len(_TAB_NAV_KEYS) else "-"
  217. self.statusBar().showMessage(f"{i18n.tr('active_tab')}: {i18n.tr(key)}")
  218. if 0 <= index < len(self._nav_tab_buttons):
  219. self._nav_tab_buttons[index].setChecked(True)
  220. def _on_language_change(self, lang: str) -> None:
  221. i18n.set_language(lang)
  222. self.retranslate_ui()
  223. def retranslate_ui(self) -> None:
  224. for i, key in enumerate(_TAB_NAV_KEYS):
  225. self._nav_tab_buttons[i].setText(i18n.tr(key))
  226. cur = self._tabs.currentIndex()
  227. key = _TAB_NAV_KEYS[cur] if 0 <= cur < len(_TAB_NAV_KEYS) else "-"
  228. self.statusBar().showMessage(f"{i18n.tr('active_tab')}: {i18n.tr(key)}")
  229. for tab in (
  230. self._seq_tab,
  231. self._scanner_tab,
  232. self._fid_tab,
  233. self._scanning_tab,
  234. self._spectroscopy_tab,
  235. ):
  236. if hasattr(tab, "retranslate_ui"):
  237. tab.retranslate_ui()
  238. def _on_fid_generated(self, path: str) -> None:
  239. self._seq_tab.load_seq_file(path)
  240. self._switch_tab(0)
  241. def _on_ready_for_scan(self, info: dict) -> None:
  242. self._scanner_tab.apply_seq_info(info)
  243. self._scanning_tab.apply_seq_info(info)
  244. self._switch_tab(1)
  245. class _VSep(QFrame):
  246. """Thin vertical separator for the nav toolbar."""
  247. def __init__(self, parent=None) -> None:
  248. super().__init__(parent)
  249. self.setFrameShape(QFrame.VLine)
  250. self.setFixedWidth(1)
  251. self.setStyleSheet("background: #1e1e38; border: none;")