feat: 懒加载面板Catch-up状态回放 + 上一步/下一步向导导航按钮
This commit is contained in:
@ -20,6 +20,7 @@ from PyQt5.QtCore import Qt
|
|||||||
|
|
||||||
from src.gui.core.panel_registry import PANEL_REGISTRY
|
from src.gui.core.panel_registry import PANEL_REGISTRY
|
||||||
from src.gui.core.dependency_subscriber import subscribe_panel_to_dependencies
|
from src.gui.core.dependency_subscriber import subscribe_panel_to_dependencies
|
||||||
|
from src.gui.core.event_bus import global_event_bus
|
||||||
|
|
||||||
|
|
||||||
class PanelFactory:
|
class PanelFactory:
|
||||||
@ -184,6 +185,90 @@ class PanelFactory:
|
|||||||
if deps:
|
if deps:
|
||||||
subscribe_panel_to_dependencies(panel, step_id, 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):
|
def _preload_neighbors(self, index):
|
||||||
"""预加载当前 tab 的邻居(根据 preload_window 配置)。"""
|
"""预加载当前 tab 的邻居(根据 preload_window 配置)。"""
|
||||||
if self._preload_window < 0:
|
if self._preload_window < 0:
|
||||||
|
|||||||
@ -88,8 +88,6 @@ class WaterQualityGUI(QMainWindow):
|
|||||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(my_appid)
|
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(my_appid)
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self._is_syncing = False
|
|
||||||
|
|
||||||
icon_path = get_resource_path("data/icons-1/uitubiao.ico")
|
icon_path = get_resource_path("data/icons-1/uitubiao.ico")
|
||||||
self.setWindowIcon(QIcon(icon_path))
|
self.setWindowIcon(QIcon(icon_path))
|
||||||
|
|
||||||
@ -282,6 +280,7 @@ class WaterQualityGUI(QMainWindow):
|
|||||||
self.addToolBar(Qt.TopToolBarArea, banner_toolbar)
|
self.addToolBar(Qt.TopToolBarArea, banner_toolbar)
|
||||||
|
|
||||||
def _create_central_layout(self):
|
def _create_central_layout(self):
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
central_widget = QWidget()
|
central_widget = QWidget()
|
||||||
main_layout = QHBoxLayout()
|
main_layout = QHBoxLayout()
|
||||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
@ -295,9 +294,27 @@ class WaterQualityGUI(QMainWindow):
|
|||||||
right_layout.setSpacing(10)
|
right_layout.setSpacing(10)
|
||||||
|
|
||||||
self._tab_widget = self._panel_factory.create_tab_widget(icons_dir="data/icons")
|
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)
|
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_layout.addWidget(self._log_manager.create_log_panel(), 1)
|
||||||
|
|
||||||
right_widget.setLayout(right_layout)
|
right_widget.setLayout(right_layout)
|
||||||
@ -419,6 +436,15 @@ class WaterQualityGUI(QMainWindow):
|
|||||||
work_dir = data.get('work_dir', '')
|
work_dir = data.get('work_dir', '')
|
||||||
self.statusBar().showMessage(f"工作目录: {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):
|
def _on_run_all_clicked(self):
|
||||||
print("==== 按钮确实被按下了 ====", flush=True)
|
print("==== 按钮确实被按下了 ====", flush=True)
|
||||||
try:
|
try:
|
||||||
@ -428,12 +454,11 @@ class WaterQualityGUI(QMainWindow):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
# ================================================================
|
# ================================================================
|
||||||
# 导航 ↔ Tab 双向同步(纯 UI 路由)
|
# 导航 → Tab 单向路由(左侧 List 驱动右侧 Tab,Tab 头部已隐藏)
|
||||||
# ================================================================
|
# ================================================================
|
||||||
|
|
||||||
def _on_step_list_changed(self, index):
|
def _on_step_list_changed(self, index):
|
||||||
if self._is_syncing:
|
"""左侧导航 → 右侧 Tab 单向路由(Tab 头部已隐藏,无反向同步)。"""
|
||||||
return
|
|
||||||
if index < 0:
|
if index < 0:
|
||||||
return
|
return
|
||||||
item = self._step_list.item(index)
|
item = self._step_list.item(index)
|
||||||
@ -446,36 +471,47 @@ class WaterQualityGUI(QMainWindow):
|
|||||||
tab_index = get_tab_index(item_data)
|
tab_index = get_tab_index(item_data)
|
||||||
if tab_index < 0:
|
if tab_index < 0:
|
||||||
return
|
return
|
||||||
self._is_syncing = True
|
# 先触发懒加载再切 Tab,避免 removeTab/insertTab 与导航事件重叠
|
||||||
try:
|
|
||||||
# ★ 先触发懒加载:get_panel 内部 _ensure_loaded 自带 blockSignals
|
|
||||||
# 保护 removeTab/insertTab,面板实例化完成后再切 Tab,
|
|
||||||
# 此时 _ensure_loaded 发现已加载直接返回,不再触发索引偏移
|
|
||||||
self._panel_factory.get_panel(item_data)
|
self._panel_factory.get_panel(item_data)
|
||||||
self._tab_widget.setCurrentIndex(tab_index)
|
self._tab_widget.setCurrentIndex(tab_index)
|
||||||
finally:
|
|
||||||
self._is_syncing = False
|
|
||||||
|
|
||||||
def _on_tab_changed(self, index):
|
# 动态更新上一步/下一步按钮可用状态
|
||||||
if self._is_syncing:
|
self._prev_btn.setEnabled(self._find_prev_step_row(index) is not None)
|
||||||
return
|
self._next_btn.setEnabled(self._find_next_step_row(index) is not None)
|
||||||
if index < 0:
|
|
||||||
return
|
def _find_prev_step_row(self, current_row):
|
||||||
from src.gui.core.panel_registry import get_step_id_by_tab_index
|
"""从 current_row 向上遍历,跳过 stage_header 和空分隔符,返回上一个有效 step 的行号。"""
|
||||||
target_step_id = get_step_id_by_tab_index(index)
|
for i in range(current_row - 1, -1, -1):
|
||||||
if target_step_id is None:
|
item = self._step_list.item(i)
|
||||||
return
|
|
||||||
for row in range(self._step_list.count()):
|
|
||||||
item = self._step_list.item(row)
|
|
||||||
if not item:
|
if not item:
|
||||||
continue
|
continue
|
||||||
if item.data(Qt.UserRole) == target_step_id:
|
data = item.data(Qt.UserRole)
|
||||||
self._is_syncing = True
|
if data and data != "stage_header":
|
||||||
try:
|
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)
|
self._step_list.setCurrentRow(row)
|
||||||
finally:
|
|
||||||
self._is_syncing = False
|
|
||||||
break
|
|
||||||
|
|
||||||
# ================================================================
|
# ================================================================
|
||||||
# 横幅自适应
|
# 横幅自适应
|
||||||
|
|||||||
Reference in New Issue
Block a user