EG/ui/panels/editor_panels_left.py
2026-03-24 20:51:39 +08:00

933 lines
41 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.

from imgui_bundle import imgui, imgui_ctx
from pathlib import Path
class EditorPanelsLeftMixin:
"""Auto-split mixin from editor_panels.py."""
@staticmethod
def _truncate_panel_text(text, limit=28):
text = text or ""
if len(text) <= limit:
return text
return text[: max(0, limit - 1)] + ""
def _draw_scene_tree(self):
"""绘制场景树面板"""
# 使用更少的限制性标志允许docking
flags = (imgui.WindowFlags_.no_collapse)
with self.app.style_manager.begin_styled_window("场景树", self.app.showSceneTree, flags) as (_, opened):
if not opened:
self.app.showSceneTree = False
self.app._scene_tree_window_rect = None
return
self.app.showSceneTree = opened
window_pos = imgui.get_window_pos()
window_size = imgui.get_window_size()
self.app._scene_tree_window_rect = (
float(window_pos.x),
float(window_pos.y),
float(window_size.x),
float(window_size.y),
)
self._draw_scene_tree_header()
imgui.separator()
self._build_scene_tree()
def _draw_scene_tree_header(self):
"""绘制场景树头部摘要与检索框。"""
model_count = self._get_scene_tree_model_display_count()
selected_node = self.app._get_selection_node()
selected_name = "未选择"
if selected_node and not selected_node.isEmpty():
selected_name = selected_node.getName() or "未命名对象"
self.app.style_manager.draw_stat_chip(f"模型 {model_count}", tint=(0.14, 0.17, 0.22, 1.0))
imgui.same_line()
self.app.style_manager.draw_stat_chip(
f"当前 {self._truncate_panel_text(selected_name, 20)}",
tint=(0.16, 0.20, 0.27, 1.0),
)
filter_text = self._get_scene_tree_filter()
if filter_text:
imgui.same_line()
self.app.style_manager.draw_stat_chip(
f"筛选 {self._truncate_panel_text(filter_text, 16)}",
tint=(0.19, 0.24, 0.33, 1.0),
)
imgui.spacing()
imgui.text_disabled("筛选场景")
imgui.set_next_item_width(-64)
changed, search_text = imgui.input_text("##scene_tree_search", self.app._scene_tree_search_text, 256)
if changed:
self.app._scene_tree_search_text = search_text
imgui.same_line()
if self.app.style_manager.draw_toolbar_button("清空", size=(52, 26), enabled=bool(self.app._scene_tree_search_text)):
self.app._scene_tree_search_text = ""
def _get_scene_tree_filter(self):
return getattr(self.app, "_scene_tree_search_text", "").strip().lower()
def _get_scene_tree_models(self):
models = []
# SSBO模式下场景树应以SSBO聚合根为唯一模型入口避免混入scene_manager残留节点
# (如 scene.bam / chunk_* 运行时包装节点)导致树结构异常。
if getattr(self.app, "use_ssbo_mouse_picking", False):
ssbo_editor = getattr(self.app, "ssbo_editor", None)
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
source_model_root = getattr(ssbo_editor, "source_model_root", None) if ssbo_editor else None
if ssbo_model and not ssbo_model.isEmpty() and ssbo_model.hasParent():
return [ssbo_model]
if source_model_root and not source_model_root.isEmpty():
return []
scene_manager = getattr(self.app, "scene_manager", None)
if scene_manager and hasattr(scene_manager, "models"):
return [m for m in scene_manager.models if m and not m.isEmpty()]
return []
if hasattr(self.app, "scene_manager") and self.app.scene_manager and hasattr(self.app.scene_manager, "models"):
models.extend([m for m in self.app.scene_manager.models if m and not m.isEmpty()])
ssbo_editor = getattr(self.app, "ssbo_editor", None)
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
if ssbo_model and not ssbo_model.isEmpty() and ssbo_model.hasParent() and ssbo_model not in models:
models.append(ssbo_model)
return models
def _get_ssbo_top_level_model_keys(self):
ssbo_editor = getattr(self.app, "ssbo_editor", None)
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
if not ssbo_editor or not controller or not getattr(controller, "tree_root_key", None):
return self._get_ssbo_virtual_top_level_model_keys(controller)
root_node = controller.tree_nodes.get(controller.tree_root_key)
if not root_node or not root_node.get("children"):
return self._get_ssbo_virtual_top_level_model_keys(controller)
keys = []
for child_key in root_node.get("children", []):
if not self._ssbo_key_matches_scene_filter(controller, child_key):
continue
keys.append(child_key)
return keys
def _get_ssbo_virtual_top_level_model_keys(self, controller):
virtual_tree = getattr(controller, "virtual_tree", None) if controller else None
if not isinstance(virtual_tree, dict):
return []
keys = []
for child in (virtual_tree.get("children", {}) or {}).values():
key = child.get("group_key") or child.get("leaf_key")
if not key:
continue
if not self._ssbo_key_matches_scene_filter(controller, key):
continue
keys.append(key)
return keys
def _find_ssbo_virtual_node_by_key(self, controller, key):
virtual_tree = getattr(controller, "virtual_tree", None) if controller else None
if not isinstance(virtual_tree, dict) or not key:
return None
stack = [virtual_tree]
while stack:
node = stack.pop()
if not isinstance(node, dict):
continue
if node.get("group_key") == key or node.get("leaf_key") == key:
return node
stack.extend(list((node.get("children", {}) or {}).values()))
return None
def _get_scene_tree_model_display_count(self):
models = self._get_scene_tree_models()
ssbo_editor = getattr(self.app, "ssbo_editor", None)
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
count = 0
for model in models:
if ssbo_model and model == ssbo_model:
count += len(self._get_ssbo_top_level_model_keys())
else:
count += 1
return count
def _get_scene_tree_epoch(self):
try:
return int(getattr(self.app, "_scene_tree_epoch", 0) or 0)
except Exception:
return 0
def _ssbo_key_matches_scene_filter(self, controller, key):
filter_text = self._get_scene_tree_filter()
if not filter_text:
return True
node_data = controller.tree_nodes.get(key)
if node_data:
display_name = controller.display_names.get(key, key)
if filter_text in display_name.lower() or filter_text in str(key).lower():
return True
return any(self._ssbo_key_matches_scene_filter(controller, child_key) for child_key in node_data["children"])
virtual_node = self._find_ssbo_virtual_node_by_key(controller, key)
if not virtual_node:
return False
display_name = str(virtual_node.get("display_name", virtual_node.get("name", key)) or key)
if filter_text in display_name.lower() or filter_text in str(key).lower():
return True
return any(
self._ssbo_key_matches_scene_filter(controller, child.get("group_key") or child.get("leaf_key"))
for child in (virtual_node.get("children", {}) or {}).values()
if child.get("group_key") or child.get("leaf_key")
)
def _node_matches_scene_filter(self, node, name):
filter_text = self._get_scene_tree_filter()
if not filter_text:
return True
display_name = (name or "").lower()
node_name = ""
try:
node_name = (node.getName() or "").lower()
except Exception:
node_name = ""
if filter_text in display_name or filter_text in node_name:
return True
ssbo_editor = getattr(self.app, "ssbo_editor", None)
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
if controller and ssbo_model and node == ssbo_model:
root_key = getattr(controller, "tree_root_key", None)
if root_key and self._ssbo_key_matches_scene_filter(controller, root_key):
return True
try:
for child in node.getChildren():
if not child or child.isEmpty():
continue
child_name = child.getName() or ""
if child_name.startswith("modelCollision_"):
continue
if self._node_matches_scene_filter(child, child_name):
return True
except Exception:
pass
return False
def _build_scene_tree(self):
"""构建动态场景树"""
render_entries = []
if hasattr(self.app, "ambient_light") and self.app.ambient_light:
render_entries.append((self.app.ambient_light, "环境光", "light"))
if hasattr(self.app, "scene_manager") and self.app.scene_manager:
if hasattr(self.app.scene_manager, "Spotlight") and self.app.scene_manager.Spotlight:
for i, spotlight in enumerate(self.app.scene_manager.Spotlight):
render_entries.append((spotlight, f"聚光灯_{i + 1}", "light"))
if hasattr(self.app.scene_manager, "Pointlight") and self.app.scene_manager.Pointlight:
for i, pointlight in enumerate(self.app.scene_manager.Pointlight):
render_entries.append((pointlight, f"点光源_{i + 1}", "light"))
if hasattr(self.app, "ground") and self.app.ground:
render_entries.append((self.app.ground, "地板", "geometry"))
render_entries = [entry for entry in render_entries if self._node_matches_scene_filter(entry[0], entry[1])]
if render_entries and imgui.tree_node(f"渲染 ({len(render_entries)})"):
for node, name, node_type in render_entries:
self._draw_scene_node(node, name, node_type)
imgui.tree_pop()
camera_entries = []
if hasattr(self.app, "camera") and self.app.camera and self._node_matches_scene_filter(self.app.camera, "主相机"):
camera_entries.append((self.app.camera, "主相机", "camera"))
if camera_entries and imgui.tree_node(f"相机 ({len(camera_entries)})"):
for node, name, node_type in camera_entries:
self._draw_scene_node(node, name, node_type)
imgui.tree_pop()
models = [model for model in self._get_scene_tree_models() if self._node_matches_scene_filter(model, model.getName() or "模型")]
model_display_count = self._get_scene_tree_model_display_count()
if model_display_count and imgui.tree_node(f"模型 ({model_display_count})"):
ssbo_editor = getattr(self.app, "ssbo_editor", None)
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
for i, model in enumerate(models):
if ssbo_model and controller and model == ssbo_model:
top_level_keys = self._get_ssbo_top_level_model_keys()
if top_level_keys:
for child_key in top_level_keys:
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
continue
self._draw_scene_node(model, model.getName() or f"模型_{i + 1}", "model")
imgui.tree_pop()
elif not render_entries and not camera_entries and not model_display_count and self._get_scene_tree_filter():
imgui.text_disabled("没有匹配的 3D 节点")
# if imgui.tree_node("GUI元素"):
# if hasattr(self,'gui_manager') and self.app.gui_manager and hasattr(self.app.gui_manager,'gui_elements'):
# if self.app.gui_manager.gui_elements:
# for gui_element in self.app.gui_manager.gui_elements:
# if gui_element and hasattr(gui_element,'node'):
# gui_type = getattr(gui_element,'gui_type','GUI_UNKNOWN')
# display_name = getattr(gui_element,'name',gui_type)
# self._draw_scene_node(gui_element.node,display_name,"gui",gui_type)
# else:
# imgui.text("(空)")
# else:
# imgui.text("(空)")
# imgui.tree_pop()
# LUI元素节点
if imgui.tree_node("GUI元素"):
if hasattr(self.app, 'lui_manager') and self.app.lui_manager.lui_enabled:
self.app.lui_manager.draw_component_tree()
imgui.tree_pop()
# if imgui.tree_node("LUI元素"):
# if hasattr(self.app, 'lui_manager') and self.app.lui_manager.lui_enabled:
# if self.app.lui_manager.components:
# for comp in self.app.lui_manager.components:
# if 'node' in comp:
# self._draw_scene_node(comp['node'], comp['name'], "ui")
# if self.app.lui_manager.canvases:
# for canvas in self.app.lui_manager.canvases:
# if imgui.tree_node(f"Canvas: {canvas['name']}"):
# # 实际上组件已经在 node 下了,可以通过 _draw_scene_node 递归显示
# # 但为了清晰,我们可以手动列出或者依赖递归
# self._draw_scene_node(canvas['node'], canvas['name'], "geometry")
# imgui.tree_pop()
# else:
# imgui.text("(空)")
# else:
# imgui.text("(空)")
# imgui.tree_pop()
def _draw_scene_node(self, node, name, node_type, gui_subtype=None):
"""绘制单个场景节点"""
if not node or node.isEmpty():
return
if not self._node_matches_scene_filter(node, name):
return
# 检查是否被选中
is_selected = (self.app._get_selection_node() == node)
force_ssbo_root_key = None
ssbo_editor_ref = None
try:
ssbo_editor = getattr(self.app, "ssbo_editor", None)
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
root_key = getattr(controller, "tree_root_key", None) if controller else None
if (
ssbo_editor and controller and ssbo_model and node == ssbo_model
and root_key and root_key in controller.tree_nodes
):
force_ssbo_root_key = root_key
ssbo_editor_ref = ssbo_editor
is_selected = (getattr(ssbo_editor, "selected_name", None) == root_key)
except Exception:
force_ssbo_root_key = None
ssbo_editor_ref = None
# 节点可见性
is_visible = node.is_hidden() == False
# 设置选择颜色
if is_selected:
imgui.push_style_color(imgui.Col_.text, (0.2, 0.6, 1.0, 1.0))
node_open = False
try:
# 显示节点
node_open = imgui.tree_node(name)
# 处理节点选择
if imgui.is_item_clicked():
if force_ssbo_root_key and ssbo_editor_ref:
try:
ssbo_editor_ref.select_node(force_ssbo_root_key)
if hasattr(self.app, 'lui_manager'):
self.app.lui_manager.selected_index = -1
except Exception:
pass
else:
if hasattr(self.app, 'selection') and self.app.selection:
self.app.selection.updateSelection(node)
# Clear LUI selection when a scene node is selected
if hasattr(self.app, 'lui_manager'):
self.app.lui_manager.selected_index = -1
if self.app.is_dragging and imgui.is_item_hovered():
self.app._drag_scene_tree_hover_node = node
# 右键菜单
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
self._show_node_context_menu(node, name, node_type)
# 显示节点属性
imgui.same_line()
if is_visible:
imgui.text_colored((0.5, 1.0, 0.5, 1.0), "可见")
else:
imgui.text_colored((0.5, 0.5, 0.5, 1.0), "隐藏")
if node_open:
# SSBO模型使用虚拟层级显示避免 flatten 后真实子级丢失)
if self._draw_ssbo_virtual_children(node):
pass
elif node.getNumChildren() > 0:
for i in range(node.getNumChildren()):
child = node.getChild(i)
if not child or child.isEmpty():
continue
child_name = child.getName() or f"child_{i+1}"
# 过滤碰撞辅助节点,避免污染场景树
if child_name.startswith("modelCollision_"):
continue
self._draw_scene_node(child, child_name, node_type)
# tree_pop moved to finally
except Exception as e:
print(f"绘制场景节点时出错: {e}")
finally:
if node_open:
imgui.tree_pop()
# Ensure style stack is balanced.
if is_selected:
imgui.pop_style_color()
def _draw_ssbo_virtual_children(self, node):
"""Draw SSBO controller tree_nodes as virtual children for scene tree."""
ssbo_editor = getattr(self.app, "ssbo_editor", None)
if not ssbo_editor:
return False
model = getattr(ssbo_editor, "model", None)
controller = getattr(ssbo_editor, "controller", None)
if not model or model != node or not controller:
return False
root_key = getattr(controller, "tree_root_key", None)
if not root_key or root_key not in controller.tree_nodes:
imgui.text_disabled("(无可用子节点)")
return True
root_node = controller.tree_nodes[root_key]
if not root_node["children"]:
imgui.text_disabled("(无可用子节点)")
return True
for child_key in root_node["children"]:
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
return True
def _draw_ssbo_virtual_tree_node(self, ssbo_editor, controller, key):
"""Recursively draw SSBO tree_nodes hierarchy in scene tree."""
if not self._ssbo_key_matches_scene_filter(controller, key):
return
node_data = controller.tree_nodes.get(key)
if not node_data:
return self._draw_ssbo_virtual_fallback_node(ssbo_editor, controller, key)
# Skip redundant wrapper nodes (e.g. ROOT), show their children instead.
if controller.should_hide_tree_node(key):
for child_key in node_data["children"]:
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
return
display = controller.display_names.get(key, key)
preferred_ids_fn = getattr(controller, "get_preferred_selection_ids", None)
if callable(preferred_ids_fn):
obj_count = len(preferred_ids_fn(key))
else:
obj_count = len(controller.name_to_ids.get(key, []))
children = node_data["children"]
is_selected = (getattr(ssbo_editor, "selected_name", None) == key)
tree_epoch = self._get_scene_tree_epoch()
if not children:
# Leaf node: selectable
label = f"{display} ({obj_count})##{tree_epoch}:{key}"
if imgui.selectable(label, is_selected)[0]:
ssbo_editor.select_node(key)
if hasattr(self.app, "lui_manager"):
self.app.lui_manager.selected_index = -1
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
try:
ssbo_editor.select_node(key)
except Exception:
pass
self._show_node_context_menu(getattr(ssbo_editor, "model", None), display, "model")
else:
# Branch node: tree node
flags = imgui.TreeNodeFlags_.open_on_arrow
if is_selected:
flags |= imgui.TreeNodeFlags_.selected
label = f"{display} ({obj_count})##{tree_epoch}:{key}"
opened = imgui.tree_node_ex(label, flags)
if imgui.is_item_clicked(0):
ssbo_editor.select_node(key)
if hasattr(self.app, "lui_manager"):
self.app.lui_manager.selected_index = -1
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
try:
ssbo_editor.select_node(key)
except Exception:
pass
self._show_node_context_menu(getattr(ssbo_editor, "model", None), display, "model")
if opened:
for child_key in children:
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
imgui.tree_pop()
def _draw_ssbo_virtual_fallback_node(self, ssbo_editor, controller, key):
virtual_node = self._find_ssbo_virtual_node_by_key(controller, key)
if not virtual_node:
return
display = str(virtual_node.get("display_name", virtual_node.get("name", key)) or key)
preferred_ids_fn = getattr(controller, "get_preferred_selection_ids", None)
if callable(preferred_ids_fn):
obj_count = len(preferred_ids_fn(key))
else:
obj_count = len(controller.name_to_ids.get(key, []))
is_selected = (getattr(ssbo_editor, "selected_name", None) == key)
children = list((virtual_node.get("children", {}) or {}).values())
tree_epoch = self._get_scene_tree_epoch()
if display.strip().lower() == "root" and children:
for child in children:
child_key = child.get("group_key") or child.get("leaf_key")
if child_key:
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
return
if not children:
label = f"{display} ({obj_count})##{tree_epoch}:{key}"
if imgui.selectable(label, is_selected)[0]:
ssbo_editor.select_node(key)
if hasattr(self.app, "lui_manager"):
self.app.lui_manager.selected_index = -1
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
try:
ssbo_editor.select_node(key)
except Exception:
pass
self._show_node_context_menu(getattr(ssbo_editor, "model", None), display, "model")
return
flags = imgui.TreeNodeFlags_.open_on_arrow
if is_selected:
flags |= imgui.TreeNodeFlags_.selected
label = f"{display} ({obj_count})##{tree_epoch}:{key}"
opened = imgui.tree_node_ex(label, flags)
if imgui.is_item_clicked(0):
ssbo_editor.select_node(key)
if hasattr(self.app, "lui_manager"):
self.app.lui_manager.selected_index = -1
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
try:
ssbo_editor.select_node(key)
except Exception:
pass
self._show_node_context_menu(getattr(ssbo_editor, "model", None), display, "model")
if opened:
for child in children:
child_key = child.get("group_key") or child.get("leaf_key")
if child_key:
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
imgui.tree_pop()
def _show_node_context_menu(self, node, name, node_type):
"""显示节点右键菜单"""
self.app._context_menu_node = True
self.app._context_menu_target = node
def _draw_resource_manager(self):
"""绘制资源管理器面板"""
flags = self.app.style_manager.get_window_flags("panel")
with self.app.style_manager.begin_styled_window("资源管理器", self.app.showResourceManager, flags) as (_, opened):
if not opened:
self.app.showResourceManager = False
self.app._resource_manager_window_rect = None
return
self.app.showResourceManager = opened
rm = self.app.resource_manager
self._update_resource_manager_window_rect(rm)
rm.refresh_if_needed()
dirs, files = rm.get_directory_contents(rm.current_path)
self._draw_resource_manager_header(rm, dirs, files)
imgui.separator()
self._draw_resource_toolbar_and_filters(rm)
imgui.separator()
self._draw_resource_directory_entries(rm, dirs)
self._draw_resource_file_entries(rm, files)
self._draw_resource_context_menu(rm)
def _update_resource_manager_window_rect(self, rm):
"""更新资源管理器窗口矩形与根拖拽目标。"""
window_pos = imgui.get_window_pos()
window_size = imgui.get_window_size()
self.app._resource_manager_window_rect = (
float(window_pos.x),
float(window_pos.y),
float(window_size.x),
float(window_size.y),
)
self.app._resource_drop_targets.append((
float(window_pos.x),
float(window_pos.y),
float(window_size.x),
float(window_size.y),
str(rm.current_path),
))
def _draw_resource_directory_entries(self, rm, dirs):
"""绘制当前目录下的文件夹列表。"""
for dir_path in dirs:
if not rm.should_show_file(dir_path):
continue
self._draw_resource_directory_entry(rm, dir_path)
def _draw_resource_directory_entry(self, rm, dir_path: Path):
"""绘制单个文件夹节点(含一层展开内容)。"""
icon_name = rm.get_file_icon(dir_path.name, is_folder=True)
is_selected = dir_path in rm.selected_files
if is_selected:
imgui.push_style_color(imgui.Col_.header, (100 / 255, 150 / 255, 200 / 255, 1.0))
icon_texture = self._load_resource_icon(icon_name)
if icon_texture:
imgui.image(icon_texture, (16, 16))
imgui.same_line()
node_open = imgui.tree_node(f"{dir_path.name}##dir_{dir_path}")
else:
node_open = imgui.tree_node(f"[{icon_name.upper()}]{dir_path.name}##dir_{dir_path}")
if is_selected:
imgui.pop_style_color()
self._append_drop_target_from_last_item(dir_path)
self._start_resource_drag_if_needed(rm, dir_path)
if imgui.is_item_clicked():
self._handle_resource_item_selection(rm, dir_path, is_selected)
if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
rm.navigate_to(dir_path)
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
self._open_resource_item_context_menu(rm, dir_path)
if node_open:
self._draw_resource_directory_children(rm, dir_path)
imgui.tree_pop()
def _draw_resource_directory_children(self, rm, parent_dir: Path):
"""绘制文件夹展开后的子目录与子文件(保持原有顺序)。"""
subdirs, subfiles = rm.get_directory_contents(parent_dir)
for subdir in subdirs:
if not rm.should_show_file(subdir):
continue
self._draw_resource_subdir_entry(rm, subdir)
for subfile in subfiles:
if not rm.should_show_file(subfile):
continue
self._draw_resource_subfile_entry(rm, subfile)
def _draw_resource_subdir_entry(self, rm, subdir: Path):
"""绘制一级子目录节点。"""
subicon_name = rm.get_file_icon(subdir.name, is_folder=True)
sub_is_selected = subdir in rm.selected_files
subicon_texture = self._load_resource_icon(subicon_name)
if subicon_texture:
imgui.image(subicon_texture, (16, 16))
imgui.same_line()
sub_node_open = imgui.tree_node(f" {subdir.name}##dir_{subdir}")
else:
sub_node_open = imgui.tree_node(f" [{subicon_name.upper()}]{subdir.name}##dir_{subdir}")
self._append_drop_target_from_last_item(subdir)
self._start_resource_drag_if_needed(rm, subdir)
if imgui.is_item_clicked():
self._handle_resource_item_selection(rm, subdir, sub_is_selected)
if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
rm.navigate_to(subdir)
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
self._open_resource_item_context_menu(rm, subdir)
if sub_node_open:
imgui.tree_pop()
def _draw_resource_subfile_entry(self, rm, subfile: Path):
"""绘制一级子文件项。"""
subicon_name = rm.get_file_icon(subfile.name)
sub_is_selected = subfile in rm.selected_files
subicon_texture = self._load_resource_icon(subicon_name)
if subicon_texture:
imgui.image(subicon_texture, (16, 16))
imgui.same_line()
selected = imgui.selectable(f" {subfile.name}##file_{subfile}", sub_is_selected)
else:
selected = imgui.selectable(f" [{subicon_name.upper()}] {subfile.name}##file_{subfile}", sub_is_selected)
selected_clicked = selected[0] if isinstance(selected, tuple) else bool(selected)
self._start_resource_drag_if_needed(rm, subfile)
if selected_clicked:
self._handle_resource_item_selection(rm, subfile, sub_is_selected)
if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
self._handle_resource_file_double_click(rm, subfile)
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
self._open_resource_item_context_menu(rm, subfile)
def _draw_resource_file_entries(self, rm, files):
"""绘制当前目录下的文件列表。"""
for file_path in files:
if not rm.should_show_file(file_path):
continue
self._draw_resource_file_entry(rm, file_path)
def _draw_resource_file_entry(self, rm, file_path: Path):
"""绘制顶层文件项。"""
icon_name = rm.get_file_icon(file_path.name)
is_selected = file_path in rm.selected_files
icon_texture = self._load_resource_icon(icon_name)
if icon_texture:
imgui.image(icon_texture, (16, 16))
imgui.same_line()
selected = imgui.selectable(f"{file_path.name}##file_{file_path}", is_selected)
else:
selected = imgui.selectable(f"[{icon_name.upper()}] {file_path.name}##file_{file_path}", is_selected)
selected_clicked = selected[0] if isinstance(selected, tuple) else bool(selected)
self._start_resource_drag_if_needed(rm, file_path)
if selected_clicked:
self._handle_resource_item_selection(rm, file_path, is_selected)
if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
self._handle_resource_file_double_click(rm, file_path)
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
self._open_resource_item_context_menu(rm, file_path)
def _draw_resource_manager_header(self, rm, dirs, files):
"""绘制资源管理器头部摘要。"""
total_items = len(dirs) + len(files)
try:
location = rm.current_path.relative_to(rm.project_root).as_posix()
except ValueError:
location = str(rm.current_path)
self.app.style_manager.draw_stat_chip(
self._truncate_panel_text(location or ".", 30),
tint=(0.14, 0.17, 0.22, 1.0),
)
imgui.same_line()
self.app.style_manager.draw_stat_chip(f"{total_items}", tint=(0.16, 0.19, 0.24, 1.0))
if rm.selected_files:
imgui.same_line()
self.app.style_manager.draw_stat_chip(
f"已选 {len(rm.selected_files)}",
tint=(0.18, 0.24, 0.32, 1.0),
)
if rm.search_filter:
imgui.same_line()
self.app.style_manager.draw_stat_chip(
f"搜索 {self._truncate_panel_text(rm.search_filter, 18)}",
tint=(0.19, 0.24, 0.33, 1.0),
)
def _sync_resource_path_input(self, rm):
current_path_text = str(rm.current_path)
if getattr(self.app, "_resource_path_source", "") != current_path_text:
self.app._resource_path_input = current_path_text
self.app._resource_path_source = current_path_text
def _navigate_resource_path_from_input(self, rm):
raw_path = (getattr(self.app, "_resource_path_input", "") or "").strip().strip('"')
if not raw_path:
return
try:
rm.navigate_to(Path(raw_path))
except Exception:
return
self.app._resource_path_input = str(rm.current_path)
self.app._resource_path_source = self.app._resource_path_input
def _draw_resource_toolbar_and_filters(self, rm):
"""绘制资源管理器顶部工具条与筛选输入。"""
self._sync_resource_path_input(rm)
if self.app.style_manager.draw_toolbar_button("", size=(36, 26), tooltip="后退"):
rm.navigate_back()
imgui.same_line()
if self.app.style_manager.draw_toolbar_button("", size=(36, 26), tooltip="前进"):
rm.navigate_forward()
imgui.same_line()
if self.app.style_manager.draw_toolbar_button("上级", size=(48, 26), tooltip="返回上级目录"):
rm.navigate_up()
imgui.same_line()
if self.app.style_manager.draw_toolbar_button("资源", size=(48, 26), tooltip="回到 Resources 根目录"):
rm.navigate_to(rm.project_root / "Resources")
imgui.same_line()
if self.app.style_manager.draw_toolbar_button("刷新", size=(48, 26), tooltip="刷新当前目录"):
rm.force_refresh()
imgui.same_line()
changed, rm.auto_refresh_enabled = imgui.checkbox("自动刷新", rm.auto_refresh_enabled)
if changed:
rm.set_auto_refresh(rm.auto_refresh_enabled)
imgui.text_disabled("路径")
imgui.set_next_item_width(-72)
changed, new_path = imgui.input_text("##resource_path", self.app._resource_path_input, 512)
if changed:
self.app._resource_path_input = new_path
imgui.same_line()
if self.app.style_manager.draw_toolbar_button("前往", size=(52, 26), tooltip="跳转到输入路径"):
self._navigate_resource_path_from_input(rm)
imgui.text_disabled("搜索")
imgui.set_next_item_width(-72)
changed, rm.search_filter = imgui.input_text("##resource_search", rm.search_filter, 256)
imgui.same_line()
if self.app.style_manager.draw_toolbar_button("清空", size=(52, 26), tooltip="清空搜索条件", enabled=bool(rm.search_filter)):
rm.search_filter = ""
def _load_resource_icon(self, icon_name: str):
"""加载资源图标;失败时返回 None。"""
try:
return self.app.style_manager.load_icon(f"file_types/{icon_name}")
except Exception:
return None
def _handle_resource_item_selection(self, rm, target_path: Path, is_selected: bool):
"""处理资源项选中逻辑(支持 Ctrl 多选)。"""
if imgui.get_io().key_ctrl:
if is_selected:
rm.selected_files.discard(target_path)
else:
rm.selected_files.add(target_path)
else:
rm.selected_files.clear()
rm.selected_files.add(target_path)
rm.focused_file = target_path
def _open_resource_item_context_menu(self, rm, target_path: Path):
"""打开资源项右键菜单。"""
rm.show_context_menu = True
rm.context_menu_file = target_path
rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y)
imgui.open_popup("resource_context_menu")
def _handle_resource_file_double_click(self, rm, file_path: Path):
"""处理文件双击:模型导入,其他文件打开。"""
if self._is_model_file(file_path):
self.app._import_model_with_menu_logic(
str(file_path),
show_info_message=True,
show_success_message=True,
)
else:
rm.open_file(file_path)
def _draw_resource_context_menu(self, rm):
"""绘制资源管理器右键菜单。保持原有菜单项与顺序。"""
if rm.context_menu_file:
imgui.set_next_window_pos((rm.context_menu_position[0], rm.context_menu_position[1]))
if imgui.begin_popup("resource_context_menu"):
if rm.context_menu_file and rm.context_menu_file.is_dir():
if imgui.menu_item("打开")[1]:
rm.navigate_to(rm.context_menu_file)
imgui.separator()
if imgui.menu_item("重命名")[1]:
print(f"重命名文件夹: {rm.context_menu_file.name}")
if imgui.menu_item("删除")[1]:
print(f"删除文件夹: {rm.context_menu_file.name}")
elif rm.context_menu_file:
if imgui.menu_item("打开")[1]:
rm.open_file(rm.context_menu_file)
imgui.separator()
if imgui.menu_item("导入到场景")[1]:
if self._is_model_file(rm.context_menu_file):
self.app._import_model_with_menu_logic(
str(rm.context_menu_file),
show_info_message=True,
show_success_message=True,
)
if imgui.menu_item("重命名")[1]:
print(f"重命名文件: {rm.context_menu_file.name}")
if imgui.menu_item("删除")[1]:
print(f"删除文件: {rm.context_menu_file.name}")
if rm.context_menu_file:
imgui.separator()
if imgui.menu_item("复制路径")[1]:
imgui.set_clipboard_text(str(rm.context_menu_file))
self.app.add_info_message("路径已复制到剪贴板")
if imgui.menu_item("在文件管理器中显示")[1]:
import platform
import subprocess
if platform.system() == "Windows":
subprocess.run(["explorer", "/select,", str(rm.context_menu_file)])
elif platform.system() == "Darwin":
subprocess.run(["open", "-R", str(rm.context_menu_file)])
else:
subprocess.run(["xdg-open", str(rm.context_menu_file.parent)])
if imgui.is_mouse_clicked(0) and not imgui.is_window_hovered():
rm.context_menu_file = None
rm.show_context_menu = False
imgui.close_current_popup()
imgui.end_popup()
@staticmethod
def _is_model_file(path: Path) -> bool:
return path.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']
def _append_drop_target_from_last_item(self, target_path: Path):
item_min = imgui.get_item_rect_min()
item_max = imgui.get_item_rect_max()
width = float(item_max.x - item_min.x)
height = float(item_max.y - item_min.y)
if width <= 0 or height <= 0:
return
self.app._resource_drop_targets.append((
float(item_min.x),
float(item_min.y),
width,
height,
str(target_path),
))
def _start_resource_drag_if_needed(self, rm, fallback_path: Path):
if not imgui.is_item_active() or not imgui.is_mouse_dragging(0):
return
drag_files = list(rm.selected_files) if rm.selected_files else [fallback_path]
rm.start_drag(drag_files)
self.app.is_dragging = True
self.app.show_drag_overlay = True