refactor: 准备独立的 Studio 控件与菜单构建模块
让界面构建可脱离主窗口独立测试。界面与绑定检查 13 项通过,运行入口尚未接线。
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
"""Shell menus and actions prepared for S2, with no business signal handlers.
|
||||
|
||||
All actions have the supplied QMainWindow as their Qt parent. The caller binds
|
||||
commands after applying initial checks, and owns reset-target wording, pause
|
||||
state and action availability. Construction never calls the window's methods
|
||||
other than QMainWindow's menu/action API.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QAction, QActionGroup
|
||||
from PySide6.QtWidgets import QMainWindow
|
||||
|
||||
|
||||
class WindowMenus:
|
||||
"""Create the existing shell actions independently of any application controller."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
window: QMainWindow,
|
||||
*,
|
||||
show_cal: bool,
|
||||
show_finger_3d: bool,
|
||||
show_fps: bool,
|
||||
show_diagnostics: bool,
|
||||
settings_expanded: bool,
|
||||
) -> None:
|
||||
menu_bar = window.menuBar()
|
||||
|
||||
self.file_menu = menu_bar.addMenu("")
|
||||
self.act_open_video = QAction(window)
|
||||
self.file_menu.addAction(self.act_open_video)
|
||||
self.act_reveal_config = QAction(window)
|
||||
self.file_menu.addAction(self.act_reveal_config)
|
||||
self.file_menu.addSeparator()
|
||||
self.act_playback = QAction(window)
|
||||
self.file_menu.addAction(self.act_playback)
|
||||
self.act_playback_latest = QAction(window)
|
||||
self.file_menu.addAction(self.act_playback_latest)
|
||||
|
||||
# Space is a window action rather than an entry in a menu.
|
||||
self.act_play = QAction(window)
|
||||
self.act_play.setShortcut("Space")
|
||||
self.act_play.setShortcutContext(Qt.ShortcutContext.ApplicationShortcut)
|
||||
window.addAction(self.act_play)
|
||||
|
||||
# Acquisition session controls (run / preview / pause). Order on the bar:
|
||||
# File | Session | Analysis | View | Help.
|
||||
self.session_menu = menu_bar.addMenu("")
|
||||
|
||||
self.act_run = QAction(window)
|
||||
self.act_run.setShortcut("F5")
|
||||
self.session_menu.addAction(self.act_run)
|
||||
|
||||
self.act_stop = QAction(window)
|
||||
self.act_stop.setShortcut("Esc")
|
||||
self.session_menu.addAction(self.act_stop)
|
||||
|
||||
self.act_preview = QAction(window)
|
||||
self.act_preview.setShortcut("F6")
|
||||
self.session_menu.addAction(self.act_preview)
|
||||
|
||||
self.act_reset = QAction(window)
|
||||
self.act_reset.setShortcut("Ctrl+R")
|
||||
self.session_menu.addAction(self.act_reset)
|
||||
|
||||
# Pause has a shortcut as well as a rail button: an operator who is holding a
|
||||
# sample still wants to stop the numbers moving without reaching for the mouse.
|
||||
self.act_pause = QAction(window)
|
||||
self.act_pause.setShortcut("F7")
|
||||
self.act_pause.setCheckable(True)
|
||||
self.session_menu.addAction(self.act_pause)
|
||||
|
||||
# Offline analysis of a recording. Separate from Session: that menu starts
|
||||
# and stops a live device session.
|
||||
self.analysis_menu = menu_bar.addMenu("")
|
||||
# Serve / leave a stored analysis result (or run the pass when there is none).
|
||||
self.act_recompute = QAction(window)
|
||||
self.act_recompute.setCheckable(True)
|
||||
self.act_recompute.setChecked(False)
|
||||
self.analysis_menu.addAction(self.act_recompute)
|
||||
# Reopen the setup dialog after the first pass (profile / calibration / reference).
|
||||
self.act_analysis_setup = QAction(window)
|
||||
self.analysis_menu.addAction(self.act_analysis_setup)
|
||||
|
||||
self.view_menu = menu_bar.addMenu("")
|
||||
self.act_cal_panel = QAction(window)
|
||||
self.act_cal_panel.setCheckable(True)
|
||||
self.act_cal_panel.setChecked(show_cal)
|
||||
self.view_menu.addAction(self.act_cal_panel)
|
||||
|
||||
self.act_finger_3d = QAction(window)
|
||||
self.act_finger_3d.setCheckable(True)
|
||||
self.act_finger_3d.setChecked(show_finger_3d)
|
||||
self.view_menu.addAction(self.act_finger_3d)
|
||||
|
||||
# Everything the 3D pane can be told lives in one dialog: the pane itself is for
|
||||
# looking at (see ``Mesh3dPanel``). This is the menu way in; the pane's header
|
||||
# has the gear, which is the same call.
|
||||
self.act_scene3d_settings = QAction(window)
|
||||
self.view_menu.addAction(self.act_scene3d_settings)
|
||||
|
||||
self.act_show_fps = QAction(window)
|
||||
self.act_show_fps.setCheckable(True)
|
||||
self.act_show_fps.setChecked(show_fps)
|
||||
self.view_menu.addAction(self.act_show_fps)
|
||||
|
||||
# How much the panels explain themselves around their pictures. Checked *before*
|
||||
# the signal is connected, so restoring the preference cannot look like a click.
|
||||
self.act_show_diagnostics = QAction(window)
|
||||
self.act_show_diagnostics.setCheckable(True)
|
||||
self.act_show_diagnostics.setChecked(show_diagnostics)
|
||||
self.view_menu.addAction(self.act_show_diagnostics)
|
||||
|
||||
self.act_settings = QAction(window)
|
||||
self.act_settings.setCheckable(True)
|
||||
self.act_settings.setChecked(settings_expanded)
|
||||
self.view_menu.addAction(self.act_settings)
|
||||
|
||||
self.act_plot_settings = QAction(window)
|
||||
self.view_menu.addAction(self.act_plot_settings)
|
||||
|
||||
self.act_rotate_arrows = QAction(window)
|
||||
self.view_menu.addAction(self.act_rotate_arrows)
|
||||
self.view_menu.addSeparator()
|
||||
|
||||
# The graphics half of the old 重置: curves, window, cursor and the 3D camera.
|
||||
# Separate because resetting the device tracker is not the same intention as
|
||||
# wanting the views back, and only the first one belongs to the device (session menu).
|
||||
self.act_reset_view = QAction(window)
|
||||
self.act_reset_view.setShortcut("F8")
|
||||
self.view_menu.addAction(self.act_reset_view)
|
||||
self.view_menu.addSeparator()
|
||||
|
||||
self.theme_menu = self.view_menu.addMenu("")
|
||||
self.theme_group = QActionGroup(window)
|
||||
self.theme_group.setExclusive(True)
|
||||
self.act_theme_dark = QAction(window)
|
||||
self.act_theme_dark.setCheckable(True)
|
||||
self.act_theme_light = QAction(window)
|
||||
self.act_theme_light.setCheckable(True)
|
||||
self.theme_group.addAction(self.act_theme_dark)
|
||||
self.theme_group.addAction(self.act_theme_light)
|
||||
self.theme_menu.addAction(self.act_theme_dark)
|
||||
self.theme_menu.addAction(self.act_theme_light)
|
||||
|
||||
self.lang_menu = self.view_menu.addMenu("")
|
||||
self.lang_group = QActionGroup(window)
|
||||
self.lang_group.setExclusive(True)
|
||||
self.act_lang_zh = QAction(window)
|
||||
self.act_lang_zh.setCheckable(True)
|
||||
self.act_lang_en = QAction(window)
|
||||
self.act_lang_en.setCheckable(True)
|
||||
self.lang_group.addAction(self.act_lang_zh)
|
||||
self.lang_group.addAction(self.act_lang_en)
|
||||
self.lang_menu.addAction(self.act_lang_zh)
|
||||
self.lang_menu.addAction(self.act_lang_en)
|
||||
|
||||
self.help_menu = menu_bar.addMenu("")
|
||||
self.act_about = QAction(window)
|
||||
self.help_menu.addAction(self.act_about)
|
||||
|
||||
def retranslate(self) -> None:
|
||||
"""Update static labels; the caller refreshes context-dependent reset wording."""
|
||||
self.file_menu.setTitle(t("menu.file"))
|
||||
self.act_open_video.setText(t("menu.open_video"))
|
||||
self.act_reveal_config.setText(t("menu.reveal_config"))
|
||||
self.act_playback.setText(t("menu.playback"))
|
||||
self.act_playback_latest.setText(t("menu.playback_latest"))
|
||||
self.session_menu.setTitle(t("menu.session"))
|
||||
self.act_run.setText(t("menu.run"))
|
||||
self.act_stop.setText(t("menu.stop"))
|
||||
self.act_preview.setText(t("menu.preview"))
|
||||
self.act_pause.setText(t("menu.pause"))
|
||||
self.analysis_menu.setTitle(t("menu.analysis"))
|
||||
self.act_recompute.setText(t("menu.recompute"))
|
||||
self.act_recompute.setToolTip(t("menu.recompute_tip"))
|
||||
self.act_analysis_setup.setText(t("menu.analysis_setup"))
|
||||
self.act_analysis_setup.setToolTip(t("menu.analysis_setup_tip"))
|
||||
self.view_menu.setTitle(t("menu.view"))
|
||||
self.act_cal_panel.setText(t("menu.calibration_panel"))
|
||||
self.act_finger_3d.setText(t("menu.finger_3d"))
|
||||
self.act_scene3d_settings.setText(t("menu.scene3d_settings"))
|
||||
self.act_show_fps.setText(t("menu.show_fps"))
|
||||
self.act_show_diagnostics.setText(t("menu.show_diagnostics"))
|
||||
self.act_settings.setText(t("menu.settings"))
|
||||
self.act_plot_settings.setText(t("menu.plot_settings"))
|
||||
self.act_rotate_arrows.setText(t("menu.rotate_arrows"))
|
||||
self.act_reset_view.setText(t("menu.reset_view"))
|
||||
self.act_reset_view.setToolTip(t("menu.reset_view_tip"))
|
||||
self.theme_menu.setTitle(t("menu.theme"))
|
||||
self.act_theme_dark.setText(t("menu.theme_dark"))
|
||||
self.act_theme_light.setText(t("menu.theme_light"))
|
||||
self.lang_menu.setTitle(t("menu.language"))
|
||||
self.act_lang_zh.setText(t("menu.lang_zh"))
|
||||
self.act_lang_en.setText(t("menu.lang_en"))
|
||||
self.help_menu.setTitle(t("menu.help"))
|
||||
self.act_about.setText(t("menu.about"))
|
||||
|
||||
def sync_appearance(self, *, theme: str, language: str) -> None:
|
||||
"""Mirror appearance preferences without saving them or changing the application."""
|
||||
self.act_theme_dark.setChecked(theme != "light")
|
||||
self.act_theme_light.setChecked(theme == "light")
|
||||
self.act_lang_zh.setChecked(language.startswith("zh"))
|
||||
self.act_lang_en.setChecked(not language.startswith("zh"))
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Central shell widgets prepared for S2, without application wiring.
|
||||
|
||||
Construct on the GUI thread. The caller owns controllers, frame delivery, the
|
||||
shared domain latch, chart store binding and preference restoration. Nothing
|
||||
here starts acquisition, applies calibration or creates the lazy 3D view.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from orisys_app_shared.paths import logo_path
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtWidgets import QMainWindow, QSplitter, QVBoxLayout, QWidget
|
||||
|
||||
from orisys_studio.ui.analysis_progress import AnalysisProgress
|
||||
from orisys_studio.ui.arrow_panel import ArrowPanel
|
||||
from orisys_studio.ui.brand_bar import BrandBar
|
||||
from orisys_studio.ui.cal_panel import CalPanel
|
||||
from orisys_studio.ui.force_plots import ForcePlotsPanel
|
||||
from orisys_studio.ui.mesh3d_panel import Mesh3dPanel
|
||||
from orisys_studio.ui.sensor_picker import SensorPickerWidget
|
||||
from orisys_studio.ui.toolbar import ToolbarWidget
|
||||
|
||||
|
||||
class WindowUi:
|
||||
"""Own widget references, leaving all workflow decisions to the caller."""
|
||||
|
||||
def __init__(self, window: QMainWindow, *, theme: str) -> None:
|
||||
icon = QIcon(str(logo_path()))
|
||||
if not icon.isNull():
|
||||
window.setWindowIcon(icon)
|
||||
|
||||
self.central = QWidget()
|
||||
window.setCentralWidget(self.central)
|
||||
root = QVBoxLayout(self.central)
|
||||
root.setContentsMargins(10, 10, 10, 10)
|
||||
root.setSpacing(8)
|
||||
|
||||
self.brand_bar = BrandBar(theme=theme)
|
||||
root.addWidget(self.brand_bar)
|
||||
# The sensor dialog adopts this picker later; it is not in the central layout.
|
||||
self.sensor_picker = SensorPickerWidget()
|
||||
self.toolbar = ToolbarWidget(theme=theme)
|
||||
root.addWidget(self.toolbar)
|
||||
self.status_chip = self.toolbar.status_chip
|
||||
self.analysis_progress = AnalysisProgress(theme=theme)
|
||||
root.addWidget(self.analysis_progress)
|
||||
|
||||
self.content_splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
self.mesh3d_panel = Mesh3dPanel(theme=theme)
|
||||
self.arrow_panel = ArrowPanel(theme=theme)
|
||||
self.force_plots = ForcePlotsPanel(theme=theme)
|
||||
self.cal_panel = CalPanel(theme=theme)
|
||||
self.content_splitter.addWidget(self.mesh3d_panel)
|
||||
self.content_splitter.addWidget(self.arrow_panel)
|
||||
self.content_splitter.addWidget(self.force_plots)
|
||||
self.content_splitter.addWidget(self.cal_panel)
|
||||
self.content_splitter.setChildrenCollapsible(False)
|
||||
root.addWidget(self.content_splitter, 1)
|
||||
|
||||
def set_theme(self, theme: str) -> None:
|
||||
"""Refresh local surfaces; the application stylesheet remains the caller's."""
|
||||
self.force_plots.set_theme(theme)
|
||||
self.cal_panel.set_theme(theme)
|
||||
self.brand_bar.set_theme(theme)
|
||||
self.toolbar.set_theme(theme)
|
||||
self.arrow_panel.set_theme(theme)
|
||||
self.mesh3d_panel.set_theme(theme)
|
||||
|
||||
def retranslate(self) -> None:
|
||||
"""Refresh widget text without changing the application locale or state."""
|
||||
self.toolbar.retranslate()
|
||||
self.sensor_picker.retranslate()
|
||||
self.mesh3d_panel.retranslate()
|
||||
self.arrow_panel.retranslate()
|
||||
self.force_plots.retranslate()
|
||||
self.cal_panel.retranslate()
|
||||
self.brand_bar.retranslate()
|
||||
Reference in New Issue
Block a user