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",
+]