接入 Studio 控件与菜单构建模块
将界面构建从主窗口移出,保留现有业务回调与标定路径。验证结果和完整门禁限制见 docs/architecture/studio-window-refactor-plan.zh.md。
This commit is contained in:
@@ -28,20 +28,16 @@ from orisys_app_shared.force.filters import FilterSettings
|
|||||||
from orisys_app_shared.force.session import SessionStore
|
from orisys_app_shared.force.session import SessionStore
|
||||||
from orisys_app_shared.i18n import set_locale, t
|
from orisys_app_shared.i18n import set_locale, t
|
||||||
from orisys_app_shared.logging import report_error
|
from orisys_app_shared.logging import report_error
|
||||||
from orisys_app_shared.paths import logo_path, recordings_dir, sensor_json_path
|
from orisys_app_shared.paths import recordings_dir, sensor_json_path
|
||||||
from orisys_app_shared.prefs import save_preferences
|
from orisys_app_shared.prefs import save_preferences
|
||||||
from orisys_app_shared.record import RecordConfig
|
from orisys_app_shared.record import RecordConfig
|
||||||
from orisys_app_shared.theme import build_qss, clear_theme_cache
|
from orisys_app_shared.theme import build_qss, clear_theme_cache
|
||||||
from PySide6.QtCore import Qt, QTimer
|
from PySide6.QtCore import QTimer
|
||||||
from PySide6.QtGui import QAction, QActionGroup, QIcon
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QApplication,
|
QApplication,
|
||||||
QMainWindow,
|
QMainWindow,
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
QSplitter,
|
|
||||||
QStatusBar,
|
QStatusBar,
|
||||||
QVBoxLayout,
|
|
||||||
QWidget,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from orisys_studio import session_metadata
|
from orisys_studio import session_metadata
|
||||||
@@ -56,12 +52,7 @@ from orisys_studio.scene3d.prefs import settings_to_prefs
|
|||||||
from orisys_studio.session_controller import SessionController
|
from orisys_studio.session_controller import SessionController
|
||||||
from orisys_studio.status_presenter import RailState, StatusPresenter
|
from orisys_studio.status_presenter import RailState, StatusPresenter
|
||||||
from orisys_studio.ui.about_dialog import AboutDialog
|
from orisys_studio.ui.about_dialog import AboutDialog
|
||||||
from orisys_studio.ui.analysis_progress import AnalysisProgress
|
from orisys_studio.ui.arrow_panel import bgr_to_qimage
|
||||||
from orisys_studio.ui.arrow_panel import ArrowPanel, bgr_to_qimage
|
|
||||||
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.finger_orientation_dialog import FingerOrientationDialog
|
from orisys_studio.ui.finger_orientation_dialog import FingerOrientationDialog
|
||||||
from orisys_studio.ui.plot_settings_dialog import (
|
from orisys_studio.ui.plot_settings_dialog import (
|
||||||
DEFAULT_PLOT_FN_MAX,
|
DEFAULT_PLOT_FN_MAX,
|
||||||
@@ -71,9 +62,9 @@ from orisys_studio.ui.plot_settings_dialog import (
|
|||||||
DEFAULT_PLOT_WINDOW_SEC,
|
DEFAULT_PLOT_WINDOW_SEC,
|
||||||
PlotSettingsDialog,
|
PlotSettingsDialog,
|
||||||
)
|
)
|
||||||
from orisys_studio.ui.sensor_picker import SensorPickerWidget
|
|
||||||
from orisys_studio.ui.toolbar import ToolbarWidget
|
|
||||||
from orisys_studio.ui.video_analysis_dialog import VideoAnalysisDialog
|
from orisys_studio.ui.video_analysis_dialog import VideoAnalysisDialog
|
||||||
|
from orisys_studio.ui.window_menus import WindowMenus
|
||||||
|
from orisys_studio.ui.window_ui import WindowUi
|
||||||
from orisys_studio.view_layout import ViewLayoutController
|
from orisys_studio.view_layout import ViewLayoutController
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -228,54 +219,25 @@ class StudioMainWindow(QMainWindow):
|
|||||||
#: Built once the panels exist; 2D paint and shared depth span live there.
|
#: Built once the panels exist; 2D paint and shared depth span live there.
|
||||||
self.frame_view: FrameView | None = None
|
self.frame_view: FrameView | None = None
|
||||||
|
|
||||||
icon = QIcon(str(logo_path()))
|
self.ui = WindowUi(self, theme=self._theme)
|
||||||
if not icon.isNull():
|
# Preserve the widget entry points used by controllers, tests and UI tools.
|
||||||
self.setWindowIcon(icon)
|
self.brand_bar = self.ui.brand_bar
|
||||||
|
self.sensor_picker = self.ui.sensor_picker
|
||||||
central = QWidget()
|
self.toolbar = self.ui.toolbar
|
||||||
self.setCentralWidget(central)
|
self.status_chip = self.ui.status_chip
|
||||||
root = QVBoxLayout(central)
|
self.analysis_progress = self.ui.analysis_progress
|
||||||
root.setContentsMargins(10, 10, 10, 10)
|
self.content_splitter = self.ui.content_splitter
|
||||||
root.setSpacing(8)
|
self.mesh3d_panel = self.ui.mesh3d_panel
|
||||||
|
self.arrow_panel = self.ui.arrow_panel
|
||||||
self.brand_bar = BrandBar(theme=self._theme)
|
self.force_plots = self.ui.force_plots
|
||||||
root.addWidget(self.brand_bar)
|
self.cal_panel = self.ui.cal_panel
|
||||||
|
|
||||||
# The sensor picker itself lives in the sensor dialog; the window owns it
|
|
||||||
# (it is the state every other panel depends on) and the rail shows a
|
|
||||||
# summary button that opens it.
|
|
||||||
self.sensor_picker = SensorPickerWidget()
|
|
||||||
#: Built on first use so the pickers are parented where they belong.
|
#: Built on first use so the pickers are parented where they belong.
|
||||||
self.sensor_dialog = None
|
self.sensor_dialog = None
|
||||||
self.toolbar = ToolbarWidget(theme=self._theme)
|
|
||||||
root.addWidget(self.toolbar)
|
|
||||||
self.status_chip = self.toolbar.status_chip
|
|
||||||
self.status_presenter = StatusPresenter(self.toolbar)
|
self.status_presenter = StatusPresenter(self.toolbar)
|
||||||
# Part B: the analysis pass a replay is served from. The surface sits directly
|
# Bind the shared mask and chart history before controllers can deliver frames.
|
||||||
# under the rail, so "what is it doing and can I stop it" is answered where the
|
|
||||||
# transport is; it is hidden until a pass (or a cache hit) has something to say.
|
|
||||||
self.analysis_progress = AnalysisProgress(theme=self._theme)
|
|
||||||
root.addWidget(self.analysis_progress)
|
|
||||||
|
|
||||||
self.content_splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
||||||
self.mesh3d_panel = Mesh3dPanel(theme=self._theme)
|
|
||||||
self.arrow_panel = ArrowPanel(theme=self._theme)
|
|
||||||
# The island latch is *upstream of both views*: the 2D canvas paints the
|
|
||||||
# latched boundary, and the 3D pane's validity texture is that same mask. Two
|
|
||||||
# views, one decision about where the measurement is — see
|
|
||||||
# ``image_backend.DomainMaskLatch`` and ``Scene3DView.set_domain_latch``.
|
|
||||||
self.frame_view = FrameView(self.arrow_panel, self.mesh3d_panel)
|
self.frame_view = FrameView(self.arrow_panel, self.mesh3d_panel)
|
||||||
self.mesh3d_panel.set_domain_latch(self.frame_view.domain_mask)
|
self.mesh3d_panel.set_domain_latch(self.frame_view.domain_mask)
|
||||||
self.force_plots = ForcePlotsPanel(theme=self._theme)
|
|
||||||
# The charts browse the whole session, not just their rolling buffer.
|
|
||||||
self.force_plots.set_store(self.session_store)
|
self.force_plots.set_store(self.session_store)
|
||||||
self.cal_panel = CalPanel(theme=self._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)
|
|
||||||
|
|
||||||
# Playback drives the *chart cursor* (the playhead) instead of a slider
|
# Playback drives the *chart cursor* (the playhead) instead of a slider
|
||||||
# under the panels: the two views share one layout, and the transport is
|
# under the panels: the two views share one layout, and the transport is
|
||||||
@@ -1037,162 +999,72 @@ class StudioMainWindow(QMainWindow):
|
|||||||
self.content_splitter.setSizes(sizes)
|
self.content_splitter.setSizes(sizes)
|
||||||
|
|
||||||
def _build_menus(self) -> None:
|
def _build_menus(self) -> None:
|
||||||
menu_bar = self.menuBar()
|
self.menus = WindowMenus(
|
||||||
|
self,
|
||||||
|
show_cal=self._show_cal,
|
||||||
|
show_finger_3d=self._show_finger_3d,
|
||||||
|
show_fps=self._show_fps,
|
||||||
|
show_diagnostics=self.display_ctl.show_diagnostics,
|
||||||
|
settings_expanded=self.toolbar.settings_expanded(),
|
||||||
|
)
|
||||||
|
# Existing controllers and tools use these names; each is the same Qt object.
|
||||||
|
self.file_menu = self.menus.file_menu
|
||||||
|
self.act_open_video = self.menus.act_open_video
|
||||||
|
self.act_reveal_config = self.menus.act_reveal_config
|
||||||
|
self.act_playback = self.menus.act_playback
|
||||||
|
self.act_playback_latest = self.menus.act_playback_latest
|
||||||
|
self.act_play = self.menus.act_play
|
||||||
|
self.session_menu = self.menus.session_menu
|
||||||
|
self.act_run = self.menus.act_run
|
||||||
|
self.act_stop = self.menus.act_stop
|
||||||
|
self.act_preview = self.menus.act_preview
|
||||||
|
self.act_reset = self.menus.act_reset
|
||||||
|
self.act_pause = self.menus.act_pause
|
||||||
|
self.analysis_menu = self.menus.analysis_menu
|
||||||
|
self.act_recompute = self.menus.act_recompute
|
||||||
|
self.act_analysis_setup = self.menus.act_analysis_setup
|
||||||
|
self.view_menu = self.menus.view_menu
|
||||||
|
self.act_cal_panel = self.menus.act_cal_panel
|
||||||
|
self.act_finger_3d = self.menus.act_finger_3d
|
||||||
|
self.act_scene3d_settings = self.menus.act_scene3d_settings
|
||||||
|
self.act_show_fps = self.menus.act_show_fps
|
||||||
|
self.act_show_diagnostics = self.menus.act_show_diagnostics
|
||||||
|
self.act_settings = self.menus.act_settings
|
||||||
|
self.act_plot_settings = self.menus.act_plot_settings
|
||||||
|
self.act_rotate_arrows = self.menus.act_rotate_arrows
|
||||||
|
self.act_reset_view = self.menus.act_reset_view
|
||||||
|
self.theme_menu = self.menus.theme_menu
|
||||||
|
self.theme_group = self.menus.theme_group
|
||||||
|
self.act_theme_dark = self.menus.act_theme_dark
|
||||||
|
self.act_theme_light = self.menus.act_theme_light
|
||||||
|
self.lang_menu = self.menus.lang_menu
|
||||||
|
self.lang_group = self.menus.lang_group
|
||||||
|
self.act_lang_zh = self.menus.act_lang_zh
|
||||||
|
self.act_lang_en = self.menus.act_lang_en
|
||||||
|
self.help_menu = self.menus.help_menu
|
||||||
|
self.act_about = self.menus.act_about
|
||||||
|
|
||||||
self.file_menu = menu_bar.addMenu("")
|
# Initial checks are set before any business handlers are connected.
|
||||||
self.act_open_video = QAction(self)
|
|
||||||
self.file_menu.addAction(self.act_open_video)
|
|
||||||
self.act_reveal_config = QAction(self)
|
|
||||||
self.act_reveal_config.triggered.connect(self._on_reveal_config)
|
self.act_reveal_config.triggered.connect(self._on_reveal_config)
|
||||||
self.file_menu.addAction(self.act_reveal_config)
|
|
||||||
self.file_menu.addSeparator()
|
|
||||||
self.act_playback = QAction(self)
|
|
||||||
self.file_menu.addAction(self.act_playback)
|
|
||||||
self.act_playback_latest = QAction(self)
|
|
||||||
self.file_menu.addAction(self.act_playback_latest)
|
|
||||||
|
|
||||||
# The transport is on the chart header; the space bar is the editing
|
|
||||||
# convention everyone's hands already know.
|
|
||||||
self.act_play = QAction(self)
|
|
||||||
self.act_play.setShortcut("Space")
|
|
||||||
self.act_play.setShortcutContext(Qt.ShortcutContext.ApplicationShortcut)
|
|
||||||
self.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(self)
|
|
||||||
self.act_run.setShortcut("F5")
|
|
||||||
self.act_run.triggered.connect(self._on_run_shortcut)
|
self.act_run.triggered.connect(self._on_run_shortcut)
|
||||||
self.session_menu.addAction(self.act_run)
|
|
||||||
|
|
||||||
self.act_stop = QAction(self)
|
|
||||||
self.act_stop.setShortcut("Esc")
|
|
||||||
self.act_stop.triggered.connect(self._on_stop_shortcut)
|
self.act_stop.triggered.connect(self._on_stop_shortcut)
|
||||||
self.session_menu.addAction(self.act_stop)
|
|
||||||
|
|
||||||
self.act_preview = QAction(self)
|
|
||||||
self.act_preview.setShortcut("F6")
|
|
||||||
self.act_preview.triggered.connect(self._on_preview_shortcut)
|
self.act_preview.triggered.connect(self._on_preview_shortcut)
|
||||||
self.session_menu.addAction(self.act_preview)
|
|
||||||
|
|
||||||
self.act_reset = QAction(self)
|
|
||||||
self.act_reset.setShortcut("Ctrl+R")
|
|
||||||
self.act_reset.triggered.connect(self._on_reset)
|
self.act_reset.triggered.connect(self._on_reset)
|
||||||
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(self)
|
|
||||||
self.act_pause.setShortcut("F7")
|
|
||||||
self.act_pause.setCheckable(True)
|
|
||||||
self.act_pause.toggled.connect(self._on_pause_action_toggled)
|
self.act_pause.toggled.connect(self._on_pause_action_toggled)
|
||||||
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(self)
|
|
||||||
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(self)
|
|
||||||
self.act_analysis_setup.triggered.connect(self.replay_ctl.on_analysis_setup)
|
self.act_analysis_setup.triggered.connect(self.replay_ctl.on_analysis_setup)
|
||||||
self.analysis_menu.addAction(self.act_analysis_setup)
|
|
||||||
|
|
||||||
self.view_menu = menu_bar.addMenu("")
|
|
||||||
self.act_cal_panel = QAction(self)
|
|
||||||
self.act_cal_panel.setCheckable(True)
|
|
||||||
self.act_cal_panel.setChecked(self._show_cal)
|
|
||||||
self.view_menu.addAction(self.act_cal_panel)
|
|
||||||
|
|
||||||
self.act_finger_3d = QAction(self)
|
|
||||||
self.act_finger_3d.setCheckable(True)
|
|
||||||
self.act_finger_3d.setChecked(self._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(self)
|
|
||||||
self.act_scene3d_settings.triggered.connect(self._on_scene3d_settings)
|
self.act_scene3d_settings.triggered.connect(self._on_scene3d_settings)
|
||||||
self.view_menu.addAction(self.act_scene3d_settings)
|
|
||||||
|
|
||||||
self.act_show_fps = QAction(self)
|
|
||||||
self.act_show_fps.setCheckable(True)
|
|
||||||
self.act_show_fps.setChecked(self._show_fps)
|
|
||||||
self.act_show_fps.toggled.connect(self.display_ctl.on_show_fps_toggled)
|
self.act_show_fps.toggled.connect(self.display_ctl.on_show_fps_toggled)
|
||||||
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(self)
|
|
||||||
self.act_show_diagnostics.setCheckable(True)
|
|
||||||
self.act_show_diagnostics.setChecked(self.display_ctl.show_diagnostics)
|
|
||||||
self.act_show_diagnostics.toggled.connect(
|
self.act_show_diagnostics.toggled.connect(
|
||||||
self.display_ctl.on_show_diagnostics_toggled
|
self.display_ctl.on_show_diagnostics_toggled
|
||||||
)
|
)
|
||||||
self.view_menu.addAction(self.act_show_diagnostics)
|
|
||||||
|
|
||||||
|
|
||||||
self.act_settings = QAction(self)
|
|
||||||
self.act_settings.setCheckable(True)
|
|
||||||
self.act_settings.setChecked(self.toolbar.settings_expanded())
|
|
||||||
self.view_menu.addAction(self.act_settings)
|
|
||||||
|
|
||||||
self.act_plot_settings = QAction(self)
|
|
||||||
self.act_plot_settings.triggered.connect(self._on_plot_settings)
|
self.act_plot_settings.triggered.connect(self._on_plot_settings)
|
||||||
self.view_menu.addAction(self.act_plot_settings)
|
|
||||||
|
|
||||||
self.act_rotate_arrows = QAction(self)
|
|
||||||
self.act_rotate_arrows.triggered.connect(self.display_ctl.on_rotate_arrows)
|
self.act_rotate_arrows.triggered.connect(self.display_ctl.on_rotate_arrows)
|
||||||
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(self)
|
|
||||||
self.act_reset_view.setShortcut("F8")
|
|
||||||
self.act_reset_view.triggered.connect(self._on_reset_view)
|
self.act_reset_view.triggered.connect(self._on_reset_view)
|
||||||
self.view_menu.addAction(self.act_reset_view)
|
|
||||||
self.view_menu.addSeparator()
|
|
||||||
|
|
||||||
self.theme_menu = self.view_menu.addMenu("")
|
|
||||||
self.theme_group = QActionGroup(self)
|
|
||||||
self.theme_group.setExclusive(True)
|
|
||||||
self.act_theme_dark = QAction(self)
|
|
||||||
self.act_theme_dark.setCheckable(True)
|
|
||||||
self.act_theme_light = QAction(self)
|
|
||||||
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.act_theme_dark.triggered.connect(lambda: self._set_theme("dark"))
|
self.act_theme_dark.triggered.connect(lambda: self._set_theme("dark"))
|
||||||
self.act_theme_light.triggered.connect(lambda: self._set_theme("light"))
|
self.act_theme_light.triggered.connect(lambda: self._set_theme("light"))
|
||||||
|
|
||||||
self.lang_menu = self.view_menu.addMenu("")
|
|
||||||
self.lang_group = QActionGroup(self)
|
|
||||||
self.lang_group.setExclusive(True)
|
|
||||||
self.act_lang_zh = QAction(self)
|
|
||||||
self.act_lang_zh.setCheckable(True)
|
|
||||||
self.act_lang_en = QAction(self)
|
|
||||||
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.act_lang_zh.triggered.connect(lambda: self._set_language("zh_CN"))
|
self.act_lang_zh.triggered.connect(lambda: self._set_language("zh_CN"))
|
||||||
self.act_lang_en.triggered.connect(lambda: self._set_language("en_US"))
|
self.act_lang_en.triggered.connect(lambda: self._set_language("en_US"))
|
||||||
|
|
||||||
self.help_menu = menu_bar.addMenu("")
|
|
||||||
self.act_about = QAction(self)
|
|
||||||
self.act_about.triggered.connect(self._show_about)
|
self.act_about.triggered.connect(self._show_about)
|
||||||
self.help_menu.addAction(self.act_about)
|
|
||||||
|
|
||||||
# The replay entries exist now: hand them over, and the controller connects
|
# The replay entries exist now: hand them over, and the controller connects
|
||||||
# them (it owns what they mean — see ``ReplayController.bind_actions``).
|
# them (it owns what they mean — see ``ReplayController.bind_actions``).
|
||||||
@@ -1216,10 +1088,7 @@ class StudioMainWindow(QMainWindow):
|
|||||||
self._sync_menu_checks()
|
self._sync_menu_checks()
|
||||||
|
|
||||||
def _sync_menu_checks(self) -> None:
|
def _sync_menu_checks(self) -> None:
|
||||||
self.act_theme_dark.setChecked(self._theme != "light")
|
self.menus.sync_appearance(theme=self._theme, language=self._language)
|
||||||
self.act_theme_light.setChecked(self._theme == "light")
|
|
||||||
self.act_lang_zh.setChecked(self._language.startswith("zh"))
|
|
||||||
self.act_lang_en.setChecked(not self._language.startswith("zh"))
|
|
||||||
# The view entries are the layout's; the theme/language ones are the window's.
|
# The view entries are the layout's; the theme/language ones are the window's.
|
||||||
self.layout_ctl.sync_view_checks()
|
self.layout_ctl.sync_view_checks()
|
||||||
self.act_recompute.setEnabled(self.replay_ctl.can_recompute())
|
self.act_recompute.setEnabled(self.replay_ctl.can_recompute())
|
||||||
@@ -1372,51 +1241,11 @@ class StudioMainWindow(QMainWindow):
|
|||||||
|
|
||||||
def retranslate(self) -> None:
|
def retranslate(self) -> None:
|
||||||
self.setWindowTitle(t("app.title"))
|
self.setWindowTitle(t("app.title"))
|
||||||
self.file_menu.setTitle(t("menu.file"))
|
self.menus.retranslate()
|
||||||
self.act_open_video.setText(t("menu.open_video"))
|
# Reset wording and pause state depend on the active workflow.
|
||||||
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"))
|
|
||||||
# Ctrl+R is the rail's reset on a second surface, so its wording follows the
|
|
||||||
# view the same way the button's does (see ``_sync_reset_action``).
|
|
||||||
self._sync_reset_action()
|
self._sync_reset_action()
|
||||||
self.act_pause.setText(t("menu.pause"))
|
|
||||||
self._sync_pause_action()
|
self._sync_pause_action()
|
||||||
self.analysis_menu.setTitle(t("menu.analysis"))
|
self.ui.retranslate()
|
||||||
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"))
|
|
||||||
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()
|
|
||||||
self._refresh_status_chip()
|
self._refresh_status_chip()
|
||||||
self._refresh_status_rail()
|
self._refresh_status_rail()
|
||||||
self._refresh_record_state()
|
self._refresh_record_state()
|
||||||
@@ -1490,12 +1319,7 @@ class StudioMainWindow(QMainWindow):
|
|||||||
app = QApplication.instance()
|
app = QApplication.instance()
|
||||||
if app is not None:
|
if app is not None:
|
||||||
app.setStyleSheet(build_qss(theme))
|
app.setStyleSheet(build_qss(theme))
|
||||||
self.force_plots.set_theme(theme)
|
self.ui.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)
|
|
||||||
self._refresh_status_chip()
|
self._refresh_status_chip()
|
||||||
self._refresh_status_rail()
|
self._refresh_status_rail()
|
||||||
self._refresh_record_state()
|
self._refresh_record_state()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Shell menus and actions prepared for S2, with no business signal handlers.
|
"""Shell menus and actions, with no business signal handlers.
|
||||||
|
|
||||||
All actions have the supplied QMainWindow as their Qt parent. The caller binds
|
All actions have the supplied QMainWindow as their Qt parent. The caller binds
|
||||||
commands after applying initial checks, and owns reset-target wording, pause
|
commands after applying initial checks, and owns reset-target wording, pause
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Central shell widgets prepared for S2, without application wiring.
|
"""Central shell widgets; application wiring belongs to the main window.
|
||||||
|
|
||||||
Construct on the GUI thread. The caller owns controllers, frame delivery, the
|
Construct on the GUI thread. The caller owns controllers, frame delivery, the
|
||||||
shared domain latch, chart store binding and preference restoration. Nothing
|
shared domain latch, chart store binding and preference restoration. Nothing
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Prepared shell surfaces work without the main window or business wiring."""
|
"""Shell surfaces work independently and retain their behavior when wired."""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from orisys_app_shared.i18n import get_translator, set_locale, t
|
from orisys_app_shared.i18n import get_translator, set_locale, t
|
||||||
@@ -164,3 +164,73 @@ def test_text_refresh_preserves_controller_owned_action_state(window):
|
|||||||
assert ui.act_reset.text() == "context-dependent reset"
|
assert ui.act_reset.text() == "context-dependent reset"
|
||||||
assert ui.act_pause.isChecked()
|
assert ui.act_pause.isChecked()
|
||||||
assert not ui.act_run.isEnabled()
|
assert not ui.act_run.isEnabled()
|
||||||
|
|
||||||
|
|
||||||
|
def test_shell_uses_the_surface_widgets_and_actions(make_window):
|
||||||
|
shell = make_window()
|
||||||
|
assert shell.centralWidget() is shell.ui.central
|
||||||
|
for name in (
|
||||||
|
"brand_bar", "sensor_picker", "toolbar", "status_chip", "analysis_progress",
|
||||||
|
"content_splitter", "mesh3d_panel", "arrow_panel", "force_plots", "cal_panel",
|
||||||
|
):
|
||||||
|
assert getattr(shell, name) is getattr(shell.ui, name)
|
||||||
|
assert shell.file_menu is shell.menus.file_menu
|
||||||
|
assert shell.act_play is shell.menus.act_play
|
||||||
|
assert shell.act_reset is shell.menus.act_reset
|
||||||
|
assert shell.mesh3d_panel.view is None
|
||||||
|
assert not shell.session.is_running
|
||||||
|
|
||||||
|
|
||||||
|
def test_shell_menu_commands_dispatch_once(make_window, monkeypatch):
|
||||||
|
from orisys_studio.app_window import StudioMainWindow
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def handler(name):
|
||||||
|
return lambda self: calls.append(name)
|
||||||
|
|
||||||
|
commands = {
|
||||||
|
"run": "_on_run_shortcut",
|
||||||
|
"stop": "_on_stop_shortcut",
|
||||||
|
"preview": "_on_preview_shortcut",
|
||||||
|
"reset": "_on_reset",
|
||||||
|
"reset_view": "_on_reset_view",
|
||||||
|
}
|
||||||
|
for name, method in commands.items():
|
||||||
|
monkeypatch.setattr(StudioMainWindow, method, handler(name))
|
||||||
|
shell = make_window()
|
||||||
|
calls.clear()
|
||||||
|
for name in commands:
|
||||||
|
action = getattr(shell.menus, f"act_{name}")
|
||||||
|
# No device is attached; exercise dispatch without availability policy.
|
||||||
|
action.setEnabled(True)
|
||||||
|
action.trigger()
|
||||||
|
assert calls == list(commands)
|
||||||
|
|
||||||
|
|
||||||
|
def test_shell_appearance_actions_refresh_widgets_and_menus(make_window, qt_app):
|
||||||
|
original_locale = get_translator().locale
|
||||||
|
original_stylesheet = qt_app.styleSheet()
|
||||||
|
shell = make_window()
|
||||||
|
try:
|
||||||
|
background = shell.force_plots.plot_widget.backgroundBrush().color()
|
||||||
|
shell.menus.act_theme_light.trigger()
|
||||||
|
assert shell.prefs["theme"] == "light"
|
||||||
|
assert shell.force_plots.plot_widget.backgroundBrush().color() != background
|
||||||
|
assert shell.menus.act_theme_light.isChecked()
|
||||||
|
assert not shell.menus.act_theme_dark.isChecked()
|
||||||
|
shell.menus.act_lang_en.trigger()
|
||||||
|
assert shell.prefs["language"] == "en_US"
|
||||||
|
assert shell.file_menu.title() == t("menu.file")
|
||||||
|
assert shell.brand_bar.btn_live.text() == t("brand.nav_live")
|
||||||
|
assert shell.act_reset.text() == t("menu.reset")
|
||||||
|
assert shell.toolbar.btn_reset.text() == t("toolbar.reset_sensor")
|
||||||
|
shell.layout_ctl.apply_preset("replay")
|
||||||
|
assert shell.act_reset.text() == t("menu.reset_display")
|
||||||
|
assert shell.toolbar.btn_reset.text() == t("toolbar.reset_display")
|
||||||
|
assert shell.menus.act_lang_en.isChecked()
|
||||||
|
assert not shell.menus.act_lang_zh.isChecked()
|
||||||
|
assert shell.mesh3d_panel.view is None
|
||||||
|
finally:
|
||||||
|
set_locale(original_locale)
|
||||||
|
qt_app.setStyleSheet(original_stylesheet)
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
# Studio 软件架构
|
# Studio 软件架构
|
||||||
|
|
||||||
本文说明已接线的 S1 和 2026-10-19 准备但尚未接线的 S2;后续工作和验收由
|
本文说明已接线的 S1–S2;后续工作和验收由
|
||||||
[分步重构计划](studio-window-refactor-plan.zh.md) 维护。下方旧扫描图表保留为历史证据,
|
[分步重构计划](studio-window-refactor-plan.zh.md) 维护。下方旧扫描图表保留为历史证据,
|
||||||
不能用其中的行数或“已完成”判断当前代码。
|
不能用其中的行数或“已完成”判断当前代码。
|
||||||
|
|
||||||
## 当前职责与数据流
|
## 当前职责与数据流
|
||||||
|
|
||||||
`app_window.py::StudioMainWindow` 创建控件和控制器并连接信号。S1 批次从 3,181 行减至
|
`app_window.py::StudioMainWindow` 装配界面、控制器并连接信号。2026-10-19 的 S2 接线
|
||||||
3,052 行,这是当日指标;当前另有并行标定修改。窗口仍承担菜单、预览、标定交互、
|
从 3,070 行减至 2,894 行。窗口仍承担业务接线、预览、标定交互、分析配置和运行协调,
|
||||||
分析配置和运行协调,尚未完成薄窗口目标。
|
尚未完成薄窗口目标。
|
||||||
|
|
||||||
`status_presenter.py::StatusPresenter` 只持有工具栏,不持有窗口或会话控制器。
|
`status_presenter.py::StatusPresenter` 只持有工具栏,不持有窗口或会话控制器。
|
||||||
窗口在 GUI 线程收集一次性的 `RailState`,Presenter 负责 phase、来源、标定、有效性及
|
窗口在 GUI 线程收集一次性的 `RailState`,Presenter 负责 phase、来源、标定、有效性及
|
||||||
@@ -28,6 +28,8 @@ graph TD
|
|||||||
P --> T[ToolbarWidget 状态轨]
|
P --> T[ToolbarWidget 状态轨]
|
||||||
W --> A[AcquirePresenter 实时呈现]
|
W --> A[AcquirePresenter 实时呈现]
|
||||||
W --> V[FrameView 二维与共享深度范围]
|
W --> V[FrameView 二维与共享深度范围]
|
||||||
|
W --> U[WindowUi 控件与布局]
|
||||||
|
W --> M[WindowMenus 菜单与快捷键]
|
||||||
```
|
```
|
||||||
|
|
||||||
会话状态仍属于 `SessionController`,录制状态仍属于 `RecordingCoordinator`。
|
会话状态仍属于 `SessionController`,录制状态仍属于 `RecordingCoordinator`。
|
||||||
@@ -40,15 +42,16 @@ graph TD
|
|||||||
执行结果、环境及尚未验证范围记录在实施计划。状态转发暂留用于现有调用方,后续按职责迁移,
|
执行结果、环境及尚未验证范围记录在实施计划。状态转发暂留用于现有调用方,后续按职责迁移,
|
||||||
不增加新的全局状态容器或将剩余逻辑全部并入回放控制器。
|
不增加新的全局状态容器或将剩余逻辑全部并入回放控制器。
|
||||||
|
|
||||||
## S2 准备模块,尚未接线
|
## S2 界面构建与接线
|
||||||
|
|
||||||
`ui/window_ui.py::WindowUi` 创建控件和布局,提供局部主题、文字更新;
|
`ui/window_ui.py::WindowUi` 创建控件和布局,提供局部主题、文字更新;
|
||||||
`ui/window_menus.py::WindowMenus` 创建菜单、快捷键和勾选初值,不绑定业务回调。
|
`ui/window_menus.py::WindowMenus` 创建菜单、快捷键和勾选初值,不绑定业务回调。
|
||||||
当前运行图不包含这两个模块,主窗口仍使用原构建路径,标定读取和应用路径由并行任务维护。
|
窗口通过 `ui`、`menus` 使用它们,旧控件和 action 属性保留同一对象引用供控制器和工具访问。
|
||||||
|
|
||||||
独立检查由 `test_window_surfaces.py` 负责。接线时仍由窗口装配 FrameView、共享 domain latch、
|
窗口在控制器接线前装配 FrameView、共享 domain latch 和曲线 store;菜单初值先于业务
|
||||||
曲线 store 和控制器信号;reset 文字及操作可用性保留原负责人。具体顺序见实施计划,
|
回调绑定。偏好保存、全局主题与语言、reset 文字、pause 状态和操作可用性仍由原负责人管理。
|
||||||
不能把独立模块测试通过等同于主窗口迁移完成。
|
标定读取与应用路径保持原实现。`test_window_surfaces.py` 覆盖独立模块与主窗口接线;
|
||||||
|
布局、传感器切换和回放由现有集成测试覆盖,结果及验证限制见实施计划。
|
||||||
|
|
||||||
## 历史测量的适用范围
|
## 历史测量的适用范围
|
||||||
|
|
||||||
|
|||||||
@@ -41,10 +41,10 @@
|
|||||||
|
|
||||||
### S2:界面构建
|
### S2:界面构建
|
||||||
|
|
||||||
- [x] 准备 `ui/window_ui.py` 和 `ui/window_menus.py`,以独立窗口验证,尚未接入运行路径。
|
- [x] 准备 `ui/window_ui.py` 和 `ui/window_menus.py`,先以独立窗口验证。
|
||||||
- [ ] 提取 `ui/window_ui.py` 的控件和布局构建,保持控件身份、顺序及 parent。
|
- [x] 接入 `ui/window_ui.py` 的控件和布局构建,保持控件身份、顺序及 parent。
|
||||||
- [ ] 单独迁移菜单和 QAction 创建;由窗口显式连接功能操作。
|
- [x] 迁移菜单和 QAction 创建;由窗口显式连接功能操作。
|
||||||
- [ ] 迁移对应文字与主题更新,保留必要属性兼容入口。
|
- [x] 迁移对应文字与主题更新,保留必要属性兼容入口。
|
||||||
|
|
||||||
依赖 S1。涉及主窗口、UI 模块及实际调用方;面板构建和菜单构建分两批验收。
|
依赖 S1。涉及主窗口、UI 模块及实际调用方;面板构建和菜单构建分两批验收。
|
||||||
验证:`test_ui_elements`、`test_view_layout`、`test_sensor_switch_window`、主题与语言检查。
|
验证:`test_ui_elements`、`test_view_layout`、`test_sensor_switch_window`、主题与语言检查。
|
||||||
@@ -95,6 +95,38 @@
|
|||||||
每个迁移步骤运行对应检查;可交付批次运行 `tools/gate.ps1 -Task studio-window-refactor`。
|
每个迁移步骤运行对应检查;可交付批次运行 `tools/gate.ps1 -Task studio-window-refactor`。
|
||||||
文档使用工作区 `scripts/doc_lint.py` 和 `scripts/doc_count.py`,不扩大基线。
|
文档使用工作区 `scripts/doc_lint.py` 和 `scripts/doc_count.py`,不扩大基线。
|
||||||
|
|
||||||
|
### 2026-10-19:S2 接线
|
||||||
|
|
||||||
|
以 `main` 的 `c6c16df` 为基准,接入已准备的两个 UI 模块;主窗口从 3,070 行减至
|
||||||
|
2,894 行。10 个控件和 35 个菜单、action 属性保留同一对象引用;18 处菜单业务回调及
|
||||||
|
控制器绑定仍在窗口。主题与文字更新委托 UI 模块,reset 目标、pause 状态仍沿用原逻辑。
|
||||||
|
|
||||||
|
FrameView、共享 domain latch 和曲线 store 在控制器接线前绑定,偏好恢复顺序不变。
|
||||||
|
只改动窗口的构造、菜单构建、菜单勾选、文字和主题刷新五个方法;其余方法的 AST 一致,
|
||||||
|
最新标定读取、应用和关闭路径未迁移。S3–S6 保持待实施。
|
||||||
|
|
||||||
|
改动前定向检查为 46 passed;接线后加上三个集成检查为 49 passed。
|
||||||
|
覆盖控件身份、菜单单次调用、主题与语言刷新、实时与回放的 reset 目标,以及既有布局、
|
||||||
|
暂停和标定对话框。首次新增断言误将菜单与按钮的不同文案视为相同,已按原契约修正。
|
||||||
|
串行门禁的 lint 通过,UI 检查为 632 checks、0 failures;共享层为 461 passed、
|
||||||
|
18 skipped、3 deselected、15 failed,涉及 SDK 映射文件缺失及几何尺寸不一致。
|
||||||
|
Studio 在垃圾回收期间访问冲突退出,无 JUnit 报告。整套门禁未通过,原因尚未完全确认。
|
||||||
|
|
||||||
|
扩展界面检查为 131 passed、1 failed,失败是分析停止后的即时状态断言。
|
||||||
|
该用例及崩溃所在的三个布局场景,在旧、新主窗口隔离运行均为 4 passed,未复现整套异常。
|
||||||
|
旧主窗口整套对照长时间无进度后已中断,不能据此判定原因。未修改失败断言或扩大基线。
|
||||||
|
临时禁用局部主题刷新时,新增测试按预期失败;仅在测试进程注入,源码未改动。
|
||||||
|
|
||||||
|
环境为 Windows、Python 3.10.19、offscreen Qt;真实设备、可见桌面和硬件 GL 未验证。
|
||||||
|
文档检查无新增问题,原架构长段落债务仍保留。证据位于
|
||||||
|
`.scratch/studio-window-refactor/20261019-095028/` 和 `gate-20261019T015301Z/`。
|
||||||
|
另一个进程的 Autocal 修改含暂存项,全部排除;本批六个文件未暂存,根仓库未修改。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File tools/gate.ps1 -Task studio-window-refactor -LintBase HEAD -Serial
|
||||||
|
& .\.venv-build\Scripts\python.exe -m pytest apps/studio/tests/test_ui_smoke.py apps/studio/tests/test_ui_elements.py apps/studio/tests/test_sensor_switch_window.py -m 'not timing' -o faulthandler_timeout=60 -p no:cacheprovider --basetemp=D:\orisys_ws\orisys-app\.scratch\studio-window-refactor\20261019-095028\pytest-ui-focused --junitxml=D:\orisys_ws\orisys-app\.scratch\studio-window-refactor\20261019-095028\ui-focused.xml
|
||||||
|
```
|
||||||
|
|
||||||
### 2026-10-19:S2 模块准备,接线延期
|
### 2026-10-19:S2 模块准备,接线延期
|
||||||
|
|
||||||
用户要求避开另一个进程正在修改的标定配置读取和应用路径,本批只新增模块及独立测试,
|
用户要求避开另一个进程正在修改的标定配置读取和应用路径,本批只新增模块及独立测试,
|
||||||
|
|||||||
Reference in New Issue
Block a user