From 5a9e31a1959a054eaa7ca45a858aaa18bb7b5120 Mon Sep 17 00:00:00 2001 From: ayuan9957 <107920784+ayuan9957@users.noreply.github.com> Date: Sat, 28 Feb 2026 15:11:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=85=E7=90=86qt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TransformGizmo/move_gizmo.py | 8 +- TransformGizmo/rotate_gizmo.py | 12 +- TransformGizmo/scale_gizmo.py | 8 +- core/InfoPanelManager.py | 49 +- core/selection.py | 40 +- core/vr/testing/test_mode.py | 8 +- core/vr_manager.py | 21 +- core/world.py | 41 +- requirements/DEPLOYMENT_README.md | 20 +- requirements/clean-requirements.txt | 11 +- requirements/environment.yml | 13 +- requirements/requirements.txt | 26 +- scene/scene_manager_convert_tiles_mixin.py | 64 +- scene/tree_roles.py | 4 +- ui/icon_manager.py | 496 +-- ui/widgets.py | 4330 +------------------- 16 files changed, 342 insertions(+), 4809 deletions(-) diff --git a/TransformGizmo/move_gizmo.py b/TransformGizmo/move_gizmo.py index f91270c6..4484456d 100644 --- a/TransformGizmo/move_gizmo.py +++ b/TransformGizmo/move_gizmo.py @@ -496,7 +496,7 @@ class MoveGizmo(DirectObject): def undo_last_move(self): """ Undo the last committed move. - Default hotkey (from Qt -> Panda3D translation): Ctrl+Z => 'control-z'. + Default hotkey: Ctrl+Z => 'control-z'. """ if not self._move_undo_stack: self._log("undo_last_move: stack empty") @@ -530,7 +530,7 @@ class MoveGizmo(DirectObject): # --- Mouse helpers ------------------------------------------------- def _get_normalized_mouse(self, extra) -> Optional[Point3]: """ - Convert Qt pixel coordinates (from QPanda3DWidget) to Panda's + Convert external UI pixel coordinates to Panda's normalized device coordinates (-1..1), or fall back to mouseWatcherNode when available. """ @@ -538,14 +538,14 @@ class MoveGizmo(DirectObject): mouse = self.world.mouseWatcherNode.getMouse() return Point3(mouse.x, mouse.y, 0) - # 1) Extra payload from QPanda3DWidget (pixels) + # 1) Extra payload from external UI (pixels) if isinstance(extra, dict) and "x" in extra and "y" in extra: parent = getattr(self.world, "parent", None) if parent is not None: w = max(parent.width(), 1) h = max(parent.height(), 1) nx = (extra["x"] / w) * 2.0 - 1.0 - # Qt origin is top‑left, Panda origin is center with +Y up + # Pixel origin is top-left; Panda origin is center with +Y up. ny = 1.0 - (extra["y"] / h) * 2.0 return Point3(nx, ny, 0) diff --git a/TransformGizmo/rotate_gizmo.py b/TransformGizmo/rotate_gizmo.py index 8a20ed05..d2b63ad4 100644 --- a/TransformGizmo/rotate_gizmo.py +++ b/TransformGizmo/rotate_gizmo.py @@ -54,10 +54,10 @@ from .events import GizmoEvent - 控件会根据摄像机距离自动缩放,屏幕大小保持近似不变 集成方式(示例): - from QPanda3D.Panda3DWorld import Panda3DWorld - from QPanda3DExamples.rotate_gizmo import RotateGizmo + from direct.showbase.ShowBase import ShowBase + from TransformGizmo.rotate_gizmo import RotateGizmo - world = Panda3DWorld() + world = ShowBase() model_np = world.render.attachNewNode("Box") # ... 在 model_np 下加载模型 @@ -72,7 +72,7 @@ from .events import GizmoEvent 鼠标事件要求: - 本类默认监听 Panda3D 的 "mouse1" / "mouse1-up" / "mouse-move" 事件 - - 如果从 Qt / 自定义 UI 传递鼠标像素坐标,可以在发送事件时传入 extra 字典: + - 如果从外部 UI 传递鼠标像素坐标,可以在发送事件时传入 extra 字典: messenger.send("mouse1", [{"x": mouse_x, "y": mouse_y}]) 本类会自动将像素坐标转换到 [-1, 1] 的标准化设备坐标 """ @@ -955,7 +955,7 @@ class RotateGizmo(DirectObject): """ 将鼠标转换到 Panda3D 的标准化设备坐标 [-1, 1]。 - 优先使用 Qt / 外部 UI 传入的像素坐标(extra 字典), + 优先使用外部 UI 传入的像素坐标(extra 字典), 如果没有,则回退到 Panda3D 的 mouseWatcherNode。 """ if self.world.mouseWatcherNode.hasMouse(): @@ -969,7 +969,7 @@ class RotateGizmo(DirectObject): w = max(parent.width(), 1) h = max(parent.height(), 1) nx = (extra["x"] / w) * 2.0 - 1.0 - # Qt 原点在左上,Panda3D 原点在中心,Y 轴向上 + # 像素坐标原点在左上,Panda3D 原点在中心,Y 轴向上 ny = 1.0 - (extra["y"] / h) * 2.0 return Point3(nx, ny, 0.0) diff --git a/TransformGizmo/scale_gizmo.py b/TransformGizmo/scale_gizmo.py index 9d015d1f..d297a748 100644 --- a/TransformGizmo/scale_gizmo.py +++ b/TransformGizmo/scale_gizmo.py @@ -48,10 +48,10 @@ from .events import GizmoEvent - 控件会根据摄像机距离自动缩放,屏幕大小保持近似不变 集成方式(示例): - from QPanda3D.Panda3DWorld import Panda3DWorld - from QPanda3DExamples.scale_gizmo import ScaleGizmo + from direct.showbase.ShowBase import ShowBase + from TransformGizmo.scale_gizmo import ScaleGizmo - world = Panda3DWorld() + world = ShowBase() model_np = world.render.attachNewNode("Box") # ... 在 model_np 下加载模型 @@ -66,7 +66,7 @@ from .events import GizmoEvent 鼠标事件要求: - 本类默认监听 Panda3D 的 "mouse1" / "mouse1-up" / "mouse-move" 事件 - - 如果从 Qt / 自定义 UI 传递鼠标像素坐标,可以在发送事件时传入 extra 字典: + - 如果从外部 UI 传递鼠标像素坐标,可以在发送事件时传入 extra 字典: messenger.send("mouse1", [{"x": mouse_x, "y": mouse_y}]) 本类会自动将像素坐标转换到 [-1, 1] 的标准化设备坐标 """ diff --git a/core/InfoPanelManager.py b/core/InfoPanelManager.py index 68ea363f..2670ad44 100644 --- a/core/InfoPanelManager.py +++ b/core/InfoPanelManager.py @@ -1,18 +1,16 @@ -# 修改后的 InfoPanelManager.py -from xml.sax.handler import property_encoding - -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QMessageBox -from direct.gui.DirectGui import DirectFrame, DirectLabel -from direct.showbase.ShowBaseGlobal import aspect2d -from panda3d.core import TextNode, Vec4, NodePath -from direct.showbase.DirectObject import DirectObject -import threading +# 修改后的 InfoPanelManager.py +from direct.gui.DirectGui import DirectFrame, DirectLabel +from direct.showbase.ShowBaseGlobal import aspect2d +from panda3d.core import TextNode, Vec4, NodePath +from direct.showbase.DirectObject import DirectObject +import threading import json -import time -from urllib.parse import urlparse -import requests - +import time +from urllib.parse import urlparse +import requests + +from scene.tree_roles import TREE_USER_ROLE + class InfoPanelManager(DirectObject): """信息面板管理器,用于创建和管理2D信息面板""" @@ -215,9 +213,15 @@ class InfoPanelManager(DirectObject): root_item = None for i in range(tree_widget.topLevelItemCount()): item = tree_widget.topLevelItem(i) - if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render: - root_item = item - break + item_node = None + if hasattr(item, "data"): + try: + item_node = item.data(0, TREE_USER_ROLE) + except Exception: + item_node = None + if item.text(0) == "render" or item_node == self.world.render: + root_item = item + break if root_item: # 使用现有的 add_node_to_tree_widget 方法添加节点 @@ -1306,11 +1310,12 @@ class InfoPanelManager(DirectObject): print("✓ 示例天气信息面板已创建") - except Exception as e: - print(f"✗ 创建示例天气信息面板失败: {e}") - import traceback - traceback.print_exc() - QMessageBox.critical(self, "错误", f"创建示例天气信息面板时出错: {str(e)}") + except Exception as e: + print(f"✗ 创建示例天气信息面板失败: {e}") + import traceback + traceback.print_exc() + if hasattr(self.world, "add_error_message"): + self.world.add_error_message(f"创建示例天气信息面板时出错: {str(e)}") def getRealWeatherData(self): """获取真实天气数据""" diff --git a/core/selection.py b/core/selection.py index b6586f76..9bb69518 100644 --- a/core/selection.py +++ b/core/selection.py @@ -11,7 +11,7 @@ from direct.showbase.ShowBaseGlobal import globalClock from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState, DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue, CollisionNode, CollisionRay, GeomNode, BitMask32, Material, LColor, DepthWriteAttrib, - TransparencyAttrib, Vec4, CollisionCapsule) + TransparencyAttrib, Vec4, CollisionCapsule, WindowProperties) from direct.task.TaskManagerGlobal import taskMgr import math from core.selection_outline import SelectionOutlineManager @@ -93,38 +93,16 @@ class SelectionSystem: # ==================== 光标设置 ==================== def _setCursor(self,cursor_type): try: - from PyQt5.QtCore import Qt if self._current_cursor == cursor_type: return - if hasattr(self.world,'main_window') and self.world.main_window: - main_window = self.world.main_window - else: - from PyQt5.QtWidgets import QApplication - main_window = QApplication.activeWindow() - if not main_window: - windows = QApplication.topLevelWindows() - for window in windows: - if hasattr(window,'isVisible') and window.isVisible(): - main_window = window - break - if main_window: - if cursor_type == "crosshair": - main_window.setCursor(Qt.CrossCursor) - elif cursor_type == "size_hor": - main_window.setCursor(Qt.SizeHorCursor) - elif cursor_type == "size_ver": - main_window.setCursor(Qt.SizeVerCursor) - elif cursor_type == "size_all": - main_window.setCursor(Qt.SizeAllCursor) - elif cursor_type == "pointing_hand": - main_window.setCursor(Qt.PointingHandCursor) - else: - main_window.unsetCursor() - self._current_cursor = cursor_type - #print(f"光标已设置:{cursor_type}") - self._current_cursor = cursor_type - else: - print("警告:无法获取主窗口,光标设置失败") + if not hasattr(self.world, "win") or not self.world.win: + return + + # Panda3D 原生接口不支持直接切换系统光标形状,这里保留状态并确保光标可见。 + props = WindowProperties() + props.setCursorHidden(False) + self.world.win.requestProperties(props) + self._current_cursor = cursor_type except Exception as e: print(f"设置光标失败{e}") def _resetCursor(self): diff --git a/core/vr/testing/test_mode.py b/core/vr/testing/test_mode.py index c12b8922..a0419801 100644 --- a/core/vr/testing/test_mode.py +++ b/core/vr/testing/test_mode.py @@ -91,10 +91,10 @@ class VRTestMode: self.vr_manager._disable_main_cam() # 设置高帧率用于测试 - if hasattr(self.vr_manager.world, 'qtWidget') and self.vr_manager.world.qtWidget: - if hasattr(self.vr_manager.world.qtWidget, 'synchronizer'): - self.vr_manager.world.qtWidget.synchronizer.setInterval(int(1000/144)) - print("✓ 测试模式:Qt Timer设置为144Hz") + host_widget = getattr(self.vr_manager.world, "host_widget", None) + if host_widget and hasattr(host_widget, "synchronizer"): + host_widget.synchronizer.setInterval(int(1000 / 144)) + print("✓ 测试模式:渲染同步器设置为144Hz") # 初始化测试显示系统 if not self._initialize_test_display(): diff --git a/core/vr_manager.py b/core/vr_manager.py index b82186a5..895e4bc2 100644 --- a/core/vr_manager.py +++ b/core/vr_manager.py @@ -1893,12 +1893,11 @@ class VRManager(DirectObject): print(" 注意:Submit后立即调用WaitGetPoses是错误实现") self.set_prediction_time(11.0) # 11ms预测时间 - OpenVR标准值,平衡准确性和延迟 - # 🚀 动态调整Qt Timer频率以支持VR - if hasattr(self.world, 'qtWidget') and self.world.qtWidget: - if hasattr(self.world.qtWidget, 'synchronizer'): - # 设置为144Hz,让OpenVR控制实际渲染节奏 - self.world.qtWidget.synchronizer.setInterval(int(1000/144)) - print("✓ Qt Timer调整为144Hz,让OpenVR控制VR渲染节奏") + # 🚀 动态调整宿主同步器频率以支持VR + host_widget = getattr(self.world, "host_widget", None) + if host_widget and hasattr(host_widget, "synchronizer"): + host_widget.synchronizer.setInterval(int(1000 / 144)) + print("✓ 渲染同步器调整为144Hz,让OpenVR控制VR渲染节奏") # 🔧 关键修复:检测并重建缺失的手柄visualizer # 当渲染模式切换时,visualizer可能被清理但控制器对象仍存在 @@ -1952,11 +1951,11 @@ class VRManager(DirectObject): # 恢复主相机 self._enable_main_cam() - # 恢复Qt Timer到60FPS - if hasattr(self.world, 'qtWidget') and self.world.qtWidget: - if hasattr(self.world.qtWidget, 'synchronizer'): - self.world.qtWidget.synchronizer.setInterval(int(1000/60)) - print("✓ Qt Timer恢复为60Hz") + # 恢复渲染同步器到60FPS + host_widget = getattr(self.world, "host_widget", None) + if host_widget and hasattr(host_widget, "synchronizer"): + host_widget.synchronizer.setInterval(int(1000 / 60)) + print("✓ 渲染同步器恢复为60Hz") print("✅ VR模式已禁用,手柄模型已隐藏") diff --git a/core/world.py b/core/world.py index 2daf6cfc..ecd26e1e 100644 --- a/core/world.py +++ b/core/world.py @@ -40,7 +40,7 @@ class CoreWorld(ShowBase): global _global_render_pipeline # 初始化基础属性 - self.qtWidget = None # Qt部件引用(用于获取准确的渲染区域尺寸) + self.host_widget = None # 外部宿主窗口引用(用于获取准确渲染区域尺寸) # 设置基本配置 loadPrcFileData("", "show-frame-rate-meter 0") @@ -686,16 +686,17 @@ class CoreWorld(ShowBase): print(f"警告: 加载中文字体时发生错误: {e}") self.chinese_font = None - def setQtWidget(self, widget): - """设置Qt部件引用""" - self.qtWidget = widget - print(f"✓ 设置Qt部件引用: {widget}") + def setHostWidget(self, widget): + """设置外部宿主窗口引用。""" + self.host_widget = widget + print(f"✓ 设置宿主窗口引用: {widget}") def getWindowSize(self): """获取准确的窗口尺寸""" - if self.qtWidget: - # 优先使用Qt部件的实际尺寸 - width, height = self.qtWidget.getActualSize() + widget = self.host_widget + if widget and hasattr(widget, "getActualSize"): + # 优先使用宿主窗口的实际尺寸 + width, height = widget.getActualSize() if width > 0 and height > 0: return width, height @@ -754,30 +755,28 @@ class CoreWorld(ShowBase): self.lastMouseX = evt['x'] self.lastMouseY = evt['y'] # - # # 通过 Qt 窗口隐藏光标并捕获鼠标 + # # 通过宿主窗口隐藏光标并捕获鼠标 # try: - # if hasattr(self, 'qtWidget') and self.qtWidget: - # from PyQt5.QtCore import Qt - # self.qtWidget.setCursor(Qt.BlankCursor) + # if hasattr(self, 'host_widget') and self.host_widget: + # self.host_widget.setCursorHidden(True) # # 捕获鼠标,使其无法离开窗口 - # self.qtWidget.grabMouse() + # self.host_widget.grabMouse() # except Exception as e: - # print(f"通过 Qt 隐藏光标时出错: {e}") + # print(f"隐藏光标时出错: {e}") def mouseReleaseEventRight(self, evt): """处理鼠标右键释放事件""" #print("右键释放") self.mouseRightPressed = False - # # 恢复 Qt 窗口光标并释放鼠标捕获 + # # 恢复宿主窗口光标并释放鼠标捕获 # try: - # if hasattr(self, 'qtWidget') and self.qtWidget: - # from PyQt5.QtCore import Qt - # self.qtWidget.unsetCursor() # 恢复默认光标 + # if hasattr(self, 'host_widget') and self.host_widget: + # self.host_widget.setCursorHidden(False) # 恢复默认光标 # # 释放鼠标捕获 - # self.qtWidget.releaseMouse() + # self.host_widget.releaseMouse() # except Exception as e: - # print(f"恢复 Qt 光标时出错: {e}") + # print(f"恢复光标时出错: {e}") def mouseMoveEvent(self, evt): """处理鼠标移动事件 - 只处理相机旋转""" @@ -966,7 +965,7 @@ class CoreWorld(ShowBase): return None # 创建材质编辑器组件 - # 使用纯 Panda3D 实现,不依赖 PyQt5 + # 使用纯 Panda3D 实现,不依赖外部 GUI 框架 print("材质编辑器组件已创建(使用 Panda3D 原生实现)") material_widget.setLayout(layout) diff --git a/requirements/DEPLOYMENT_README.md b/requirements/DEPLOYMENT_README.md index 1100873f..dabd8a11 100644 --- a/requirements/DEPLOYMENT_README.md +++ b/requirements/DEPLOYMENT_README.md @@ -64,13 +64,13 @@ python main.py - 选择虚拟环境中的Python路径 3. **验证环境**:检查状态栏显示正确的Python版本 -## 📦 项目依赖说明 - -- **Panda3D**: 3D图形引擎 -- **PyQt5/PySide6**: GUI框架 -- **Pillow**: 图像处理 -- **python-dotenv**: 环境变量管理 -- **pyassimp**: 3D模型加载 +## 📦 项目依赖说明 + +- **Panda3D**: 3D图形引擎 +- **imgui-bundle / p3dimgui**: ImGui GUI框架 +- **Pillow**: 图像处理 +- **python-dotenv**: 环境变量管理 +- **pyassimp**: 3D模型加载 ## 🌍 跨平台注意事项 @@ -83,8 +83,8 @@ python main.py **Q: 无法安装Panda3D?** A: 确保系统有OpenGL支持,或使用conda安装 -**Q: PyQt5无法运行?** -A: 可能需要安装系统级GUI依赖 +**Q: ImGui界面显示异常?** +A: 检查显卡驱动/OpenGL支持,并确认已安装 `imgui-bundle` **Q: 导入错误?** -A: 检查Python版本(需要3.10+) \ No newline at end of file +A: 检查Python版本(需要3.10+) diff --git a/requirements/clean-requirements.txt b/requirements/clean-requirements.txt index 55a12ec3..a30e51bb 100644 --- a/requirements/clean-requirements.txt +++ b/requirements/clean-requirements.txt @@ -1,6 +1,5 @@ -Panda3D>=1.10.15 -PyQt5>=5.15.9 -PySide6>=6.8.1 -Pillow>=9.0.1 -python-dotenv>=1.0.1 -pyassimp>=5.2.5 +Panda3D>=1.10.15 +imgui-bundle +Pillow>=9.0.1 +python-dotenv>=1.0.1 +pyassimp>=5.2.5 diff --git a/requirements/environment.yml b/requirements/environment.yml index e2800e1e..311faa55 100644 --- a/requirements/environment.yml +++ b/requirements/environment.yml @@ -5,11 +5,10 @@ channels: dependencies: - python=3.10 - pip - - pip: - - Panda3D>=1.10.15 - - PyQt5>=5.15.9 - - PySide6>=6.8.1 - - Pillow>=9.0.1 - - python-dotenv>=1.0.1 - - pyassimp>=5.2.5 + - pip: + - Panda3D>=1.10.15 + - imgui-bundle + - Pillow>=9.0.1 + - python-dotenv>=1.0.1 + - pyassimp>=5.2.5 - six diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 69fd9bb3..179de9ca 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -58,33 +58,21 @@ PyGObject==3.42.1 PyJWT==2.3.0 pymacaroons==0.13.0 PyNaCl==1.5.0 -pyparsing==2.4.7 -PyQt5==5.15.9 -pyqt5-plugins==5.15.9.2.3 -PyQt5-Qt5==5.15.2 -pyqt5-tools==5.15.9.3.3 -PyQt5_sip==12.16.1 -pyRFC3339==1.1 -PySide6==6.8.1 -PySide6_Addons==6.8.1 -PySide6_Essentials==6.8.1 -python-apt==2.4.0+ubuntu4 +pyparsing==2.4.7 +pyRFC3339==1.1 +python-apt==2.4.0+ubuntu4 python-dateutil==2.8.1 python-debian==0.1.43+ubuntu1.1 python-dotenv==1.0.1 pytz==2022.1 pyxdg==0.27 -PyYAML==5.4.1 -QPanda3D==0.2.10 -qt5-applications==5.15.2.2.3 -qt5-tools==5.15.2.1.3 -reportlab==3.6.8 +PyYAML==5.4.1 +reportlab==3.6.8 requests==2.25.1 SceneEditor==22.5 screen-resolution-extra==0.0.0 -SecretStorage==3.3.1 -shiboken6==6.8.1 -six==1.16.0 +SecretStorage==3.3.1 +six==1.16.0 soupsieve==2.3.1 systemd-python==234 ubuntu-drivers-common==0.0.0 diff --git a/scene/scene_manager_convert_tiles_mixin.py b/scene/scene_manager_convert_tiles_mixin.py index f72ee0c0..0b94414e 100644 --- a/scene/scene_manager_convert_tiles_mixin.py +++ b/scene/scene_manager_convert_tiles_mixin.py @@ -39,28 +39,45 @@ class SceneManagerConvertTilesMixin: def _convertToGLBWithProgress(self, filepath): """带进度显示的GLB转换""" - try: - from PyQt5.QtWidgets import QProgressDialog, QApplication - from PyQt5.QtCore import Qt - - # 创建进度对话框 - progress = QProgressDialog("正在转换模型格式以获得更好的动画支持...", "取消", 0, 100) - progress.setWindowTitle("模型格式转换") - progress.setWindowModality(Qt.WindowModal) - progress.show() - QApplication.processEvents() - - try: - result = self._convertToGLB(filepath, progress) - progress.hide() - return result - except Exception as e: - progress.hide() - print(f"转换过程出错: {e}") + class _ConsoleProgress: + def __init__(self, label): + self._value = 0 + self._label = label + + def setWindowTitle(self, title): + print(f"[GLB转换] {title}") + + def setWindowModality(self, _): return None - - except ImportError: - # 如果没有 PyQt5,直接转换 + + def show(self): + print(f"[GLB转换] {self._label}") + + def hide(self): + print("[GLB转换] 完成") + + def setValue(self, value): + value = int(value) + if value != self._value: + self._value = value + print(f"[GLB转换] 进度: {self._value}%") + + def setLabelText(self, text): + if text != self._label: + self._label = text + print(f"[GLB转换] {self._label}") + + progress = _ConsoleProgress("正在转换模型格式以获得更好的动画支持...") + progress.setWindowTitle("模型格式转换") + progress.show() + + try: + result = self._convertToGLB(filepath, progress) + progress.hide() + return result + except Exception as e: + progress.hide() + print(f"转换过程出错: {e}") return self._convertToGLB(filepath) def _convertToGLB(self, filepath, progress=None): @@ -71,8 +88,6 @@ class SceneManagerConvertTilesMixin: if progress: progress.setValue(10) progress.setLabelText("准备转换...") - from PyQt5.QtWidgets import QApplication - QApplication.processEvents() # 准备输出路径 base_name = os.path.splitext(os.path.basename(filepath))[0] @@ -93,7 +108,6 @@ class SceneManagerConvertTilesMixin: if progress: progress.setValue(20) progress.setLabelText("尝试 Blender 转换...") - QApplication.processEvents() # 方法1: 使用 Blender 进行转换 if self._convertWithBlender(filepath, glb_path, progress): @@ -102,7 +116,6 @@ class SceneManagerConvertTilesMixin: if progress: progress.setValue(60) progress.setLabelText("尝试 FBX2glTF 转换...") - QApplication.processEvents() # 方法2: 使用 FBX2glTF (如果可用) if self._convertWithFBX2glTF(filepath, glb_path, progress): @@ -111,7 +124,6 @@ class SceneManagerConvertTilesMixin: if progress: progress.setValue(80) progress.setLabelText("尝试 Assimp 转换...") - QApplication.processEvents() # 方法3: 使用 Assimp if self._convertWithAssimp(filepath, glb_path, progress): diff --git a/scene/tree_roles.py b/scene/tree_roles.py index 8cb53037..6f489941 100644 --- a/scene/tree_roles.py +++ b/scene/tree_roles.py @@ -1,5 +1,5 @@ -"""Shared tree data-role constants (Qt-independent).""" +"""Shared tree data-role constants.""" -# Qt.ItemDataRole.UserRole +# Keep this value stable for legacy tree-item data payloads. TREE_USER_ROLE = 256 diff --git a/ui/icon_manager.py b/ui/icon_manager.py index 1a63980e..470c9e65 100644 --- a/ui/icon_manager.py +++ b/ui/icon_manager.py @@ -1,342 +1,154 @@ -""" -图标管理工具 - -负责统一管理应用程序中的所有图标: -- 图标路径解析 -- 图标缓存 -- 图标预加载 -- 图标错误处理 -""" -import os -import sys -from typing import Dict, Optional -from PyQt5.QtGui import QIcon, QPixmap -from PyQt5.QtCore import QSize - - -class IconManager: - """图标管理器类""" - - def __init__(self): - """初始化图标管理器""" - self.icon_cache: Dict[str, QIcon] = {} - self.icon_directory = self._get_icon_directory() - self.default_icon = None - - # 预定义的图标映射 - self.icon_map = { - # 主窗口图标 - 'app_logo': 'logo.png', - - # 工具栏图标 - 'select_tool': 'select_tool.png', - 'move_tool': 'move_tool.png', - 'rotate_tool': 'rotate_tool.png', - 'scale_tool': 'scale_tool.png', - - # 菜单图标(如果有的话) - 'new_file': 'new_file.png', - 'open_file': 'open_file.png', - 'save_file': 'save_file.png', - 'exit': 'exit.png', - - # 对象类型图标 - 'object_3d': 'object_3d.png', - 'light': 'light.png', - 'camera': 'camera.png', - 'terrain': 'terrain.png', - 'script': 'script.png', - - # 状态图标 - 'success': 'success.png', - 'warning': 'warning.png', - 'error': 'error.png', - 'info': 'info.png', - - 'up_arrow': 'up_arrows.png', - 'down_arrow': 'down_arrows.png', - 'left_arrow': 'left_arrows.png', - 'right_arrow': 'right_arrows.png', - - 'solid_down_arrows': 'solid_down_arrows.png', - 'solid_right_arrows': 'solid_right_arrows.png', - - 'minimize_icon': 'minimize_icon.png', - 'windowing_icon': 'windowing_icon.png', - 'close_icon': 'close_icon.png', - - # 弹窗图标 - 'success_icon': 'success_icon.png', - 'warning_icon': 'warning_icon.png', - 'fail_icon': 'delete_fail_icon.png', - - # 属性面板图标 - 'property_select_image': 'property_select_image.png', - } - - # 初始化默认图标 - self._create_default_icon() - - # 预加载常用图标 - self._preload_icons() - - def _get_icon_directory(self) -> str: - """获取图标目录的绝对路径""" - # 获取当前文件的目录(ui目录) - current_dir = os.path.dirname(os.path.abspath(__file__)) - # 获取项目根目录(ui的父目录) - project_root = os.path.dirname(current_dir) - # 拼接icons目录路径 - icon_dir = os.path.join(project_root, "icons") - - print(f"🔍 图标目录路径: {icon_dir}") - - # 检查目录是否存在 - if not os.path.exists(icon_dir): - print(f"⚠️ 图标目录不存在: {icon_dir}") - # 尝试创建目录 - try: - os.makedirs(icon_dir, exist_ok=True) - print(f"✅ 已创建图标目录: {icon_dir}") - except Exception as e: - print(f"❌ 创建图标目录失败: {e}") - - return icon_dir - - def _create_default_icon(self): - """创建默认图标""" - # 创建一个简单的默认图标 - pixmap = QPixmap(16, 16) - pixmap.fill() # 填充为白色 - self.default_icon = QIcon(pixmap) - - def _preload_icons(self): - """预加载常用图标""" - print("🔄 开始预加载图标...") - - for icon_name, file_name in self.icon_map.items(): - icon_path = os.path.join(self.icon_directory, file_name) - if os.path.exists(icon_path): - try: - icon = QIcon(icon_path) - self.icon_cache[icon_name] = icon - print(f"✅ 已加载图标: {icon_name} -> {file_name}") - except Exception as e: - print(f"❌ 加载图标失败: {icon_name} -> {file_name}, 错误: {e}") - else: - print(f"⚠️ 图标文件不存在: {icon_path}") - - print(f"📊 预加载完成,共加载 {len(self.icon_cache)} 个图标") - - def get_icon(self, icon_name: str, size: Optional[QSize] = None) -> QIcon: - """ - 获取图标 - - Args: - icon_name: 图标名称(可以是预定义名称或文件名) - size: 图标尺寸 - - Returns: - QIcon对象 - """ - # 首先检查缓存 - if icon_name in self.icon_cache: - icon = self.icon_cache[icon_name] - if size: - # 如果指定了尺寸,返回指定尺寸的图标 - pixmap = icon.pixmap(size) - return QIcon(pixmap) - return icon - - # 如果不在缓存中,尝试从映射中获取 - if icon_name in self.icon_map: - file_name = self.icon_map[icon_name] - icon_path = os.path.join(self.icon_directory, file_name) - else: - # 直接使用文件名 - icon_path = os.path.join(self.icon_directory, icon_name) - if not icon_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')): - icon_path += '.png' # 默认添加.png扩展名 - - # 尝试加载图标 - if os.path.exists(icon_path): - try: - icon = QIcon(icon_path) - # 缓存图标 - self.icon_cache[icon_name] = icon - print(f"✅ 动态加载图标: {icon_name} -> {os.path.basename(icon_path)}") - - if size: - pixmap = icon.pixmap(size) - return QIcon(pixmap) - return icon - except Exception as e: - print(f"❌ 加载图标失败: {icon_path}, 错误: {e}") - else: - print(f"⚠️ 图标文件不存在: {icon_path}") - - # 返回默认图标 - return self.default_icon - - def get_icon_path(self, icon_name: str) -> str: - """ - 获取图标文件的完整路径 - - Args: - icon_name: 图标名称 - - Returns: - 图标文件的完整路径 - """ - if icon_name in self.icon_map: - file_name = self.icon_map[icon_name] - else: - file_name = icon_name - if not file_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')): - file_name += '.png' - - icon_path = os.path.join(self.icon_directory, file_name) - - if os.path.exists(icon_path): - return icon_path - else: - print(f"⚠️ 图标文件不存在: {icon_path}") - return "" - - def has_icon(self, icon_name: str) -> bool: - """ - 检查图标是否存在 - - Args: - icon_name: 图标名称 - - Returns: - 是否存在 - """ - return bool(self.get_icon_path(icon_name)) - - def add_icon(self, icon_name: str, icon_path: str) -> bool: - """ - 添加新图标到缓存 - - Args: - icon_name: 图标名称 - icon_path: 图标文件路径 - - Returns: - 是否添加成功 - """ - try: - if os.path.exists(icon_path): - icon = QIcon(icon_path) - self.icon_cache[icon_name] = icon - print(f"✅ 已添加图标到缓存: {icon_name} -> {icon_path}") - return True - else: - print(f"❌ 图标文件不存在: {icon_path}") - return False - except Exception as e: - print(f"❌ 添加图标失败: {icon_name} -> {icon_path}, 错误: {e}") - return False - - def refresh_cache(self): - """刷新图标缓存""" - print("🔄 刷新图标缓存...") - self.icon_cache.clear() - self._preload_icons() - - def get_available_icons(self) -> list: - """获取所有可用的图标列表""" - available_icons = [] - - # 添加预定义的图标 - available_icons.extend(self.icon_map.keys()) - - # 扫描图标目录中的所有图标文件 - if os.path.exists(self.icon_directory): - for file_name in os.listdir(self.icon_directory): - if file_name.lower().endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')): - icon_name = os.path.splitext(file_name)[0] - if icon_name not in available_icons: - available_icons.append(icon_name) - - return sorted(available_icons) - - def get_cache_info(self) -> dict: - """获取缓存信息""" - return { - 'cached_icons': len(self.icon_cache), - 'icon_directory': self.icon_directory, - 'available_icons': len(self.get_available_icons()), - 'cache_keys': list(self.icon_cache.keys()) - } - - def debug_info(self): - """打印调试信息""" - print("=" * 50) - print("📋 图标管理器调试信息") - print("=" * 50) - - info = self.get_cache_info() - print(f"图标目录: {info['icon_directory']}") - print(f"目录存在: {os.path.exists(info['icon_directory'])}") - print(f"缓存图标数: {info['cached_icons']}") - print(f"可用图标数: {info['available_icons']}") - - if info['cache_keys']: - print("\n已缓存的图标:") - for key in info['cache_keys']: - print(f" - {key}") - - print("\n图标目录内容:") - if os.path.exists(self.icon_directory): - for file_name in os.listdir(self.icon_directory): - file_path = os.path.join(self.icon_directory, file_name) - size = os.path.getsize(file_path) if os.path.isfile(file_path) else 0 - print(f" - {file_name} ({size} bytes)") - else: - print(" 目录不存在") - - print("=" * 50) - - -# 全局图标管理器实例 -_icon_manager = None - - -def get_icon_manager() -> IconManager: - """获取全局图标管理器实例""" - global _icon_manager - if _icon_manager is None: - _icon_manager = IconManager() - return _icon_manager - - -def get_icon(icon_name: str, size: Optional[QSize] = None) -> QIcon: - """便捷函数:获取图标""" - return get_icon_manager().get_icon(icon_name, size) - - -def get_icon_path(icon_name: str) -> str: - """便捷函数:获取图标路径""" - return get_icon_manager().get_icon_path(icon_name) - - -def has_icon(icon_name: str) -> bool: - """便捷函数:检查图标是否存在""" - return get_icon_manager().has_icon(icon_name) - - -if __name__ == "__main__": - # 测试代码 - print("🧪 测试图标管理器...") - - manager = IconManager() - manager.debug_info() - - # 测试获取图标 - logo_icon = manager.get_icon('app_logo') - print(f"\n📱 应用图标是否有效: {not logo_icon.isNull()}") - - move_tool_icon = manager.get_icon('move_tool') - print(f"🔧 移动工具图标是否有效: {not move_tool_icon.isNull()}") \ No newline at end of file +""" +Icon manager compatibility layer for the ImGui-only runtime. + +This module keeps the legacy public API so older imports do not break +immediately. +""" + +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, Optional, Tuple + + +@dataclass(frozen=True) +class IconHandle: + """Lightweight icon placeholder used by legacy call sites.""" + + path: str = "" + + def isNull(self) -> bool: + return not self.path + + def pixmap(self, _size: Optional[Tuple[int, int]] = None) -> "IconHandle": + return self + + +class IconManager: + def __init__(self): + self.icon_cache: Dict[str, IconHandle] = {} + self.icon_directory = self._get_icon_directory() + self.default_icon = IconHandle("") + + self.icon_map = { + "app_logo": "logo.png", + "select_tool": "select_tool.png", + "move_tool": "move_tool.png", + "rotate_tool": "rotate_tool.png", + "scale_tool": "scale_tool.png", + "new_file": "new_file.png", + "open_file": "open_file.png", + "save_file": "save_file.png", + "exit": "exit.png", + "object_3d": "object_3d.png", + "light": "light.png", + "camera": "camera.png", + "terrain": "terrain.png", + "script": "script.png", + "success": "success.png", + "warning": "warning.png", + "error": "error.png", + "info": "info.png", + "up_arrow": "up_arrows.png", + "down_arrow": "down_arrows.png", + "left_arrow": "left_arrows.png", + "right_arrow": "right_arrows.png", + "solid_down_arrows": "solid_down_arrows.png", + "solid_right_arrows": "solid_right_arrows.png", + "minimize_icon": "minimize_icon.png", + "windowing_icon": "windowing_icon.png", + "close_icon": "close_icon.png", + "success_icon": "success_icon.png", + "warning_icon": "warning_icon.png", + "fail_icon": "delete_fail_icon.png", + "property_select_image": "property_select_image.png", + } + + def _get_icon_directory(self) -> str: + current_dir = Path(__file__).resolve().parent + project_root = current_dir.parent + icon_dir = project_root / "icons" + return str(icon_dir) + + def _resolve_icon_path(self, icon_name: str) -> Path: + if icon_name in self.icon_map: + file_name = self.icon_map[icon_name] + else: + file_name = icon_name + if not file_name.lower().endswith((".png", ".jpg", ".jpeg", ".svg", ".ico")): + file_name += ".png" + return Path(self.icon_directory) / file_name + + def get_icon(self, icon_name: str, _size: Optional[Tuple[int, int]] = None) -> IconHandle: + if icon_name in self.icon_cache: + return self.icon_cache[icon_name] + + icon_path = self._resolve_icon_path(icon_name) + if icon_path.exists(): + icon = IconHandle(str(icon_path)) + self.icon_cache[icon_name] = icon + return icon + return self.default_icon + + def get_icon_path(self, icon_name: str) -> str: + icon_path = self._resolve_icon_path(icon_name) + return str(icon_path) if icon_path.exists() else "" + + def has_icon(self, icon_name: str) -> bool: + return bool(self.get_icon_path(icon_name)) + + def add_icon(self, icon_name: str, icon_path: str) -> bool: + path_obj = Path(icon_path) + if not path_obj.exists(): + return False + self.icon_cache[icon_name] = IconHandle(str(path_obj)) + return True + + def refresh_cache(self): + self.icon_cache.clear() + + def get_available_icons(self) -> list: + available_icons = sorted(self.icon_map.keys()) + icon_dir = Path(self.icon_directory) + if icon_dir.exists(): + for p in icon_dir.iterdir(): + if p.suffix.lower() in {".png", ".jpg", ".jpeg", ".svg", ".ico"}: + name = p.stem + if name not in available_icons: + available_icons.append(name) + return sorted(available_icons) + + def get_cache_info(self) -> dict: + return { + "cached_icons": len(self.icon_cache), + "icon_directory": self.icon_directory, + "available_icons": len(self.get_available_icons()), + "cache_keys": list(self.icon_cache.keys()), + } + + def debug_info(self): + info = self.get_cache_info() + print(f"icon_directory: {info['icon_directory']}") + print(f"cached_icons: {info['cached_icons']}") + print(f"available_icons: {info['available_icons']}") + + +_icon_manager: Optional[IconManager] = None + + +def get_icon_manager() -> IconManager: + global _icon_manager + if _icon_manager is None: + _icon_manager = IconManager() + return _icon_manager + + +def get_icon(icon_name: str, size: Optional[Tuple[int, int]] = None) -> IconHandle: + return get_icon_manager().get_icon(icon_name, size) + + +def get_icon_path(icon_name: str) -> str: + return get_icon_manager().get_icon_path(icon_name) + + +def has_icon(icon_name: str) -> bool: + return get_icon_manager().has_icon(icon_name) diff --git a/ui/widgets.py b/ui/widgets.py index c667635b..b6b27401 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -1,4319 +1,61 @@ """ -自定义Qt部件模块 +Legacy widgets module (removed). -包含所有自定义的Qt界面组件: -- NewProjectDialog: 新建项目对话框 -- CustomPanda3DWidget: 自定义Panda3D显示部件 -- CustomFileView: 自定义文件浏览器 -- CustomTreeWidget: 自定义场景树部件 +The editor runtime has migrated to ImGui. This module keeps old export names +as compatibility stubs so accidental imports fail with clear guidance. """ -import os -import re -from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, - QLineEdit, QPushButton, QLabel, - QTreeView, QTreeWidget, QTreeWidgetItem, QWidget, - QFileDialog, QMessageBox, QAbstractItemView, QMenu, QDockWidget, QButtonGroup, QToolButton, QFrame, QSizePolicy) -from PyQt5.QtCore import Qt, QUrl, QMimeData, QPoint, QSize -from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush, QFont -from PyQt5.sip import wrapinstance -from direct.showbase.ShowBaseGlobal import aspect2d -from panda3d.core import ModelRoot, NodePath, CollisionNode -from QMeta3D.QMeta3DWidget import QMeta3DWidget -from scene import util -from ui.icon_manager import get_icon_manager +_REMOVED_MESSAGE = ( + "ui.widgets has been removed: the editor now runs on ImGui only. " + "Use ui.panels/* and ui.lui_manager instead." +) -class NewProjectDialog(QDialog): - """新建项目对话框""" - - def __init__(self, parent=None): - super().__init__(parent) - self.setWindowTitle("新建项目") - self.setObjectName("newProjectDialog") - self.resize(508, 274) - self.setMinimumSize(508, 274) - self.setModal(True) - - # 移除默认窗口装饰,使用自定义顶部栏 - self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) - self.setAttribute(Qt.WA_TranslucentBackground, True) - - # 拖拽和窗口状态 - self.dragging = False - self.drag_position = QPoint() - self.icon_manager = get_icon_manager() - self._title_icon_size = QSize(18, 18) - self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size) - # 设置严格按照Figma设计的样式 - self.setStyleSheet(""" - QDialog#newProjectDialog { - background-color: transparent; - color: #EBEBEB; - border: none; - } - QFrame#baseFrame { - background-color: #000000; - border: 1px solid #3E3E42; - border-radius: 5px; - } - QWidget#titleBar { - background-color: transparent; - border: none; - border-radius: 5px 5px 0px 0px; - min-height: 32px; - max-height: 32px; - } - QWidget#titleBar QWidget { - background-color: transparent; - border: none; - } - QLabel#titleLabel { - color: #FFFFFF; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 14px; - font-weight: 500; - letter-spacing: 0.7px; - } - QWidget#controlButtons QPushButton { - background-color: transparent; - border: none; - color: #EBEBEB; - font-size: 14px; - min-width: 18px; - max-width: 18px; - min-height: 18px; - max-height: 18px; - padding: 0px; - border-radius: 3px; - } - QWidget#controlButtons QPushButton:hover { - background-color: #2A2D2E; - color: #FFFFFF; - } - QPushButton#closeButton { - border-radius: 0px 5px 0px 0px; - } - QPushButton#closeButton:hover { - background-color: #2A2D2E; - color: #FFFFFF; - } - QWidget#contentWidget { - background-color: transparent; - border-radius: 0px 0px 5px 5px; - } - QFrame#contentContainer { - background-color: #19191B; - border: 1px solid #2C2F36; - border-radius: 5px; - } - QLabel[role="sectionTitle"] { - color: #EBEBEB; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 12px; - font-weight: 500; - letter-spacing: 0.6px; - padding: 0px; - margin-bottom: 0px; - } - QLabel[role="hint"] { - color: rgba(235, 235, 235, 0.6); - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 11px; - font-weight: 300; - letter-spacing: 0.55px; - margin-top: 0px; - padding: 0px; - } - QLineEdit { - background-color: rgba(89, 100, 113, 0.2); - color: #EBEBEB; - border: 1px solid rgba(76, 92, 110, 0.6); - border-radius: 2px; - padding: 6px 10px; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 11px; - font-weight: 300; - letter-spacing: 0.55px; - min-height: 14px; - max-height: 30px; - } - QLineEdit:focus { - border: 1px solid #3067C0; - background-color: rgba(48, 103, 192, 0.1); - } - QLineEdit:hover { - border: 1px solid #3067C0; - background-color: rgba(89, 100, 113, 0.3); - } - QLineEdit:disabled { - background-color: rgba(89, 100, 113, 0.1); - color: rgba(235, 235, 235, 0.4); - border: 1px solid rgba(76, 92, 110, 0.2); - } - QPushButton { - background-color: rgba(89, 98, 118, 0.5); - color: #EBEBEB; - border: none; - border-radius: 2px; - padding: 0px 0px; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-weight: 300; - font-size: 10px; - letter-spacing: 0.5px; - min-width: 90px; - min-height: 30px; - max-height: 30px; - } - QPushButton:hover { - background-color: #3067C0; - color: #FFFFFF; - } - QPushButton:pressed { - background-color: #2556A0; - color: #FFFFFF; - } - QPushButton:disabled { - background-color: rgba(89, 98, 118, 0.3); - color: rgba(235, 235, 235, 0.4); - } - QPushButton#primaryButton { - background-color: rgba(89, 98, 118, 0.5); - border: none; - color: #EBEBEB; - font-weight: 300; - min-width: 120px; - max-width: 120px; - } - QPushButton#primaryButton:hover { - background-color: #2556A0; - } - QPushButton#primaryButton:pressed { - background-color: #1E4A8C; - } - QPushButton#secondaryButton { - background-color: rgba(89, 98, 118, 0.5); - border: none; - color: #EBEBEB; - min-width: 120px; - max-width: 120px; - } - QPushButton#secondaryButton:hover { - background-color: #3067C0; - color: #FFFFFF; - } - QPushButton#secondaryButton:pressed { - background-color: #2556A0; - color: #FFFFFF; - } - QPushButton#browseButton { - min-width: 90px; - max-width: 90px; - min-height: 28px; - max-height: 28px; - } - """) +class _RemovedLegacyWidget: + def __init__(self, *args, **kwargs): + raise RuntimeError(_REMOVED_MESSAGE) - # 创建主布局 - main_layout = QVBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(0) - # 创建基础容器,负责绘制圆角和边框 - base_frame = QFrame() - base_frame.setObjectName('baseFrame') - base_frame.setFrameShape(QFrame.NoFrame) - base_frame.setAttribute(Qt.WA_StyledBackground, True) +class NewProjectDialog(_RemovedLegacyWidget): + pass - base_layout = QVBoxLayout(base_frame) - base_layout.setContentsMargins(0, 0, 0, 0) - base_layout.setSpacing(0) - # 创建自定义顶部栏 - self.createTitleBar() - base_layout.addWidget(self.title_bar) +class CustomMeta3DWidget(_RemovedLegacyWidget): + pass - # 创建内容区域 - content_widget = QWidget() - content_widget.setObjectName('contentWidget') - content_layout = QVBoxLayout(content_widget) - content_layout.setContentsMargins(10, 10, 10, 10) - content_layout.setSpacing(0) - content_container = QFrame() - content_container.setObjectName('contentContainer') - content_container.setFrameShape(QFrame.NoFrame) - content_container.setAttribute(Qt.WA_StyledBackground, True) - content_container.setFixedWidth(488) - content_container.setMinimumHeight(213) +class CustomFileView(_RemovedLegacyWidget): + pass - container_layout = QVBoxLayout(content_container) - container_layout.setContentsMargins(15, 10, 15, 10) - container_layout.setSpacing(10) - pathLabel = QLabel('项目路径') - pathLabel.setProperty('role', 'sectionTitle') - container_layout.addWidget(pathLabel) +class CustomAssetsTreeWidget(_RemovedLegacyWidget): + pass - path_row_widget = QWidget() - path_row_layout = QHBoxLayout(path_row_widget) - path_row_layout.setContentsMargins(0, 0, 0, 0) - path_row_layout.setSpacing(10) - path_row_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - self.pathEdit = QLineEdit() - self.pathEdit.setReadOnly(True) - self.pathEdit.setPlaceholderText('请选择项目路径') - self.pathEdit.setMinimumWidth(358) - self.pathEdit.setFixedHeight(30) - path_row_layout.addWidget(self.pathEdit) - browseButton = QPushButton('浏览...') - browseButton.setObjectName('browseButton') - browseButton.clicked.connect(self.browsePath) - path_row_layout.addWidget(browseButton) - container_layout.addWidget(path_row_widget) +class CustomConsoleDockWidget(_RemovedLegacyWidget): + pass - nameLabel = QLabel('项目名称') - nameLabel.setProperty('role', 'sectionTitle') - container_layout.addWidget(nameLabel) - self.nameEdit = QLineEdit() - self.nameEdit.setText('新项目') - self.nameEdit.setPlaceholderText('请输入项目名称') - self.nameEdit.selectAll() - self.nameEdit.setFixedHeight(30) - container_layout.addWidget(self.nameEdit) +class UniversalMessageDialog(_RemovedLegacyWidget): + pass - self.tipLabel = QLabel('项目名称只能包含字母、数字、下划线、中划线和中文') - self.tipLabel.setProperty('role', 'hint') - container_layout.addWidget(self.tipLabel) - separator = QFrame() - separator.setFrameShape(QFrame.HLine) - separator.setFrameShadow(QFrame.Plain) - separator.setFixedHeight(1) - separator.setStyleSheet('background-color: #2C2F36; border: none;') - container_layout.addWidget(separator) +class StyledTextInputDialog(_RemovedLegacyWidget): + pass - button_row_widget = QWidget() - button_row_layout = QHBoxLayout(button_row_widget) - button_row_layout.setContentsMargins(0, 0, 0, 0) - button_row_layout.setSpacing(10) - button_row_layout.addStretch() - button_row_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - - self.cancelButton = QPushButton('取消') - self.cancelButton.setObjectName('secondaryButton') - self.cancelButton.clicked.connect(self.reject) - self.cancelButton.setFixedSize(120, 30) - button_row_layout.addWidget(self.cancelButton) - - self.confirmButton = QPushButton('确认') - self.confirmButton.setObjectName('primaryButton') - self.confirmButton.clicked.connect(self.validate) - self.confirmButton.setFixedSize(120, 30) - button_row_layout.addWidget(self.confirmButton) - - container_layout.addWidget(button_row_widget) - - self.confirmButton.setDefault(True) - self.confirmButton.setAutoDefault(True) - self.cancelButton.setAutoDefault(False) - - content_layout.addWidget(content_container, 0, Qt.AlignTop) - base_layout.addWidget(content_widget) - main_layout.addWidget(base_frame) - - self.projectPath = "" - self.projectName = "" - - def createTitleBar(self): - """创建自定义顶部栏""" - self.title_bar = QFrame() - self.title_bar.setObjectName("titleBar") - - title_layout = QHBoxLayout(self.title_bar) - title_layout.setContentsMargins(12, 0, 12, 0) - title_layout.setSpacing(0) - - # Control buttons area - controls = QWidget() - controls.setObjectName("controlButtons") - controls_layout = QHBoxLayout(controls) - controls_layout.setContentsMargins(0, 0, 0, 0) - controls_layout.setSpacing(0) - - self.close_button = QPushButton() - self.close_button.setObjectName("closeButton") - self.close_button.clicked.connect(self.reject) - self.close_button.setFocusPolicy(Qt.NoFocus) - controls_layout.addWidget(self.close_button) - - self._applyTitleBarIcons() - - # Reserve left space equal to control buttons to keep title centered - left_placeholder = QWidget() - left_placeholder.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - title_layout.addWidget(left_placeholder) - - self.title_label = QLabel(self.windowTitle()) - self.title_label.setObjectName("titleLabel") - self.title_label.setAlignment(Qt.AlignCenter) - title_layout.addWidget(self.title_label, 1) - - title_layout.addWidget(controls) - - left_placeholder.setFixedWidth(controls.sizeHint().width()) - - - def _applyTitleBarIcons(self): - """为标题栏按钮应用图标""" - if self._icon_close: - self.close_button.setIcon(self._icon_close) - self.close_button.setIconSize(self._title_icon_size) - self.close_button.setText("") - self.close_button.setToolTip("关闭") - - - def toggleMaximize(self): - """切换窗口最大化/还原""" - if self.isMaximized(): - self.showNormal() - else: - self.showMaximized() - - - def setWindowTitle(self, title): - super().setWindowTitle(title) - if hasattr(self, "title_label"): - self.title_label.setText(title) - - - def mousePressEvent(self, event): - """鼠标按下事件 - 用于拖拽窗口""" - if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): - if self.isMaximized(): - self.showNormal() - self.dragging = True - self.drag_position = event.globalPos() - self.frameGeometry().topLeft() - event.accept() - super().mousePressEvent(event) - - def mouseDoubleClickEvent(self, event): - """双击标题栏切换最大化""" - if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): - self.toggleMaximize() - event.accept() - return - super().mouseDoubleClickEvent(event) - - - def mouseMoveEvent(self, event): - """鼠标移动事件 - 用于拖拽窗口""" - if event.buttons() == Qt.LeftButton and self.dragging: - self.move(event.globalPos() - self.drag_position) - event.accept() - super().mouseMoveEvent(event) - - def mouseReleaseEvent(self, event): - """鼠标释放事件 - 停止拖拽""" - if event.button() == Qt.LeftButton: - self.dragging = False - super().mouseReleaseEvent(event) - - def browsePath(self): - """浏览选择项目路径""" - path = QFileDialog.getExistingDirectory(self, "选择项目路径") - if path: - self.pathEdit.setText(path) - - def validate(self): - """验证输入并关闭对话框""" - # 获取并验证路径 - self.projectPath = self.pathEdit.text() - if not self.projectPath: - UniversalMessageDialog.show_warning(self, "错误", "请选择项目路径!", show_cancel=False, confirm_text="确认") - return - - # 获取并验证项目名称 - self.projectName = self.nameEdit.text().strip() - if not self.projectName: - UniversalMessageDialog.show_warning(self, "错误", "请输入项目名称!", show_cancel=False, confirm_text="确认") - return - - # 验证项目名称格式 - if not re.match(r'^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$', self.projectName): - UniversalMessageDialog.show_warning(self, "错误", - "项目名称只能包含字母、数字、下划线、中划线和中文!", show_cancel=False, confirm_text="确认") - return - - # 检查项目是否已存在 - full_path = os.path.join(self.projectPath, self.projectName) - if os.path.exists(full_path): - UniversalMessageDialog.show_warning(self, "错误", "项目已存在!", show_cancel=False, confirm_text="确认") - return - - self.accept() - -class CustomMeta3DWidget(QMeta3DWidget): - """自定义Panda3D显示部件""" - - def __init__(self, world, parent=None): - if parent is None: - parent = wrapinstance(0, QWidget) - super().__init__(world, parent) - self.world = world - self.setFocusPolicy(Qt.StrongFocus) # 允许接收键盘焦点 - self.setAcceptDrops(True) # 允许接收拖放 - self.setMouseTracking(True) # 启用鼠标追踪 - - # 让world引用这个widget,以便获取准确的尺寸 - if hasattr(world, 'setQtWidget'): - world.setQtWidget(self) - - def getActualSize(self): - """获取Qt部件的实际渲染尺寸""" - return (self.width(), self.height()) - - def dragEnterEvent(self, event): - """处理拖拽进入事件""" - # 检查是否是文件拖拽 - if event.mimeData().hasUrls(): - # 检查是否包含支持的模型文件 - for url in event.mimeData().urls(): - filepath = url.toLocalFile() - if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - event.acceptProposedAction() - return - event.ignore() - - def dragMoveEvent(self, event): - """处理拖拽移动事件""" - if event.mimeData().hasUrls(): - event.acceptProposedAction() - else: - event.ignore() - - def dropEvent(self, event): - """处理拖放事件""" - if event.mimeData().hasUrls(): - for url in event.mimeData().urls(): - filepath = url.toLocalFile() - if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - # 使用关键字参数确保兼容性 - self.world.importModel(filepath) - event.acceptProposedAction() - else: - event.ignore() - - def wheelEvent(self, event): - """处理滚轮事件""" - if event.angleDelta().y() > 0: - # 滚轮向前滚动 - self.world.wheelForward() - else: - # 滚轮向后滚动 - self.world.wheelBackward() - event.accept() - - def mousePressEvent(self, event): - """处理 Qt 鼠标按下事件""" - if event.button() == Qt.LeftButton: - self.world.mousePressEventLeft({ - 'x': event.x(), - 'y': event.y() - }) - elif event.button() == Qt.RightButton: - self.world.mousePressEventRight({ - 'x': event.x(), - 'y': event.y() - }) - elif event.button() == Qt.MiddleButton: # 添加滑轮按下事件处理 - self.world.mousePressEventMiddle({ - 'x': event.x(), - 'y': event.y() - }) - event.accept() - - def mouseReleaseEvent(self, event): - """处理 Qt 鼠标释放事件""" - if event.button() == Qt.LeftButton: - self.world.mouseReleaseEventLeft({ - 'x': event.x(), - 'y': event.y() - }) - elif event.button() == Qt.RightButton: - self.world.mouseReleaseEventRight({ - 'x': event.x(), - 'y': event.y() - }) - elif event.button() == Qt.MiddleButton: # 添加滑轮释放事件处理 - self.world.mouseReleaseEventMiddle({ - 'x': event.x(), - 'y': event.y() - }) - event.accept() - - def mouseMoveEvent(self, event): - """处理 Qt 鼠标移动事件""" - self.world.mouseMoveEvent({ - 'x': event.x(), - 'y': event.y() - }) - event.accept() - - def cleanup(self): - """清理Panda3D资源""" - try: - print("🧹 清理CustomPanda3DWidget资源...") - - # 清理world资源 - if hasattr(self, 'world') and self.world: - # 如果world有cleanup方法,调用它 - if hasattr(self.world, 'cleanup'): - self.world.cleanup() - # 清理world引用 - self.world = None - - # 调用父类的清理方法(如果存在) - if hasattr(super(), 'cleanup'): - super().cleanup() - - print("✅ CustomPanda3DWidget资源清理完成") - - except Exception as e: - print(f"⚠️ 清理CustomPanda3DWidget资源时出错: {e}") - -class CustomFileView(QTreeView): - """自定义文件浏览器""" - - def __init__(self, world, parent=None): - if parent is None: - parent = wrapinstance(0, QWidget) - super().__init__(parent) - base_font = self.font() - base_font.setFamily("Inter") - base_font.setPointSize(10) - base_font.setWeight(QFont.Normal) - self.setFont(base_font) - if self.model(): - self.model().rowsInserted.connect(self._handle_rows_inserted) - self.world = world - self.setupUI() - self.setupDragDrop() - - def setupUI(self): - """初始化UI设置""" - self.setHeaderHidden(True) - # 启用多选和拖拽 - self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) - self.setDropIndicatorShown(True) # 启用拖放指示线 - - def setupDragDrop(self): - """设置拖拽功能""" - # 使用自定义拖拽模式 - self.setDragDropMode(QTreeView.DragOnly) # 只允许拖出,不允许拖入 - self.setDefaultDropAction(Qt.DropAction.MoveAction) - self.setDragEnabled(True) - self.setAcceptDrops(True) - - def startDrag(self, supportedActions): - """开始拖拽操作""" - # 获取选中的文件 - indexes = self.selectedIndexes() - if not indexes: - return - - # 只处理文件名列 - indexes = [idx for idx in indexes if idx.column() == 0] - - # 创建 MIME 数据 - mimeData = self.model().mimeData(indexes) - - # 检查是否包含支持的模型文件 - urls = [] - for index in indexes: - filepath = self.model().filePath(index) - if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - urls.append(QUrl.fromLocalFile(filepath)) - - if not urls: - return - - # 设置 URL 列表 - mimeData.setUrls(urls) - - # 创建拖拽对象 - drag = QDrag(self) - drag.setMimeData(mimeData) - - # 设置拖拽图标(可选) - pixmap = QPixmap(32, 32) - pixmap.fill(Qt.transparent) - painter = QPainter(pixmap) - painter.drawText(pixmap.rect(), Qt.AlignCenter, str(len(urls))) - painter.end() - drag.setPixmap(pixmap) - - # 执行拖拽 - drag.exec_(supportedActions) - - def mouseDoubleClickEvent(self, event): - """双击标题栏切换最大化""" - index = self.indexAt(event.pos()) - if index.isValid(): - model = self.model() - filepath = model.filePath(index) - - # 检查是否是模型文件 - if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - self.world.importModel(filepath) - else: - print("不支持的文件类型") - super().mouseDoubleClickEvent(event) - -from PyQt5.QtCore import QFileSystemWatcher,QTimer -import os - -class CustomAssetsTreeWidget(QTreeWidget): - def __init__(self, world, parent=None): - if parent is None: - parent = wrapinstance(0, QWidget) - super().__init__(parent) - self.world = world - self.root_path = None - self.setupUI() - self.setupDragDrop() - - #添加文件系统监控器 - self.file_watcher = QFileSystemWatcher() - self.file_watcher.directoryChanged.connect(self.onDirectoryChanged) - self.file_watcher.fileChanged.connect(self.onFileChanged) - - self.refresh_timer = QTimer() - self.refresh_timer.setSingleShot(True) - self.refresh_timer.timeout.connect(self.refreshView) - - #存储监控的目录 - self.watched_directories = set() - - # 默认加载项目根路径 - self.load_file_tree() - # 设置右键菜单 - self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self.customContextMenuRequested.connect(self.showContextMenu) - - def showContextMenu(self, position): - """显示右键菜单""" - item = self.itemAt(position) - if not item: - return - - filepath = item.data(0, Qt.UserRole) - is_folder = item.data(0, Qt.UserRole + 1) - - menu = QMenu(self) - - if is_folder: - # 文件夹右键菜单 - menu.addAction("📁 新建文件夹", lambda: self.createNewFolder(item)) - menu.addAction("📄 新建文件", lambda: self.createNewFile(item)) - menu.addSeparator() - menu.addAction("✏️ 重命名", lambda: self.renameItem(item)) - menu.addAction("🗑️ 删除", lambda: self.deleteItem(item)) - menu.addSeparator() - menu.addAction("📋 复制路径", lambda: self.copyPath(filepath)) - menu.addAction("🔍 查看属性", lambda: self.showProperties(item)) - else: - # 文件右键菜单 - menu.addAction("📂 打开文件", lambda: self.openFile(filepath)) - menu.addSeparator() - menu.addAction("✏️ 重命名", lambda: self.renameItem(item)) - menu.addAction("🗑️ 删除", lambda: self.deleteItem(item)) - menu.addSeparator() - menu.addAction("📋 复制路径", lambda: self.copyPath(filepath)) - menu.addAction("🔍 查看属性", lambda: self.showProperties(item)) - - # 显示菜单 - menu.exec_(self.mapToGlobal(position)) - - def _saveExpandedState(self): - """保存展开状态""" - expanded_paths = set() - - def collectExpanded(item): - if item.isExpanded(): - path = item.data(0, Qt.UserRole) - if path: - expanded_paths.add(path) - - for i in range(item.childCount()): - collectExpanded(item.child(i)) - - # 遍历所有顶级项目 - for i in range(self.topLevelItemCount()): - collectExpanded(self.topLevelItem(i)) - - return expanded_paths - - def _restoreExpandedState(self, expanded_paths): - """恢复展开状态""" - def restoreExpanded(item): - path = item.data(0, Qt.UserRole) - if path in expanded_paths: - item.setExpanded(True) - - for i in range(item.childCount()): - restoreExpanded(item.child(i)) - - # 遍历所有顶级项目 - for i in range(self.topLevelItemCount()): - restoreExpanded(self.topLevelItem(i)) - - def _refreshWithStatePreservation(self): - """刷新树形视图并保持状态""" - # 保存当前状态 - expanded_paths = self._saveExpandedState() - - # 刷新树形结构 - self.load_file_tree() - - # 恢复展开状态 - self._restoreExpandedState(expanded_paths) - - def createNewFolder(self, parent_item): - """新建文件夹""" - import os - - parent_path = parent_item.data(0, Qt.UserRole) - dialog = StyledTextInputDialog(self, "新建文件夹", "文件夹名称", "请输入文件夹名称") - if dialog.exec_() != QDialog.Accepted: - return - - folder_name = dialog.text() - if not folder_name: - return - - new_folder_path = os.path.join(parent_path, folder_name) - try: - os.makedirs(new_folder_path, exist_ok=True) - self._refreshWithStatePreservation() - print(f"新建文件夹: {new_folder_path}") - except OSError as e: - print(f"新建文件夹失败: {e}") - - - def createNewFile(self, parent_item): - """新建文件""" - import os - - parent_path = parent_item.data(0, Qt.UserRole) - dialog = StyledTextInputDialog(self, "新建文件", "文件名称", "请输入文件名称") - if dialog.exec_() != QDialog.Accepted: - return - - file_name = dialog.text() - if not file_name: - return - - new_file_path = os.path.join(parent_path, file_name) - try: - with open(new_file_path, "w", encoding="utf-8") as f: - f.write("") - self._refreshWithStatePreservation() - print(f"新建文件: {new_file_path}") - except OSError as e: - print(f"新建文件失败: {e}") - - - def renameItem(self, item): - """重命名文件或文件夹""" - import os - - old_path = item.data(0, Qt.UserRole) - old_name = os.path.basename(old_path) - - dialog = StyledTextInputDialog(self, "重命名", "新名称", "请输入新名称", old_name) - if dialog.exec_() != QDialog.Accepted: - return - - new_name = dialog.text() - if not new_name or new_name == old_name: - return - - parent_dir = os.path.dirname(old_path) - new_path = os.path.join(parent_dir, new_name) - - try: - os.rename(old_path, new_path) - item.setText(0, new_name) - item.setData(0, Qt.UserRole, new_path) - self._refreshWithStatePreservation() - except OSError as e: - print(f"重命名失败: {e}") - - - def deleteItem(self, item): - """删除文件或文件夹""" - import os - import shutil - - filepath = item.data(0, Qt.UserRole) - is_folder = item.data(0, Qt.UserRole + 1) - - item_type = "文件夹" if is_folder else "文件" - result = UniversalMessageDialog.show_warning( - self, - "确认删除", - f"确定要删除这个{item_type}吗?\n{filepath}", - show_cancel=True, - confirm_text="删除", - cancel_text="取消" - ) - - if result == QDialog.Accepted: - try: - if is_folder: - shutil.rmtree(filepath) - else: - os.remove(filepath) - self._refreshWithStatePreservation() - print(f"删除{item_type}: {filepath}") - except OSError as e: - print(f"删除{item_type}失败: {e}") - UniversalMessageDialog.show_error( - self, - "错误", - f"删除{item_type}失败: {e}", - show_cancel=False, - confirm_text="确认" - ) - - def copyPath(self, filepath): - """复制路径到剪贴板""" - from PyQt5.QtWidgets import QApplication - - clipboard = QApplication.clipboard() - clipboard.setText(filepath) - print(f"已复制路径: {filepath}") - - def openFile(self, filepath): - """打开文件""" - import os - import subprocess - import platform - - try: - system = platform.system() - if system == "Windows": - os.startfile(filepath) - elif system == "Darwin": # macOS - subprocess.run(["open", filepath]) - else: # Linux - subprocess.run(["xdg-open", filepath]) - print(f"打开文件: {filepath}") - except Exception as e: - print(f"打开文件失败: {e}") - - def showProperties(self, item): - """显示属性面板""" - import os - - filepath = item.data(0, Qt.UserRole) - is_folder = item.data(0, Qt.UserRole + 1) - - try: - stat = os.stat(filepath) - size = stat.st_size - modified = os.path.getmtime(filepath) - - import datetime - modified_str = datetime.datetime.fromtimestamp(modified).strftime('%Y-%m-%d %H:%M:%S') - - item_type = "文件夹" if is_folder else "文件" - size_str = f"{size} 字节" if not is_folder else "文件夹" - - properties = f""" - 路径: {filepath} - 类型: {item_type} - 大小: {size_str} - 修改时间: {modified_str} - """ - - UniversalMessageDialog.show_info( - self, - "属性", - properties.strip(), - show_cancel=False, - confirm_text="确认" - ) - - except OSError as e: - UniversalMessageDialog.show_warning( - self, - "错误", - f"无法获取属性: {e}", - show_cancel=False, - confirm_text="确认" - ) - - - # def mouseDoubleClickEvent(self, event): - # """处理双击事件""" - # item = self.itemAt(event.pos()) - # if item: - # filepath = item.data(0, Qt.UserRole) - # is_folder = item.data(0, Qt.UserRole + 1) - # - # if is_folder: - # # 文件夹:展开/折叠 - # item.setExpanded(not item.isExpanded()) - # else: - # # 文件:检查是否是模型文件 - # if filepath and filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - # self.world.importModel(filepath) - # else: - # # 其他文件:用系统默认程序打开 - # self.openFile(filepath) - # - # super().mouseDoubleClickEvent(event) - - def setupUI(self): - """初始化UI设置""" - self.setHeaderHidden(True) - # 启用多选和拖拽 - self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) - self.setDropIndicatorShown(True) # 启用拖放指示线 - - def setupDragDrop(self): - """设置拖拽功能""" - # 使用InternalMove模式以正确显示插入指示线 - self.setDragDropMode(QAbstractItemView.InternalMove) - self.setDefaultDropAction(Qt.DropAction.MoveAction) - self.setDragEnabled(True) - self.setAcceptDrops(True) - self.setDropIndicatorShown(True) - - def getProjectRootPath(self): - """获取项目根路径下的Resources文件夹,考虑跨平台""" - import os - - # 获取当前文件所在目录,然后向上查找项目根目录 - current_dir = os.path.dirname(os.path.abspath(__file__)) - - # 向上查找直到找到项目根目录(包含特定标识文件或文件夹) - project_root = current_dir - max_depth = 10 # 限制向上查找的深度 - depth = 0 - - while depth < max_depth: - # 检查是否是项目根目录(可以根据实际情况调整判断条件) - if (os.path.exists(os.path.join(project_root, "main.py")) or - os.path.exists(os.path.join(project_root, "setup.py")) or - os.path.exists(os.path.join(project_root, ".git"))): - break - parent_dir = os.path.dirname(project_root) - if parent_dir == project_root: # 已经到达文件系统根目录 - # 回退到使用当前工作目录 - project_root = os.getcwd() - break - project_root = parent_dir - depth += 1 - - # 构建Resources文件夹路径(跨平台) - resources_path = os.path.join(project_root, "Resources") - - # 如果Resources文件夹不存在,创建它 - if not os.path.exists(resources_path): - try: - os.makedirs(resources_path, exist_ok=True) - print(f"创建Resources文件夹: {resources_path}") - except OSError as e: - print(f"无法创建Resources文件夹: {e}") - # 如果无法创建,回退到项目根路径 - return project_root - - return resources_path - - # def getProjectRootPath(self): - # """获取项目根路径下的Resources文件夹,考虑跨平台""" - # import os - # - # # 获取项目根路径 - # project_root = os.getcwd() - # - # # 构建Resources文件夹路径(跨平台) - # resources_path = os.path.join(project_root, "Resources") - # - # # 如果Resources文件夹不存在,创建它 - # if not os.path.exists(resources_path): - # try: - # os.makedirs(resources_path, exist_ok=True) - # print(f"创建Resources文件夹: {resources_path}") - # except OSError as e: - # print(f"无法创建Resources文件夹: {e}") - # # 如果无法创建,回退到项目根路径 - # return project_root - # - # return resources_path - - def load_file_tree(self): - """加载树形视图""" - self.clear() - self.current_path = self.getProjectRootPath() - try: - # 创建当前目录的根节点 - root_name = os.path.basename(self.current_path) or self.current_path - root_item = QTreeWidgetItem([f"📁 {root_name}"]) - root_item.setData(0, Qt.UserRole, self.current_path) - root_item.setData(0, Qt.UserRole + 1, True) - font = QFont("Microsoft YaHei", 10, QFont.Light) # 假设您希望字体大小为10 - root_item.setFont(0, font) - self.addTopLevelItem(root_item) - - # 加载当前目录内容 - self.load_directory_tree(self.current_path, root_item) - - #添加目录到监控器 - self.addWatchedDirectory(self.current_path) - - # 展开根节点 - root_item.setExpanded(True) - - except PermissionError: - error_item = QTreeWidgetItem(["❌ 无权限访问此目录"]) - self.addTopLevelItem(error_item) - - def addWatchedDirectory(self,directory): - """添加监控目录""" - if os.path.exists(directory) and directory not in self.watched_directories: - if self.file_watcher.addPath(directory): - self.watched_directories.add(directory) - #print(f"开始监控目录:{directory}") - try: - for item in os.listdir(directory): - item_path = os.path.join(directory,item) - if os.path.isdir(item_path): - self.addWatchedDirectory(item_path) - except Exception as e: - pass - else: - print(f"无法监控目录:{directory}") - - def removeWatchedDirectory(self,directory): - """移除监控目录""" - if directory in self.watched_directories: - if self.file_watcher.removePath(directory): - self.watched_directories.discard(directory) - print(f"停止监控目录:{directory}") - else: - print(f"无法停止监控目录:{directory}") - - def onDirectoryChanged(self,path): - """目录发生变化时处理""" - print(f"目录变化{path}") - if not self.refresh_timer.isActive(): - self.refresh_timer.start(1000) - - def onFileChanged(self,path): - """目录发生变化时的处理""" - print(f"目录变化{path}") - if not self.refresh_timer.isActive(): - self.refresh_timer.start(1000) - - def refreshView(self): - """刷新视图""" - print("刷新资源视图...") - try: - expanded_paths = self._saveExpandedState() - self.load_file_tree() - self._restoreExpandedState(expanded_paths) - print("资源视图刷新完成") - except Exception as e: - print(f"刷新资源视图失败{e}") - - def load_directory_tree(self, path, parent_item, max_depth=3, current_depth=0): - """递归加载目录树(类似左侧导航面板)""" - if current_depth >= max_depth: - return - - try: - items = os.listdir(path) - items.sort() - - # 分别处理文件夹和文件 - folders = [] - files = [] - - for item in items: - # 跳过隐藏文件和系统文件 - if item.startswith('.') or item.startswith('__'): - continue - - item_path = os.path.join(path, item) - if os.path.isdir(item_path): - folders.append(item) - elif os.path.isfile(item_path): - files.append(item) - - # 先添加文件夹 - for folder in folders: - folder_path = os.path.join(path, folder) - folder_item = self.create_simple_tree_item(folder, folder_path, True) - parent_item.addChild(folder_item) - - # 递归加载子目录(限制深度) - if current_depth < max_depth - 1: - self.load_directory_tree(folder_path, folder_item, max_depth, current_depth + 1) - - # 再添加文件(显示重要文件类型,包括图片和模型) - important_extensions = { - # 编程文件 - '.py', '.js', '.html', '.css', '.java', '.cpp', '.c', '.php', '.rb', '.go', '.rs', - # 文档文件 - '.md', '.txt', '.pdf', '.doc', '.docx', '.rtf', - # 配置和数据文件 - '.json', '.xml', '.yaml', '.yml', '.ini', '.cfg', '.toml', - # 图片文件 - '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.ico', '.webp', '.tiff', '.tif', - # 3D模型文件 - '.fbx', '.obj', '.3ds', '.max', '.blend', '.dae', '.gltf', '.glb', '.stl', '.ply', - # 音视频文件 - '.mp3', '.wav', '.mp4', '.avi', '.mov', '.wmv', '.flac', '.aac', - # 压缩文件 - '.zip', '.rar', '.7z', '.tar', '.gz', - # 材质和纹理 - '.mtl', '.mat', '.exr', '.hdr', '.hdri', '.dds', - # CAD文件 - '.dwg', '.dxf', '.step', '.stp', '.iges', '.sldprt', '.sldasm', - # 其他重要文件 - '.exe', '.dll', '.bat', '.sh', '.log' - } - - # 重要文件名(不依赖扩展名) - important_files = {'requirements.txt', 'README.md', 'main.py', 'LICENSE', 'CHANGELOG.md', 'INSTALL.md'} - - for file in files: - file_ext = os.path.splitext(file)[1].lower() - if file_ext in important_extensions or file in important_files: - file_path = os.path.join(path, file) - file_item = self.create_simple_tree_item(file, file_path, False) - parent_item.addChild(file_item) - - except (OSError, PermissionError): - pass - - def create_simple_tree_item(self, name, path, is_folder): - """创建简单的树形项目""" - try: - if is_folder: - # 文件夹项目,展开/折叠图标由CSS样式控制 - item = QTreeWidgetItem([f"📁 {name}"]) - item.setData(0, Qt.UserRole + 1, True) # 标记为文件夹 - else: - icon = self.get_file_icon(name) - item = QTreeWidgetItem([f"{icon} {name}"]) - item.setData(0, Qt.UserRole + 1, False) # 标记为文件 - - item.setData(0, Qt.UserRole, path) - - return item - - except (OSError, PermissionError): - item = QTreeWidgetItem([name]) - item.setData(0, Qt.UserRole, path) - return item - - def get_file_icon(self, filename): - """ - 根据文件扩展名获取图标 - - 这个函数根据文件的扩展名返回对应的Unicode图标字符。 - 如果文件类型不在映射表中,返回默认的文档图标。 - - 参数: - filename (str): 文件名(包含扩展名) - - 返回值: - str: 对应的Unicode图标字符 - """ - # 获取文件扩展名并转换为小写 - ext = os.path.splitext(filename)[1].lower() - - # 文件扩展名到图标的映射表 - icon_map = { - # 编程语言文件 - '.py': '🐍', # Python文件 - '.js': '⚡', # JavaScript文件 - '.html': '🌐', # HTML文件 - '.css': '🎨', # CSS样式文件 - '.java': '☕', # Java文件 - '.cpp': '⚙️', # C++文件 - '.c': '⚙️', # C文件 - '.h': '📋', # 头文件 - '.php': '🐘', # PHP文件 - '.rb': '💎', # Ruby文件 - '.go': '🐹', # Go文件 - '.rs': '🦀', # Rust文件 - '.swift': '🐦', # Swift文件 - '.kt': '🎯', # Kotlin文件 - - # 文档文件 - '.txt': '📄', # 纯文本文件 - '.md': '📝', # Markdown文档 - '.rst': '📝', # reStructuredText文档 - '.pdf': '📕', # PDF文档 - '.doc': '📘', # Word文档 - '.docx': '📘', # Word文档 - '.rtf': '📄', # RTF文档 - '.odt': '📄', # OpenDocument文本 - - # 数据文件 - '.json': '📋', # JSON数据文件 - '.xml': '📋', # XML文件 - '.yaml': '📋', # YAML文件 - '.yml': '📋', # YAML文件 - '.csv': '📊', # CSV表格文件 - '.xls': '📗', # Excel表格 - '.xlsx': '📗', # Excel表格 - '.ods': '📊', # OpenDocument表格 - - # 图像文件 - '.jpg': '🖼️', # JPEG图像 - '.jpeg': '🖼️', # JPEG图像 - '.png': '🖼️', # PNG图像 - '.gif': '🖼️', # GIF图像 - '.bmp': '🖼️', # BMP图像 - '.svg': '🎨', # SVG矢量图 - '.ico': '🎯', # 图标文件 - '.webp': '🖼️', # WebP图像 - '.tiff': '🖼️', # TIFF图像 - '.tif': '🖼️', # TIFF图像 - - # 音视频文件 - '.mp4': '🎬', # MP4视频 - '.avi': '🎬', # AVI视频 - '.mov': '🎬', # MOV视频 - '.wmv': '🎬', # WMV视频 - '.flv': '🎬', # FLV视频 - '.webm': '🎬', # WebM视频 - '.mp3': '🎵', # MP3音频 - '.wav': '🎵', # WAV音频 - '.flac': '🎵', # FLAC音频 - '.aac': '🎵', # AAC音频 - '.ogg': '🎵', # OGG音频 - - # 压缩文件 - '.zip': '📦', # ZIP压缩包 - '.rar': '📦', # RAR压缩包 - '.7z': '📦', # 7Z压缩包 - '.tar': '📦', # TAR归档 - '.gz': '📦', # GZIP压缩 - '.bz2': '📦', # BZIP2压缩 - '.xz': '📦', # XZ压缩 - - # 可执行文件 - '.exe': '⚙️', # Windows可执行文件 - '.msi': '📦', # Windows安装包 - '.deb': '📦', # Debian包 - '.rpm': '📦', # RPM包 - '.dmg': '💿', # macOS磁盘镜像 - '.app': '📱', # macOS应用程序 - - # 系统文件 - '.dll': '🔧', # 动态链接库 - '.so': '🔧', # 共享库 - '.dylib': '🔧', # macOS动态库 - '.lib': '📚', # 静态库 - - # 脚本文件 - '.bat': '📜', # Windows批处理 - '.cmd': '📜', # Windows命令脚本 - '.sh': '📜', # Shell脚本 - '.ps1': '💙', # PowerShell脚本 - '.vbs': '📜', # VBScript脚本 - - # 配置文件 - '.ini': '⚙️', # INI配置文件 - '.cfg': '⚙️', # 配置文件 - '.conf': '⚙️', # 配置文件 - '.config': '⚙️', # 配置文件 - '.toml': '⚙️', # TOML配置文件 - - # 3D模型文件 - '.fbx': '🎭', # FBX模型文件 - '.obj': '🎭', # OBJ模型文件 - '.3ds': '🎭', # 3DS Max模型 - '.max': '🎭', # 3DS Max场景 - '.blend': '🎭', # Blender模型 - '.dae': '🎭', # COLLADA模型 - '.gltf': '🎭', # glTF模型 - '.glb': '🎭', # glTF二进制模型 - '.x3d': '🎭', # X3D模型 - '.ply': '🎭', # PLY模型 - '.stl': '🎭', # STL模型(3D打印) - '.off': '🎭', # OFF模型 - '.3mf': '🎭', # 3MF模型 - '.amf': '🎭', # AMF模型 - '.x': '🎭', # DirectX模型 - '.md2': '🎭', # Quake II模型 - '.md3': '🎭', # Quake III模型 - '.mdl': '🎭', # Source引擎模型 - '.mesh': '🎭', # OGRE模型 - '.scene': '🎭', # OGRE场景 - '.ac': '🎭', # AC3D模型 - '.ase': '🎭', # ASCII Scene Export - '.assbin': '🎭', # Assimp二进制 - '.b3d': '🎭', # Blitz3D模型 - '.bvh': '🎭', # BioVision层次 - '.csm': '🎭', # CharacterStudio Motion - '.hmp': '🎭', # 3D GameStudio模型 - '.irr': '🎭', # Irrlicht场景 - '.irrmesh': '🎭', # Irrlicht网格 - '.lwo': '🎭', # LightWave对象 - '.lws': '🎭', # LightWave场景 - '.ms3d': '🎭', # MilkShape 3D - '.nff': '🎭', # Neutral文件格式 - '.q3o': '🎭', # Quick3D对象 - '.q3s': '🎭', # Quick3D场景 - '.raw': '🎭', # RAW三角形 - '.smd': '🎭', # Valve SMD - '.ter': '🎭', # Terragen地形 - '.uc': '🎭', # Unreal模型 - '.vta': '🎭', # Valve VTA - '.xgl': '🎭', # XGL模型 - '.zgl': '🎭', # ZGL模型 - - # 纹理和材质文件 - '.mtl': '🎨', # OBJ材质文件 - '.mat': '🎨', # 材质文件 - '.sbsar': '🎨', # Substance Archive - '.sbs': '🎨', # Substance Designer - '.sbsm': '🎨', # Substance材质 - '.exr': '🖼️', # OpenEXR高动态范围图像 - '.hdr': '🖼️', # HDR图像 - '.hdri': '🖼️', # HDRI环境贴图 - '.dds': '🖼️', # DirectDraw Surface - '.ktx': '🖼️', # Khronos纹理 - '.astc': '🖼️', # ASTC纹理 - '.pvr': '🖼️', # PowerVR纹理 - '.etc1': '🖼️', # ETC1纹理 - '.etc2': '🖼️', # ETC2纹理 - - # 动画文件 - '.anim': '🎬', # 动画文件 - '.fbx': '🎬', # FBX动画(也可以是模型) - '.bip': '🎬', # Character Studio Biped - '.cal3d': '🎬', # Cal3D动画 - '.motion': '🎬', # 动作文件 - '.mocap': '🎬', # 动作捕捉数据 - - # 其他常见文件 - '.log': '📋', # 日志文件 - '.tmp': '🗂️', # 临时文件 - '.bak': '💾', # 备份文件 - '.old': '📦', # 旧文件 - } - - # 返回对应的图标,如果找不到则返回默认文档图标 - return icon_map.get(ext, '📄') - - def startDrag(self, supportedActions): - """开始拖拽操作""" - selected_items = self.selectedItems() - if not selected_items: - return - - # 创建 MIME 数据 - mimeData = QMimeData() - - # 收集文件路径用于向外拖拽 - urls = [] - internal_paths = [] - - for item in selected_items: - filepath = item.data(0, Qt.UserRole) - if filepath: - internal_paths.append(filepath) - # 检查是否是模型文件(用于向外拖拽) - if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - urls.append(QUrl.fromLocalFile(filepath)) - - # 设置内部拖拽数据 - mimeData.setText('\n'.join(internal_paths)) - - # 设置向外拖拽数据 - if urls: - mimeData.setUrls(urls) - - # 创建拖拽对象 - drag = QDrag(self) - drag.setMimeData(mimeData) - - # 设置拖拽图标 - pixmap = QPixmap(32, 32) - pixmap.fill(Qt.transparent) - painter = QPainter(pixmap) - painter.drawText(pixmap.rect(), Qt.AlignCenter, str(len(selected_items))) - painter.end() - drag.setPixmap(pixmap) - - # 执行拖拽 - drag.exec_(supportedActions) - - def dragEnterEvent(self, event): - """处理拖拽进入事件""" - # 检查是否是内部拖拽 - if event.mimeData().hasText(): - event.acceptProposedAction() - else: - event.ignore() - - def dragMoveEvent(self, event): - """处理拖拽移动事件""" - if not event.mimeData().hasText(): - event.ignore() - return - - # 获取目标项 - target_item = self.itemAt(event.pos()) - selected_items = self.selectedItems() - - # 检查是否拖拽到多选区域内的项目 - if target_item and target_item in selected_items: - event.ignore() - return - - # 检查是否拖拽到自己的子级 - if target_item: - for selected_item in selected_items: - if self._isChildOf(target_item, selected_item): - event.ignore() - return - - # 如果拖到文件夹,允许拖到文件夹内部 - if target_item: - is_folder = target_item.data(0, Qt.UserRole + 1) - if is_folder: - # 接受拖放,显示指示框 - event.acceptProposedAction() - return - - - # 调用父类方法处理插入指示线 - event.accept() - super().dragMoveEvent(event) - - def _isChildOf(self, potential_child, potential_parent): - """检查 potential_child 是否是 potential_parent 的子级""" - current = potential_child.parent() - while current: - if current == potential_parent: - return True - current = current.parent() - return False - - def dropEvent(self, event): - """处理拖放事件""" - if not event.mimeData().hasText(): - event.ignore() - return - - drag_paths = event.mimeData().text().split('\n') - if not drag_paths: - event.ignore() - return - - # 获取目标项 - target_item = self.itemAt(event.pos()) - if not target_item: - # 如果拖到空白处,默认放到根目录 - target_path = self.current_path - else: - # 如果是文件夹,就放到里面 - is_folder = target_item.data(0, Qt.UserRole + 1) - if is_folder: - target_path = target_item.data(0, Qt.UserRole) - else: - # 如果是文件,则放到其父目录 - parent_item = target_item.parent() - if parent_item: - target_path = parent_item.data(0, Qt.UserRole) - else: - target_path = self.current_path - - # 执行移动 - self._moveFiles(drag_paths, target_path) - event.acceptProposedAction() - # 让 Qt 更新界面 - super().dropEvent(event) - - def _moveFiles(self, source_paths, target_dir): - """移动文件到目标目录""" - import os - import shutil - from PyQt5.QtWidgets import QMessageBox - - moved_files = [] - failed_files = [] - - for source_path in source_paths: - if not source_path or not os.path.exists(source_path): - continue - - if os.path.isdir(source_path) and target_dir.startswith(source_path): - failed_files.append(f"{source_path} (不能移动到子目录)") - continue - - if os.path.dirname(source_path) == target_dir: - continue - - filename = os.path.basename(source_path) - target_path = os.path.join(target_dir, filename) - - if os.path.exists(target_path): - failed_files.append(f"{filename} (目标已存在)") - continue - - try: - shutil.move(source_path, target_path) - moved_files.append(filename) - print(f"移动文件: {source_path} -> {target_path}") - except Exception as e: - failed_files.append(f"{filename} ({str(e)})") - - # 使用状态保持刷新 - if moved_files: - self._refreshWithStatePreservation() - - if failed_files: - StyledMessageBox.warning( - self, - "移动失败", - f"以下文件移动失败:\n" + "\n".join(failed_files) - ) - - if moved_files: - print(f"成功移动 {len(moved_files)} 个文件") - - # def mouseDoubleClickEvent(self, event): - # """处理双击事件""" - # item = self.itemAt(event.pos()) - # if item: - # filepath = item.data(0, Qt.UserRole) - # is_folder = item.data(0, Qt.UserRole + 1) - # - # if is_folder: - # # 文件夹:展开/折叠 - # item.setExpanded(not item.isExpanded()) - # else: - # # 文件:检查是否是模型文件 - # if filepath and filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - # self.world.importModel(filepath) - # else: - # # 其他文件:用系统默认程序打开 - # self.openFile(filepath) - # - # super().mouseDoubleClickEvent(event) - - -class CustomConsoleDockWidget(QWidget): - """自定义控制台停靠部件""" - - def __init__(self, world, parent=None): - if parent is None: - parent = wrapinstance(0, QWidget) - super().__init__(parent) - self.world = world - self.setupUI() - # 注释掉控制台重定向,让输出正常显示在终端 - # self.setupConsoleRedirect() - - def setupUI(self): - """初始化控制台UI""" - layout = QVBoxLayout(self) - - # 控制台工具栏 - toolbar = QHBoxLayout() - - # 清空按钮 - self.clearBtn = QPushButton("清空") - self.clearBtn.setStyleSheet(""" - QPushButton { - background-color: rgba(84, 89, 98, 0.5); - color: #ffffff; - border: none; - padding: 4px 12px; - border-radius: 2px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 12px; - font-weight: 300; - letter-spacing: 0.7px; - } - QPushButton:hover { - background-color: rgba(84, 89, 98, 0.7); - } - QPushButton:pressed { - background-color: rgba(84, 89, 98, 0.9); - } - """) - self.clearBtn.clicked.connect(self.clearConsole) - toolbar.addWidget(self.clearBtn) - - # 自动滚动开关 - self.autoScrollBtn = QPushButton("自动滚动") - self.autoScrollBtn.setCheckable(True) - self.autoScrollBtn.setChecked(True) - self.autoScrollBtn.setStyleSheet(""" - QPushButton { - background-color: rgba(84, 89, 98, 0.5); - color: #ffffff; - border: none; - padding: 4px 12px; - border-radius: 2px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 12px; - font-weight: 300; - letter-spacing: 0.7px; - } - QPushButton:checked { - background-color: #3067c0; - color: #ffffff; - } - QPushButton:hover { - background-color: rgba(84, 89, 98, 0.7); - } - QPushButton:pressed { - background-color: rgba(84, 89, 98, 0.9); - } - """) - toolbar.addWidget(self.autoScrollBtn) - - # 时间戳开关 - self.timestampBtn = QPushButton("显示时间") - self.timestampBtn.setCheckable(True) - self.timestampBtn.setChecked(True) - self.timestampBtn.setStyleSheet(""" - QPushButton { - background-color: rgba(84, 89, 98, 0.5); - color: #ffffff; - border: none; - padding: 4px 12px; - border-radius: 2px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 12px; - font-weight: 300; - letter-spacing: 0.7px; - } - QPushButton:checked { - background-color: #3067c0; - color: #ffffff; - } - QPushButton:hover { - background-color: rgba(84, 89, 98, 0.7); - } - QPushButton:pressed { - background-color: rgba(84, 89, 98, 0.9); - } - """) - toolbar.addWidget(self.timestampBtn) - - self.fpsLabel = QLabel("FPS:0.0") - self.fpsLabel.setStyleSheet(""" - QLabel { - background-color: #3067c0; - color: #ffffff; - border: none; - padding: 4px 12px; - border-radius: 2px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 12px; - font-weight: 300; - letter-spacing: 0.7px; - } - """) - self.fpsLabel.setMinimumWidth(100) - self.fpsLabel.setAlignment(Qt.AlignCenter) - toolbar.addWidget(self.fpsLabel) - - # 帧率更新定时器 - self.fpsTimer = QTimer() - self.fpsTimer.timeout.connect(self.updateFPS) - self.fpsTimer.start(105) # 每秒更新一次 - - toolbar.addStretch() - layout.addLayout(toolbar) - - # 控制台文本区域 - from PyQt5.QtWidgets import QTextEdit - self.consoleText = QTextEdit() - self.consoleText.setReadOnly(True) - self.consoleText.setStyleSheet(""" - QTextEdit { - background-color: #19191b; - color: #ebebeb; - font-family: 'Consolas', 'Monaco', 'Microsoft YaHei', monospace; - font-size: 10px; - font-weight: 300; - border: none; - letter-spacing: 0.5px; - line-height: 12px; - } - """) - layout.addWidget(self.consoleText) - - # # 命令输入区域 - # inputLayout = QHBoxLayout() - # inputLayout.addWidget(QLabel(">>> ")) - # - # self.commandInput = QLineEdit() - # self.commandInput.setStyleSheet(""" - # QLineEdit { - # background-color: #2d2d2d; - # color: #ffffff; - # font-family: 'Consolas', 'Monaco', monospace; - # font-size: 10pt; - # border: 1px solid #3e3e3e; - # padding: 5px; - # } - # """) - # self.commandInput.returnPressed.connect(self.executeCommand) - # inputLayout.addWidget(self.commandInput) - # - # layout.addLayout(inputLayout) - - # 添加欢迎信息 - self.addMessage("🎮 编辑器控制台已启动", "INFO") - - def updateFPS(self): - try: - if hasattr(self.world,'clock'): - fps = self.world.clock.getAverageFrameRate() - self.fpsLabel.setText(f"FPS:{fps:.1f}") - - # 根据帧率设置颜色 - if fps >= 50: - color = "#80ff80" # 绿色 - 优秀 - elif fps >= 30: - color = "#ffff80" # 黄色 - 一般 - else: - color = "#ff8080" # 红色 - 较差 - - self.fpsLabel.setStyleSheet(f""" - QLabel {{ - background-color: #3067c0; - color: {color}; - border: 1px solid #3a3a4a; - padding: 6px 12px; - border-radius: 4px; - font-weight: 500; - font-family: 'Consolas', 'Monaco', monospace; - }} - """) - except Exception as e: - pass # 静默处理错误,避免影响控制台功能 - - def setupConsoleRedirect(self): - """设置控制台重定向""" - import sys - - # 保存原始的stdout和stderr - self.original_stdout = sys.stdout - self.original_stderr = sys.stderr - - # 创建自定义输出流 - sys.stdout = ConsoleRedirect(self, "STDOUT") - sys.stderr = ConsoleRedirect(self, "STDERR") - - def addMessage(self, message, msg_type="INFO"): - """添加消息到控制台""" - import datetime - - # 获取当前时间 - timestamp = datetime.datetime.now().strftime("%H:%M:%S") - - # 根据消息类型设置颜色 - color_map = { - "INFO": "#ffffff", # 白色 - "WARNING": "#ffaa00", # 橙色 - "ERROR": "#ff4444", # 红色 - "SUCCESS": "#44ff44", # 绿色 - "STDOUT": "#cccccc", # 浅灰色 - "STDERR": "#ff6666", # 浅红色 - } - - color = color_map.get(msg_type, "#ffffff") - - # 构建HTML格式的消息 - if self.timestampBtn.isChecked(): - html_message = f'[{timestamp}] {message}' - else: - html_message = f'{message}' - - # 添加到控制台 - self.consoleText.append(html_message) - - # 自动滚动到底部 - if self.autoScrollBtn.isChecked(): - scrollbar = self.consoleText.verticalScrollBar() - scrollbar.setValue(scrollbar.maximum()) - - def clearConsole(self): - """清空控制台""" - self.consoleText.clear() - self.addMessage("控制台已清空", "INFO") - - def executeCommand(self): - """执行命令""" - command = self.commandInput.text().strip() - if not command: - return - - # 显示输入的命令 - self.addMessage(f">>> {command}", "INFO") - self.commandInput.clear() - - try: - # 执行Python命令 - if hasattr(self.world, 'executeCommand'): - result = self.world.executeCommand(command) - if result: - self.addMessage(str(result), "SUCCESS") - else: - # 简单的eval执行 - result = eval(command) - if result is not None: - self.addMessage(str(result), "SUCCESS") - - except Exception as e: - self.addMessage(f"错误: {str(e)}", "ERROR") - - def cleanup(self): - """清理资源""" - import sys - - # 恢复原始的stdout和stderr - if hasattr(self, 'original_stdout'): - sys.stdout = self.original_stdout - if hasattr(self, 'original_stderr'): - sys.stderr = self.original_stderr - - -class ConsoleRedirect: - """控制台重定向类""" - - def __init__(self, console_widget, stream_type): - self.console_widget = console_widget - self.stream_type = stream_type - # 保存原始输出流的引用 - if stream_type == "STDOUT": - self.original_stream = sys.stdout - else: - self.original_stream = sys.stderr - - def write(self, text): - """重定向写入 - 同时输出到GUI控制台和原始终端""" - # 先输出到原始终端 - self.original_stream.write(text) - - # 然后输出到GUI控制台(仅非空行) - if text.strip(): # 忽略空行 - self.console_widget.addMessage(text.strip(), self.stream_type) - - def flush(self): - """刷新缓冲区 - 同时刷新原始流""" - self.original_stream.flush() - -class StyledMessageBox: - """统一样式的消息框辅助类""" - - MESSAGEBOX_STYLE = """ - QMessageBox { - background-color: #1e1e1f; - color: #ebebeb; - border: 1px solid rgba(77, 116, 189, 0.3); - border-radius: 8px; - padding: 20px; - min-width: 300px; - } - QMessageBox QLabel { - color: #ffffff; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 13px; - font-weight: 400; - line-height: 1.4; - padding: 10px 0px; - } - QMessageBox QPushButton { - background-color: rgba(89, 100, 113, 0.4); - color: rgba(255, 255, 255, 0.8); - border: 1px solid rgba(76, 92, 110, 0.4); - border-radius: 6px; - padding: 8px 16px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-weight: 400; - font-size: 11px; - min-width: 80px; - min-height: 28px; - } - QMessageBox QPushButton:hover { - background-color: rgba(89, 100, 113, 0.6); - border: 1px solid rgba(77, 116, 189, 0.6); - color: #ffffff; - } - QMessageBox QPushButton:pressed, QMessageBox QPushButton:checked { - background-color: rgba(77, 116, 189, 0.8); - border: 1px solid #4d74bd; - color: #ffffff; - } - QMessageBox QPushButton:disabled { - background-color: rgba(89, 100, 113, 0.2); - color: rgba(235, 235, 235, 0.4); - border: 1px solid rgba(76, 92, 110, 0.2); - } - QMessageBox QPushButton:default { - background-color: rgba(77, 116, 189, 0.8); - border: 1px solid #4d74bd; - color: #ffffff; - font-weight: 500; - } - QMessageBox QPushButton:default:hover { - background-color: rgba(77, 116, 189, 1.0); - border: 1px solid rgba(77, 116, 189, 1.0); - } - QMessageBox QIcon { - padding: 0px 10px 0px 0px; - } - """ - - INPUTDIALOG_STYLE = """ - QInputDialog { - background-color: #1e1e1f; - color: #ebebeb; - border: 1px solid rgba(77, 116, 189, 0.3); - border-radius: 8px; - padding: 20px; - min-width: 350px; - } - QLabel { - color: #ffffff; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-weight: 500; - font-size: 13px; - padding: 0px 0px 10px 0px; - } - QLineEdit { - background-color: rgba(89, 100, 113, 0.15); - color: #ebebeb; - border: 1px solid rgba(76, 92, 110, 0.4); - border-radius: 6px; - padding: 10px 12px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 11px; - font-weight: 400; - min-height: 16px; - } - QLineEdit:focus { - border: 1px solid #4d74bd; - background-color: rgba(77, 116, 189, 0.1); - } - QLineEdit:hover { - border: 1px solid rgba(77, 116, 189, 0.6); - background-color: rgba(89, 100, 113, 0.2); - } - QPushButton { - background-color: rgba(89, 100, 113, 0.4); - color: rgba(255, 255, 255, 0.8); - border: 1px solid rgba(76, 92, 110, 0.4); - border-radius: 6px; - padding: 8px 16px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-weight: 400; - font-size: 11px; - min-width: 80px; - min-height: 28px; - } - QPushButton:hover { - background-color: rgba(89, 100, 113, 0.6); - border: 1px solid rgba(77, 116, 189, 0.6); - color: #ffffff; - } - QPushButton:pressed, QPushButton:checked { - background-color: rgba(77, 116, 189, 0.8); - border: 1px solid #4d74bd; - color: #ffffff; - } - QPushButton:disabled { - background-color: rgba(89, 100, 113, 0.2); - color: rgba(235, 235, 235, 0.4); - border: 1px solid rgba(76, 92, 110, 0.2); - } - QPushButton:default { - background-color: rgba(77, 116, 189, 0.8); - border: 1px solid #4d74bd; - color: #ffffff; - font-weight: 500; - } - """ - - BUTTON_STYLE = """ - QPushButton { - background-color: rgba(89, 100, 113, 0.4); - color: rgba(255, 255, 255, 0.8); - border: 1px solid rgba(76, 92, 110, 0.4); - border-radius: 6px; - padding: 8px 16px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-weight: 400; - font-size: 11px; - min-width: 80px; - min-height: 28px; - } - QPushButton:hover { - background-color: rgba(89, 100, 113, 0.6); - border: 1px solid rgba(77, 116, 189, 0.6); - color: #ffffff; - } - QPushButton:pressed, QPushButton:checked { - background-color: rgba(77, 116, 189, 0.8); - border: 1px solid #4d74bd; - color: #ffffff; - } - QPushButton:disabled { - background-color: rgba(89, 100, 113, 0.2); - color: rgba(235, 235, 235, 0.4); - border: 1px solid rgba(76, 92, 110, 0.2); - } - """ - - @staticmethod - def information(parent, title, message): - """显示信息提示框""" - msg = QMessageBox(QMessageBox.Information, title, message, QMessageBox.Ok, parent) - msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) - # 强制设置所有按钮的样式 - for button in msg.buttons(): - button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) - return msg.exec_() - - @staticmethod - def question(parent, title, message, buttons=QMessageBox.Yes | QMessageBox.No, defaultButton=QMessageBox.No): - """显示确认对话框""" - msg = QMessageBox(QMessageBox.Question, title, message, buttons, parent) - msg.setDefaultButton(defaultButton) - msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) - # 强制设置所有按钮的样式 - for button in msg.buttons(): - button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) - return msg.exec_() - - @staticmethod - def warning(parent, title, message): - """显示警告对话框""" - msg = QMessageBox(QMessageBox.Warning, title, message, QMessageBox.Ok, parent) - msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) - # 强制设置所有按钮的样式 - for button in msg.buttons(): - button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) - return msg.exec_() - - @staticmethod - def critical(parent, title, message): - """显示错误对话框""" - msg = QMessageBox(QMessageBox.Critical, title, message, QMessageBox.Ok, parent) - msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) - # 强制设置所有按钮的样式 - for button in msg.buttons(): - button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) - return msg.exec_() - - @staticmethod - def getText(parent, title, label, text=""): - """显示样式统一的文本输入对话框""" - from PyQt5.QtWidgets import QInputDialog - dialog = QInputDialog(parent) - dialog.setWindowTitle(title) - dialog.setLabelText(label) - dialog.setTextValue(text) - dialog.setStyleSheet(StyledMessageBox.INPUTDIALOG_STYLE) - - # 强制设置所有按钮的样式 - for button in dialog.findChildren(QPushButton): - button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) - - ok = dialog.exec_() - return dialog.textValue(), ok - -class UniversalMessageDialog(QDialog): - """通用消息对话框类 - 支持不同图标和按钮配置""" - - # 消息类型枚举 - SUCCESS = "success_icon" - WARNING = "warning_icon" - ERROR = "fail_icon" - INFO = "info" - - def __init__(self, parent, title, message, message_type=INFO, show_cancel=True, - confirm_text="确认", cancel_text="取消", icon_size=QSize(20, 20)): - """ - 初始化通用消息对话框 - - Args: - parent: 父窗口 - title: 对话框标题 - message: 消息内容 - message_type: 消息类型 (SUCCESS, WARNING, ERROR, INFO) - show_cancel: 是否显示取消按钮 - confirm_text: 确认按钮文字 - cancel_text: 取消按钮文字 - icon_size: 图标尺寸 - """ - super().__init__(parent) - self.setWindowTitle(title) - self.setObjectName("universalMessageDialog") - self.setModal(True) - self.resize(508, 134) - self.setMinimumSize(508, 134) - self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) - self.setAttribute(Qt.WA_TranslucentBackground, True) - - # 对话框配置 - self.message_type = message_type - self.show_cancel = show_cancel - self.confirm_text = confirm_text - self.cancel_text = cancel_text - self.icon_size = icon_size - - # 拖拽相关 - self.dragging = False - self.drag_position = QPoint() - - # 图标管理 - self.icon_manager = get_icon_manager() - self._title_icon_size = QSize(18, 18) - self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size) - - # 根据消息类型获取对应图标 - self._message_icon = self._get_message_icon() - - # 设置样式 - self._setup_styles() - - # 创建UI - self._create_ui(message) - - def _get_message_icon(self): - """根据消息类型获取对应图标""" - icon_map = { - self.SUCCESS: 'success_icon', - self.WARNING: 'warning_icon', - self.ERROR: 'fail_icon', - self.INFO: 'success_icon' # 默认使用成功图标 - } - - icon_name = icon_map.get(self.message_type, 'success_icon') - return self.icon_manager.get_icon(icon_name, self.icon_size) - - def _setup_styles(self): - """设置对话框样式""" - self.setStyleSheet(""" - QDialog#universalMessageDialog { - background-color: transparent; - border: none; - } - QFrame#baseFrame { - background-color: #19191B; - border: 1px solid #3E3E42; - border-radius: 5px; - } - QWidget#titleBar { - background-color: transparent; - border: none; - border-radius: 5px 5px 0px 0px; - min-height: 32px; - max-height: 32px; - } - QWidget#titleBar QWidget { - background-color: transparent; - border: none; - } - QLabel#titleLabel { - color: #FFFFFF; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 14px; - font-weight: 500; - letter-spacing: 0.7px; - } - QWidget#controlButtons QPushButton { - background-color: transparent; - border: none; - color: #EBEBEB; - font-size: 14px; - min-width: 18px; - max-width: 18px; - min-height: 18px; - max-height: 18px; - padding: 0px; - border-radius: 3px; - } - QWidget#controlButtons QPushButton:hover { - background-color: #2A2D2E; - color: #FFFFFF; - } - QPushButton#closeButton { - border-radius: 0px 5px 0px 0px; - } - QPushButton#closeButton:hover { - background-color: #2A2D2E; - color: #FFFFFF; - } - QFrame#titleSeparator { - background-color: #3E3E42; - border: none; - min-height: 1px; - max-height: 1px; - } - QWidget#contentWidget { - background-color: transparent; - border: none; - } - QLabel#messageLabel { - color: #EBEBEB; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 13px; - font-weight: 400; - letter-spacing: 0.6px; - line-height: 1.4; - padding: 0px; - margin: 0px; - } - QPushButton { - background-color: rgba(89, 98, 118, 0.5); - color: #EBEBEB; - border: none; - border-radius: 2px; - padding: 0px 12px; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-weight: 300; - font-size: 10px; - letter-spacing: 0.5px; - min-width: 90px; - max-width: 90px; - min-height: 28px; - max-height: 28px; - } - QPushButton:hover { - background-color: #3067C0; - color: #FFFFFF; - } - QPushButton:pressed { - background-color: #2556A0; - color: #FFFFFF; - } - QPushButton#primaryButton { - background-color: rgba(89, 98, 118, 0.5); - border: none; - color: #EBEBEB; - font-weight: 300; - } - QPushButton#primaryButton:hover { - background-color: #2556A0; - } - QPushButton#primaryButton:pressed { - background-color: #1E4A8C; - } - QPushButton#secondaryButton { - background-color: rgba(89, 98, 118, 0.5); - border: none; - color: #EBEBEB; - } - QPushButton#secondaryButton:hover { - background-color: #3067C0; - color: #FFFFFF; - } - QPushButton#secondaryButton:pressed { - background-color: #2556A0; - color: #FFFFFF; - } - """) - - def _create_ui(self, message): - """构建用户界面""" - main_layout = QVBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(0) - - base_frame = QFrame() - base_frame.setObjectName('baseFrame') - base_frame.setFrameShape(QFrame.NoFrame) - base_frame.setAttribute(Qt.WA_StyledBackground, True) - - base_layout = QVBoxLayout(base_frame) - base_layout.setContentsMargins(25, 14, 25, 14) - base_layout.setSpacing(0) - - self._create_title_bar() - base_layout.addWidget(self.title_bar) - base_layout.addSpacing(4) - - title_separator = QFrame() - title_separator.setObjectName('titleSeparator') - title_separator.setFrameShape(QFrame.HLine) - title_separator.setFrameShadow(QFrame.Plain) - base_layout.addWidget(title_separator) - base_layout.addSpacing(10) - - content_widget = QWidget() - content_widget.setObjectName('contentWidget') - content_layout = QVBoxLayout(content_widget) - content_layout.setContentsMargins(0, 0, 0, 0) - content_layout.setSpacing(10) - - message_area = QHBoxLayout() - message_area.setContentsMargins(0, 0, 0, 0) - message_area.setSpacing(10) - - # 用一个垂直布局包裹icon_label,确保顶部对齐 - icon_vbox = QVBoxLayout() - icon_vbox.setContentsMargins(0, 0, 0, 0) - icon_vbox.setSpacing(0) - icon_label = QLabel() - if self._message_icon and not self._message_icon.isNull(): - icon_label.setPixmap(self._message_icon.pixmap(self.icon_size)) - icon_label.setAlignment(Qt.AlignTop | Qt.AlignLeft) - icon_label.setFixedSize(self.icon_size) - icon_vbox.addWidget(icon_label, alignment=Qt.AlignTop) - icon_vbox.addStretch() - message_area.addLayout(icon_vbox) - - self.message_label = QLabel(message) - self.message_label.setObjectName('messageLabel') - self.message_label.setWordWrap(True) - self.message_label.setAlignment(Qt.AlignTop | Qt.AlignLeft) - self.message_label.setMinimumHeight(self.icon_size.height()) - self.message_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) - message_area.addWidget(self.message_label, 1) - message_area.addStretch() - - content_layout.addLayout(message_area) - base_layout.addWidget(content_widget) - base_layout.addSpacing(10) - - button_widget = QWidget() - button_layout = QHBoxLayout(button_widget) - button_layout.setContentsMargins(0, 0, 0, 0) - button_layout.setSpacing(10) - button_layout.addStretch() - - if self.show_cancel: - self.cancel_button = QPushButton(self.cancel_text) - self.cancel_button.setObjectName("secondaryButton") - self.cancel_button.clicked.connect(self.reject) - self.cancel_button.setFixedSize(90, 28) - button_layout.addWidget(self.cancel_button) - - self.confirm_button = QPushButton(self.confirm_text) - self.confirm_button.setObjectName("primaryButton") - self.confirm_button.clicked.connect(self.accept) - self.confirm_button.setFixedSize(90, 28) - button_layout.addWidget(self.confirm_button) - - self.confirm_button.setDefault(True) - self.confirm_button.setAutoDefault(True) - if self.show_cancel: - self.cancel_button.setAutoDefault(False) - - base_layout.addWidget(button_widget) - - main_layout.addWidget(base_frame) - - def _create_title_bar(self): - """创建自定义标题栏""" - self.title_bar = QFrame() - self.title_bar.setObjectName("titleBar") - - title_layout = QHBoxLayout(self.title_bar) - title_layout.setContentsMargins(0, 0, 0, 0) - title_layout.setSpacing(0) - - self.title_label = QLabel(self.windowTitle()) - self.title_label.setObjectName("titleLabel") - self.title_label.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) - title_layout.addWidget(self.title_label, 1) - - title_layout.addStretch() - - controls = QWidget() - controls.setObjectName("controlButtons") - controls_layout = QHBoxLayout(controls) - controls_layout.setContentsMargins(0, 0, 0, 0) - controls_layout.setSpacing(0) - - self.close_button = QPushButton() - self.close_button.setObjectName("closeButton") - self.close_button.clicked.connect(self.reject) - self.close_button.setFocusPolicy(Qt.NoFocus) - controls_layout.addWidget(self.close_button) - - self._apply_title_bar_icons() - - title_layout.addWidget(controls) - - - def _apply_title_bar_icons(self): - """应用标题栏图标""" - if self._icon_close: - self.close_button.setIcon(self._icon_close) - self.close_button.setIconSize(self._title_icon_size) - self.close_button.setText("") - self.close_button.setToolTip("关闭") - - def setWindowTitle(self, title): - """设置窗口标题""" - super().setWindowTitle(title) - if hasattr(self, "title_label"): - self.title_label.setText(title) - - def mousePressEvent(self, event): - """鼠标按下事件 - 用于拖拽窗口""" - if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): - self.dragging = True - self.drag_position = event.globalPos() - self.frameGeometry().topLeft() - event.accept() - super().mousePressEvent(event) - - def mouseMoveEvent(self, event): - """鼠标移动事件 - 用于拖拽窗口""" - if event.buttons() == Qt.LeftButton and self.dragging: - self.move(event.globalPos() - self.drag_position) - event.accept() - super().mouseMoveEvent(event) - - def mouseReleaseEvent(self, event): - """鼠标释放事件 - 停止拖拽""" - if event.button() == Qt.LeftButton: - self.dragging = False - super().mouseReleaseEvent(event) - - @staticmethod - def show_success(parent, title, message, show_cancel=False, confirm_text="确认"): - """显示成功消息对话框""" - dialog = UniversalMessageDialog( - parent, title, message, - UniversalMessageDialog.SUCCESS, - show_cancel, confirm_text - ) - return dialog.exec_() - - @staticmethod - def show_warning(parent, title, message, show_cancel=True, confirm_text="确认", cancel_text="取消"): - """显示警告消息对话框""" - dialog = UniversalMessageDialog( - parent, title, message, - UniversalMessageDialog.WARNING, - show_cancel, confirm_text, cancel_text - ) - return dialog.exec_() - - @staticmethod - def show_error(parent, title, message, show_cancel=False, confirm_text="确认"): - """显示错误消息对话框""" - dialog = UniversalMessageDialog( - parent, title, message, - UniversalMessageDialog.ERROR, - show_cancel, confirm_text - ) - return dialog.exec_() - - @staticmethod - def show_info(parent, title, message, show_cancel=True, confirm_text="确认", cancel_text="取消"): - """显示信息消息对话框""" - dialog = UniversalMessageDialog( - parent, title, message, - UniversalMessageDialog.INFO, - show_cancel, confirm_text, cancel_text - ) - return dialog.exec_() - - -class StyledTextInputDialog(QDialog): - """与新建项目样式一致的文本输入对话框""" - - def __init__(self, parent, title, label_text="", placeholder="", default_text=""): - super().__init__(parent) - self.setWindowTitle(title) - self.setObjectName("styledTextInputDialog") - self.setModal(True) - self.resize(420, 186) - self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) - self.setAttribute(Qt.WA_TranslucentBackground, True) - - self.dragging = False - self.drag_position = QPoint() - self.icon_manager = get_icon_manager() - self._title_icon_size = QSize(18, 18) - self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size) - - self.setStyleSheet(""" - QDialog#styledTextInputDialog { - background-color: transparent; - border: none; - } - QFrame#baseFrame { - background-color: #000000; - border: 1px solid #3E3E42; - border-radius: 5px; - } - QWidget#titleBar { - background-color: transparent; - border: none; - border-radius: 5px 5px 0px 0px; - min-height: 32px; - max-height: 32px; - } - QWidget#titleBar QWidget { - background-color: transparent; - border: none; - } - QLabel#titleLabel { - color: #FFFFFF; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 14px; - font-weight: 500; - letter-spacing: 0.7px; - } - QWidget#controlButtons QPushButton { - background-color: transparent; - border: none; - color: #EBEBEB; - font-size: 14px; - min-width: 18px; - max-width: 18px; - min-height: 18px; - max-height: 18px; - padding: 0px; - border-radius: 3px; - } - QWidget#controlButtons QPushButton:hover { - background-color: #2A2D2E; - color: #FFFFFF; - } - QPushButton#closeButton { - border-radius: 0px 5px 0px 0px; - } - QPushButton#closeButton:hover { - background-color: #2A2D2E; - color: #FFFFFF; - } - QWidget#contentWidget { - background-color: transparent; - border-radius: 0px 0px 5px 5px; - } - QFrame#contentContainer { - background-color: #19191B; - border: 1px solid #2C2F36; - border-radius: 5px; - } - QLabel[role="fieldLabel"] { - color: #EBEBEB; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 12px; - font-weight: 400; - letter-spacing: 0.6px; - } - QLineEdit { - background-color: rgba(89, 100, 113, 0.2); - color: #EBEBEB; - border: 1px solid rgba(76, 92, 110, 0.6); - border-radius: 2px; - padding: 0px 10px; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 11px; - font-weight: 300; - letter-spacing: 0.55px; - min-height: 30px; - max-height: 30px; - } - QLineEdit:focus { - border: 1px solid #3067C0; - background-color: rgba(48, 103, 192, 0.1); - } - QLineEdit:hover { - border: 1px solid #3067C0; - background-color: rgba(89, 100, 113, 0.3); - } - QPushButton { - background-color: rgba(89, 98, 118, 0.5); - color: #EBEBEB; - border: none; - border-radius: 2px; - padding: 0px 12px; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-weight: 300; - font-size: 10px; - letter-spacing: 0.5px; - min-width: 120px; - min-height: 30px; - max-height: 30px; - } - QPushButton:hover { - background-color: #3067C0; - color: #FFFFFF; - } - QPushButton:pressed { - background-color: #2556A0; - color: #FFFFFF; - } - """) - - main_layout = QVBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(0) - - base_frame = QFrame() - base_frame.setObjectName('baseFrame') - base_frame.setFrameShape(QFrame.NoFrame) - base_frame.setAttribute(Qt.WA_StyledBackground, True) - - base_layout = QVBoxLayout(base_frame) - base_layout.setContentsMargins(0, 0, 0, 0) - base_layout.setSpacing(0) - - self._createTitleBar() - base_layout.addWidget(self.title_bar) - base_layout.addSpacing(10) - - content_widget = QWidget() - content_widget.setObjectName('contentWidget') - content_layout = QVBoxLayout(content_widget) - content_layout.setContentsMargins(10, 0, 10, 10) - content_layout.setSpacing(0) - - content_container = QFrame() - content_container.setObjectName('contentContainer') - content_container.setFrameShape(QFrame.NoFrame) - content_container.setAttribute(Qt.WA_StyledBackground, True) - content_container.setFixedWidth(400) - - container_layout = QVBoxLayout(content_container) - container_layout.setContentsMargins(15, 10, 15, 10) - container_layout.setSpacing(10) - - if label_text: - label = QLabel(label_text) - label.setProperty('role', 'fieldLabel') - container_layout.addWidget(label) - - self.line_edit = QLineEdit() - self.line_edit.setPlaceholderText(placeholder) - self.line_edit.setText(default_text) - container_layout.addWidget(self.line_edit) - - separator = QFrame() - separator.setFrameShape(QFrame.HLine) - separator.setFrameShadow(QFrame.Plain) - separator.setFixedHeight(1) - separator.setStyleSheet("background-color: #2C2F36; border: none;") - container_layout.addWidget(separator) - - button_row = QHBoxLayout() - button_row.setContentsMargins(0, 0, 0, 0) - button_row.setSpacing(10) - button_row.addStretch() - - self.confirmButton = QPushButton("确认") - self.confirmButton.setObjectName("primaryButton") - self.confirmButton.clicked.connect(self._onAccept) - button_row.addWidget(self.confirmButton) - - self.cancelButton = QPushButton("取消") - self.cancelButton.setObjectName("secondaryButton") - self.cancelButton.clicked.connect(self.reject) - button_row.addWidget(self.cancelButton) - - container_layout.addLayout(button_row) - - content_layout.addWidget(content_container, 0, Qt.AlignTop) - base_layout.addWidget(content_widget) - main_layout.addWidget(base_frame) - - self.confirmButton.setDefault(True) - self.confirmButton.setAutoDefault(True) - self.line_edit.selectAll() - self.line_edit.setFocus() - - def _createTitleBar(self): - self.title_bar = QFrame() - self.title_bar.setObjectName("titleBar") - - title_layout = QHBoxLayout(self.title_bar) - title_layout.setContentsMargins(12, 6, 12, 6) - title_layout.setSpacing(0) - - controls = QWidget() - controls.setObjectName("controlButtons") - controls_layout = QHBoxLayout(controls) - controls_layout.setContentsMargins(0, 0, 0, 0) - controls_layout.setSpacing(0) - - self.close_button = QPushButton() - self.close_button.setObjectName("closeButton") - self.close_button.clicked.connect(self.reject) - self.close_button.setFocusPolicy(Qt.NoFocus) - self.close_button.setFixedSize(18, 18) - controls_layout.addWidget(self.close_button) - - self._applyTitleBarIcons() - - left_placeholder = QWidget() - left_placeholder.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - title_layout.addWidget(left_placeholder) - - self.title_label = QLabel(self.windowTitle()) - self.title_label.setObjectName("titleLabel") - self.title_label.setAlignment(Qt.AlignCenter) - title_layout.addWidget(self.title_label, 1) - - title_layout.addWidget(controls) - left_placeholder.setFixedWidth(controls.sizeHint().width()) - - def _applyTitleBarIcons(self): - if self._icon_close: - self.close_button.setIcon(self._icon_close) - self.close_button.setIconSize(self._title_icon_size) - self.close_button.setText("") - self.close_button.setToolTip("关闭") - - def setWindowTitle(self, title): - super().setWindowTitle(title) - if hasattr(self, "title_label"): - self.title_label.setText(title) - - def _onAccept(self): - if self.line_edit.text().strip(): - self.accept() - - def text(self): - return self.line_edit.text().strip() - - def mousePressEvent(self, event): - if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): - self.dragging = True - self.drag_position = event.globalPos() - self.frameGeometry().topLeft() - event.accept() - super().mousePressEvent(event) - - def mouseMoveEvent(self, event): - if event.buttons() == Qt.LeftButton and self.dragging: - self.move(event.globalPos() - self.drag_position) - event.accept() - super().mouseMoveEvent(event) - - def mouseReleaseEvent(self, event): - if event.button() == Qt.LeftButton: - self.dragging = False - super().mouseReleaseEvent(event) - - -class CustomTreeWidget(QTreeWidget): - """自定义场景树部件""" - - def __init__(self, world, parent=None): - if parent is None: - parent = wrapinstance(0, QWidget) - super().__init__(parent) - self.world = world - # self.selectedItems = None - self.initData() - self.setupUI() # 初始化界面 - self.setupContextMenu() # 初始化右键菜单 - self.setupDragDrop() # 设置拖拽功能 - self.original_scales={} - - def initData(self): - """初始化变量""" - # 定义2D GUI元素类型 - self.gui_2d_types = { - "GUI_BUTTON", # GUI 按钮 - "GUI_LABEL", # GUI 标签 - "GUI_ENTRY", # GUI 输入框 - "GUI_IMAGE", # GUI 图片 - "GUI_2D_VIDEO_SCREEN", # GUI 2D视频 - "GUI_SPHERICAL_VIDEO", # GUI 3D球形视频 - "GUI_NODE" # 其他2D GUI容器 - } - - # 定义3D GUI元素类型 - self.gui_3d_types = { - "GUI_3DTEXT", # 3D 文本节点 - "GUI_3DIMAGE", # 3D 图片节点 - "GUI_VIRTUAL_SCREEN", # 3D视频 - "GUI_VirtualScreen" # 3D虚拟视频 - } - - # 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素) - self.scene_3d_types = { - "SCENE_ROOT", - "SCENE_NODE", - "LIGHT_NODE", # 灯节点 - "CAMERA_NODE", - "IMPORTED_MODEL_NODE", # 导入模型节点 - "MODEL_NODE", - "TERRAIN_NODE", # 地形节点 - "CESIUM_TILESET_NODE" # 3D Tileset - } - - # 这是一个最佳实践,它让代码的意图变得非常清晰。 - self.valid_3d_parent_types = self.scene_3d_types.union(self.gui_3d_types) - - def setupUI(self): - """初始化UI设置""" - self.setHeaderHidden(True) - # 启用多选和拖拽 - self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) - self.setDropIndicatorShown(True) # 启用拖放指示线 - - def setupDragDrop(self): - """设置拖拽功能""" - # 使用自定义拖拽模式 - self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) # 或者使用 DragDrop - self.setDefaultDropAction(Qt.DropAction.MoveAction) - self.setDragEnabled(True) - self.setAcceptDrops(True) - - def setupContextMenu(self): - """设置右键菜单""" - self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self.customContextMenuRequested.connect(self.showContextMenu) - - def showContextMenu(self, position): - """显示右键菜单 - 复用主菜单的创建动作""" - if self.selectedItems(): - item = self.selectedItems()[0] - print(f"为项目 '{item.text(0)}' 显示右键菜单") - - # 创建右键菜单 - menu = QMenu(self) - - # 获取主窗口的创建菜单动作 - 关键修改 - if hasattr(self.world, 'main_window'): - main_window = self.world.main_window - create_actions = main_window.getCreateMenuActions() - - # 创建子菜单 - 复用主菜单结构 - createMenu = menu.addMenu('创建') - - # 基础对象 - createMenu.addAction(create_actions['createEmpty']) - - # 3D对象菜单 - create3dObjectMenu = createMenu.addMenu('3D对象') - - # 3D GUI菜单 - create3dGUIMenu = createMenu.addMenu('3D GUI') - create3dGUIMenu.addAction(create_actions['create3DText']) - create3dGUIMenu.addAction(create_actions['create3DImage']) - - # GUI菜单 - createGUIMenu = createMenu.addMenu('GUI') - createGUIMenu.addAction(create_actions['createButton']) - createGUIMenu.addAction(create_actions['createLabel']) - createGUIMenu.addAction(create_actions['createEntry']) - createGUIMenu.addAction(create_actions['createImage']) - createGUIMenu.addSeparator() - createGUIMenu.addAction(create_actions['createVideoScreen']) - createGUIMenu.addAction(create_actions['createSphericalVideo']) - createGUIMenu.addSeparator() - createGUIMenu.addAction(create_actions['createVirtualScreen']) - - # 光源菜单 - createLightMenu = createMenu.addMenu('光源') - createLightMenu.addAction(create_actions['createSpotLight']) - createLightMenu.addAction(create_actions['createPointLight']) - - else: - # 备用方案:如果无法获取主窗口动作,显示提示 - createMenu = menu.addMenu('创建') - noActionItem = createMenu.addAction('功能不可用') - noActionItem.setEnabled(False) - - # 添加删除选项 - menu.addSeparator() - deleteAction = menu.addAction('删除') - if hasattr(self, 'delete_items'): - deleteAction.triggered.connect(lambda: self.delete_items(self.selectedItems())) - - # 显示菜单 - global_pos = self.mapToGlobal(position) - action = menu.exec_(global_pos) - - print(f"右键菜单已显示在位置: {global_pos}") - if action: - print(f"用户选择了动作: {action.text()}") - else: - print("用户取消了菜单选择") - - # 在 CustomTreeWidget 类的 dropEvent 方法中替换缩放处理部分 - def dropEvent(self, event): - # 1. 获取所有被拖拽的项 - dragged_items = self.selectedItems() - if not dragged_items: - event.ignore() - return - - # 2. 在执行Qt的默认拖拽前,记录所有拖拽项的原始状态 - drag_info = [] - for item in dragged_items: - panda_node = item.data(0, Qt.UserRole) - if not panda_node or panda_node.is_empty(): - continue # 跳过无效节点 - - drag_info.append({ - "item": item, - "node": panda_node, - "old_parent_node": item.parent().data(0, Qt.UserRole) if item.parent() else None - }) - - # 3. 执行Qt的默认拖拽,让UI树先行更新 - # 这一步会自动处理移动或复制,并将项目从旧父节点移除,添加到新父节点 - super().dropEvent(event) - - # 4. 遍历记录下的信息,同步每一个Panda3D节点的状态 - try: - for info in drag_info: - dragged_item = info["item"] - dragged_node = info["node"] - old_parent_node = info["old_parent_node"] - - # 获取拖拽后的新父节点 - new_parent_item = dragged_item.parent() - new_parent_node = new_parent_item.data(0, Qt.UserRole) if new_parent_item else None - - # 仅当父节点实际发生变化时才执行重新父化 - if old_parent_node != new_parent_node: - print(f"跨层级拖拽:从 {old_parent_node} 移动到 {new_parent_node}") - - # # 保存世界坐标位置 - # world_pos = dragged_node.getPos(self.world.render) - # world_hpr = dragged_node.getHpr(self.world.render) - # world_scale = dragged_node.getScale(self.world.render) - - # 检查是否是2D GUI元素 - dragged_type = dragged_item.data(0, Qt.UserRole + 1) - is_2d_gui = dragged_type in self.gui_2d_types - - # 重新父化到新的父节点 - if new_parent_node: - if is_2d_gui: - # 2D GUI元素需要特殊处理 - if hasattr(new_parent_node, 'getTag') and new_parent_node.getTag("is_gui_element") == "1": - # 目标是GUI元素,直接重新父化 - dragged_node.wrtReparentTo(new_parent_node) - else: - # 目标是3D节点,保持GUI特性,重新父化到aspect2d - # from direct.showbase.ShowBase import aspect2d - dragged_node.wrtReparentTo(self.world.aspect2d) - print(f"2D GUI元素 {dragged_item.text(0)} 保持在aspect2d下") - else: - # 非GUI元素正常重新父化 - dragged_node.wrtReparentTo(new_parent_node) - else: - # 如果新父节点为None,根据元素类型决定父节点 - if is_2d_gui: - # from direct.showbase.ShowBase import aspect2d - dragged_node.wrtReparentTo(self.world.aspect2d) - print(f"2D GUI元素 {dragged_item.text(0)} 重新父化到aspect2d") - else: - dragged_node.wrtReparentTo(self.world.render) - - # # 恢复世界坐标位置(对于2D GUI可能需要调整) - # if is_2d_gui: - # # 2D GUI元素使用屏幕坐标系,可能需要特殊处理 - # dragged_node.setPos(world_pos) - # dragged_node.setHpr(world_hpr) - # dragged_node.setScale(world_scale) - # else: - # dragged_node.setPos(self.world.render, world_pos) - # dragged_node.setHpr(self.world.render, world_hpr) - # dragged_node.setScale(self.world.render, world_scale) - - print(f"✅ Panda3D父子关系已更新") - else: - print(f"同层级移动:父节点未变化,跳过Panda3D重新父化") - - except Exception as e: - print(f"⚠️ 同步Panda3D场景图失败: {e}") - # 不影响Qt树的更新,继续执行 - - # 事后验证:确保节点仍在"场景"根节点下 - self._ensureUnderSceneRoot(dragged_item) - self.world.property_panel._syncEffectiveVisibility(dragged_node) - event.accept() - - def _ensureUnderSceneRoot(self, item): - """确保节点在场景根节点下,如果不是则自动修正""" - if not item: - return - - # 检查是否成为了顶级节点 - if not item.parent(): - # 通过数据标识判断是否是场景根节点 - scene_root_marker = item.data(0, Qt.UserRole + 1) - if scene_root_marker != "SCENE_ROOT": - print(f"⚠️ 检测到节点 {item.text(0)} 意外成为顶级节点,正在修正...") - - # 找到场景根节点 - scene_root = None - for i in range(self.topLevelItemCount()): - top_item = self.topLevelItem(i) - if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": - scene_root = top_item - break - - if scene_root: - # 将节点移回场景根节点下 - self.takeTopLevelItem(self.indexOfTopLevelItem(item)) - scene_root.addChild(item) - print(f"✅ 已将节点 {item.text(0)} 移回场景根节点下") - - def isValidParentChild(self, dragged_item, target_item): - """检查是否是有效的父子关系(防止循环)+ GUI类型验证""" - - # 1. 禁止拖放到自身 - if dragged_item == target_item: - return False - - # 2. 禁止拖到根节点之外(根节点本身除外) - target_root = self._getRootNode(target_item) - if not target_root or target_root.data(0, Qt.UserRole + 1) != "SCENE_ROOT": - print(f"❌ 目标节点 {target_item.text(0)} 不在场景下") - return False - - # 3. 禁止拖拽场景根节点 - dragged_root = self._getRootNode(dragged_item) - if (dragged_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT" or - not dragged_root or dragged_root.data(0, Qt.UserRole + 1) != "SCENE_ROOT"): - print(f"❌ 禁止拖拽场景根节点或根节点外的节点") - return False - - # 4. Qt 树循环检查 - current = target_item - while current: - if current == dragged_item: - print(f"❌ Qt 树检测:{target_item.text(0)} 是 {dragged_item.text(0)} 的后代") - return False - current = current.parent() - - # 5. GUI元素类型验证 - 新增功能 - if not self._validateGUITypeCompatibility(dragged_item, target_item): - return False - - return True - - def _validateGUITypeCompatibility(self, dragged_item, target_item): - """验证GUI元素类型兼容性""" - try: - # 获取节点类型标识 - dragged_type = dragged_item.data(0, Qt.UserRole + 1) - target_type = target_item.data(0, Qt.UserRole + 1) - - # 定义2D GUI元素类型 - gui_2d_types = self.gui_2d_types - - # 定义3D GUI元素类型 - gui_3d_types = self.gui_3d_types - - # 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素) - scene_3d_types = self.scene_3d_types - - # 检查拖拽元素的类型 - is_dragged_2d_gui = dragged_type in gui_2d_types - is_dragged_3d_gui = dragged_type in gui_3d_types - is_dragged_3d_scene = dragged_type in scene_3d_types - - # 检查目标的类型 - is_target_2d_gui = target_type in gui_2d_types - is_target_3d_gui = target_type in gui_3d_types - is_target_3d_scene = target_type in scene_3d_types - - # === 严格的类型隔离验证逻辑 === - - # 1. 2D GUI元素的拖拽限制 - 只能拖拽到其他2D GUI元素下 - if is_dragged_2d_gui: - if target_type == "SCENE_ROOT": - return True - elif is_target_2d_gui: - print(f"✅ 2D GUI元素 {dragged_item.text(0)} 可以拖拽到2D GUI父节点 {target_item.text(0)}") - return True - elif is_target_3d_gui: - print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到3D GUI元素 {target_item.text(0)} 下") - print(" 💡 提示: 2D GUI元素只能作为其他2D GUI元素的子节点") - return False - elif is_target_3d_scene: - print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到3D场景节点 {target_item.text(0)} 下") - print(" 💡 提示: 2D GUI元素应该保持在2D GUI层级结构中") - return False - else: - print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") - return False - - # 2. 3D GUI元素的拖拽限制 - 只能拖拽到3D场景节点下 - elif is_dragged_3d_gui: - if is_target_3d_scene: - print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}") - return False - elif is_target_2d_gui: - print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下") - print(" 💡 提示: 3D GUI元素不能与2D GUI元素建立父子关系") - return False - elif is_target_3d_gui: - print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到其他3D GUI元素 {target_item.text(0)} 下") - print(" 💡 提示: 允许3D GUI元素之间建立父子关系") - return True - else: - print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") - return False - - # 3. 3D场景元素的拖拽限制 - 只能拖拽到3D场景节点或3D GUI元素下 - elif is_dragged_3d_scene: - if is_target_3d_scene: - print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}") - return True - elif is_target_2d_gui: - print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下") - print(" 💡 提示: 3D场景元素不能与2D GUI元素建立父子关系") - print(" 💡 建议: 将3D场景元素拖拽到其他3D场景节点或场景根节点下") - return False - elif is_target_3d_gui: - print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D GUI元素 {target_item.text(0)} 下") - print(" 💡 提示: 允许3D场景元素挂载在3D GUI元素下") - return False - else: - print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") - return False - - # 4. 其他未分类元素 - 严格禁止拖拽到2D GUI下 - else: - if is_target_2d_gui: - print(f"❌ 元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下") - print(" 💡 提示: 只有2D GUI元素才能作为其他2D GUI元素的子节点") - return False - elif is_target_3d_gui or is_target_3d_scene: - print(f"✅ 允许元素 {dragged_item.text(0)} 拖拽到3D节点 {target_item.text(0)}") - return True - else: - print(f"❌ 元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") - return False - - # 默认情况(理论上不应该到达这里) - print(f"⚠️ GUI类型验证遇到未处理的情况: {dragged_type} -> {target_type}") - return False - - except Exception as e: - print(f"❌ GUI类型兼容性验证失败: {str(e)}") - # 出错时采用保守策略,禁止拖拽 - return False - - def _getRootNode(self, item): - """获取树中节点的根节点项""" - if not item: - return None - - current = item - while current.parent(): - current = current.parent() - return current - - def dragEnterEvent(self, event): - """处理拖入事件""" - if event.source() == self: - event.accept() - else: - event.ignore() - - def dragMoveEvent(self, event): - """处理拖动事件""" - indicator_pos = self.dropIndicatorPosition() - indicator_str = "Unknown" - - if indicator_pos == QAbstractItemView.DropIndicatorPosition.OnItem: - indicator_str = "OnItem" - elif indicator_pos == QAbstractItemView.DropIndicatorPosition.AboveItem: - indicator_str = "AboveItem" - elif indicator_pos == QAbstractItemView.DropIndicatorPosition.BelowItem: - indicator_str = "BelowItem" - elif indicator_pos == QAbstractItemView.DropIndicatorPosition.OnViewport: - indicator_str = "OnViewport" - - #print(f'indicator pos: {indicator_str} (value: {int(indicator_pos)})') - - if event.source() != self: - event.ignore() - return - - # 获取当前拖拽的项目和目标位置 - target_item = self.itemAt(event.pos()) - selected_items = self.selectedItems() - - # 检查是否拖拽到多选区域内的项目 - if target_item and target_item in selected_items: - event.ignore() - return - - # 检查其他禁止条件 - if target_item and selected_items: - for dragged_item in selected_items: - if not self.isValidParentChild(dragged_item, target_item): - event.ignore() - return - - super().dragMoveEvent(event) - event.accept() - - def keyPressEvent(self, event): - """处理键盘按键事件""" - if event.key() == Qt.Key_Delete: - # currentItem = self.currentItem() - # if currentItem and currentItem.parent(): - # # 检查是否是模型节点或其子节点 - # if self.world.interface_manager.isModelOrChild(currentItem): - # nodePath = currentItem.data(0, Qt.UserRole) - # if nodePath: - # print("正在删除节点...") - # self.world.interface_manager.deleteNode(nodePath, currentItem) - # print("删除完成") - selected_items = self.selectedItems() - if selected_items: - # 执行删除操作 - # if selected_items.data(0, Qt.UserRole + 1) == "LIGHT_NODE": - # self._preprocess_light_items_for_deletion(selected_items) - self.delete_items(selected_items) - else: - # 没有选中任何项目,执行默认操作 - super().keyPressEvent(event) - else: - super().keyPressEvent(event) - - def _preprocess_light_items_for_deletion(self, selected_items): - """预处理灯光节点删除,特别处理最后一个灯光节点的问题""" - if not selected_items: - return selected_items - - # 检查选中的项目中是否包含灯光节点 - light_items = [] - for item in selected_items: - node_type = item.data(0, Qt.UserRole + 1) - if node_type == "LIGHT_NODE": - light_items.append(item) - - # 如果没有灯光节点,直接返回 - if not light_items: - return selected_items - - # 检查是否只有最后一个灯光节点被选中 - processed_items = list(selected_items) # 创建副本 - - for item in light_items: - panda_node = item.data(0, Qt.UserRole) - if not panda_node: - continue - - # 获取灯光类型 - if hasattr(panda_node, 'getTag'): - light_type = panda_node.getTag("light_type") - - # 检查是否是最后一个Spotlight - if (light_type == "spot_light" and hasattr(self.world, 'Spotlight') and - self.world.Spotlight and self.world.Spotlight[-1] == panda_node and - len(self.world.Spotlight) > 1): - - print(f"⚠️ 检测到选中最后一个Spotlight节点: {item.text(0)}") - # 这里可以添加特殊处理逻辑,比如提示用户或阻止删除 - - # 检查是否是最后一个Pointlight - elif (light_type == "point_light" and hasattr(self.world, 'Pointlight') and - self.world.Pointlight and self.world.Pointlight[-1] == panda_node and - len(self.world.Pointlight) > 1): - - print(f"⚠️ 检测到选中最后一个Pointlight节点: {item.text(0)}") - # 这里可以添加特殊处理逻辑,比如提示用户或阻止删除 - - return processed_items - - def delete_items(self, selected_items): - """删除选中的item - 简化版本""" - if not selected_items: - return - - # 1. 过滤掉不能删除的节点 - deletable_items = [] - for item in selected_items: - node_type = item.data(0, Qt.UserRole + 1) - panda_node = item.data(0, Qt.UserRole) - - # 跳过场景根节点和主相机 - if (node_type == "SCENE_ROOT" or - (panda_node and hasattr(self.world, 'cam') and panda_node == self.world.cam)): - continue - deletable_items.append(item) - - if not deletable_items: - StyledMessageBox.information(self, "提示", "没有可删除的节点") - return - - # 2. 确认删除 - item_count = len(deletable_items) - if item_count == 1: - message = f"确定要删除节点 \"{deletable_items[0].text(0)}\" 吗?" - else: - message = f"确定要删除 {item_count} 个节点吗?" - - dialog = UniversalMessageDialog( - self, - "确认删除", - message, - message_type=UniversalMessageDialog.WARNING, - show_cancel=True, - confirm_text="确认", - cancel_text="取消" - ) - - if dialog.exec_() != QDialog.Accepted: - return - - # 默认选中场景根节点,通常是第一个顶级节点 - #next_item_to_select = self.topLevelItem(0) - - # 3. 执行删除循环 - deleted_count = 0 - for item in deletable_items: - try: - # 在删除前,记录其父节点,作为删除后的新选择 - # 选择最后一个被删除项的父节点作为新的焦点 - if item.parent(): - next_item_to_select = item.parent() - panda_node = item.data(0, Qt.UserRole) - - if panda_node: - # 清理选择状态 - if (hasattr(self.world, 'selection') and - hasattr(self.world.selection, 'selectedNode') and - self.world.selection.selectedNode == panda_node): - self.world.selection.updateSelection(None) - - # 清理特殊资源(参考interface_manager.py的逻辑) - if hasattr(self.world, 'property_panel'): - self.world.property_panel.removeActorForModel(panda_node) - - # 清理灯光 - if hasattr(panda_node, 'getPythonTag'): - light_object = panda_node.getPythonTag('rp_light_object') - if light_object and hasattr(self.world, 'render_pipeline'): - self.world.render_pipeline.remove_light(light_object) - - # 从world列表中移除 - if hasattr(self.world, 'gui_elements') and panda_node in self.world.gui_elements: - self.world.gui_elements.remove(panda_node) - if hasattr(self.world, 'models') and panda_node in self.world.models: - self.world.models.remove(panda_node) - if hasattr(self.world, 'Spotlight') and panda_node in self.world.Spotlight: - self.world.Spotlight.remove(panda_node) - if hasattr(self.world, 'Pointlight') and panda_node in self.world.Pointlight: - self.world.Pointlight.remove(panda_node) - if hasattr(self.world, 'terrains') and panda_node in self.world.terrains: - self.world.terrains.remove(panda_node) - if hasattr(self.world, 'tilesets') and panda_node in self.world.tilesets: - # self.world.tilesets.remove(panda_node) - # 从 tilesets 列表中移除 - if hasattr(self.world, 'scene_manager'): - tilesets_to_remove = [] - for i, tileset_info in enumerate(self.world.scene_manager.tilesets): - if tileset_info['node'] == panda_node: - tilesets_to_remove.append(i) - - # 从后往前删除,避免索引问题 - for i in reversed(tilesets_to_remove): - del self.world.scene_manager.tilesets[i] - - # 从Panda3D场景中移除 - try: - if not panda_node.isEmpty(): - panda_node.removeNode() - except Exception as e: - print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}") - - # 从Qt树中移除 - parent_item = item.parent() - if parent_item: - parent_item.removeChild(item) - else: - index = self.indexOfTopLevelItem(item) - if index >= 0: - self.takeTopLevelItem(index) - - deleted_count += 1 - print(f"✅ 删除节点: {item.text(0)}") - - except Exception as e: - print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}") - import traceback - traceback.print_exc() - - # 最终清理 - # if hasattr(self.world, 'property_panel'): - # self.world.property_panel.clearPropertyPanel() - - # 4. 删除操作完成后,更新UI --- - if deleted_count > 0: - print(f"🎉 成功删除 {deleted_count} 个节点。正在更新UI...") - self.update_selection_and_properties(None, None) - - def delete_item(self, panda_node): - """删除指定节点 panda3D(node)- 优化和修复版本""" - if not panda_node or panda_node.is_empty(): - print("ℹ️ 尝试删除一个空的或无效的节点,操作取消。") - return - - # #如果有命令管理系统,则使用命令系统 - # if hasattr(self.world,'command_manager') and self.world.command_manager: - # from core.Command_System import DeleteNodeCommand - # parent_node = panda_node.getParent() - # command = DeleteNodeCommand(panda_node,parent_node) - # self.world.command_manager.execute_command(command) - # return - - # --- 关键修复:在操作前,安全地获取节点名字 --- - node_name_for_logging = panda_node.getName() - - # 1. 寻找对应的Qt Item - item = self.world.interface_manager.findTreeItem(panda_node, self._findSceneRoot()) - - # 场景清理(无论是否找到item,都应该执行) - self._cleanup_panda_node_resources(panda_node) - panda_node.removeNode() - - # 如果没有找到item,说明UI已经移除或不同步,清理完Panda3D资源后即可退出 - if not item: - print(f"✅ Panda3D节点 '{node_name_for_logging}' 已清理并移除。UI树中未找到对应项。") - return - try: - # 2. 过滤受保护节点 - node_type = item.data(0, Qt.UserRole + 1) - if node_type == "SCENE_ROOT": # 相机检查已包含在panda_node判空中 - print(f"ℹ️ 节点 {item.text(0)} 是受保护节点,无法删除。") - return - - # 3. 从UI树中移除 - parent_for_next_selection = item.parent() - if item.parent(): - item.parent().removeChild(item) - else: - index = self.indexOfTopLevelItem(item) - if index >= 0: - self.takeTopLevelItem(index) - - print(f"✅ 成功删除节点: {node_name_for_logging}") - - # 4. 更新UI - print(f"🔄 正在更新UI...") - if parent_for_next_selection and self.indexFromItem(parent_for_next_selection).isValid(): - new_selection_item = parent_for_next_selection - else: - new_selection_item = self.topLevelItem(0) - - if new_selection_item: - self.setCurrentItem(new_selection_item) - new_panda_node_to_select = new_selection_item.data(0, Qt.UserRole) - self.update_selection_and_properties(new_panda_node_to_select, new_selection_item) - else: - self.update_selection_and_properties(None, None) - - except Exception as e: - print(f"❌ 删除节点 {node_name_for_logging} 时发生意外错误: {str(e)}") - import traceback - traceback.print_exc() - - def clear_tree(self): - """清空UI树""" - print("Clear") - self.clear() - # 创建场景根节点 - sceneRoot = QTreeWidgetItem(self, ['场景']) - sceneRoot.setData(0, Qt.UserRole, self.world.render) - sceneRoot.setData(0, Qt.UserRole + 1, "SCENE_ROOT") - self._apply_font_to_item(sceneRoot) - # 添加相机节点 - cameraItem = QTreeWidgetItem(sceneRoot, ['相机']) - cameraItem.setData(0, Qt.UserRole, self.world.cam) - cameraItem.setData(0, Qt.UserRole + 1, "CAMERA_NODE") - self._apply_font_to_item(cameraItem) - # 添加地板节点 - if hasattr(self.world, 'ground') and self.world.ground: - groundItem = QTreeWidgetItem(sceneRoot, ['地板']) - groundItem.setData(0, Qt.UserRole, self.world.ground) - groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE") - self._apply_font_to_item(groundItem) - - def _apply_font_to_item(self, item): - """根据节点层级设置字体大小""" - if not item: - return - font = QFont(self.font()) - marker = item.data(0, Qt.UserRole + 1) - if item.parent() is None and (marker == "SCENE_ROOT" or item.text(0) == '场景'): - font = QFont("Microsoft YaHei", 12) - font.setWeight(QFont.Light) - else: - font = QFont("Microsoft YaHei", 10) - font.setWeight(QFont.Light) - item.setFont(0, font) - - def _apply_font_recursively(self, item): - """递归应用字体样式""" - if not item: - return - self._apply_font_to_item(item) - for index in range(item.childCount()): - self._apply_font_recursively(item.child(index)) - - def _handle_rows_inserted(self, parent_index, first, last): - """在插入新节点时自动调整字体""" - model = self.model() - if not model: - return - for row in range(first, last + 1): - index = model.index(row, 0, parent_index) - item = self.itemFromIndex(index) - if item: - self._apply_font_recursively(item) - - def _cleanup_panda_node_resources(self, panda_node): - """一个集中的辅助函数,用于清理与Panda3D节点相关的所有资源。""" - if not panda_node or panda_node.is_empty(): - return - try: - # 清理选择状态 - if hasattr(self.world, 'selection') and self.world.selection.selectedNode == panda_node: - self.world.selection.updateSelection(None) - # 清理属性面板 - if hasattr(self.world, 'property_panel'): - self.world.property_panel.removeActorForModel(panda_node) - # 清理灯光 - if hasattr(panda_node, 'getPythonTag'): - light_object = panda_node.getPythonTag('rp_light_object') - if light_object and hasattr(self.world, 'render_pipeline'): - self.world.render_pipeline.remove_light(light_object) - # 从各种world管理列表中移除 - lists_to_check = ['gui_elements', 'models', 'Spotlight', 'Pointlight', 'terrains'] - for list_name in lists_to_check: - if hasattr(self.world, list_name): - world_list = getattr(self.world, list_name) - if panda_node in world_list: - world_list.remove(panda_node) - # 特殊处理tilesets - if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'tilesets'): - tilesets_to_remove = [i for i, info in enumerate(self.world.scene_manager.tilesets) if - info.get('node') == panda_node] - for i in reversed(tilesets_to_remove): - del self.world.scene_manager.tilesets[i] - print(f"🧹 已清理节点 {panda_node.getName()} 的所有关联资源。") - except Exception as e: - # 即便这里出错,也要打印信息,但不要让整个删除流程中断 - print(f"⚠️ 清理节点 {panda_node.getName()} 资源时出错: {e}") - - # def mousePressEvent(self, event): - # """鼠标按下事件""" - # if event.button() == Qt.LeftButton: - # if self.currentItem(): - # print(f"self.currentItem() = {self.currentItem()}") - # else: - # print(f"self.currentItem() = None") - # - # # 调用父类处理其他事件 - # super().mousePressEvent(event) - - def update_item_name(self, text, item): - """ 树节点名字 """ - if not item: - return - try: - # 正确的代码 - node = item.data(0, Qt.UserRole) - - item.setText(0, text) - node.setName(text) - except Exception as e: - print(e) - - def _findSceneRoot(self): - """查找场景根节点""" - for i in range(self.topLevelItemCount()): - top_item = self.topLevelItem(i) - if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": - return top_item - return None - - def create_model_items(self, model: NodePath): - """ - 【此函数保持不变】 - 创建模型项。 - 只寻找模型下一层带有 'is_scene_element' 标签的子节点作为分支的根, - 然后完整地展示这些分支。 - """ - if not model: - print("传入的参数model为空") - return - - # 找到场景树的根节点,我们将把模型节点添加到这里 - root_item = self._findSceneRoot() - if not root_item: - print("错误:未能找到场景根节点项") - return - - # 1. 在模型的第一层子节点中进行筛选 - for child_node in model.getChildren(): - if child_node.hasTag("is_scene_element"): - print(f"找到带标签的根节点:{child_node.getName()}") - if (child_node.hasTag("gui_type")and - child_node.getTag("gui_type") in ["3d_text","3d_image","video_screen"]): - print(f"跳过3dGUI节点{child_node.getName()}") - continue - - # 为这个带标签的节点创建一个树项 - child_item = QTreeWidgetItem(root_item) - child_item.setText(0, child_node.getName() or "Unnamed Tagged Node") - child_item.setData(0, Qt.UserRole, child_node) - child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type")) - # self._add_node_info(child_item, child_node) # 可选信息 - - # 2. 对这个节点的所有后代进行“无条件”递归添加 (但会跳过碰撞体) - self._add_all_children_unconditionally(child_item, child_node) - - def _add_all_children_unconditionally(self, parent_item: QTreeWidgetItem, node_path: NodePath): - """ - 【此函数已更新】 - 无条件地、递归地添加一个节点下的所有子节点,但会跳过碰撞节点。 - """ - for child_node in node_path.getChildren(): - - # 新增:检查节点是否为碰撞节点 - if isinstance(child_node.node(), CollisionNode): - # print(f"跳过碰撞节点: {child_node.getName()}") # 用于调试 - continue # 如果是,则跳过此节点及其所有子节点 - - # 创建子项 - child_item = QTreeWidgetItem(parent_item) - child_item.setText(0, child_node.getName() or "Unnamed Child") - child_item.setData(0, Qt.UserRole, child_node) - child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type")) - # self._add_node_info(child_item, child_node) # 可选信息 - - # 继续无条件地递归 - if not child_node.is_empty(): - self._add_all_children_unconditionally(child_item, child_node) - - # ==================== 辅助方法 ==================== - def _findSceneRoot(self): - """查找场景根节点""" - for i in range(self.topLevelItemCount()): - top_item = self.topLevelItem(i) - if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": - return top_item - return - - def _generateUniqueNodeName(self, base_name, parent_node): - """生成唯一的节点名称""" - # 获取父节点下所有子节点的名称 - existing_names = set() - for child in parent_node.getChildren(): - existing_names.add(child.getName()) - - # 如果基础名称不存在,直接使用 - if base_name not in existing_names: - return base_name - - # 否则添加数字后缀 - counter = 1 - while f"{base_name}_{counter}" in existing_names: - counter += 1 - - return f"{base_name}_{counter}" - - def add_node_to_tree_widget(self, node, parent_item, node_type): - """将node元素添加到树形控件""" - if hasattr(node, 'getTag'): - if node.hasTag('tree_item_type'): - print(f"node0: {node.getName()},{node.getTag('tree_item_type')}") - tree_type = node.getTag('tree_item_type') - else: - node.setTag('tree_item_type', node_type) - else: - print(f"node2: {node.getName()},{node_type}") - tree_type = node_type - - - # BLACK_LIST 和依赖项导入保持不变 - BLACK_LIST = {'', '**', 'temp', 'collision'} - from panda3d.core import CollisionNode, ModelRoot - from PyQt5.QtWidgets import QTreeWidgetItem - from PyQt5.QtCore import Qt - - # 1. 修改内部函数,让它返回创建的节点 - def addNodeToTree(node, parentItem, force=False): - """内部递归函数,现在会返回创建的顶级节点项""" - if not force and should_skip(node): - return None # 如果跳过,返回None - - nodeItem = QTreeWidgetItem(parentItem, [node.getName()]) - nodeItem.setData(0, Qt.UserRole, node) - nodeItem.setData(0, Qt.UserRole + 1, tree_type) - - for child in node.getChildren(): - # 递归调用,但我们只关心顶级的nodeItem - addNodeToTree(child, nodeItem, force=False) - - return nodeItem # <-- 新增:返回创建的QTreeWidgetItem - - def should_skip(node): - name = node.getName() - return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(), CollisionNode) or isinstance( - node.node(), ModelRoot) or name == "" - - # 使用一个变量来确保无论哪个分支都有返回值 - new_qt_item = None - node_name = "" - - try: - if tree_type == "IMPORTED_MODEL_NODE": - # getTag('file') 可能是你自己设置的tag,这里假设它存在 - node_name = node.getTag("file") if hasattr(node, 'getTag') and node.hasTag("file") else node.getName() - - # 2. 接收 addNodeToTree 的返回值 - new_qt_item = addNodeToTree(node, parent_item, force=True) - - else: - node_name = node.getName() if hasattr(node, 'getName') else "node" - new_qt_item = QTreeWidgetItem(parent_item, [node_name]) - new_qt_item.setData(0, Qt.UserRole, node) - new_qt_item.setData(0, Qt.UserRole + 1, tree_type) - - # 确保 new_qt_item 成功创建后再继续操作 - if new_qt_item: - # 展开父节点 - if hasattr(parent_item, 'setExpanded'): - parent_item.setExpanded(True) - - print(f"✅ Qt树节点添加成功: {node_name}") - return new_qt_item - else: - # 如果 addNodeToTree 因为 should_skip 返回了 None - print(f"ℹ️ 节点 {node_name} 被跳过,未添加到树中。") - return None - - except Exception as e: - import traceback - print(f"❌ 添加node到树形控件失败: {str(e)}") - traceback.print_exc() # 打印更详细的错误堆栈,方便调试 - return None - - def update_selection_and_properties(self, node, qt_item): - """更新选择状态和属性面板""" - try: - # 更新选择状态 - if hasattr(self.world, 'selection'): - self.world.selection.updateSelection(node) - - # 更新属性面板 - if hasattr(self.world, 'property_panel'): - self.world.property_panel.updatePropertyPanel(qt_item) - elif hasattr(self.world, 'updatePropertyPanel'): - self.world.updatePropertyPanel(qt_item) - - print(f"✅ 更新选择和属性面板: {qt_item.text(0)}") - - except Exception as e: - print(f"❌ 更新选择和属性面板失败: {str(e)}") - - # ==================== 3D辅助方法 ==================== - def get_target_parents_for_creation(self): - """获取创建目标的父节点列表""" - from PyQt5.QtCore import Qt - - target_parents = [] - - try: - selected_items = self.selectedItems() - - # 检查选中的项目,找出所有有效的父节点 - for item in selected_items: - if self._isValidParentForNewNode(item): - parent_node = item.data(0, Qt.UserRole) - if parent_node: - target_parents.append((item, parent_node)) - print(f"📍 找到有效父节点: {item.text(0)}") - - # 如果没有有效的选中节点,使用场景根节点 - if not target_parents: - print("⚠️ 没有有效选中节点,使用场景根节点") - scene_root = self._findSceneRoot() - if scene_root: - parent_node = scene_root.data(0, Qt.UserRole) - if parent_node: - target_parents.append((scene_root, parent_node)) - else: - # 如果没有_findSceneRoot方法,使用world.render作为默认父节点 - if hasattr(self.world, 'render'): - # 创建一个虚拟的树项目来表示render节点 - class MockTreeItem: - def text(self, column): - return "render" - - mock_item = MockTreeItem() - target_parents.append((mock_item, self.world.render)) - - print(f"📊 总共找到 {len(target_parents)} 个目标父节点") - return target_parents - - except Exception as e: - print(f"❌ 获取目标父节点失败: {str(e)}") - return [] - - def _isValidParentForNewNode(self, item): - """检查节点是否适合作为新节点的父节点-3d""" - if not item: - return False - - # 获取节点类型标识 - node_type = item.data(0, Qt.UserRole + 1) - - # 场景根节点和普通场景节点可以作为父节点 - if node_type in self.valid_3d_parent_types: - return True - - # # 模型节点也可以作为父节点 - # panda_node = item.data(0, Qt.UserRole) - # if panda_node and panda_node.hasTag("is_model_root"): - # return True - # - # # 其他类型的节点也可以,但排除一些特殊情况 - # if panda_node: - # # # 排除相机节点 - # # if panda_node == self.world.cam: - # # return False - # # 排除碰撞节点 - # from panda3d.core import CollisionNode - # if isinstance(panda_node.node(), CollisionNode): - # return False - # return True - - return False - - - def get_target_parents_for_gui_creation(self): - """获取GUI创建目标的父节点列表 - 支持GUI元素作为父节点""" - from PyQt5.QtCore import Qt - - target_parents = [] - - try: - selected_items = self.selectedItems() - - # 检查选中的项目,找出所有有效的父节点 - for item in selected_items: - if self.isValidParentForGUI(item): - parent_node = item.data(0, Qt.UserRole) - if parent_node: - target_parents.append((item, parent_node)) # 修复:确保添加到列表 - print(f"📍 找到有效GUI父节点: {item.text(0)}") - else: - print(f"⚠️ GUI父节点 {item.text(0)} 的Panda3D数据为空") - - # 如果没有有效的选中节点,使用场景根节点 - if not target_parents: - print("⚠️ 没有有效选中节点,使用场景根节点") - scene_root = self._findSceneRoot() - if scene_root: - parent_node = scene_root.data(0, Qt.UserRole) - if parent_node: - target_parents.append((scene_root, parent_node)) - else: - if hasattr(self.world, 'render'): - class MockTreeItem: - def text(self, column): - return "render" - - mock_item = MockTreeItem() - target_parents.append((mock_item, self.world.render)) - - print(f"📊 总共找到 {len(target_parents)} 个目标父节点") - return target_parents - - except Exception as e: - print(f"❌ 获取目标父节点失败: {str(e)}") - return [] - - def isValidParentForGUI(self, item): - """检查节点是否适合作为GUI元素的父节点""" - if not item: - return False - - # 获取节点类型标识 - node_type = item.data(0, Qt.UserRole + 1) - - # GUI元素可以作为其他GUI元素的父节点 - if node_type in self.gui_2d_types: - return True - - # 场景根节点和普通场景节点也可以作为父节点 - if node_type in ["SCENE_ROOT"]: - return True - - return False - - def is_gui_element(self, node): - """判断节点是否是GUI元素""" - if not node: - return False - - # 检查是否有GUI标签 - if hasattr(node, 'getTag'): - return node.getTag("is_gui_element") == "1" or node.getTag("gui_type") in ["info_panel", "button", "label", "entry", "3d_text", "virtual_screen"] - - # 检查是否是DirectGUI对象 - try: - from direct.gui.DirectGuiBase import DirectGuiBase - return isinstance(node, DirectGuiBase) - except ImportError: - return False - - def calculate_relative_gui_position(self, pos, parent_gui): - """计算相对于GUI父节点的位置""" - try: - # 对于GUI子元素,使用相对坐标 - # 这里使用较小的缩放因子,因为是相对于父GUI的位置 - relative_pos = (pos[0] * 0.05, 0, pos[2] * 0.05) - print(f"📐 计算GUI相对位置: {pos} -> {relative_pos}") - return relative_pos - except Exception as e: - print(f"❌ 计算GUI相对位置失败: {str(e)}") - # 如果计算失败,返回默认的屏幕坐标 - return (pos[0] * 0.1, 0, pos[2] * 0.1) - - #---------------------------暂时无用------------------------------- - - def add_existing_node(self, panda_node, node_type="SCENE_NODE", parent_item=None): - """将已存在的Panda3D节点添加到Qt树形控件中 - - Args: - panda_node: 已创建的Panda3D节点 - node_type: 节点类型标识 - parent_item: 父Qt项目,如果为None则使用当前选中项或根节点 - - Returns: - QTreeWidgetItem: 创建的Qt树项目 - """ - try: - if not panda_node: - print("❌ 传入的Panda3D节点为空") - return None - - # 确定父项目 - 保持简单,只处理单个父节点 - if parent_item is None: - # 优先使用当前选中的第一个有效节点作为父节点 - selected_items = self.selectedItems() - if selected_items: - # 找到第一个有效的父节点 - for potential_parent in selected_items: - if self._isValidParentForNewNode(potential_parent): - parent_item = potential_parent - print(f"📍 使用选中节点作为父节点: {parent_item.text(0)}") - break - - # 如果没有找到有效的选中节点 - if not parent_item: - print("⚠️ 所有选中节点都不适合作为父节点,查找场景根节点") - parent_item = self._findSceneRoot() - else: - # 没有选中任何节点,使用场景根节点 - print("📍 没有选中节点,使用场景根节点作为父节点") - parent_item = self._findSceneRoot() - - # 如果场景根节点也找不到,最后尝试render节点 - if not parent_item: - print("📍 场景根节点未找到,尝试使用render节点") - parent_item = self._findRenderItem() - - if not parent_item: - print("❌ 无法找到合适的父节点") - return None - - # 创建Qt树项目 - node_name = panda_node.getName() - new_qt_item = QTreeWidgetItem(parent_item, [node_name]) - new_qt_item.setData(0, Qt.UserRole, panda_node) - new_qt_item.setData(0, Qt.UserRole + 1, node_type) - - # 展开父节点 - parent_item.setExpanded(True) - - print(f"✅ 成功将现有节点添加到树形控件: {node_name} -> 父节点: {parent_item.text(0)}") - return new_qt_item - - except Exception as e: - print(f"❌ 添加现有节点到树形控件失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def _findRenderItem(self): - """查找render根节点项目""" - try: - root = self.invisibleRootItem() - for i in range(root.childCount()): - item = root.child(i) - if item.text(0) == "render": - return item - - # 如果没找到render节点,返回第一个子项目 - if root.childCount() > 0: - return root.child(0) - - return None - except Exception as e: - print(f"查找render节点失败: {e}") - return None - - def create_item(self, node_type="empty", selected_items=None): - """创建不同类型的场景节点 - - Args: - node_type: 节点类型 ("empty", "spot_light", "point_light") - selected_items: 选中的父节点项目列表,如果为None则使用当前选中项或根节点 - """ - try: - # 确定父节点 - parent_items = self._determineParentItems(selected_items) - if not parent_items: - print("⚠️ 无法确定父节点") - return [] - - created_nodes = [] - - for parent_item in parent_items: - # 验证父节点的有效性 - if not self._isValidParentForNewNode(parent_item): - print(f"⚠️ 节点 {parent_item.text(0)} 不适合作为父节点") - continue - - # 获取父节点的Panda3D对象 - parent_node = parent_item.data(0, Qt.UserRole) - if not parent_node: - print(f"⚠️ 父节点 {parent_item.text(0)} 没有对应的Panda3D对象") - continue - - # 根据节点类型创建不同的节点 - if node_type == "empty": - new_node = self._createEmptyNode(parent_node, parent_item) - elif node_type == "spot_light": - new_node = self._createSpotLightNode(parent_node, parent_item) - elif node_type == "point_light": - new_node = self._createPointLightNode(parent_node, parent_item) - else: - print(f"❌ 不支持的节点类型: {node_type}") - continue - - if new_node: - created_nodes.append(new_node) - - # 如果只创建了一个节点,自动选中它 - if len(created_nodes) == 1: - _, qt_item = created_nodes[0] - self.setCurrentItem(qt_item) - # 更新选择和属性面板 - if hasattr(self.world, 'selection'): - self.world.selection.updateSelection(qt_item.data(0, Qt.UserRole)) - if hasattr(self.world, 'property_panel'): - self.world.property_panel.updatePropertyPanel(qt_item) - - print(f"✅ 总共创建了 {len(created_nodes)} 个 {node_type} 节点") - return created_nodes - - except Exception as e: - print(f"❌ 创建 {node_type} 节点失败: {str(e)}") - import traceback - traceback.print_exc() - return [] - - def _determineParentItems(self, selected_items): - """确定父节点项目列表""" - if selected_items is not None: - return selected_items - - # 使用当前选中的项目 - current_selected = self.selectedItems() - if current_selected: - return current_selected - - # 如果没有选中任何项目,使用场景根节点 - scene_root = self._findSceneRoot() - if scene_root: - return [scene_root] - - return [] - - def _setupNewNodeDefaults(self, node): - """设置新节点的默认属性""" - # 设置默认位置(相对于父节点) - node.setPos(0, 0, 0) - node.setHpr(0, 0, 0) - node.setScale(1, 1, 1) - - # 设置默认可见性 - node.show() - - # 可以根据需要添加更多默认设置 - # 例如:默认材质、碰撞检测等 - - # ==================== 创建节点 ==================== - def _createEmptyNode(self, parent_node, parent_item): - """创建空节点""" - # 生成唯一的节点名称 - node_name = self._generateUniqueNodeName("空节点", parent_node) - - # 在Panda3D场景中创建新节点 - new_panda_node = parent_node.attachNewNode(node_name) - - # 设置新节点的默认属性 - self._setupNewNodeDefaults(new_panda_node) - - # 设置节点标签 - new_panda_node.setTag("is_scene_element", "1") - new_panda_node.setTag("node_type", "empty_node") - new_panda_node.setTag("created_by_user", "1") - - # 在Qt树中创建对应的项目 - new_qt_item = QTreeWidgetItem(parent_item, [node_name]) - new_qt_item.setData(0, Qt.UserRole, new_panda_node) - new_qt_item.setData(0, Qt.UserRole + 1, "SCENE_NODE") - - # 展开父节点 - parent_item.setExpanded(True) - - print(f"✅ 成功创建空节点: {node_name}") - return (new_panda_node, new_qt_item) - - def _createSpotLightNode(self, parent_node, parent_item): - """创建聚光灯节点""" - from RenderPipelineFile.rpcore import SpotLight - from QMeta3D.Meta3DWorld import get_render_pipeline - from panda3d.core import Vec3, NodePath - - try: - render_pipeline = get_render_pipeline() - - # 生成唯一的节点名称 - light_name = self._generateUniqueNodeName(f"Spotlight_{len(self.world.Spotlight)}", parent_node) - - # 创建挂载节点 - light_np = NodePath(light_name) - light_np.reparentTo(parent_node) - - # 创建聚光灯对象 - light = SpotLight() - light.direction = Vec3(0, 0, -1) - light.fov = 70 - light.set_color_from_temperature(5 * 1000.0) - light.energy = 5000 - light.radius = 1000 - light.casts_shadows = True - light.shadow_map_resolution = 256 - light.setPos(0, 0, 0) # 相对于父节点的位置 - - # 添加到渲染管线 - render_pipeline.add_light(light) - - # 设置节点属性和标签 - light_np.setTag("light_type", "spot_light") - light_np.setTag("is_scene_element", "1") - light_np.setTag("light_energy", str(light.energy)) - light_np.setTag("created_by_user", "1") - - # 保存光源对象引用 - light_np.setPythonTag("rp_light_object", light) - - # 添加到管理列表 - self.world.Spotlight.append(light_np) - - # 在Qt树中创建对应的项目 - new_qt_item = QTreeWidgetItem(parent_item, [light_name]) - new_qt_item.setData(0, Qt.UserRole, light_np) - new_qt_item.setData(0, Qt.UserRole + 1, "LIGHT_NODE") - - # 展开父节点 - parent_item.setExpanded(True) - - print(f"✅ 成功创建聚光灯: {light_name}") - - except Exception as e: - print(f"❌ 创建聚光灯失败: {str(e)}") - return None - - def _createPointLightNode(self, parent_node, parent_item): - """创建点光源节点""" - from RenderPipelineFile.rpcore import PointLight - from QMeta3D.Meta3DWorld import get_render_pipeline - from panda3d.core import Vec3, NodePath - - try: - render_pipeline = get_render_pipeline() - - # 生成唯一的节点名称 - light_name = self._generateUniqueNodeName(f"Pointlight_{len(self.world.Pointlight)}", parent_node) - - # 创建挂载节点 - light_np = NodePath(light_name) - light_np.reparentTo(parent_node) - - # 创建点光源对象 - light = PointLight() - light.setPos(0, 0, 0) # 相对于父节点的位置 - light.energy = 5000 - light.radius = 1000 - light.inner_radius = 0.4 - light.set_color_from_temperature(5 * 1000.0) - light.casts_shadows = True - light.shadow_map_resolution = 256 - - # 添加到渲染管线 - render_pipeline.add_light(light) - - # 设置节点属性和标签 - light_np.setTag("light_type", "point_light") - light_np.setTag("is_scene_element", "1") - light_np.setTag("light_energy", str(light.energy)) - light_np.setTag("created_by_user", "1") - - # 保存光源对象引用 - light_np.setPythonTag("rp_light_object", light) - - # 添加到管理列表 - self.world.Pointlight.append(light_np) - - # 在Qt树中创建对应的项目 - new_qt_item = QTreeWidgetItem(parent_item, [light_name]) - new_qt_item.setData(0, Qt.UserRole, light_np) - new_qt_item.setData(0, Qt.UserRole + 1, "LIGHT_NODE") - - # 展开父节点 - parent_item.setExpanded(True) - - print(f"✅ 成功创建点光源: {light_name}") - return (light_np, new_qt_item) - - except Exception as e: - print(f"❌ 创建点光源失败: {str(e)}") - return None +class CustomTreeWidget(_RemovedLegacyWidget): + pass +__all__ = [ + "NewProjectDialog", + "CustomMeta3DWidget", + "CustomFileView", + "CustomAssetsTreeWidget", + "CustomConsoleDockWidget", + "UniversalMessageDialog", + "StyledTextInputDialog", + "CustomTreeWidget", +]