From f61a3dfb1d1416bc664b51e5180a5ca5a35ebd85 Mon Sep 17 00:00:00 2001 From: DXC Date: Thu, 18 Jun 2026 11:18:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=87=92=E5=8A=A0=E8=BD=BD=E9=9D=A2?= =?UTF-8?q?=E6=9D=BFCatch-up=E7=8A=B6=E6=80=81=E5=9B=9E=E6=94=BE=20+=20?= =?UTF-8?q?=E4=B8=8A=E4=B8=80=E6=AD=A5/=E4=B8=8B=E4=B8=80=E6=AD=A5?= =?UTF-8?q?=E5=90=91=E5=AF=BC=E5=AF=BC=E8=88=AA=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/core/panel_factory.py | 85 ++++++++++++++++++++++++++ src/gui/water_quality_gui_v2.py | 102 +++++++++++++++++++++----------- 2 files changed, 154 insertions(+), 33 deletions(-) diff --git a/src/gui/core/panel_factory.py b/src/gui/core/panel_factory.py index 6b1d68d..25189ce 100644 --- a/src/gui/core/panel_factory.py +++ b/src/gui/core/panel_factory.py @@ -20,6 +20,7 @@ from PyQt5.QtCore import Qt from src.gui.core.panel_registry import PANEL_REGISTRY from src.gui.core.dependency_subscriber import subscribe_panel_to_dependencies +from src.gui.core.event_bus import global_event_bus class PanelFactory: @@ -184,6 +185,90 @@ class PanelFactory: if deps: subscribe_panel_to_dependencies(panel, step_id, deps) + # ★ Catch-up:向刚苏醒的懒加载面板回放已累积的状态 + # (面板在 OutputUpdated 事件广播之后才实例化,错过了事件, + # 必须主动回放 step_outputs + 全局输入,否则输入框全空) + self._replay_state_to_panel(panel) + + # ── Catch-up 状态追溯 ──────────────────────────────────── + + def _replay_state_to_panel(self, panel): + """向刚实例化的懒加载面板回放已累积的状态。 + + 三步回放: + 1. update_from_config —— 生成默认输出路径 + 跨面板参数读取 + 2. 回放 WorkspaceManager.step_outputs —— 已运行步骤的产出文件路径 + 3. 实时扫描已加载面板 —— 读取被依赖的属性值(如 Step1 的 img_file) + 发布为 OutputUpdated,触发 dependency_subscriber 回填输入框 + """ + # 1. update_from_config:生成默认输出路径 + if hasattr(panel, 'update_from_config'): + try: + work_dir = self._get_current_work_dir() + panel.update_from_config(work_dir=work_dir, pipeline=None) + except Exception: + pass + + # 2. 回放 WorkspaceManager 中已累积的 step_outputs + ws_manager = self._get_workspace_manager() + if ws_manager: + for src_step_id, outputs in ws_manager.step_outputs.items(): + for output_type, path in outputs.items(): + if not path: + continue + global_event_bus.publish('OutputUpdated', { + 'step_id': src_step_id, + 'output_type': output_type, + 'path': path, + }) + + # 3. 实时扫描已加载面板中被依赖的属性(覆盖全局输入如 reference_img) + self._replay_live_panel_inputs() + + def _replay_live_panel_inputs(self): + """遍历 PANEL_REGISTRY 依赖声明,从已加载面板实时读取属性值。 + + 若源面板已实例化,读取其 widget 的当前值并发布为 OutputUpdated, + 确保懒加载面板能收到全局输入(如 Step1.img_file → reference_img)。 + """ + for entry in self._registry: + deps = entry.get('dependencies') + if not deps: + continue + for _input_field, (dep_step, output_type, panel_attr) in deps.items(): + src_panel = self._panels.get(dep_step) + if src_panel is None: + continue + widget = getattr(src_panel, panel_attr, None) + if widget is None: + continue + path = '' + if hasattr(widget, 'get_path'): + path = widget.get_path().strip() + elif hasattr(widget, 'text'): + path = widget.text().strip() + if not path: + continue + global_event_bus.publish('OutputUpdated', { + 'step_id': dep_step, + 'output_type': output_type, + 'path': path, + }) + + def _get_current_work_dir(self): + """从 WorkspaceInitializer 获取当前工作目录。""" + try: + return self._main_window._workspace_initializer.work_dir + except Exception: + return None + + def _get_workspace_manager(self): + """从 WorkspaceInitializer 获取 WorkspaceManager 实例。""" + try: + return self._main_window._workspace_initializer.workspace_manager + except Exception: + return None + def _preload_neighbors(self, index): """预加载当前 tab 的邻居(根据 preload_window 配置)。""" if self._preload_window < 0: diff --git a/src/gui/water_quality_gui_v2.py b/src/gui/water_quality_gui_v2.py index f3abda5..8d7f80e 100644 --- a/src/gui/water_quality_gui_v2.py +++ b/src/gui/water_quality_gui_v2.py @@ -88,8 +88,6 @@ class WaterQualityGUI(QMainWindow): ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(my_appid) super().__init__() - self._is_syncing = False - icon_path = get_resource_path("data/icons-1/uitubiao.ico") self.setWindowIcon(QIcon(icon_path)) @@ -282,6 +280,7 @@ class WaterQualityGUI(QMainWindow): self.addToolBar(Qt.TopToolBarArea, banner_toolbar) def _create_central_layout(self): + from src.gui.styles import ModernStylesheet central_widget = QWidget() main_layout = QHBoxLayout() main_layout.setContentsMargins(0, 0, 0, 0) @@ -295,9 +294,27 @@ class WaterQualityGUI(QMainWindow): right_layout.setSpacing(10) self._tab_widget = self._panel_factory.create_tab_widget(icons_dir="data/icons") - self._tab_widget.currentChanged.connect(self._on_tab_changed) + self._tab_widget.tabBar().setVisible(False) right_layout.addWidget(self._tab_widget, 3) + # 上一步 / 下一步 导航按钮 + nav_btn_layout = QHBoxLayout() + nav_btn_layout.setSpacing(10) + self._prev_btn = QPushButton("< 上一步") + self._prev_btn.setMinimumHeight(32) + self._prev_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('default')) + self._prev_btn.clicked.connect(self._on_prev_clicked) + nav_btn_layout.addWidget(self._prev_btn) + + self._next_btn = QPushButton("下一步 >") + self._next_btn.setMinimumHeight(32) + self._next_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('default')) + self._next_btn.clicked.connect(self._on_next_clicked) + nav_btn_layout.addWidget(self._next_btn) + + nav_btn_layout.addStretch() + right_layout.addLayout(nav_btn_layout) + right_layout.addWidget(self._log_manager.create_log_panel(), 1) right_widget.setLayout(right_layout) @@ -419,6 +436,15 @@ class WaterQualityGUI(QMainWindow): work_dir = data.get('work_dir', '') self.statusBar().showMessage(f"工作目录: {work_dir}") + # 遍历已加载面板,调用 update_from_config 重建内存级参数和默认输出路径 + for step_id, panel in self._panel_factory.get_loaded_panels().items(): + if not hasattr(panel, 'update_from_config'): + continue + try: + panel.update_from_config(work_dir=work_dir, pipeline=None) + except Exception: + pass + def _on_run_all_clicked(self): print("==== 按钮确实被按下了 ====", flush=True) try: @@ -428,12 +454,11 @@ class WaterQualityGUI(QMainWindow): traceback.print_exc() # ================================================================ - # 导航 ↔ Tab 双向同步(纯 UI 路由) + # 导航 → Tab 单向路由(左侧 List 驱动右侧 Tab,Tab 头部已隐藏) # ================================================================ def _on_step_list_changed(self, index): - if self._is_syncing: - return + """左侧导航 → 右侧 Tab 单向路由(Tab 头部已隐藏,无反向同步)。""" if index < 0: return item = self._step_list.item(index) @@ -446,36 +471,47 @@ class WaterQualityGUI(QMainWindow): tab_index = get_tab_index(item_data) if tab_index < 0: return - self._is_syncing = True - try: - # ★ 先触发懒加载:get_panel 内部 _ensure_loaded 自带 blockSignals - # 保护 removeTab/insertTab,面板实例化完成后再切 Tab, - # 此时 _ensure_loaded 发现已加载直接返回,不再触发索引偏移 - self._panel_factory.get_panel(item_data) - self._tab_widget.setCurrentIndex(tab_index) - finally: - self._is_syncing = False + # 先触发懒加载再切 Tab,避免 removeTab/insertTab 与导航事件重叠 + self._panel_factory.get_panel(item_data) + self._tab_widget.setCurrentIndex(tab_index) - def _on_tab_changed(self, index): - if self._is_syncing: - return - if index < 0: - return - from src.gui.core.panel_registry import get_step_id_by_tab_index - target_step_id = get_step_id_by_tab_index(index) - if target_step_id is None: - return - for row in range(self._step_list.count()): - item = self._step_list.item(row) + # 动态更新上一步/下一步按钮可用状态 + self._prev_btn.setEnabled(self._find_prev_step_row(index) is not None) + self._next_btn.setEnabled(self._find_next_step_row(index) is not None) + + def _find_prev_step_row(self, current_row): + """从 current_row 向上遍历,跳过 stage_header 和空分隔符,返回上一个有效 step 的行号。""" + for i in range(current_row - 1, -1, -1): + item = self._step_list.item(i) if not item: continue - if item.data(Qt.UserRole) == target_step_id: - self._is_syncing = True - try: - self._step_list.setCurrentRow(row) - finally: - self._is_syncing = False - break + data = item.data(Qt.UserRole) + if data and data != "stage_header": + return i + return None + + def _find_next_step_row(self, current_row): + """从 current_row 向下遍历,跳过 stage_header 和空分隔符,返回下一个有效 step 的行号。""" + for i in range(current_row + 1, self._step_list.count()): + item = self._step_list.item(i) + if not item: + continue + data = item.data(Qt.UserRole) + if data and data != "stage_header": + return i + return None + + def _on_prev_clicked(self): + """上一步按钮:跳转到上一个有效步骤。""" + row = self._find_prev_step_row(self._step_list.currentRow()) + if row is not None: + self._step_list.setCurrentRow(row) + + def _on_next_clicked(self): + """下一步按钮:跳转到下一个有效步骤。""" + row = self._find_next_step_row(self._step_list.currentRow()) + if row is not None: + self._step_list.setCurrentRow(row) # ================================================================ # 横幅自适应