"""面板管理器(Step 6 重构)。 register_panel / show_panel / hide_all_panels —— 在右侧栏不同浮动面板间切换。 """ from __future__ import annotations import tkinter as tk import tkinter.font as tkfont from typing import TYPE_CHECKING, Any, Callable if TYPE_CHECKING: from pqAutomationApp import PQAutomationApp # 叠层切换:lift/lower,避免 pack_forget 触发 Matplotlib 等大面板重绘。 _STACK_PANELS = frozenset( {"log", "ai_image", "pantone_baseline", "gamma_pattern", "calman", "single_step"} ) def create_tool_panel_sidebar_item( self: "PQAutomationApp", parent: tk.Misc, label: str, command: Callable[[], None] ) -> dict[str, Any]: """创建工具面板侧栏项:名称左对齐,选中时 × 贴右端,整体可点击。""" from app.views.modern_styles import _contrast_text, _is_dark, get_theme_palette row = tk.Frame(parent, bd=0, highlightthickness=0, cursor="hand2") name_lbl = tk.Label(row, text=label, anchor=tk.W, bd=0, padx=18, pady=9) close_lbl = tk.Label( row, text="×", anchor=tk.CENTER, bd=0, padx=0, pady=9, width=2, font=("Segoe UI", 9), ) name_lbl.pack(side=tk.LEFT, fill=tk.X, expand=True) item: dict[str, Any] = { "row": row, "name_label": name_lbl, "close_label": close_lbl, "label": label, "selected": False, "hover": False, } def _apply_style() -> None: palette = get_theme_palette() sidebar_bg = palette["sidebar_bg"] sidebar_hover = palette["sidebar_hover"] sidebar_selected = palette["sidebar_selected"] sidebar_fg = palette["sidebar_fg"] selected_fg = _contrast_text( sidebar_selected, dark_text=palette["badge_fg"], light_text=sidebar_fg, ) if item["selected"]: bg = sidebar_selected fg = selected_fg name_font = ("Segoe UI Semibold", 10) elif item["hover"]: bg = sidebar_hover fg = "#ffffff" if _is_dark(sidebar_hover) else sidebar_fg name_font = ("Segoe UI", 10) else: bg = sidebar_bg fg = sidebar_fg name_font = ("Segoe UI", 10) row.configure(bg=bg) for widget in (name_lbl, close_lbl): widget.configure( bg=bg, fg=fg, activebackground=bg, activeforeground=fg, ) name_lbl.configure(font=name_font) def _truncate_name() -> None: full = item["label"] if not item["selected"]: name_lbl.configure(text=full) return row.update_idletasks() close_w = close_lbl.winfo_width() if close_lbl.winfo_ismapped() else 20 avail = max(24, row.winfo_width() - close_w - 18 - 14) font = tkfont.Font(font=name_lbl.cget("font")) if font.measure(full) <= avail: name_lbl.configure(text=full) return for i in range(len(full), 0, -1): candidate = full[:i].rstrip() + "…" if font.measure(candidate) <= avail: name_lbl.configure(text=candidate) return name_lbl.configure(text="…") def set_selected(selected: bool) -> None: item["selected"] = selected if selected: close_lbl.pack(side=tk.RIGHT, padx=(0, 14)) else: close_lbl.pack_forget() _apply_style() row.after_idle(_truncate_name) def _on_click(_event=None) -> None: command() def _on_enter(_event=None) -> None: if not item["selected"]: item["hover"] = True _apply_style() def _on_leave(_event=None) -> None: px, py = row.winfo_pointerxy() rx, ry = row.winfo_rootx(), row.winfo_rooty() rw, rh = row.winfo_width(), row.winfo_height() if rx <= px < rx + rw and ry <= py < ry + rh: return item["hover"] = False _apply_style() def _on_configure(_event=None) -> None: if item["selected"]: _truncate_name() for widget in (row, name_lbl, close_lbl): widget.bind("", _on_click) widget.bind("", _on_enter) widget.bind("", _on_leave) row.bind("", _on_configure) item["set_selected"] = set_selected item["apply_theme"] = _apply_style item["pack"] = lambda **kw: row.pack(fill=tk.X, padx=0, pady=1, **kw) _apply_style() return item def _set_tool_panel_sidebar_selected( panel_info: dict[str, Any], selected: bool ) -> None: sidebar_item = panel_info.get("sidebar_item") if sidebar_item is not None: sidebar_item["set_selected"](selected) return button = panel_info.get("button") if button is not None: button.configure( style="SidebarSelected.TButton" if selected else "Sidebar.TButton" ) def refresh_tool_panel_sidebar_theme(self: "PQAutomationApp") -> None: """主题切换后刷新工具面板侧栏项配色。""" for panel_info in self.panels.values(): sidebar_item = panel_info.get("sidebar_item") if sidebar_item is not None: sidebar_item["apply_theme"]() def register_panel(self: "PQAutomationApp", panel_name, frame, button, visible_attr): """注册一个面板到管理系统""" self.panels[panel_name] = { "frame": frame, "button": button, "button_label": None, "sidebar_item": None, "visible_attr": visible_attr, } def setup_view_stack(self: "PQAutomationApp") -> None: """将所有工具面板叠放在 view_stack 上,切换时仅调整 Z 序。""" if getattr(self, "_view_stack_ready", False): return self.main_view.place(relx=0, rely=0, relwidth=1, relheight=1) for panel_name, panel_info in self.panels.items(): if panel_name == "local_dimming": continue panel_info["frame"].place(relx=0, rely=0, relwidth=1, relheight=1) panel_info["frame"].lower() self._view_stack_ready = True def _lower_stack_panels(self: "PQAutomationApp") -> None: for panel_name, panel_info in self.panels.items(): if panel_name in _STACK_PANELS: panel_info["frame"].lower() def _clear_panel_selection(self: "PQAutomationApp") -> None: for panel_info in self.panels.values(): _set_tool_panel_sidebar_selected(panel_info, False) setattr(self, panel_info["visible_attr"], False) def _restore_main_view_layout(self: "PQAutomationApp") -> None: """恢复主界面三区布局,并收起嵌入的 Local Dimming 面板。""" if getattr(self, "_ld_embedded", False): self.local_dimming_frame.pack_forget() self._ld_embedded = False self.control_frame_top.pack_forget() self.control_frame_middle.pack_forget() self.control_frame_bottom.pack_forget() self.control_frame_top.pack( side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5 ) self.control_frame_middle.pack( side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5 ) self.control_frame_bottom.pack( side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5 ) def _show_main_content(self: "PQAutomationApp") -> None: """显示主内容区并清除当前面板状态。""" setup_view_stack(self) _lower_stack_panels(self) _restore_main_view_layout(self) self.main_view.lift() _clear_panel_selection(self) self.current_panel = None if hasattr(self, "_sync_save_button_state"): try: self._sync_save_button_state() except Exception: pass def _notify_panel_shown(self: "PQAutomationApp", panel_name: str) -> None: """面板显示后的轻量刷新(延迟执行,不阻塞切换)。""" if getattr(self, "_theme_transition", False): return if panel_name == "calman": refresh = getattr(self, "_on_calman_panel_shown", None) if refresh is not None: self.root.after_idle(refresh) return if panel_name == "ai_image" and getattr(self, "_ai_image_theme_dirty", False): self.root.after_idle(lambda: self.refresh_ai_image_theme()) return if panel_name == "log" and hasattr(self, "log_gui") and hasattr( self.log_gui, "refresh_log_theme" ): self.root.after_idle(self.log_gui.refresh_log_theme) def _show_stack_panel(self: "PQAutomationApp", panel_name: str) -> None: """叠层模式显示工具面板。""" setup_view_stack(self) panel_info = self.panels[panel_name] _set_tool_panel_sidebar_selected(panel_info, True) previous = self.current_panel if previous == "local_dimming": self.local_dimming_frame.pack_forget() self._ld_embedded = False elif previous is not None and previous in _STACK_PANELS: prev_info = self.panels[previous] _set_tool_panel_sidebar_selected(prev_info, False) setattr(self, prev_info["visible_attr"], False) prev_info["frame"].lower() self.main_view.lower() panel_info["frame"].lift() setattr(self, panel_info["visible_attr"], True) self.current_panel = panel_name _notify_panel_shown(self, panel_name) def _show_local_dimming_panel(self: "PQAutomationApp") -> None: """Local Dimming:保留顶部配置区,下方嵌入 LD 面板。""" setup_view_stack(self) panel_info = self.panels["local_dimming"] if self.current_panel in _STACK_PANELS: prev_info = self.panels[self.current_panel] _set_tool_panel_sidebar_selected(prev_info, False) setattr(self, prev_info["visible_attr"], False) prev_info["frame"].lower() _set_tool_panel_sidebar_selected(panel_info, True) _lower_stack_panels(self) self.main_view.lift() self.control_frame_top.pack_forget() self.control_frame_top.pack( side=tk.TOP, fill=tk.X, expand=False, padx=0, pady=5 ) self.control_frame_middle.pack_forget() self.control_frame_bottom.pack_forget() self.local_dimming_frame.pack( side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5 ) self._ld_embedded = True setattr(self, panel_info["visible_attr"], True) self.current_panel = "local_dimming" def _maybe_restore_local_dimming_panel( self: "PQAutomationApp", closed_panel: str ) -> None: """关闭工具面板后,Local Dimming 测试类型下自动恢复 LD 面板。""" if closed_panel == "local_dimming": return try: if ( getattr(self, "config", None) and getattr(self.config, "current_test_type", None) == "local_dimming" ): self.show_panel("local_dimming") except Exception: pass def show_panel(self: "PQAutomationApp", panel_name): """显示指定面板,隐藏其他所有面板""" if panel_name not in self.panels: return if self.current_panel == panel_name: _show_main_content(self) _maybe_restore_local_dimming_panel(self, panel_name) return if panel_name == "local_dimming": _show_local_dimming_panel(self) return if panel_name in _STACK_PANELS: _show_stack_panel(self, panel_name) return # 未知面板类型:回退到叠层显示 _show_stack_panel(self, panel_name) def hide_all_panels(self: "PQAutomationApp"): """隐藏所有面板,显示主内容区域""" _show_main_content(self) class PanelManagerMixin: """由 tools/refactor_to_mixins.py 自动生成。 把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。 """ create_tool_panel_sidebar_item = create_tool_panel_sidebar_item register_panel = register_panel setup_view_stack = setup_view_stack show_panel = show_panel hide_all_panels = hide_all_panels refresh_tool_panel_sidebar_theme = refresh_tool_panel_sidebar_theme