EG/ui/lui_manager.py
2026-05-25 14:07:58 +08:00

458 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import panda3d.core as p3d
from imgui_bundle import imgui, imgui_ctx
from ui.LUI.lui_manager_editor import LUIManagerEditorMixin
from ui.LUI.lui_manager_interaction import LUIManagerInteractionMixin
try:
from ui.LUI.lui_world_space import WorldSpaceLUIRuntime
except Exception as exc:
WorldSpaceLUIRuntime = None
LUI_WORLD_SPACE_IMPORT_ERROR = exc
else:
LUI_WORLD_SPACE_IMPORT_ERROR = None
from ui.LUI.lui_shared import (
CardMaker,
LUIButton,
LUIDefaultSkin,
LUIFrame,
LUIInputField,
LUIInputHandler,
LUILabel,
LUIObject,
LUIRegion,
LUISlider,
LUISprite,
LUI_IMPORT_DETAILS,
NodePath,
Path,
lui,
)
from ui.lui_function import luiFunction
class LUIManager(LUIManagerInteractionMixin, LUIManagerEditorMixin):
"""Manager for LUI system integration"""
def __init__(self, world):
self.world = world
self.lui_enabled = (lui is not None)
# Always initialize selection fields, even when LUI is unavailable.
self.selected_index = -1
self._last_selected_index = -1
self.luiFunction = luiFunction
# Editor State
self.show_editor = True
self.play_mode = False
self._selected_type_index = 0
if not self.lui_enabled:
details = str(LUI_IMPORT_DETAILS or "").strip()
if details:
print(f"LUI is not available, LUIManager will be disabled. {details}")
else:
print("LUI is not available, LUIManager will be disabled.")
return
# 1. 优先注册中文字体
self._register_chinese_font()
# 2. 初始化核心 LUI 区域
self._init_ui_regions()
# 3. 加载皮肤资源
self.lui_skin = LUIDefaultSkin()
self.lui_skin.load()
# 3.1 皮肤会覆盖 default/label/header 字体,这里再次注册中文字体
# 以避免 LUIText 输出中文时出现 “Font does not support character ...”
self._register_chinese_font()
# 4. 状态管理 - 与engine保持一致
self.canvases = []
self.current_canvas_index = -1
self.canvas_counter = 0
self.components = []
self.root_order = []
self._tree_drag_src = None
self.selected_index = -1
self._last_selected_index = -1
# 拖拽和选择状态
self.dragging_comp = None
self.pending_drag_comp = None
self.pending_drag_index = -1
self.pending_drag_start_mouse = None
self.pending_drag_start_abs = None
self.pending_drag_start_parent = None
self.dragging_index = -1
self.drag_start_threshold = 6.0
self.drag_offset = (0, 0)
# 锚点功能相关变量
self.anchor_popup_open = False
self.pending_child_creation = False
self.pending_parent_index = -1
# 子父对齐功能
self.child_parent_alignment_enabled = True
# Resize handles 相关变量
self.resize_handles = []
self.selection_box = []
self.debug_outlines = {} # comp_index -> [border sprites]
self.resizing_handle = None
self.resize_start_pos = (0, 0)
self.resize_start_bounds = {}
self.min_size = 20
# 使用外部字典管理状态,避免修改 C++ 对象属性导致 AttributeError
self.drag_states = {}
self._last_interaction_frame = -1 # 记录最后一次交互的帧数,用于防止事件穿透
self._input_enabled = True
self.debug_world_lui = str(os.environ.get("EG_LUI_WORLD_DEBUG", "1")).lower() not in ("0", "false", "off", "no")
self._debug_lui_visibility_state = {}
self.world_space_runtime = None
self._init_world_space_runtime()
# 创建resize handles和selection box延迟创建确保在Canvas之后
if hasattr(self.world, 'taskMgr'):
self.world.taskMgr.doMethodLater(0.1, self._delayed_create_resize_handles, "delayed_create_resize_handles")
else:
self._create_resize_handles()
# 添加任务来处理拖动和resize handles更新
if hasattr(self.world, 'taskMgr'):
self.world.taskMgr.add(self._update_drag, "update_lui_drag")
self.world.taskMgr.add(self._update_resize_handles, "update_resize_handles")
self.world.taskMgr.add(self._update_component_outlines, "update_lui_outlines")
self.world.taskMgr.add(self._update_http_components_task, "update_http_components")
print("✓ LUIManager initialized with complete functionality")
def _init_world_space_runtime(self):
"""Attach the optional Panda3D scene-space LUI mirror."""
if WorldSpaceLUIRuntime is None:
print(f"⚠ LUI 3D 场景运行时不可用: {LUI_WORLD_SPACE_IMPORT_ERROR}")
return
try:
self.world_space_runtime = WorldSpaceLUIRuntime(self)
print("✓ LUI 3D 场景运行时已启用")
except Exception as exc:
self.world_space_runtime = None
print(f"⚠ LUI 3D 场景运行时初始化失败: {exc}")
def _canvas_render_mode(self, canvas_data):
mode = str((canvas_data or {}).get("render_mode") or "screen").lower().strip()
return "world" if mode == "world" else "screen"
def _debug_world_lui_enabled(self):
return bool(getattr(self, "debug_world_lui", False))
def _debug_lui_parent_label(self, obj):
if obj is None:
return "None"
try:
get_name = getattr(obj, "getName", None) or getattr(obj, "get_name", None)
if callable(get_name):
name = get_name()
if name:
return f"{obj.__class__.__name__}:{name}"
except Exception:
pass
try:
name = getattr(obj, "name", None)
if name:
return f"{obj.__class__.__name__}:{name}"
except Exception:
pass
return f"{obj.__class__.__name__}@{id(obj):x}"
def _debug_lui_visible_value(self, obj):
if obj is None:
return None
try:
return getattr(obj, "visible", None)
except Exception:
return "?"
def _debug_lui_screen_visibility(self, canvas_data, requested_visible):
if not self._debug_world_lui_enabled():
return
panel = (canvas_data or {}).get("panel")
panel_parent = getattr(panel, "parent", None)
parent_state = "None" if panel_parent is None else "parented"
state = (
bool(requested_visible),
parent_state,
self._debug_lui_visible_value(panel),
self._debug_lui_visible_value((canvas_data or {}).get("background")),
self._debug_lui_visible_value((canvas_data or {}).get("title_label")),
self._canvas_render_mode(canvas_data),
)
key = id(canvas_data)
if self._debug_lui_visibility_state.get(key) == state:
return
self._debug_lui_visibility_state[key] = state
print(
"[LUI3D Debug] screen overlay visibility "
f"canvas='{(canvas_data or {}).get('name')}' "
f"mode={state[5]} requested={state[0]} "
f"panel_parent={self._debug_lui_parent_label(panel_parent)} "
f"panel_parent_state={state[1]} panel_visible={state[2]} "
f"bg_visible={state[3]} title_visible={state[4]}"
)
def _set_lui_object_visible_deep(self, obj, visible, visited=None):
if obj is None:
return
if visited is None:
visited = set()
obj_id = id(obj)
if obj_id in visited:
return
visited.add(obj_id)
try:
if visible and hasattr(obj, "show"):
obj.show()
elif (not visible) and hasattr(obj, "hide"):
obj.hide()
elif hasattr(obj, "visible"):
obj.visible = bool(visible)
except Exception:
pass
try:
if hasattr(obj, "visible"):
obj.visible = bool(visible)
except Exception:
pass
child_attrs = (
"_layout", "_label", "_text", "_shadow_text", "_knob",
"_slider_bg", "_slider_fill", "_sprite_left", "_sprite_mid",
"_sprite_right", "content_node", "root_layout", "header_bar",
"main_frame", "label", "text_label", "sprite", "widget",
"layout_obj", "object",
)
for attr_name in child_attrs:
try:
child = getattr(obj, attr_name, None)
except Exception:
child = None
if child is not None:
self._set_lui_object_visible_deep(child, visible, visited)
try:
obj_dict = getattr(obj, "__dict__", {}) or {}
except Exception:
obj_dict = {}
for value in obj_dict.values():
if value is obj:
continue
if isinstance(value, dict):
for nested in value.values():
self._set_lui_object_visible_deep(nested, visible, visited)
elif isinstance(value, (list, tuple, set)):
for nested in value:
self._set_lui_object_visible_deep(nested, visible, visited)
elif value is not None and value.__class__.__name__.lower().startswith("lui"):
self._set_lui_object_visible_deep(value, visible, visited)
def _set_lui_canvas_screen_visible(self, canvas_data, visible):
panel = (canvas_data or {}).get("panel")
if panel is None:
return
visible = bool(visible)
try:
if visible:
parent = canvas_data.get("_screen_parent")
if parent is None:
parent = getattr(self, "overlay_root", None)
if parent is not None and getattr(panel, "parent", None) is None:
panel.parent = parent
else:
current_parent = getattr(panel, "parent", None)
if current_parent is not None:
canvas_data["_screen_parent"] = current_parent
panel.parent = None
self._set_lui_object_visible_deep(panel, visible)
self._set_lui_object_visible_deep(canvas_data.get("background"), visible)
self._set_lui_object_visible_deep(canvas_data.get("title_label"), visible)
except Exception:
pass
self._debug_lui_screen_visibility(canvas_data, visible)
def _set_lui_component_screen_visible(self, comp_data, visible):
comp_obj = (comp_data or {}).get("object")
if comp_obj is None:
return
visible = bool(visible)
try:
if visible:
parent = comp_data.get("_screen_parent")
if parent is None:
canvas_index = comp_data.get("canvas_index")
if canvas_index is not None and 0 <= int(canvas_index) < len(getattr(self, "canvases", [])):
parent = self.canvases[int(canvas_index)].get("panel")
if parent is not None and getattr(comp_obj, "parent", None) is None:
comp_obj.parent = parent
else:
current_parent = getattr(comp_obj, "parent", None)
if current_parent is not None:
comp_data["_screen_parent"] = current_parent
comp_obj.parent = None
except Exception:
pass
self._set_lui_object_visible_deep(comp_obj, visible)
for key in (
"widget", "sprite", "text_label", "layout_obj",
"keep_alive", "keep_alive_node",
):
self._set_lui_object_visible_deep(comp_data.get(key), visible)
def is_world_space_pointer_over_ui(self):
runtime = getattr(self, "world_space_runtime", None)
if runtime is None:
return False
try:
return bool(runtime.is_pointer_over_ui())
except Exception:
return False
def has_world_space_pointer_capture(self):
runtime = getattr(self, "world_space_runtime", None)
if runtime is None:
return False
return bool(getattr(runtime, "_active_hit", None))
def handle_world_space_pointer_press(self):
runtime = getattr(self, "world_space_runtime", None)
if runtime is None:
return False
try:
return bool(runtime.handle_pointer_press_from_event())
except Exception as exc:
print(f"LUI 3D UI pointer handling failed: {exc}")
return False
def invalidate_world_space_canvas(self, canvas_index=None):
runtime = getattr(self, "world_space_runtime", None)
if runtime is None:
return
try:
if canvas_index is None:
runtime._signatures.clear()
else:
runtime._signatures.pop(int(canvas_index), None)
except Exception:
pass
def set_input_enabled(self, enabled):
"""Enable/disable LUI input handling (used to avoid ImGui click-through)."""
if not self.lui_enabled:
return
if enabled == self._input_enabled:
return
self._input_enabled = enabled
try:
if enabled:
if hasattr(self, "overlay_handler_node") and self.overlay_handler_node:
self.overlay_handler_node.reparent_to(self.world.mouseWatcher)
if hasattr(self, "overlay_region") and self.overlay_region:
self.overlay_region.set_input_handler(self.overlay_handler)
else:
if hasattr(self, "overlay_region") and self.overlay_region:
self.overlay_region.set_input_handler(None)
if hasattr(self, "overlay_handler_node") and self.overlay_handler_node:
self.overlay_handler_node.detach_node()
except Exception as e:
print(f"Warning: Failed to toggle LUI input state: {e}")
def _register_chinese_font(self):
"""注册支持中文的字体"""
from panda3d.lui import LUIFontPool
import os
# 尝试常见的系统路径,确保使用完整的 OS 路径
candidate_fonts = [
"C:/Windows/Fonts/msyh.ttc",
"C:/Windows/Fonts/msyhbd.ttc",
"C:/Windows/Fonts/simhei.ttf",
"C:/Windows/Fonts/simsun.ttc",
"C:/Windows/Fonts/arialuni.ttf",
"C:/Windows/Fonts/arial.ttf"
]
font_registered = False
for fpath in candidate_fonts:
if os.path.exists(fpath):
try:
panda_path = fpath
filename_from_os = getattr(p3d.Filename, "from_os_specific", None) or getattr(p3d.Filename, "fromOsSpecific", None)
if filename_from_os:
panda_path = filename_from_os(fpath).get_fullpath()
font_default = self.world.loader.loadFont(panda_path)
font_label = self.world.loader.loadFont(panda_path)
font_header = self.world.loader.loadFont(panda_path)
if font_default and font_label and font_header:
if hasattr(font_label, "setPixelsPerUnit"):
font_label.setPixelsPerUnit(32)
if hasattr(font_header, "setPixelsPerUnit"):
font_header.setPixelsPerUnit(80)
font_pool = LUIFontPool.get_global_ptr()
font_pool.register_font("default", font_default)
font_pool.register_font("label", font_label)
font_pool.register_font("header", font_header)
print(f"✓ LUI 成功注册中文字体: {panda_path}")
font_registered = True
break
except Exception as e:
print(f"LUI 字体注册失败 {fpath}: {e}")
if not font_registered:
# 如果没有系统字体,至少注册一个默认的避免崩溃
print("⚠️ 警告: 未能找到合适的 LUI 中文字体")
def _init_ui_regions(self):
"""初始化 LUI 区域 (Overlay, Camera, WorldSpace)"""
# 渲染层级说明参考engine/main.py的设置
# - 3D场景render默认sort=0
# - LUI组件设置sort=5在3D场景之上
# - pixel2d (ImGui) 默认sort=20在LUI之上
# 这样3D场景 < LUI组件 < ImGui面板
self.overlay_region = LUIRegion.make("OverlayUI", self.world.win)
self.overlay_handler = LUIInputHandler()
self.overlay_handler_node = self.world.mouseWatcher.attach_new_node(self.overlay_handler)
self.overlay_region.set_input_handler(self.overlay_handler)
self.overlay_region.set_sort(5) # 在3D场景之上在ImGui之下与engine保持一致
self.overlay_root = self.overlay_region.root
# 调试输出:确认层级设置
actual_sort = self.overlay_region.getSort()
print(f"✓ LUI Overlay Region 层级设置: {actual_sort}")
# 显示所有DisplayRegion的层级信息
num_regions = self.world.win.getNumDisplayRegions()
print(f"✓ 窗口总共有 {num_regions} 个DisplayRegion:")
for i in range(num_regions):
dr = self.world.win.getDisplayRegion(i)
sort_value = dr.getSort()
camera = dr.getCamera()
camera_name = camera.getName() if not camera.isEmpty() else "无摄像机"
print(f" Region {i}: sort={sort_value}, camera={camera_name}")
# 当前操作的根节点
self.root = self.overlay_root