|
|
@@ -8,14 +8,16 @@ provides a unified, flat navigation strip.
|
|
|
from __future__ import annotations
|
|
|
|
|
|
import os
|
|
|
+import threading
|
|
|
|
|
|
-from PySide6.QtCore import Qt, QSize, QTimer
|
|
|
+from PySide6.QtCore import Qt, QObject, QSize, QTimer, Signal
|
|
|
from PySide6.QtWidgets import (
|
|
|
QApplication,
|
|
|
QButtonGroup,
|
|
|
QFrame,
|
|
|
QLabel,
|
|
|
QMainWindow,
|
|
|
+ QMessageBox,
|
|
|
QPushButton,
|
|
|
QSizePolicy,
|
|
|
QStatusBar,
|
|
|
@@ -25,6 +27,7 @@ from PySide6.QtWidgets import (
|
|
|
)
|
|
|
|
|
|
from src import i18n, theme
|
|
|
+from src.clients.orchestrator_client import OrchestratorClient, OrchestratorError
|
|
|
from src.tabs.fid_tab import FidTab
|
|
|
from src.tabs.scanner_tab import ScannerTab
|
|
|
from src.tabs.scanning_tab import ScanningTab
|
|
|
@@ -111,6 +114,44 @@ QPushButton:hover:!checked {{
|
|
|
"""
|
|
|
|
|
|
|
|
|
+def _mode_btn_css(dark: bool, mode: str) -> str:
|
|
|
+ if mode == "real":
|
|
|
+ color = "#2ecc71"
|
|
|
+ border = "#27ae60"
|
|
|
+ else:
|
|
|
+ color = "#f39c12"
|
|
|
+ border = "#e67e00"
|
|
|
+ return f"""
|
|
|
+QPushButton {{
|
|
|
+ background: transparent;
|
|
|
+ color: {color};
|
|
|
+ border: 1px solid {border};
|
|
|
+ border-radius: 3px;
|
|
|
+ padding: 0px 10px;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: bold;
|
|
|
+ min-height: 22px;
|
|
|
+ max-height: 22px;
|
|
|
+ letter-spacing: 0.5px;
|
|
|
+}}
|
|
|
+QPushButton:hover {{
|
|
|
+ background: {color}22;
|
|
|
+}}
|
|
|
+QPushButton:disabled {{
|
|
|
+ color: #555577;
|
|
|
+ border-color: #333355;
|
|
|
+}}
|
|
|
+"""
|
|
|
+
|
|
|
+
|
|
|
+class _ModeSignalBridge(QObject):
|
|
|
+ """Carries results of background mode HTTP calls back to the Qt main thread."""
|
|
|
+
|
|
|
+ mode_fetched = Signal(str) # mode string on successful GET /mode
|
|
|
+ mode_set = Signal(str) # mode string on successful POST /mode
|
|
|
+ mode_error = Signal(str) # error message (empty = silent startup failure)
|
|
|
+
|
|
|
+
|
|
|
def _spec_btn_blink_css(dark: bool) -> str:
|
|
|
active_txt = "#ffffff" if dark else "#1a1a2e"
|
|
|
accent = "#f0c040" if dark else "#c09000"
|
|
|
@@ -196,14 +237,28 @@ class LFMRIWindow(QMainWindow):
|
|
|
self._spec_blink_timer.setInterval(700)
|
|
|
self._spec_blink_timer.timeout.connect(self._tick_spec_blink)
|
|
|
|
|
|
+ # Mode selector state
|
|
|
+ self._orchestrator_url = orchestrator_url
|
|
|
+ self._current_mode: str = "plug" # updated by background fetch after startup
|
|
|
+ self._mode_bridge = _ModeSignalBridge(self)
|
|
|
+
|
|
|
self.menuBar().hide()
|
|
|
self._build_nav_bar()
|
|
|
+
|
|
|
+ # Connect mode signals after nav bar is built (_mode_btn exists)
|
|
|
+ self._mode_bridge.mode_fetched.connect(self._on_mode_fetched)
|
|
|
+ self._mode_bridge.mode_set.connect(self._on_mode_set)
|
|
|
+ self._mode_bridge.mode_error.connect(self._on_mode_error)
|
|
|
+
|
|
|
self._build_status_bar()
|
|
|
self._size_and_center()
|
|
|
|
|
|
# Apply default dark theme to the whole application
|
|
|
self._apply_theme(theme.is_dark())
|
|
|
|
|
|
+ # Fetch current mode from orchestrator once the event loop is running
|
|
|
+ QTimer.singleShot(600, self._start_fetch_mode)
|
|
|
+
|
|
|
if seq_file and os.path.isfile(seq_file):
|
|
|
self._seq_tab.load_seq_file(os.path.abspath(seq_file))
|
|
|
|
|
|
@@ -246,6 +301,17 @@ class LFMRIWindow(QMainWindow):
|
|
|
self._nav_spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
|
|
tb.addWidget(self._nav_spacer)
|
|
|
|
|
|
+ # Mode indicator / switcher
|
|
|
+ self._mode_btn = QPushButton("…")
|
|
|
+ self._mode_btn.setFixedHeight(22)
|
|
|
+ self._mode_btn.setCursor(Qt.PointingHandCursor)
|
|
|
+ self._mode_btn.setToolTip("Click to switch operating mode")
|
|
|
+ self._mode_btn.clicked.connect(self._on_mode_btn_clicked)
|
|
|
+ tb.addWidget(self._mode_btn)
|
|
|
+
|
|
|
+ self._nav_sep_mode = _VSep(tb)
|
|
|
+ tb.addWidget(self._nav_sep_mode)
|
|
|
+
|
|
|
# Language toggle (EN / RU)
|
|
|
self._lang_btn_group = QButtonGroup(self)
|
|
|
self._lang_btn_group.setExclusive(True)
|
|
|
@@ -308,6 +374,7 @@ class LFMRIWindow(QMainWindow):
|
|
|
self._nav_toolbar.setStyleSheet(_nav_toolbar_css(dark))
|
|
|
self._nav_sep1.setStyleSheet(f"background: {sep_clr}; border: none;")
|
|
|
self._nav_sep2.setStyleSheet(f"background: {sep_clr}; border: none;")
|
|
|
+ self._nav_sep_mode.setStyleSheet(f"background: {sep_clr}; border: none;")
|
|
|
self._nav_logo_lbl.setStyleSheet(
|
|
|
f"color: {logo_clr}; font-weight: bold; font-size: 11px; "
|
|
|
f"background: {bg}; padding: 0 4px;"
|
|
|
@@ -321,6 +388,7 @@ class LFMRIWindow(QMainWindow):
|
|
|
self._btn_lang_ru.setStyleSheet(lang_css)
|
|
|
self._btn_theme_dark.setStyleSheet(lang_css)
|
|
|
self._btn_theme_light.setStyleSheet(lang_css)
|
|
|
+ self._update_mode_btn()
|
|
|
|
|
|
sb_bg = "#0c0c1a" if dark else "#dce0f0"
|
|
|
sb_color = "#555577" if dark else "#787faa"
|
|
|
@@ -382,6 +450,7 @@ class LFMRIWindow(QMainWindow):
|
|
|
def retranslate_ui(self) -> None:
|
|
|
for i, key in enumerate(_TAB_NAV_KEYS):
|
|
|
self._nav_tab_buttons[i].setText(i18n.tr(key))
|
|
|
+ self._update_mode_btn()
|
|
|
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)}")
|
|
|
@@ -433,6 +502,79 @@ class LFMRIWindow(QMainWindow):
|
|
|
_tab_btn_css(theme.is_dark())
|
|
|
)
|
|
|
|
|
|
+ # ------------------------------------------------------------------ #
|
|
|
+ # Mode selector #
|
|
|
+ # ------------------------------------------------------------------ #
|
|
|
+
|
|
|
+ def _update_mode_btn(self) -> None:
|
|
|
+ """Refresh mode button label and colour from _current_mode."""
|
|
|
+ text = i18n.tr(f"mode_{self._current_mode}")
|
|
|
+ self._mode_btn.setText(text)
|
|
|
+ self._mode_btn.setStyleSheet(_mode_btn_css(theme.is_dark(), self._current_mode))
|
|
|
+
|
|
|
+ # -- background fetch (startup) ----------------------------------------
|
|
|
+
|
|
|
+ def _start_fetch_mode(self) -> None:
|
|
|
+ threading.Thread(
|
|
|
+ target=self._fetch_mode_bg,
|
|
|
+ daemon=True,
|
|
|
+ name="mode-fetch",
|
|
|
+ ).start()
|
|
|
+
|
|
|
+ def _fetch_mode_bg(self) -> None:
|
|
|
+ try:
|
|
|
+ client = OrchestratorClient(self._orchestrator_url)
|
|
|
+ mode = client.get_mode()
|
|
|
+ self._mode_bridge.mode_fetched.emit(mode)
|
|
|
+ except OrchestratorError:
|
|
|
+ pass # Orchestrator may not be up yet — keep the default label
|
|
|
+
|
|
|
+ # -- slots (main thread) -----------------------------------------------
|
|
|
+
|
|
|
+ def _on_mode_fetched(self, mode: str) -> None:
|
|
|
+ self._current_mode = mode
|
|
|
+ self._update_mode_btn()
|
|
|
+
|
|
|
+ def _on_mode_set(self, mode: str) -> None:
|
|
|
+ self._current_mode = mode
|
|
|
+ self._mode_btn.setEnabled(True)
|
|
|
+ self._update_mode_btn()
|
|
|
+
|
|
|
+ def _on_mode_error(self, msg: str) -> None:
|
|
|
+ self._mode_btn.setEnabled(True)
|
|
|
+ if msg:
|
|
|
+ QMessageBox.warning(self, i18n.tr("mode_error"), msg)
|
|
|
+
|
|
|
+ # -- click handler (main thread) ---------------------------------------
|
|
|
+
|
|
|
+ def _on_mode_btn_clicked(self) -> None:
|
|
|
+ target = "real" if self._current_mode == "plug" else "plug"
|
|
|
+ confirm_key = f"mode_confirm_to_{target}"
|
|
|
+ reply = QMessageBox.question(
|
|
|
+ self,
|
|
|
+ i18n.tr("mode_confirm_title"),
|
|
|
+ i18n.tr(confirm_key),
|
|
|
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
|
+ QMessageBox.StandardButton.No,
|
|
|
+ )
|
|
|
+ if reply != QMessageBox.StandardButton.Yes:
|
|
|
+ return
|
|
|
+ self._mode_btn.setEnabled(False)
|
|
|
+ threading.Thread(
|
|
|
+ target=self._set_mode_bg,
|
|
|
+ args=(target,),
|
|
|
+ daemon=True,
|
|
|
+ name="mode-set",
|
|
|
+ ).start()
|
|
|
+
|
|
|
+ def _set_mode_bg(self, mode: str) -> None:
|
|
|
+ try:
|
|
|
+ client = OrchestratorClient(self._orchestrator_url)
|
|
|
+ result = client.set_mode(mode)
|
|
|
+ self._mode_bridge.mode_set.emit(result)
|
|
|
+ except OrchestratorError as exc:
|
|
|
+ self._mode_bridge.mode_error.emit(str(exc))
|
|
|
+
|
|
|
|
|
|
class _VSep(QFrame):
|
|
|
"""Thin vertical separator for the nav toolbar."""
|