diff --git a/config/vr_settings.json b/config/vr_settings.json
index 1688b9bf..902d31fb 100644
--- a/config/vr_settings.json
+++ b/config/vr_settings.json
@@ -1,15 +1,15 @@
-{
- "render_mode": "render_pipeline",
- "resolution_scale": 0.75,
- "pipeline_resolution_scale": 0.75,
- "quality_preset": "quality",
- "pipeline_vr_config": {
- "enable_shadows": true,
- "enable_ao": true,
- "enable_bloom": false,
- "enable_motion_blur": false,
- "enable_ssr": false,
- "shadow_quality": "medium",
- "ao_quality": "low"
- }
+{
+ "render_mode": "render_pipeline",
+ "resolution_scale": 0.75,
+ "pipeline_resolution_scale": 0.75,
+ "quality_preset": "quality",
+ "pipeline_vr_config": {
+ "enable_shadows": true,
+ "enable_ao": true,
+ "enable_bloom": false,
+ "enable_motion_blur": false,
+ "enable_ssr": false,
+ "shadow_quality": "medium",
+ "ao_quality": "low"
+ }
}
\ No newline at end of file
diff --git a/core/vr/config/vr_settings.json b/core/vr/config/vr_settings.json
index efc5f6db..902d31fb 100644
--- a/core/vr/config/vr_settings.json
+++ b/core/vr/config/vr_settings.json
@@ -1,18 +1,15 @@
-{
- "render_mode": "render_pipeline",
- "resolution_scale": 0.75,
- "pipeline_resolution_scale": 0.75,
- "quality_preset": "quality",
- "pipeline_vr_config": {
- "enable_shadows": true,
- "enable_ao": true,
- "enable_bloom": false,
- "enable_motion_blur": false,
- "enable_ssr": false,
- "shadow_quality": "medium",
- "ao_quality": "low"
- },
- "anti_aliasing": "4x",
- "refresh_rate": "90Hz",
- "async_reprojection": true
+{
+ "render_mode": "render_pipeline",
+ "resolution_scale": 0.75,
+ "pipeline_resolution_scale": 0.75,
+ "quality_preset": "quality",
+ "pipeline_vr_config": {
+ "enable_shadows": true,
+ "enable_ao": true,
+ "enable_bloom": false,
+ "enable_motion_blur": false,
+ "enable_ssr": false,
+ "shadow_quality": "medium",
+ "ao_quality": "low"
+ }
}
\ No newline at end of file
diff --git a/imgui.ini b/imgui.ini
index 1c34eff5..57093078 100644
--- a/imgui.ini
+++ b/imgui.ini
@@ -144,6 +144,16 @@ Pos=625,308
Size=600,400
Collapsed=0
+[Window][VR状态]
+Pos=162,152
+Size=138,296
+Collapsed=0
+
+[Window][VR设置]
+Pos=474,130
+Size=120,384
+Collapsed=0
+
[Docking][Data]
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1850,996 Split=X
DockNode ID=0x00000003 Parent=0x08BD597D SizeRef=1521,996 Split=Y
diff --git a/main.py b/main.py
index 0c9a7c26..19a09963 100644
--- a/main.py
+++ b/main.py
@@ -273,6 +273,11 @@ class MyWorld(CoreWorld):
self.showToolbar = True
self.showResourceManager = True
+ # VR窗口控制
+ self.showVRSettingsWindow = False
+ self.showVRStatusWindow = False
+ self.showVRDebugWindow = False
+
# 脚本系统状态变量
self.hotReloadEnabled = True
self._new_script_name = "new_script"
@@ -727,6 +732,11 @@ class MyWorld(CoreWorld):
if self.showDemoWindow:
imgui.show_demo_window()
+ # 绘制VR窗口
+ self._draw_vr_settings_window()
+ self._draw_vr_status_window()
+ self._draw_vr_debug_window()
+
# 绘制对话框
self._draw_new_project_dialog()
self._draw_open_project_dialog()
@@ -4239,6 +4249,431 @@ class MyWorld(CoreWorld):
self._color_picker_current_color = initial_color
self._color_picker_callback = callback
+ def _draw_vr_settings_window(self):
+ """绘制VR设置窗口"""
+ if not self.showVRSettingsWindow:
+ return
+
+ flags = imgui.WindowFlags_.no_collapse
+ with imgui_ctx.begin("VR设置", self.showVRSettingsWindow, flags) as (expanded, opened):
+ self.showVRSettingsWindow = opened
+
+ if expanded:
+ # 加载VR配置
+ if not hasattr(self, '_vr_config'):
+ self._load_vr_config()
+
+ config = self._vr_config
+
+ # 渲染模式选择
+ imgui.text("渲染模式:")
+ render_modes = ["normal", "render_pipeline"]
+ current_render_mode = 0 if config.get('render_mode') == 'normal' else 1
+
+ changed, new_render_mode = imgui.combo(
+ "##render_mode", current_render_mode, ["普通渲染", "RenderPipeline高级渲染"]
+ )
+ if changed:
+ config['render_mode'] = render_modes[new_render_mode]
+
+ imgui.separator()
+
+ # 质量预设选择
+ imgui.text("质量预设:")
+ quality_presets = ["performance", "balanced", "quality"]
+ quality_map = {"performance": 0, "balanced": 1, "quality": 2}
+ current_quality = quality_map.get(config.get('quality_preset', 'quality'), 2)
+
+ changed, new_quality = imgui.combo(
+ "##quality_preset", current_quality, ["性能", "平衡", "质量"]
+ )
+ if changed:
+ config['quality_preset'] = quality_presets[new_quality]
+
+ imgui.separator()
+
+ # 分辨率缩放
+ imgui.text("分辨率缩放:")
+ resolution_scale = config.get('resolution_scale', 0.75)
+ changed, new_scale = imgui.slider_float(
+ "##resolution_scale", resolution_scale, 0.5, 1.0, "%.2f"
+ )
+ if changed:
+ config['resolution_scale'] = new_scale
+ config['pipeline_resolution_scale'] = new_scale
+
+ imgui.separator()
+
+ # 渲染管线设置
+ imgui.text("渲染管线设置:")
+
+ if 'pipeline_vr_config' not in config:
+ config['pipeline_vr_config'] = {}
+
+ pipeline_config = config['pipeline_vr_config']
+
+ # 阴影设置
+ enable_shadows = pipeline_config.get('enable_shadows', True)
+ changed, new_shadows = imgui.checkbox("启用阴影", enable_shadows)
+ if changed:
+ pipeline_config['enable_shadows'] = new_shadows
+
+ # AO设置
+ enable_ao = pipeline_config.get('enable_ao', True)
+ changed, new_ao = imgui.checkbox("启用环境光遮蔽", enable_ao)
+ if changed:
+ pipeline_config['enable_ao'] = new_ao
+
+ # Bloom设置
+ enable_bloom = pipeline_config.get('enable_bloom', False)
+ changed, new_bloom = imgui.checkbox("启用Bloom效果", enable_bloom)
+ if changed:
+ pipeline_config['enable_bloom'] = new_bloom
+
+ # Motion Blur设置
+ enable_motion_blur = pipeline_config.get('enable_motion_blur', False)
+ changed, new_motion_blur = imgui.checkbox("启用动态模糊", enable_motion_blur)
+ if changed:
+ pipeline_config['enable_motion_blur'] = new_motion_blur
+
+ # SSR设置
+ enable_ssr = pipeline_config.get('enable_ssr', False)
+ changed, new_ssr = imgui.checkbox("启用屏幕空间反射", enable_ssr)
+ if changed:
+ pipeline_config['enable_ssr'] = new_ssr
+
+ # 阴影质量
+ shadow_quality_options = ["low", "medium", "high", "ultra"]
+ shadow_quality_map = {"low": 0, "medium": 1, "high": 2, "ultra": 3}
+ current_shadow_quality = shadow_quality_map.get(pipeline_config.get('shadow_quality', 'medium'), 1)
+
+ changed, new_shadow_quality = imgui.combo(
+ "阴影质量", current_shadow_quality, ["低", "中", "高", "极高"]
+ )
+ if changed:
+ pipeline_config['shadow_quality'] = shadow_quality_options[new_shadow_quality]
+
+ # AO质量
+ ao_quality_options = ["low", "medium", "high"]
+ ao_quality_map = {"low": 0, "medium": 1, "high": 2}
+ current_ao_quality = ao_quality_map.get(pipeline_config.get('ao_quality', 'low'), 0)
+
+ changed, new_ao_quality = imgui.combo(
+ "AO质量", current_ao_quality, ["低", "中", "高"]
+ )
+ if changed:
+ pipeline_config['ao_quality'] = ao_quality_options[new_ao_quality]
+
+ imgui.separator()
+
+ # 应用和重置按钮
+ if imgui.button("应用设置"):
+ self._save_vr_config()
+ self._apply_vr_config()
+ self.add_info_message("VR设置已应用")
+
+ imgui.same_line()
+ if imgui.button("重置默认"):
+ self._reset_vr_config()
+ self.add_info_message("VR设置已重置")
+
+ def _load_vr_config(self):
+ """加载VR配置"""
+ try:
+ import json
+ config_path = os.path.join(os.getcwd(), 'config', 'vr_settings.json')
+ if os.path.exists(config_path):
+ with open(config_path, 'r', encoding='utf-8') as f:
+ self._vr_config = json.load(f)
+ else:
+ # 默认配置
+ self._vr_config = {
+ "render_mode": "render_pipeline",
+ "resolution_scale": 0.75,
+ "pipeline_resolution_scale": 0.75,
+ "quality_preset": "quality",
+ "pipeline_vr_config": {
+ "enable_shadows": True,
+ "enable_ao": True,
+ "enable_bloom": False,
+ "enable_motion_blur": False,
+ "enable_ssr": False,
+ "shadow_quality": "medium",
+ "ao_quality": "low"
+ }
+ }
+ except Exception as e:
+ print(f"加载VR配置失败: {e}")
+ self._vr_config = {}
+
+ def _save_vr_config(self):
+ """保存VR配置"""
+ try:
+ import json
+ config_path = os.path.join(os.getcwd(), 'config', 'vr_settings.json')
+ os.makedirs(os.path.dirname(config_path), exist_ok=True)
+ with open(config_path, 'w', encoding='utf-8') as f:
+ json.dump(self._vr_config, f, indent=4, ensure_ascii=False)
+ except Exception as e:
+ print(f"保存VR配置失败: {e}")
+ self.add_error_message("保存VR配置失败")
+
+ def _apply_vr_config(self):
+ """应用VR配置"""
+ if not self.vr_manager:
+ return
+
+ try:
+ # 应用VR配置到VR管理器
+ config = self._vr_config
+
+ # 如果VR已启用,可能需要重新配置
+ if self.vr_manager.vr_enabled:
+ # TODO: 根据配置重新配置VR
+ pass
+
+ except Exception as e:
+ print(f"应用VR配置失败: {e}")
+ self.add_error_message("应用VR配置失败")
+
+ def _reset_vr_config(self):
+ """重置VR配置到默认值"""
+ self._vr_config = {
+ "render_mode": "render_pipeline",
+ "resolution_scale": 0.75,
+ "pipeline_resolution_scale": 0.75,
+ "quality_preset": "quality",
+ "pipeline_vr_config": {
+ "enable_shadows": True,
+ "enable_ao": True,
+ "enable_bloom": False,
+ "enable_motion_blur": False,
+ "enable_ssr": False,
+ "shadow_quality": "medium",
+ "ao_quality": "low"
+ }
+ }
+
+ def _draw_vr_status_window(self):
+ """绘制VR状态窗口"""
+ if not self.showVRStatusWindow:
+ return
+
+ flags = imgui.WindowFlags_.no_collapse | imgui.WindowFlags_.always_auto_resize
+ with imgui_ctx.begin("VR状态", self.showVRStatusWindow, flags) as (expanded, opened):
+ self.showVRStatusWindow = opened
+
+ if expanded and self.vr_manager:
+ # 控制更新频率,避免每帧都获取VR状态
+ current_time = imgui.get_io().delta_time
+ if not hasattr(self, '_vr_status_update_timer'):
+ self._vr_status_update_timer = 0.0
+ self._cached_vr_status = {}
+
+ self._vr_status_update_timer += current_time
+
+ # 每2秒更新一次VR状态
+ if self._vr_status_update_timer >= 2.0:
+ self._vr_status_update_timer = 0.0
+ try:
+ self._cached_vr_status = self.vr_manager.get_vr_status()
+ except Exception as e:
+ print(f"获取VR状态失败: {e}")
+ self._cached_vr_status = {}
+
+ vr_status = self._cached_vr_status
+
+ # 基本状态
+ imgui.text("基本状态:")
+ imgui.separator()
+
+ available = vr_status.get('available', False)
+ imgui.text(f"VR系统可用: {'是' if available else '否'}")
+
+ initialized = vr_status.get('initialized', False)
+ imgui.text(f"VR已初始化: {'是' if initialized else '否'}")
+
+ enabled = vr_status.get('enabled', False)
+ imgui.text(f"VR已启用: {'是' if enabled else '否'}")
+
+ imgui.separator()
+
+ # 渲染信息
+ imgui.text("渲染信息:")
+ imgui.separator()
+
+ eye_resolution = vr_status.get('eye_resolution', (0, 0))
+ imgui.text(f"眼睛分辨率: {eye_resolution[0]} x {eye_resolution[1]}")
+
+ device_count = vr_status.get('device_count', 0)
+ imgui.text(f"连接的设备数量: {device_count}")
+
+ vr_fps = vr_status.get('vr_fps', 0)
+ imgui.text(f"VR帧率: {vr_fps:.1f} FPS")
+
+ frame_count = vr_status.get('frame_count', 0)
+ imgui.text(f"总帧数: {frame_count}")
+
+ imgui.separator()
+
+ # 错误统计
+ imgui.text("错误统计:")
+ imgui.separator()
+
+ submit_failures = vr_status.get('submit_failures', 0)
+ imgui.text(f"提交失败次数: {submit_failures}")
+
+ pose_failures = vr_status.get('pose_failures', 0)
+ imgui.text(f"姿态失败次数: {pose_failures}")
+
+ imgui.separator()
+
+ # 刷新按钮和更新时间显示
+ if imgui.button("立即刷新"):
+ self._vr_status_update_timer = 0.5 # 强制更新
+ try:
+ self._cached_vr_status = self.vr_manager.get_vr_status()
+ except Exception as e:
+ print(f"获取VR状态失败: {e}")
+
+ imgui.same_line()
+ imgui.text(f"(更新间隔: 2秒)")
+
+ def _draw_vr_debug_window(self):
+ """绘制VR调试窗口"""
+ if not self.showVRDebugWindow:
+ return
+
+ flags = imgui.WindowFlags_.no_collapse
+ with imgui_ctx.begin("VR调试", self.showVRDebugWindow, flags) as (expanded, opened):
+ self.showVRDebugWindow = opened
+
+ if expanded:
+ # 调试选项
+ imgui.text("调试选项:")
+ imgui.separator()
+
+ changed, self.vr_debug_enabled = imgui.checkbox("启用调试输出", self.vr_debug_enabled)
+ if changed and self.vr_manager:
+ if self.vr_debug_enabled:
+ self.vr_manager.enable_debug_output()
+ else:
+ self.vr_manager.disable_debug_output()
+
+ changed, self.vr_performance_monitor = imgui.checkbox("启用性能监控", self.vr_performance_monitor)
+ if changed and self.vr_manager:
+ if self.vr_performance_monitor:
+ self.vr_manager.enable_performance_monitoring()
+ else:
+ self.vr_manager.disable_performance_monitoring()
+
+ imgui.separator()
+
+ # 输出模式
+ imgui.text("输出模式:")
+ if imgui.radio_button("简短模式", not self.vr_detailed_mode):
+ self.vr_detailed_mode = False
+ if imgui.radio_button("详细模式", self.vr_detailed_mode):
+ self.vr_detailed_mode = True
+
+ imgui.separator()
+
+ # 性能监控选项
+ imgui.text("性能监控:")
+ gpu_timing = False
+ changed, gpu_timing = imgui.checkbox("GPU时间监控", gpu_timing)
+ if changed and self.vr_manager:
+ if gpu_timing:
+ self.vr_manager.enable_gpu_timing_monitoring()
+ else:
+ self.vr_manager.disable_gpu_timing_monitoring()
+
+ pipeline_monitoring = False
+ changed, pipeline_monitoring = imgui.checkbox("管线监控", pipeline_monitoring)
+ if changed and self.vr_manager:
+ if pipeline_monitoring:
+ self.vr_manager.enable_pipeline_monitoring(True)
+ else:
+ self.vr_manager.enable_pipeline_monitoring(False)
+
+ imgui.separator()
+
+ # 姿态策略
+ imgui.text("姿态策略:")
+ pose_strategies = ["默认", "Running Start", "Low Latency"]
+ current_pose_strategy = 0
+
+ changed, new_strategy = imgui.combo(
+ "##pose_strategy", current_pose_strategy, pose_strategies
+ )
+ if changed and self.vr_manager:
+ # TODO: 应用姿态策略
+ if new_strategy == 0:
+ pass # 默认策略
+ elif new_strategy == 1:
+ self.vr_manager.set_prediction_time(11.0) # Running Start
+ elif new_strategy == 2:
+ self.vr_manager.set_prediction_time(5.0) # Low Latency
+
+ imgui.separator()
+
+ # 测试模式
+ imgui.text("测试模式:")
+ test_mode = False
+ changed, test_mode = imgui.checkbox("启用测试模式", test_mode)
+ if changed and self.vr_manager:
+ if test_mode:
+ self.vr_manager.enable_vr_test_mode()
+ else:
+ self.vr_manager.disable_vr_test_mode()
+
+ texture_test = False
+ changed, texture_test = imgui.checkbox("纹理提交测试", texture_test)
+ if changed and self.vr_manager:
+ # TODO: 应用纹理提交测试
+ pass
+
+ pose_wait_test = False
+ changed, pose_wait_test = imgui.checkbox("姿态等待测试", pose_wait_test)
+ if changed and self.vr_manager:
+ # TODO: 应用姿态等待测试
+ pass
+
+ step_by_step_test = False
+ changed, step_by_step_test = imgui.checkbox("分步骤测试", step_by_step_test)
+ if changed and self.vr_manager:
+ # TODO: 应用分步骤测试
+ pass
+
+ imgui.separator()
+
+ # 实时性能数据
+ if self.vr_manager and self.vr_manager.vr_enabled:
+ imgui.text("实时性能数据:")
+ imgui.separator()
+
+ # 使用状态窗口的缓存数据,避免重复获取
+ if hasattr(self, '_cached_vr_status') and self._cached_vr_status:
+ vr_status = self._cached_vr_status
+ else:
+ # 如果没有缓存数据,创建一个空字典
+ vr_status = {}
+
+ vr_fps = vr_status.get('vr_fps', 0)
+ imgui.text(f"VR帧率: {vr_fps:.1f} FPS")
+
+ frame_count = vr_status.get('frame_count', 0)
+ imgui.text(f"总帧数: {frame_count}")
+
+ submit_failures = vr_status.get('submit_failures', 0)
+ imgui.text(f"提交失败: {submit_failures}")
+
+ pose_failures = vr_status.get('pose_failures', 0)
+ imgui.text(f"姿态失败: {pose_failures}")
+
+ # 显示数据更新状态
+ imgui.text(f"(数据更新率: 0.5秒)")
+
def _draw_color_picker(self):
"""绘制颜色选择器对话框"""
if not self._color_picker_active:
@@ -8071,10 +8506,10 @@ class MyWorld(CoreWorld):
def _toggle_vr_mode(self):
"""切换VR模式"""
if self.vr_manager:
- if self.vr_manager.is_enabled():
+ if self.vr_manager.vr_enabled:
self._exit_vr_mode()
else:
- self.vr_manager.enable()
+ self.vr_manager.enable_vr()
self.add_info_message("已进入VR模式")
else:
self.add_error_message("VR管理器未初始化")
@@ -8082,39 +8517,29 @@ class MyWorld(CoreWorld):
def _exit_vr_mode(self):
"""退出VR模式"""
if self.vr_manager:
- self.vr_manager.disable()
+ self.vr_manager.disable_vr()
self.add_info_message("已退出VR模式")
def _show_vr_status(self):
- """显示VR状态"""
+ """显示VR状态窗口"""
if self.vr_manager:
- status = "已启用" if self.vr_manager.is_enabled() else "未启用"
- self.add_info_message(f"VR状态: {status}")
-
- # 显示设备信息
- if self.vr_manager.is_enabled():
- devices = self.vr_manager.get_connected_devices()
- if devices:
- self.add_info_message(f"连接的设备: {', '.join(devices)}")
- else:
- self.add_info_message("未检测到VR设备")
+ self.showVRStatusWindow = not self.showVRStatusWindow
else:
self.add_error_message("VR管理器未初始化")
def _show_vr_settings(self):
- """显示VR设置"""
+ """显示VR设置窗口"""
if self.vr_manager:
- self.add_info_message("VR设置对话框待实现")
+ self.showVRSettingsWindow = not self.showVRSettingsWindow
else:
self.add_error_message("VR管理器未初始化")
def _show_vr_performance_report(self):
- """显示VR性能报告"""
- if self.vr_manager and self.vr_manager.is_enabled():
- report = self.vr_manager.get_performance_report()
- self.add_info_message(f"VR性能报告: {report}")
+ """显示VR调试窗口"""
+ if self.vr_manager:
+ self.showVRDebugWindow = not self.showVRDebugWindow
else:
- self.add_info_message("VR未启用或管理器未初始化")
+ self.add_error_message("VR管理器未初始化")
def _get_chinese_font(self):
"""获取中文字体"""
diff --git a/ui/main_window.py b/ui/main_window.py
new file mode 100644
index 00000000..0c64dff7
--- /dev/null
+++ b/ui/main_window.py
@@ -0,0 +1,6724 @@
+"""
+主窗口设置模块
+
+负责主窗口的界面构建和事件绑定:
+- 菜单栏、工具栏创建
+- 停靠窗口设置
+- 事件连接和信号处理
+"""
+WEB_BROWSER_PROCESS_CODE = r"""
+import sys
+import os
+from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout
+from PyQt5.QtCore import Qt, QUrl, QTimer
+from PyQt5.QtGui import QPalette, QColor
+
+# 禁用硬件加速
+os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu --disable-software-rasterizer --disable-gpu-compositing --in-process-gpu"
+os.environ["QT_WEBENGINE_DISABLE_GPU"] = "1"
+os.environ["QT_OPENGL"] = "software"
+
+try:
+ from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings
+ WEB_ENGINE_AVAILABLE = True
+except ImportError:
+ WEB_ENGINE_AVAILABLE = False
+ print("⚠️ QtWebEngine 不可用")
+
+
+class EmbeddedWebBrowser(QWidget):
+
+ def __init__(self, parent=None, url="https://www.baidu.com", parent_window_id=0):
+ super().__init__(parent)
+ self.setWindowTitle("Web 浏览器")
+ self.parent_window_id = parent_window_id
+ self.url = url
+
+ # 设置窗口标志 - 如果要嵌入,使用子窗口样式
+ if parent_window_id > 0:
+ # 使用 FramelessWindowHint 去除边框,便于嵌入
+ self.setWindowFlags(Qt.FramelessWindowHint)
+ else:
+ self.setWindowFlags(Qt.Window)
+
+ self.resize(800, 600)
+
+ # 设置背景色
+ palette = self.palette()
+ palette.setColor(QPalette.Window, QColor(25, 25, 27))
+ self.setPalette(palette)
+ self.setAutoFillBackground(True)
+
+ # 创建布局
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(0)
+
+ if WEB_ENGINE_AVAILABLE:
+ # 创建 WebEngine 视图
+ self.web_view = QWebEngineView()
+
+ # 设置样式
+ self.web_view.setStyleSheet("background-color: #19191b;")
+
+ # 禁用硬件加速
+ settings = self.web_view.settings()
+ settings.setAttribute(QWebEngineSettings.Accelerated2dCanvasEnabled, False)
+ settings.setAttribute(QWebEngineSettings.WebGLEnabled, False)
+ settings.setAttribute(QWebEngineSettings.PluginsEnabled, False)
+ settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True)
+ settings.setAttribute(QWebEngineSettings.AutoLoadImages, True)
+ settings.setAttribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls, True)
+
+ # 连接加载信号
+ self.web_view.loadStarted.connect(self.on_load_started)
+ self.web_view.loadProgress.connect(self.on_load_progress)
+ self.web_view.loadFinished.connect(self.on_load_finished)
+
+ layout.addWidget(self.web_view)
+ print("[WebEngine] WebEngine 视图已创建")
+
+ # 延迟加载网页,确保窗口完全初始化
+ QTimer.singleShot(500, lambda: self.load_url(url))
+ else:
+ from PyQt5.QtWidgets import QLabel
+ label = QLabel("QtWebEngine 不可用\n请安装: pip install PyQtWebEngine")
+ label.setAlignment(Qt.AlignCenter)
+ label.setStyleSheet("color: white; font-size: 14px;")
+ layout.addWidget(label)
+
+ def on_load_started(self):
+ print("[WebEngine] 开始加载网页...")
+
+ def on_load_progress(self, progress):
+ if progress % 20 == 0: # 每20%打印一次
+ print(f"[WebEngine] 加载进度: {progress}%")
+
+ def on_load_finished(self, success):
+ if success:
+ print("[WebEngine] 网页加载成功")
+ else:
+ print("[WebEngine] 网页加载失败")
+
+ def load_url(self, url):
+ if WEB_ENGINE_AVAILABLE and hasattr(self, 'web_view'):
+ print(f"[WebEngine] 准备加载 URL: {url}")
+ qurl = QUrl(url)
+ print(f"[WebEngine] QUrl 有效性: {qurl.isValid()}")
+ print(f"[WebEngine] QUrl 内容: {qurl.toString()}")
+ self.web_view.load(qurl)
+ print(f"[WebEngine] load() 方法已调用")
+ else:
+ print(f"[WebEngine] 无法加载 URL - WEB_ENGINE_AVAILABLE={WEB_ENGINE_AVAILABLE}, has_web_view={hasattr(self, 'web_view')}")
+
+ def get_native_window_id(self):
+
+ return int(self.winId())
+
+ def embed_into_parent(self):
+
+ if self.parent_window_id > 0:
+ try:
+ print(f"[WebEngine] 开始嵌入窗口到父窗口 ID: {self.parent_window_id}")
+
+ # Windows 平台使用 win32 API 嵌入窗口
+ if sys.platform == 'win32':
+ import ctypes
+ from ctypes import wintypes
+
+ # 获取窗口句柄
+ hwnd = int(self.winId())
+ parent_hwnd = self.parent_window_id
+
+ print(f"[WebEngine] 子窗口句柄: {hwnd}")
+ print(f"[WebEngine] 父窗口句柄: {parent_hwnd}")
+
+ user32 = ctypes.windll.user32
+
+ # 先获取父窗口大小
+ rect = wintypes.RECT()
+ user32.GetClientRect(parent_hwnd, ctypes.byref(rect))
+ width = rect.right - rect.left
+ height = rect.bottom - rect.top
+ print(f"[WebEngine] 父窗口大小: {width}x{height}")
+
+ # 设置父窗口
+ result = user32.SetParent(hwnd, parent_hwnd)
+ print(f"[WebEngine] SetParent 结果: {result}")
+
+ # 设置窗口样式为子窗口
+ GWL_STYLE = -16
+ WS_CHILD = 0x40000000
+ WS_VISIBLE = 0x10000000
+ WS_CLIPCHILDREN = 0x02000000
+ WS_CLIPSIBLINGS = 0x04000000
+
+ old_style = user32.GetWindowLongW(hwnd, GWL_STYLE)
+ print(f"[WebEngine] 旧窗口样式: {hex(old_style)}")
+
+ # 设置为子窗口样式,移除边框和标题栏
+ new_style = (WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS)
+ user32.SetWindowLongW(hwnd, GWL_STYLE, new_style)
+
+ print(f"[WebEngine] 新窗口样式: {hex(new_style)}")
+
+ # 调整窗口位置和大小,填充整个父窗口
+ SWP_FRAMECHANGED = 0x0020
+ SWP_SHOWWINDOW = 0x0040
+ SWP_NOZORDER = 0x0004
+
+ result = user32.SetWindowPos(
+ hwnd,
+ 0, # HWND_TOP
+ 0, 0, # x, y
+ width, height, # width, height
+ SWP_FRAMECHANGED | SWP_SHOWWINDOW | SWP_NOZORDER
+ )
+ print(f"[WebEngine] SetWindowPos 结果: {result}")
+
+ # 强制刷新窗口
+ user32.UpdateWindow(hwnd)
+ user32.InvalidateRect(hwnd, None, True)
+
+ print(f"[WebEngine] 已成功嵌入到父窗口 (Windows)")
+ return True
+ else:
+ # 其他平台使用 Qt 的方式
+ from PyQt5.QtGui import QWindow
+ parent_window = QWindow.fromWinId(self.parent_window_id)
+ if parent_window:
+ self.windowHandle().setParent(parent_window)
+ print(f"[WebEngine] 已嵌入到父窗口 (Qt): {self.parent_window_id}")
+ return True
+ except Exception as e:
+ print(f"[WebEngine] 嵌入窗口失败: {e}")
+ import traceback
+ traceback.print_exc()
+ return False
+
+
+def main():
+
+ # 注释掉标准输出重定向,让输出正常显示在终端
+ # if sys.platform == 'win32':
+ # import io
+ # sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
+ # sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
+
+ print("=" * 60)
+ print("[WebEngine] 启动独立 WebEngine 进程")
+ print("=" * 60)
+
+ # 创建应用
+ app = QApplication(sys.argv)
+
+ # 从命令行参数获取 URL 和窗口 ID
+ url = sys.argv[1] if len(sys.argv) > 1 else "https://www.baidu.com"
+ parent_window_id = int(sys.argv[2]) if len(sys.argv) > 2 else 0
+
+ print(f"[WebEngine] 参数:")
+ print(f" - URL: {url}")
+ print(f" - 父窗口 ID: {parent_window_id}")
+
+ # 创建浏览器窗口
+ browser = EmbeddedWebBrowser(url=url, parent_window_id=parent_window_id)
+
+ # 显示窗口
+ browser.show()
+
+ # 输出窗口 ID 供父进程使用
+ window_id = browser.get_native_window_id()
+ print(f"WINDOW_ID:{window_id}")
+ sys.stdout.flush()
+
+ # 等待网页开始加载后再嵌入窗口
+ if parent_window_id > 0:
+ # 延迟2秒,确保网页已经开始加载和渲染
+ QTimer.singleShot(10, browser.embed_into_parent)
+
+ print("[WebEngine] 进程已就绪")
+ print("=" * 60)
+
+ sys.exit(app.exec_())
+
+
+if __name__ == "__main__":
+ main()
+"""
+
+import os
+import sys
+
+# ==================== 禁用 QtWebEngine 硬件加速 ====================
+# 必须在导入 QtWebEngine 之前设置环境变量
+os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu --disable-software-rasterizer --disable-gpu-compositing --in-process-gpu"
+os.environ["QT_WEBENGINE_DISABLE_GPU"] = "1"
+os.environ["QT_OPENGL"] = "software"
+print("✓ QtWebEngine 硬件加速已禁用,使用软件渲染模式")
+# ==================== 结束配置 ====================
+
+from PyQt5.QtGui import QKeySequence, QIcon, QPalette, QColor
+# from PyQt5.QtWebEngineWidgets import QWebEngineView
+from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction,
+ QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem,
+ QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea,
+ QFileSystemModel, QButtonGroup, QToolButton, QPushButton, QHBoxLayout,
+ QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget, QDialog,
+ QSpinBox, QFrame, QRadioButton, QTextEdit, QCheckBox, QTabWidget, QSizePolicy,
+ QListWidgetItem)
+from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint, QUrl, QRect, QCoreApplication
+from direct.showbase.ShowBaseGlobal import aspect2d
+from panda3d.core import OrthographicLens
+
+from ui.widgets import (CustomMeta3DWidget, CustomFileView, CustomTreeWidget,
+ CustomAssetsTreeWidget, CustomConsoleDockWidget,
+ UniversalMessageDialog)
+from ui.icon_manager import get_icon_manager, get_icon
+
+# import yaml
+from PyQt5.QtWidgets import QDialog, QVBoxLayout, QListWidget, QPushButton, QDialogButtonBox, QCheckBox, QLabel, QHBoxLayout
+from PyQt5.QtCore import Qt
+
+
+class StyledTerrainDialog(QDialog):
+ """与新建项目对话框风格一致的参数输入对话框"""
+
+ def __init__(self, parent, title, fields, primary_text="确认", secondary_text="取消"):
+ super().__init__(parent)
+ self.setWindowTitle(title)
+ self.setObjectName("styledTerrainDialog")
+ self.setModal(True)
+ self.resize(508, 241)
+ 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.field_widgets = {}
+
+ self.setStyleSheet("""
+ QDialog#styledTerrainDialog {
+ background-color: transparent;
+ border: none;
+ }
+ QFrame#baseFrame {
+ background-color: #000000;
+ border: 1px solid #3E3E42;
+ border-radius: 5px;
+ }
+ QWidget#titleBar {
+ background-color: transparent;
+ border-radius: 5px 5px 0px 0px;
+ min-height: 32px;
+ max-height: 32px;
+ }
+ QLabel#titleLabel {
+ color: #FFFFFF;
+ font-family: 'Microsoft YaHei', 'Inter', 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: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 12px;
+ font-weight: 400;
+ letter-spacing: 0.6px;
+ }
+ QLabel[role="hint"] {
+ color: rgba(235, 235, 235, 0.6);
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 11px;
+ font-weight: 300;
+ letter-spacing: 0.55px;
+ padding: 0px;
+ }
+ QDoubleSpinBox, QSpinBox {
+ 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: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 11px;
+ font-weight: 300;
+ letter-spacing: 0.55px;
+ min-height: 30px;
+ max-height: 30px;
+ }
+ QDoubleSpinBox:focus, QSpinBox:focus {
+ border: 1px solid #3067C0;
+ background-color: rgba(48, 103, 192, 0.1);
+ }
+ QDoubleSpinBox:hover, QSpinBox:hover {
+ border: 1px solid #3067C0;
+ background-color: rgba(89, 100, 113, 0.3);
+ }
+ QDoubleSpinBox:disabled, QSpinBox: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 12px;
+ font-family: 'Microsoft YaHei', 'Inter', 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 {
+ min-width: 120px;
+ max-width: 120px;
+ }
+ QPushButton#secondaryButton {
+ min-width: 120px;
+ max-width: 120px;
+ }
+ """)
+
+ 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)
+
+ content_widget = QWidget()
+ content_widget.setObjectName('contentWidget')
+ content_layout = QVBoxLayout(content_widget)
+ content_layout.setContentsMargins(15, 10, 15, 10)
+ content_layout.setSpacing(0)
+
+ content_container = QFrame()
+ content_container.setObjectName('contentContainer')
+ content_container.setFrameShape(QFrame.NoFrame)
+ content_container.setAttribute(Qt.WA_StyledBackground, True)
+
+ container_layout = QVBoxLayout(content_container)
+ container_layout.setContentsMargins(15, 10, 15, 10)
+ container_layout.setSpacing(10)
+
+ for field in fields:
+ row_widget = QWidget()
+ row_layout = QHBoxLayout(row_widget)
+ row_layout.setContentsMargins(0, 0, 0, 0)
+ row_layout.setSpacing(10)
+
+ label = QLabel(field.get("label", ""))
+ label.setProperty('role', 'fieldLabel')
+ label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
+ label.setMinimumWidth(field.get("label_width", 80))
+ label.setMaximumWidth(field.get("label_width", 120))
+ row_layout.addWidget(label)
+
+ widget_type = field.get("type", "double")
+ if widget_type == "int":
+ widget = QSpinBox()
+ widget.setRange(field.get("min", 0), field.get("max", 1000))
+ widget.setSingleStep(field.get("step", 1))
+ widget.setValue(field.get("value", field.get("default", 0)))
+ elif widget_type == "text":
+ widget = QLineEdit()
+ widget.setText(field.get("value", field.get("default", "")))
+ if field.get("placeholder"):
+ widget.setPlaceholderText(field.get("placeholder"))
+ widget.setClearButtonEnabled(True)
+ else:
+ widget = QDoubleSpinBox()
+ widget.setRange(field.get("min", 0.0), field.get("max", 1000.0))
+ widget.setSingleStep(field.get("step", 0.1))
+ widget.setDecimals(field.get("decimals", 2))
+ widget.setValue(field.get("value", field.get("default", 0.0)))
+
+ widget.setFixedHeight(30)
+ widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ row_layout.addWidget(widget, 1)
+
+ if field.get("suffix"):
+ suffix_label = QLabel(field["suffix"])
+ suffix_label.setProperty('role', 'fieldLabel')
+ row_layout.addWidget(suffix_label)
+
+ self.field_widgets[field.get("name", field.get("label", ""))] = widget
+ container_layout.addWidget(row_widget)
+
+ 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_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()
+
+ self.confirmButton = QPushButton(primary_text)
+ self.confirmButton.setObjectName('primaryButton')
+ self.confirmButton.setFixedSize(120, 30)
+ self.confirmButton.clicked.connect(self.accept)
+ button_row_layout.addWidget(self.confirmButton)
+
+ self.cancelButton = QPushButton(secondary_text)
+ self.cancelButton.setObjectName('secondaryButton')
+ self.cancelButton.setFixedSize(120, 30)
+ self.cancelButton.clicked.connect(self.reject)
+ button_row_layout.addWidget(self.cancelButton)
+
+ container_layout.addWidget(button_row_widget)
+
+ content_layout.addWidget(content_container, 0, Qt.AlignTop)
+ base_layout.addWidget(content_widget)
+ main_layout.addWidget(base_frame)
+
+ 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)
+
+ 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()
+
+ 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 mousePressEvent(self, event):
+ if event.button() == Qt.LeftButton and hasattr(self, "title_bar"):
+ if 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)
+
+ def get_value(self, name):
+ widget = self.field_widgets.get(name)
+ if isinstance(widget, (QDoubleSpinBox, QSpinBox)):
+ return widget.value()
+ if isinstance(widget, QLineEdit):
+ return widget.text()
+ return None
+try:
+ from PyQt5.QtWebEngineWidgets import QWebEngineView
+ WEB_ENGINE_AVAILABLE = True
+except ImportError:
+ QWebEngineView = None
+ WEB_ENGINE_AVAILABLE = False
+
+class MainWindow(QMainWindow):
+ """主窗口类"""
+
+ def __init__(self, world):
+ super().__init__()
+ self.world = world
+ self.world.main_window = self # 关键:让world对象能访问主窗口
+
+ #剪切板相关属性
+ self.clipboard = []
+ self.clipboard_mode = None
+
+ # 初始化图标管理器并打印调试信息
+ self.icon_manager = get_icon_manager()
+ print("🔧 图标管理器初始化完成")
+ self.icon_manager.debug_info()
+
+ self.setStyleSheet("""
+ QMainWindow {
+ background-color: #000000;
+ }
+ QMenuBar {
+ background-color: #000000;
+ color: #ffffff;
+ border-bottom: 1px solid #4c5c6e;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 12px;
+ font-weight: 400;
+ }
+ QMenuBar::item {
+ background-color: transparent;
+ padding: 6px 12px;
+ color: #D4D4D4;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 12px;
+ }
+ QMenuBar::item:selected {
+ background-color: rgba(48, 103, 192, 0.4);
+ }
+ QMenuBar::item:pressed {
+ background-color: rgba(48, 103, 192, 0.6);
+ }
+ QMenu {
+ background-color: #2E3035;
+ color: #ebebeb;
+ border: 1px solid #4c5c6e;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 12px;
+ }
+ QMenu::item {
+ padding: 6px 20px;
+ }
+ QMenu::item:selected {
+ background-color: rgba(48, 103, 192, 0.4);
+ }
+ QPushButton {
+ background-color: rgba(89, 98, 118, 0.4);
+ color: #ebebeb;
+ border: none;
+ padding: 6px 12px;
+ border-radius: 2px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-weight: 300;
+ font-size: 10px;
+ }
+ QPushButton:hover {
+ background-color: #3067c0;
+ color: #ffffff;
+ }
+ QPushButton:pressed {
+ background-color: #2556a0;
+ }
+ QPushButton:disabled {
+ background-color: #394560;
+ color: rgba(235, 235, 235, 0.5);
+ }
+ QComboBox {
+ background-color: rgba(89, 100, 113, 0.2);
+ color: #ebebeb;
+ border: 1px solid rgba(76, 92, 110, 0.6);
+ border-radius: 2px;
+ padding: 4px 8px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ }
+ QComboBox::drop-down {
+ border: none;
+ border-left: 1px solid rgba(76, 92, 110, 0.6);
+ background-color: rgba(89, 100, 113, 0.3);
+ width: 16px;
+ }
+ QComboBox::down-arrow {
+ image: url(icons/down_arrows.png);
+ width: 10px;
+ height: 10px;
+ }
+ QComboBox QAbstractItemView {
+ background-color: #596471;
+ color: #ebebeb;
+ selection-background-color: rgba(48, 103, 192, 0.4);
+ }
+ QLineEdit {
+ background-color: rgba(89, 100, 113, 0.2);
+ color: #ebebeb;
+ border: 1px solid rgba(76, 92, 110, 0.6);
+ border-radius: 2px;
+ padding: 4px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ }
+ QSpinBox, QDoubleSpinBox {
+ background-color: rgba(89, 100, 113, 0.2);
+ color: #ebebeb;
+ border: 1px solid rgba(76, 92, 110, 0.6);
+ border-radius: 2px;
+ padding: 4px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ }
+ QScrollBar:vertical {
+ background-color: #19191b;
+ width: 12px;
+ border: none;
+ }
+ QScrollBar::handle:vertical {
+ background-color: #596471;
+ border-radius: 2px;
+ min-height: 20px;
+ }
+ QScrollBar::handle:vertical:hover {
+ background-color: #3067c0;
+ }
+ QScrollBar:horizontal {
+ background-color: #19191b;
+ height: 12px;
+ border: none;
+ }
+ QScrollBar::handle:horizontal {
+ background-color: #596471;
+ border-radius: 2px;
+ min-width: 20px;
+ }
+ QScrollBar::handle:horizontal:hover {
+ background-color: #3067c0;
+ }
+ """)
+ # 设置 QMessageBox 样式表
+ self.setupMessageBoxStyles()
+
+ self.setupCenterWidget() # 创建中间部分Panda3D
+ self.setupMenus() # 创建菜单栏
+ self.setupDockWindows()
+ # self.setupToolbar()
+ self.connectEvents()
+
+ # 移动窗口到屏幕中央
+ self.move_center()
+
+ # 创建定时器来更新脚本管理面板状态
+ self.updateTimer = QTimer()
+ self.updateTimer.timeout.connect(self.updateScriptPanel)
+ self.updateTimer.start(500) # 每500毫秒更新一次
+
+ self.toolbarDragging = False
+ self.dragStartPos = QPoint(0, 0)
+ self.toolbarStartPos = QPoint(0, 0)
+
+ def setupMessageBoxStyles(self):
+ """设置 QMessageBox 的全局样式"""
+ # 设置 QMessageBox 的样式表
+ msg_box_style = """
+ QMessageBox {
+ background-color: #19191b;
+ color: #ebebeb;
+ border: 1px solid #4c5c6e;
+ }
+ QMessageBox QLabel {
+ color: #ebebeb;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 12px;
+ font-weight: 300;
+ }
+ QMessageBox QPushButton {
+ background-color: rgba(89, 98, 118, 0.4);
+ color: #ebebeb;
+ border: none;
+ padding: 6px 12px;
+ border-radius: 2px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-weight: 300;
+ font-size: 10px;
+ min-width: 80px;
+ }
+ QMessageBox QPushButton:hover {
+ background-color: #3067c0;
+ color: #ffffff;
+ }
+ QMessageBox QPushButton:pressed {
+ background-color: #2556a0;
+ }
+ QMessageBox QPushButton:disabled {
+ background-color: #394560;
+ color: rgba(235, 235, 235, 0.5);
+ }
+ """
+
+ # 应用全局样式
+ self.setStyleSheet(self.styleSheet() + msg_box_style)
+
+ def createStyledInputDialog(self, parent, title, label, mode=QLineEdit.Normal, text=""):
+ """创建带有统一主题样式的 QInputDialog"""
+ dialog = QInputDialog(parent)
+ dialog.setWindowTitle(title)
+ dialog.setLabelText(label)
+ dialog.setTextEchoMode(mode)
+ dialog.setTextValue(text)
+
+ # 设置样式表
+ dialog.setStyleSheet("""
+ QInputDialog {
+ background-color: #19191b;
+ color: #ebebeb;
+ }
+ QLabel {
+ color: #ffffff;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-weight: 500;
+ font-size: 14px;
+ }
+ 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;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ }
+ QPushButton {
+ background-color: rgba(89, 98, 118, 0.4);
+ color: #ebebeb;
+ border: none;
+ padding: 6px 12px;
+ border-radius: 2px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-weight: 300;
+ font-size: 10px;
+ min-width: 80px;
+ }
+ QPushButton:hover {
+ background-color: #3067c0;
+ color: #ffffff;
+ }
+ QPushButton:pressed {
+ background-color: #2556a0;
+ }
+ QPushButton:disabled {
+ background-color: #394560;
+ color: rgba(235, 235, 235, 0.5);
+ }
+ """)
+
+ return dialog
+
+ def createStyledFileDialog(self, parent, caption, directory="", filter=""):
+ """创建带有统一主题样式的 QFileDialog"""
+ dialog = QFileDialog(parent)
+ dialog.setWindowTitle(caption)
+
+ # 设置样式表
+ dialog.setStyleSheet("""
+ QFileDialog {
+ background-color: #19191b;
+ color: #ebebeb;
+ }
+ QLabel {
+ color: #ebebeb;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ }
+ QListView {
+ background-color: #19191b;
+ color: #ebebeb;
+ border: 1px solid #4c5c6e;
+ alternate-background-color: #19191b;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ }
+ QListView::item:hover {
+ background-color: #394560;
+ }
+ QListView::item:selected {
+ background-color: rgba(48, 103, 192, 0.4);
+ color: #ffffff;
+ }
+ QTreeView {
+ background-color: #19191b;
+ color: #ebebeb;
+ border: 1px solid #4c5c6e;
+ alternate-background-color: #19191b;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ }
+ QTreeView::item:hover {
+ background-color: #394560;
+ }
+ QTreeView::item:selected {
+ background-color: rgba(48, 103, 192, 0.4);
+ color: #ffffff;
+ }
+ QComboBox {
+ background-color: rgba(89, 100, 113, 0.2);
+ color: #ebebeb;
+ border: 1px solid rgba(76, 92, 110, 0.6);
+ border-radius: 2px;
+ padding: 4px 8px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ }
+ QComboBox::drop-down {
+ border: none;
+ }
+ QComboBox QAbstractItemView {
+ background-color: #596471;
+ color: #ebebeb;
+ selection-background-color: rgba(48, 103, 192, 0.4);
+ }
+ QPushButton {
+ background-color: rgba(89, 98, 118, 0.4);
+ color: #ebebeb;
+ border: none;
+ padding: 6px 12px;
+ border-radius: 2px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-weight: 300;
+ font-size: 10px;
+ }
+ QPushButton:hover {
+ background-color: #3067c0;
+ color: #ffffff;
+ }
+ QPushButton:pressed {
+ background-color: #2556a0;
+ }
+ QPushButton:disabled {
+ background-color: #394560;
+ color: rgba(235, 235, 235, 0.5);
+ }
+ QLineEdit {
+ background-color: rgba(89, 100, 113, 0.2);
+ color: #ebebeb;
+ border: 1px solid rgba(76, 92, 110, 0.6);
+ border-radius: 2px;
+ padding: 4px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ }
+ """)
+
+ return dialog
+
+ def setupCenterWidget(self):
+ """设置窗口基本属性"""
+ self.setWindowTitle("引擎编辑器")
+ # 使用图标管理器设置窗口图标
+ app_icon = get_icon('app_logo')
+ if not app_icon.isNull():
+ self.setWindowIcon(app_icon)
+ print("✅ 应用图标设置成功")
+ else:
+ print("⚠️ 应用图标设置失败,使用默认图标")
+ # 使用自定义的 Panda3D 部件作为中央部件
+ self.pandaWidget = CustomMeta3DWidget(self.world)
+ self.setCentralWidget(self.pandaWidget)
+
+ # 创建内嵌工具栏
+ self.setupEmbeddedToolbar()
+
+ def move_center(self):
+ """设置窗口居中显示"""
+ self.setGeometry(50, 50, 1920, 1080)
+
+ screen = QDesktopWidget().screenGeometry()
+ self.move(
+ int(screen.width() / 2 - self.width() / 2),
+ int(screen.height() / 2 - self.height() / 2),
+ )
+
+ @staticmethod
+ def get_icon_path(icon_name):
+ """获取图标文件的完整路径"""
+ # 假设 icons 文件夹在项目根目录下
+ project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+ icon_path = os.path.join(project_root, "icons", icon_name)
+
+ # 检查文件是否存在,如果不存在则返回默认值或None
+ if not os.path.exists(icon_path):
+ print(f"警告: 图标文件不存在: {icon_path}")
+ return "" # 返回空字符串,QIcon会处理空路径
+
+ return icon_path
+
+ def setupEmbeddedToolbar(self):
+ """创建Unity风格的内嵌工具栏"""
+ # 创建工具栏容器
+ self.embeddedToolbar = QFrame(self.pandaWidget)
+ self.embeddedToolbar.setObjectName("UnityToolbar")
+ # self.embeddedToolbar.setStyleSheet("""
+ # QFrame#UnityToolbar {
+ # background-color: rgba(240, 240, 240, 180);
+ # border: 1px solid rgba(200, 200, 200, 200);
+ # border-radius: 4px;
+ # }
+ # QToolButton {
+ # background-color: rgba(250, 250, 250, 150);
+ # border: none;
+ # color: #333333;
+ # padding: 5px;
+ # border-radius: 3px;
+ # }
+ # QToolButton:hover {
+ # background-color: rgba(220, 220, 220, 200);
+ # }
+ # QToolButton:checked {
+ # background-color: rgba(100, 150, 220, 180);
+ # color: white;
+ # }
+ # QToolButton:pressed {
+ # background-color: rgba(80, 130, 200, 200);
+ # }
+ # """)
+ self.embeddedToolbar.setStyleSheet("""
+ QFrame#UnityToolbar {
+ background-color: transparent;
+ border: none;
+ padding: 0px;
+ }
+ QToolButton {
+ background-color: #1e1e1f;
+ border: 1px solid rgba(184, 211, 241, 0.2);
+ color: #ebebeb;
+ padding: 0px;
+ border-radius: 2px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ min-width: 31px;
+ max-width: 31px;
+ min-height: 30px;
+ max-height: 30px;
+ }
+ QToolButton:hover {
+ background-color: rgba(89, 100, 113, 0.3);
+ border: 1px solid rgba(184, 211, 241, 0.3);
+ }
+ QToolButton:checked {
+ background-color: #3067c0;
+ color: #ffffff;
+ border: 1px solid rgba(48, 103, 192, 0.6);
+ }
+ QToolButton:pressed {
+ background-color: #2556a0;
+ }
+ """)
+
+ # 水平布局
+ self.toolbarLayout = QHBoxLayout(self.embeddedToolbar)
+ self.toolbarLayout.setContentsMargins(0, 0, 0, 0)
+ self.toolbarLayout.setSpacing(4)
+
+ # 创建工具按钮组
+ self.toolGroup = QButtonGroup()
+
+ # 选择工具
+ self.selectTool = QToolButton()
+ select_icon = get_icon('select_tool', QSize(20, 20))
+ if not select_icon.isNull():
+ self.selectTool.setIcon(select_icon)
+ self.selectTool.setText('选择') # 如果没有图标则显示文字
+ self.selectTool.setIconSize(QSize(20, 20))
+ self.selectTool.setCheckable(True)
+ self.selectTool.setToolTip("选择工具 (Q)")
+ # self.selectTool.setShortcut(QKeySequence("Q"))
+ self.toolGroup.addButton(self.selectTool)
+ self.toolbarLayout.addWidget(self.selectTool)
+
+ # 移动工具
+ self.moveTool = QToolButton()
+ icon_path = self.get_icon_path("move_tool.png")
+ if icon_path and os.path.exists(icon_path):
+ self.moveTool.setIcon(QIcon(icon_path))
+ self.moveTool.setText("移动")
+ self.moveTool.setIconSize(QSize(20, 20))
+ self.moveTool.setCheckable(True)
+ self.moveTool.setToolTip("移动工具 (W)")
+ # self.moveTool.setShortcut(QKeySequence("W"))
+ self.toolGroup.addButton(self.moveTool)
+ self.toolbarLayout.addWidget(self.moveTool)
+
+ # 旋转工具
+ self.rotateTool = QToolButton()
+ rotate_icon = get_icon('rotate_tool', QSize(20, 20))
+ if not rotate_icon.isNull():
+ self.rotateTool.setIcon(rotate_icon)
+ self.rotateTool.setText("旋转")
+ self.rotateTool.setIconSize(QSize(20, 20))
+ self.rotateTool.setCheckable(True)
+ self.rotateTool.setToolTip("旋转工具 (E)")
+ # self.rotateTool.setShortcut(QKeySequence("E"))
+ self.toolGroup.addButton(self.rotateTool)
+ self.toolbarLayout.addWidget(self.rotateTool)
+
+ # 缩放工具
+ self.scaleTool = QToolButton()
+ scale_icon = get_icon('scale_tool', QSize(20, 20))
+ if not scale_icon.isNull():
+ self.scaleTool.setIcon(scale_icon)
+ self.scaleTool.setText("缩放")
+ self.scaleTool.setIconSize(QSize(20, 20))
+ self.scaleTool.setCheckable(True)
+ self.scaleTool.setToolTip("缩放工具 (R)")
+ # self.scaleTool.setShortcut(QKeySequence("R"))
+ self.toolGroup.addButton(self.scaleTool)
+ self.toolbarLayout.addWidget(self.scaleTool)
+
+ # 添加分隔符
+ # separator = QFrame()
+ # separator.setFrameShape(QFrame.VLine)
+ # separator.setFrameShadow(QFrame.Sunken)
+ # separator.setStyleSheet("background-color: rgba(240, 240, 240, 255);")
+ # separator.setFixedWidth(1)
+ # self.toolbarLayout.addWidget(separator)
+
+ # 设置位置到左上角
+ self.embeddedToolbar.move(10, 10)
+ self.embeddedToolbar.adjustSize()
+ self.embeddedToolbar.show()
+
+ # 连接事件
+ self.toolGroup.buttonClicked.connect(self.onToolChanged)
+ # 设置工具栏拖拽事件
+ self.embeddedToolbar.mousePressEvent = self.toolbarMousePressEvent
+ self.embeddedToolbar.mouseMoveEvent = self.toolbarMouseMoveEvent
+ self.embeddedToolbar.mouseReleaseEvent = self.toolbarMouseReleaseEvent
+
+ # 默认选择"选择"工具
+ self.selectTool.setChecked(True)
+ self.world.setCurrentTool("选择")
+
+ def toolbarMousePressEvent(self, event):
+ """工具栏鼠标按下事件"""
+ if event.button() == Qt.LeftButton:
+ self.toolbarDragging = True
+ self.dragStartPos = event.globalPos()
+ self.toolbarStartPos = self.embeddedToolbar.pos()
+ event.accept()
+
+ def toolbarMouseMoveEvent(self, event):
+ """工具栏鼠标移动事件"""
+ try:
+ if self.toolbarDragging and event.buttons() == Qt.LeftButton:
+ # 计算新位置
+ delta = event.globalPos() - self.dragStartPos
+ new_pos = self.toolbarStartPos + delta
+
+ # 边界检测
+ panda_rect = self.pandaWidget.geometry()
+ toolbar_size = self.embeddedToolbar.size()
+
+ # 限制在Panda3D区域内
+ new_pos.setX(max(0, min(new_pos.x(), panda_rect.width() - toolbar_size.width())))
+ new_pos.setY(max(0, min(new_pos.y(), panda_rect.height() - toolbar_size.height())))
+
+ self.embeddedToolbar.move(new_pos)
+ event.accept()
+ except Exception as e:
+ print(f"工具栏鼠标移动事件出错")
+ import traceback
+ traceback.print_exc()
+
+
+ def toolbarMouseReleaseEvent(self, event):
+ """工具栏鼠标释放事件"""
+ if event.button() == Qt.LeftButton and self.toolbarDragging:
+ self.toolbarDragging = False
+
+ # 自动吸附到最近的预设位置
+ self.snapToNearestPosition()
+ event.accept()
+
+ def snapToNearestPosition(self):
+ """自动吸附到最近的预设位置"""
+ current_pos = self.embeddedToolbar.pos()
+ panda_rect = self.pandaWidget.geometry()
+ toolbar_size = self.embeddedToolbar.size()
+
+ margin = 10
+
+ # 定义所有预设位置
+ positions = {
+ "top_center": QPoint(
+ (panda_rect.width() - toolbar_size.width()) // 2,
+ margin
+ ),
+ "top_left": QPoint(margin, margin),
+ "top_right": QPoint(
+ panda_rect.width() - toolbar_size.width() - margin,
+ margin
+ ),
+ "bottom_center": QPoint(
+ (panda_rect.width() - toolbar_size.width()) // 2,
+ panda_rect.height() - toolbar_size.height() - margin
+ ),
+ "bottom_left": QPoint(
+ margin,
+ panda_rect.height() - toolbar_size.height() - margin
+ ),
+ "bottom_right": QPoint(
+ panda_rect.width() - toolbar_size.width() - margin,
+ panda_rect.height() - toolbar_size.height() - margin
+ )
+ }
+
+ # 找到最近的位置
+ min_distance = float('inf')
+ nearest_position = "top_center"
+
+ for pos_name, pos_point in positions.items():
+ distance = ((current_pos.x() - pos_point.x()) ** 2 +
+ (current_pos.y() - pos_point.y()) ** 2) ** 0.5
+ if distance < min_distance:
+ min_distance = distance
+ nearest_position = pos_name
+
+ # 更新位置并平滑移动
+ self.toolbarPosition = nearest_position
+ target_pos = positions[nearest_position]
+
+ # 简单的平滑移动动画
+ from PyQt5.QtCore import QPropertyAnimation, QEasingCurve
+ self.toolbarAnimation = QPropertyAnimation(self.embeddedToolbar, b"pos")
+ self.toolbarAnimation.setDuration(200)
+ self.toolbarAnimation.setStartValue(current_pos)
+ self.toolbarAnimation.setEndValue(target_pos)
+ self.toolbarAnimation.setEasingCurve(QEasingCurve.OutCubic)
+ self.toolbarAnimation.start()
+
+ def setupMenus(self):
+ """创建菜单栏"""
+ menubar = self.menuBar()
+
+ # 文件菜单
+ self.fileMenu = menubar.addMenu('文件')
+ self.newAction = self.fileMenu.addAction('新建')
+ self.openAction = self.fileMenu.addAction('打开')
+ self.saveAction = self.fileMenu.addAction('保存')
+ self.buildAction = self.fileMenu.addAction('打包')
+ self.fileMenu.addSeparator()
+ self.exitAction = self.fileMenu.addAction('退出')
+
+ # 编辑菜单
+ self.editMenu = menubar.addMenu('编辑')
+ self.undoAction = self.editMenu.addAction('撤销')
+ self.redoAction = self.editMenu.addAction('重做')
+ self.editMenu.addSeparator()
+ self.cutAction = self.editMenu.addAction('剪切')
+ self.copyAction = self.editMenu.addAction('复制')
+ self.pasteAction = self.editMenu.addAction('粘贴')
+
+ # # 视图菜单
+ # self.viewMenu = menubar.addMenu('视图')
+ # self.viewPerspectiveAction = self.viewMenu.addAction('透视图')
+ # self.viewTopAction = self.viewMenu.addAction('俯视图')
+ # self.viewFrontAction = self.viewMenu.addAction('前视图')
+ # self.viewMenu.addSeparator()
+ # self.viewGridAction = self.viewMenu.addAction('显示网格')
+
+ # 工具菜单
+ self.toolsMenu = menubar.addMenu('工具')
+ self.selectAction = self.toolsMenu.addAction('选择工具')
+ self.moveAction = self.toolsMenu.addAction('移动工具')
+ self.rotateAction = self.toolsMenu.addAction('旋转工具')
+ self.scaleAction = self.toolsMenu.addAction('缩放工具')
+ self.toolsMenu.addSeparator()
+ self.sunsetAction = self.toolsMenu.addAction('光照编辑')
+ self.pluginAction = self.toolsMenu.addAction('图形编辑')
+ # self.toolsMenu.addSeparator()
+ # self.iconManagerAction = self.toolsMenu.addAction('图标管理器')
+ # self.iconManagerAction.triggered.connect(self.onOpenIconManager)
+
+ # VR子菜单
+ self.vrMenu = self.toolsMenu.addMenu('VR')
+ self.enterVRAction = self.vrMenu.addAction('进入VR模式')
+ self.exitVRAction = self.vrMenu.addAction('退出VR模式')
+ self.vrMenu.addSeparator()
+ self.vrStatusAction = self.vrMenu.addAction('VR状态')
+ self.vrSettingsAction = self.vrMenu.addAction('VR设置')
+ self.vrMenu.addSeparator()
+
+ # VR调试子菜单
+ self.vrDebugMenu = self.vrMenu.addMenu('VR调试')
+ self.vrDebugToggleAction = self.vrDebugMenu.addAction('启用调试输出')
+ self.vrDebugToggleAction.setCheckable(True)
+ self.vrDebugToggleAction.setChecked(False) # 默认关闭(节省资源)
+
+ self.vrShowPerformanceAction = self.vrDebugMenu.addAction('立即显示性能报告')
+
+ self.vrDebugMenu.addSeparator()
+
+ # 调试模式切换
+ self.vrDebugModeMenu = self.vrDebugMenu.addMenu('输出模式')
+ self.vrDebugBriefAction = self.vrDebugModeMenu.addAction('简短模式')
+ self.vrDebugDetailedAction = self.vrDebugModeMenu.addAction('详细模式')
+ self.vrDebugBriefAction.setCheckable(True)
+ self.vrDebugDetailedAction.setCheckable(True)
+ self.vrDebugDetailedAction.setChecked(True) # 默认详细模式
+
+ # 创建调试模式动作组(单选)
+ from PyQt5.QtWidgets import QActionGroup
+ self.vrDebugModeGroup = QActionGroup(self)
+ self.vrDebugModeGroup.addAction(self.vrDebugBriefAction)
+ self.vrDebugModeGroup.addAction(self.vrDebugDetailedAction)
+
+ self.vrDebugMenu.addSeparator()
+
+ # 性能监控选项
+ self.vrPerformanceMonitorAction = self.vrDebugMenu.addAction('启用性能监控')
+ self.vrPerformanceMonitorAction.setCheckable(True)
+ self.vrPerformanceMonitorAction.setChecked(False) # 默认关闭(节省资源)
+
+ self.vrGpuTimingAction = self.vrDebugMenu.addAction('启用GPU时间监控')
+ self.vrGpuTimingAction.setCheckable(True)
+ self.vrGpuTimingAction.setChecked(False) # 默认关闭(节省资源)
+
+ # 管线监控选项
+ self.vrPipelineMonitorAction = self.vrDebugMenu.addAction('启用管线监控')
+ self.vrPipelineMonitorAction.setCheckable(True)
+ self.vrPipelineMonitorAction.setChecked(False) # 默认关闭(节省资源)
+
+ self.vrDebugMenu.addSeparator()
+
+ # 姿态策略选项
+ self.vrPoseStrategyMenu = self.vrDebugMenu.addMenu('姿态策略')
+ self.vrPoseRenderCallbackAction = self.vrPoseStrategyMenu.addAction('渲染回调策略')
+ self.vrPoseUpdateTaskAction = self.vrPoseStrategyMenu.addAction('更新任务策略')
+ self.vrPoseRenderCallbackAction.setCheckable(True)
+ self.vrPoseUpdateTaskAction.setCheckable(True)
+ self.vrPoseRenderCallbackAction.setChecked(True) # 默认策略
+
+ # 创建姿态策略动作组(单选)
+ self.vrPoseStrategyGroup = QActionGroup(self)
+ self.vrPoseStrategyGroup.addAction(self.vrPoseRenderCallbackAction)
+ self.vrPoseStrategyGroup.addAction(self.vrPoseUpdateTaskAction)
+
+ self.vrDebugMenu.addSeparator()
+
+ # 测试功能
+ self.vrTestPipelineAction = self.vrDebugMenu.addAction('测试管线监控')
+
+ # VR测试模式
+ self.vrTestModeAction = self.vrDebugMenu.addAction('VR测试模式')
+ self.vrTestModeAction.setCheckable(True)
+ self.vrTestModeAction.setChecked(False) # 默认关闭
+
+ # VR测试模式调试子菜单
+ self.vrTestDebugMenu = self.vrDebugMenu.addMenu('测试模式调试')
+
+ # 渐进式功能开关
+ self.vrTestSubmitTextureAction = self.vrTestDebugMenu.addAction('启用纹理提交')
+ self.vrTestSubmitTextureAction.setCheckable(True)
+ self.vrTestSubmitTextureAction.setChecked(False) # 默认关闭
+
+ self.vrTestWaitPosesAction = self.vrTestDebugMenu.addAction('启用姿态等待')
+ self.vrTestWaitPosesAction.setCheckable(True)
+ self.vrTestWaitPosesAction.setChecked(False) # 默认关闭
+
+ self.vrTestDebugMenu.addSeparator()
+
+ # 快捷测试预设
+ self.vrTestStep1Action = self.vrTestDebugMenu.addAction('步骤1: 只启用纹理提交')
+ self.vrTestStep2Action = self.vrTestDebugMenu.addAction('步骤2: 只启用姿态等待')
+ self.vrTestStep3Action = self.vrTestDebugMenu.addAction('步骤3: 同时启用两者')
+ self.vrTestResetAction = self.vrTestDebugMenu.addAction('重置: 禁用所有功能')
+
+ self.vrDebugSettingsAction = self.vrDebugMenu.addAction('调试设置...')
+
+ # 初始状态下禁用退出VR选项
+ self.exitVRAction.setEnabled(False)
+
+ # 统一创建菜单 - 关键修改
+ self.createMenu = menubar.addMenu('创建')
+ self.setupCreateMenuActions() # 统一创建菜单动作
+
+ #添加地形菜单
+ self.createTerrainMenu = self.createMenu.addMenu('地形')
+ self.createFlatTerrainAction = self.createTerrainMenu.addAction('创建平面地形')
+ self.createHeightmapTerrainAction = self.createTerrainMenu.addAction('从高度图创建地形')
+ self.createTerrainMenu.addSeparator()
+ # self.terrainEditModeAction = self.createTerrainMenu.addAction('地形编辑模式')
+
+
+ # 帮助菜单
+ self.helpMenu = menubar.addMenu('帮助')
+ self.aboutAction = self.helpMenu.addAction('关于')
+ self.aboutAction.triggered.connect(self.showAboutDialog)
+
+ tool_menu = self.menuBar().addMenu("配置")
+ plugin_config_action = tool_menu.addAction("渲染管线插件配置")
+ plugin_config_action.triggered.connect(self.open_plugin_config_dialog)
+
+ def showAboutDialog(self):
+ msgBox = QMessageBox()
+ msgBox.setWindowTitle("关于")
+ msgBox.setText(f'元泰引擎系统\nMetaCore\n版本:v1.0')
+ msgBox.setIcon(QMessageBox.NoIcon)
+ msgBox.exec_()
+
+ def open_plugin_config_dialog(self):
+ """打开插件配置对话框"""
+ try:
+ dialog = PluginConfigDialog(self)
+ dialog.exec_()
+ except Exception as e:
+ print(f"打开插件配置对话框失败: {e}")
+
+ def refreshAssetsView(self):
+ """"刷新资源视图"""
+ if hasattr(self,'fileView') and self.fileView:
+ self.fileView.refreshView()
+ print("资源视图已刷新")
+
+ def setupCreateMenuActions(self):
+ """统一设置创建菜单的所有动作 - 避免重复代码"""
+ # 基础对象
+ self.createEnptyaddAction = self.createMenu.addAction('空对象')
+
+ # 3D对象子菜单
+ self.create3dObjectaddMenu = self.createMenu.addMenu('3D对象')
+ # 可以在这里添加更多3D对象类型
+
+ # 3D GUI子菜单
+ self.create3dGUIaddMenu = self.createMenu.addMenu('3D GUI')
+ self.create3DTextAction = self.create3dGUIaddMenu.addAction('3D文本')
+ self.create3DImageAction = self.create3dGUIaddMenu.addAction('3D图片')
+
+ # GUI子菜单
+ self.createGUIaddMenu = self.createMenu.addMenu('GUI')
+ self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮')
+ self.createLabelAction = self.createGUIaddMenu.addAction('创建标签')
+ self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框')
+ self.createImageAction = self.createGUIaddMenu.addAction('创建图片')
+ self.createGUIaddMenu.addSeparator()
+ self.createVideoScreen = self.createGUIaddMenu.addAction('创建视频屏幕')
+ self.create2DVideoScreen = self.createGUIaddMenu.addAction('创建2D视频屏幕')
+ self.createSphericalVideo = self.createGUIaddMenu.addAction('创建球形视频')
+ self.createGUIaddMenu.addSeparator()
+ self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕')
+
+ # 光源子菜单
+ self.createLightaddMenu = self.createMenu.addMenu('光源')
+ self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯')
+ self.createPointLightAction = self.createLightaddMenu.addAction('点光源')
+
+ self.createScriptMenu = self.createMenu.addMenu('脚本')
+ self.createScriptAction = self.createScriptMenu.addAction('创建脚本...')
+ self.loadScriptAction = self.createScriptMenu.addAction('加载脚本文件...')
+ self.loadAllScriptsAction = self.createScriptMenu.addAction('重载所有脚本')
+ self.createScriptMenu.addSeparator()
+ self.toggleHotReloadAction = self.createScriptMenu.addAction('启用热重载')
+ self.toggleHotReloadAction.setCheckable(True)
+ self.toggleHotReloadAction.setChecked(True) # 默认启用
+ self.createScriptMenu.addSeparator()
+ self.openScriptsManagerAction = self.createScriptMenu.addAction('脚本管理器')
+
+ self.createInfoPanelMenu = self.createMenu.addMenu('信息面板')
+ self.createSamplePanelAction = self.createInfoPanelMenu.addAction('创建2D示例面板')
+ self.create3DSamplePanelAction = self.createInfoPanelMenu.addAction('创建3D实例面板')
+ self.createInfoPanelMenu.addSeparator()
+ self.webBrowserAction = self.createInfoPanelMenu.addAction("Web面板")
+
+ # 统一连接信号到处理方法
+ self.connectCreateMenuActions()
+
+ def setupViewMenuActions(self):
+ """设置视图菜单动作"""
+ # 连接视图菜单事件
+ self.viewPerspectiveAction.triggered.connect(self.onViewPerspective)
+ #self.viewTopAction.triggered.connect(self.onViewTop)
+ #self.viewFrontAction.triggered.connect(self.onViewFront)
+ self.viewOrthographicAction = self.viewMenu.addAction('正交视图') # 添加正交视图动作
+ #self.viewOrthographicAction.triggered.connect(self.onViewOrthographic)
+ #self.viewGridAction.triggered.connect(self.onViewGrid) # 添加网格显示的信号连接
+
+ # 保存原始相机设置
+ self._original_camera_fov = 80
+ self._original_camera_pos = (0, -50, 20)
+ self._original_camera_lookat = (0, 0, 0)
+
+ self._grid_visible = False
+ def onViewPerspective(self):
+ """切换到透视视图"""
+ try:
+ lens = self.world.cam.node().getLens()
+ lens.setFov(self._original_camera_fov)
+ except Exception as e:
+ print(f"切换到透视视图失败{e}")
+
+ def onViewOrthographic(self):
+ """切换到正交视图"""
+ try:
+ # 保存当前相机设置(如果是透视模式)
+ lens = self.world.cam.node().getLens()
+ if not hasattr(self, '_saved_perspective_settings'):
+ self._saved_perspective_settings = {
+ 'fov': lens.getFov()[0],
+ 'pos': self.world.cam.getPos(),
+ 'hpr': self.world.cam.getHpr()
+ }
+
+ # 获取窗口尺寸
+ win_width, win_height = self.world.getWindowSize()
+ aspect_ratio = win_width / win_height if win_height != 0 else 16 / 9
+
+ # 修改现有镜头为正交投影
+ if not isinstance(lens, OrthographicLens):
+ # 保存当前镜头类型
+ self._original_lens = lens
+
+ # 创建正交镜头并替换现有镜头
+ ortho_lens = OrthographicLens()
+ ortho_lens.setFilmSize(20 * aspect_ratio, 20) # 设置正交镜头大小
+ ortho_lens.setNearFar(-1000, 1000) # 设置较大的近远裁剪面
+
+ # 应用正交镜头
+ self.world.cam.node().setLens(ortho_lens)
+ else:
+ # 如果已经是正交镜头,则调整其参数
+ film_height = 20
+ film_width = film_height * aspect_ratio
+ lens.setFilmSize(film_width, film_height)
+
+ print("切换到正交视图")
+ except Exception as e:
+ print(f"切换正交视图失败: {e}")
+
+ def onViewTop(self):
+ """切换到俯视图(正交)"""
+ try:
+ # 保存当前设置
+ self._saveCurrentCameraSettings()
+
+ # 设置正交投影
+ self._setupOrthographicLens()
+
+ # 设置摄像机位置(从上方俯视)
+ self.world.cam.setPos(0, 0, 30)
+ self.world.cam.lookAt(0, 0, 0)
+ self.world.cam.setHpr(0, -90, 0) # 朝下看
+
+ # 更新菜单项文本
+ self._updateViewMenuText()
+
+ print("切换到俯视图")
+ except Exception as e:
+ print(f"切换俯视图失败: {e}")
+
+ def onViewFront(self):
+ """切换到前视图(正交)"""
+ try:
+ # 保存当前设置
+ self._saveCurrentCameraSettings()
+
+ # 设置正交投影
+ self._setupOrthographicLens()
+
+ # 设置摄像机位置(从前方向看)
+ self.world.cam.setPos(0, -30, 0)
+ self.world.cam.lookAt(0, 0, 0)
+ self.world.cam.setHpr(0, 0, 0) # 正面朝向
+
+ # 更新菜单项文本
+ self._updateViewMenuText()
+
+ print("切换到前视图")
+ except Exception as e:
+ print(f"切换前视图失败: {e}")
+
+ def onViewGrid(self):
+ """切换网格显示/隐藏"""
+ try:
+ # 切换网格显示状态
+ self._grid_visible = not self._grid_visible
+
+ # 查找网格节点
+ grid_node = self.world.render.find("**/grid")
+ if grid_node.isEmpty():
+ # 如果网格不存在则创建
+ self._createGridView()
+ grid_node = self.world.render.find("**/grid")
+
+ # 设置网格可见性
+ if not grid_node.isEmpty():
+ if self._grid_visible:
+ grid_node.show()
+ self.viewGridAction.setText("隐藏网格")
+ print("网格已显示")
+ else:
+ grid_node.hide()
+ self.viewGridAction.setText("显示网格")
+ print("网格已隐藏")
+ else:
+ print("网格节点未找到")
+
+ except Exception as e:
+ print(f"切换网格显示失败: {e}")
+ def _createGridView(self):
+ """创建网格视图"""
+ try:
+ from panda3d.core import LineSegs,Vec3
+ grid_node = self.world.render.attachNewNode("grid")
+ lines = LineSegs()
+ lines.setThickness(1.0)
+ lines.setColor(0.3,0.3,0.3,1.0)
+ grid_size = 20
+ grid_step = 1
+
+ for i in range(-grid_size,grid_size+1,grid_step):
+ lines.moveTo(Vec3(-grid_size,i,0))
+ lines.drawTo(Vec3(grid_size,i,0))
+
+ grid_node.attachNewNode(lines.create())
+ # 添加中心轴线(红色X轴,绿色Y轴)
+ axis_lines = LineSegs()
+ axis_lines.setThickness(2.0)
+ # X轴(红色)
+ axis_lines.setColor(1.0,0.0,0.0,1.0)
+ axis_lines.moveTo(Vec3(0,0,0))
+ axis_lines.drawTo(Vec3(grid_size,0,0))
+ # Y轴(绿色)
+ axis_lines.setColor(0.0, 1.0, 0.0, 1.0)
+ axis_lines.moveTo(Vec3(0, 0, 0))
+ axis_lines.drawTo(Vec3(0, grid_size, 0))
+
+ grid_node.attachNewNode(axis_lines.create())
+
+ print("网格已创建")
+ except Exception as e:
+ print(f"创建网格失败{e}")
+
+ def _saveCurrentCameraSettings(self):
+ """保存当前相机设置"""
+ try:
+ lens = self.world.cam.node().getLens()
+ self._saved_camera_settings = {
+ 'lens_type': 'perspective' if not isinstance(lens, OrthographicLens) else 'orthographic',
+ 'fov': lens.getFov()[0] if hasattr(lens, 'getFov') else None,
+ 'film_size': (lens.getFilmSize()[0], lens.getFilmSize()[1]) if hasattr(lens, 'getFilmSize') else None,
+ 'pos': self.world.cam.getPos(),
+ 'hpr': self.world.cam.getHpr()
+ }
+ except Exception as e:
+ print(f"保存相机设置失败: {e}")
+
+ def _setupOrthographicLens(self):
+ """设置正交镜头"""
+ try:
+ win_width, win_height = self.world.getWindowSize()
+ aspect_ratio = win_width / win_height if win_height != 0 else 16 / 9
+
+ from panda3d.core import OrthographicLens
+ ortho_lens = OrthographicLens()
+ ortho_lens.setFilmSize(20 * aspect_ratio, 20) # 设置正交镜头大小
+ ortho_lens.setNearFar(-1000, 1000) # 设置较大的近远裁剪面
+
+ self.world.cam.node().setLens(ortho_lens)
+ except Exception as e:
+ print(f"设置正交镜头失败: {e}")
+
+ def _updateViewMenuText(self):
+ """更新视图菜单文本"""
+ try:
+ lens = self.world.cam.node().getLens()
+ from panda3d.core import OrthographicLens
+
+ # 更新正交/透视视图动作文本
+ if isinstance(lens, OrthographicLens):
+ self.viewOrthographicAction.setText("切换到透视视图")
+ self.viewOrthographicAction.triggered.disconnect()
+ self.viewOrthographicAction.triggered.connect(self.onViewPerspective)
+ else:
+ self.viewOrthographicAction.setText("切换到正交视图")
+ self.viewOrthographicAction.triggered.disconnect()
+ self.viewOrthographicAction.triggered.connect(self.onViewOrthographic)
+ except Exception as e:
+ print(f"更新视图菜单文本失败: {e}")
+
+ # 如果需要在窗口大小改变时调整正交镜头,可以添加以下方法
+ def _onWindowResized(self):
+ """窗口大小改变时的处理"""
+ try:
+ lens = self.world.cam.node().getLens()
+ from panda3d.core import OrthographicLens
+
+ # 如果当前是正交镜头,需要根据新窗口大小调整
+ if isinstance(lens, OrthographicLens):
+ win_width, win_height = self.world.getWindowSize()
+ if win_height != 0:
+ aspect_ratio = win_width / win_height
+ film_height = 20
+ film_width = film_height * aspect_ratio
+ lens.setFilmSize(film_width, film_height)
+ except Exception as e:
+ print(f"窗口大小调整失败: {e}")
+
+
+ def connectCreateMenuActions(self):
+ """统一连接创建菜单的信号到处理方法"""
+ # 连接到world对象的创建方法
+ # self.createEnptyaddAction.triggered.connect(self.world.createEmptyObject)
+ self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText())
+ self.create3DImageAction.triggered.connect(lambda: self.world.createGUI3DImage())
+ self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton())
+ self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel())
+ self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry())
+ self.createImageAction.triggered.connect(lambda: self.world.createGUI2DImage())
+ self.createVideoScreen.triggered.connect(self.world.createVideoScreen)
+ self.create2DVideoScreen.triggered.connect(self.world.create2DVideoScreen)
+ self.createSphericalVideo.triggered.connect(self.world.createSphericalVideo)
+ self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen())
+ self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
+ self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight())
+
+ self.createScriptAction.triggered.connect(self.onCreateScript)
+ self.loadScriptAction.triggered.connect(self.onLoadScript)
+ #self.loadAllScriptsAction.triggered.connect(self.onLoadAllScripts)
+ self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload)
+ self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager)
+
+ self.createSamplePanelAction.triggered.connect(self.world.info_panel_manager.onCreateSampleInfoPanel)
+ self.create3DSamplePanelAction.triggered.connect(self.onCreate3DSampleInfoPanel)
+ #self.webBrowserAction.triggered.connect(self.openWebBrowser_win)
+
+ if sys.platform == 'win32':
+ self.webBrowserAction.triggered.connect(self.openWebBrowser_win)
+ else:
+ self.webBrowserAction.triggered.connect(self.openWebBrowser)
+
+ def getCreateMenuActions(self):
+ """获取所有创建菜单动作的字典 - 供右键菜单使用"""
+ return {
+ 'createEmpty': self.createEnptyaddAction,
+ 'create3DText': self.create3DTextAction,
+ 'create3DImage': self.create3DImageAction,
+ 'createButton': self.createButtonAction,
+ 'createLabel': self.createLabelAction,
+ 'createEntry': self.createEntryAction,
+ 'createImage': self.createImageAction,
+ 'createVideoScreen': self.createVideoScreen,
+ 'create2DVideoScreen':self.create2DVideoScreen,
+ 'createSphericalVideo': self.createSphericalVideo,
+ 'createVirtualScreen': self.createVirtualScreenAction,
+ 'createSpotLight': self.createSpotLightAction,
+ 'createPointLight': self.createPointLightAction,
+ }
+
+ def setupDockWindows(self):
+ """创建停靠窗口"""
+ # 创建左侧停靠窗口(层级窗口)
+ self.leftDock = QDockWidget("层级", self)
+ self.leftDock.setStyleSheet("""
+ QDockWidget {
+ background-color: #19191b;
+ color: #ffffff;
+ border: none;
+ }
+ QDockWidget::title {
+ background-color: #19191b;
+ padding: 8px 10px;
+ border-bottom: none;
+ text-align: left;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 12px;
+ font-weight: 500;
+ }
+ QDockWidget::close-button {
+ background-color: rgba(89, 100, 113, 0.3);
+ border: 1px solid rgba(184, 211, 241, 0.2);
+ icon-size: 10px;
+ border-radius: 2px;
+ width: 16px;
+ height: 16px;
+ right: 6px;
+ top: 6px;
+ }
+ QDockWidget::float-button {
+ background-color: rgba(89, 100, 113, 0.3);
+ border: 1px solid rgba(184, 211, 241, 0.2);
+ icon-size: 10px;
+ border-radius: 2px;
+ width: 16px;
+ height: 16px;
+ right: 26px;
+ top: 6px;
+ }
+ QDockWidget::close-button:hover, QDockWidget::float-button:hover {
+ background-color: rgba(89, 100, 113, 0.5);
+ border: 1px solid rgba(184, 211, 241, 0.3);
+ }
+ QDockWidget::close-button:pressed, QDockWidget::float-button:pressed {
+ background-color: #3067c0;
+ border: 1px solid rgba(48, 103, 192, 0.6);
+ }
+ QTreeView {
+ background-color: #19191b;
+ color: #ebebeb;
+ border: none;
+ padding: 0px 0px 0px 8px;
+ alternate-background-color: #19191b;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 400;
+ outline: none;
+ }
+ QTreeView::item {
+ padding: 3px 0px;
+ border: none;
+ }
+ QTreeView::item:hover {
+ background-color: rgba(89, 100, 113, 0.3);
+ }
+ QTreeView::item:selected {
+ background-color: rgba(48, 103, 192, 0.4);
+ color: #ffffff;
+ }
+ QTreeView::branch {
+ background-color: #19191b;
+ }
+ QTreeView::branch:has-children:!has-siblings:closed,
+ QTreeView::branch:closed:has-children:has-siblings {
+ image: url(icons/solid_right_arrows.png);
+ width: 12px;
+ height: 12px;
+ }
+ QTreeView::branch:open:has-children:!has-siblings,
+ QTreeView::branch:open:has-children:has-siblings {
+ image: url(icons/solid_down_arrows.png);
+ width: 12px;
+ height: 12px;
+ }
+ """)
+ self.treeWidget = CustomTreeWidget(self.world)
+ self.world.setTreeWidget(self.treeWidget) # 设置树形控件引用
+
+ # 创建包装容器,添加标题下方的分隔线
+ leftDockContainer = QWidget()
+ leftDockContainer.setStyleSheet("QWidget { background-color: #19191b; }")
+ leftDockLayout = QVBoxLayout(leftDockContainer)
+ leftDockLayout.setContentsMargins(0, 0, 0, 0)
+ leftDockLayout.setSpacing(0)
+
+ # 添加带左右间距的分隔线
+ leftSeparator = QFrame()
+ leftSeparator.setFrameShape(QFrame.HLine)
+ leftSeparator.setFrameShadow(QFrame.Plain)
+ leftSeparator.setStyleSheet("""
+ QFrame {
+ background-color: rgba(77, 116, 189, 0.4);
+ max-height: 1px;
+ margin-left: 8px;
+ margin-right: 8px;
+ border: none;
+ }
+ """)
+ leftDockLayout.addWidget(leftSeparator)
+ leftDockLayout.addWidget(self.treeWidget)
+
+ self.leftDock.setWidget(leftDockContainer)
+ self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.leftDock)
+
+ # 创建右侧停靠窗口(属性窗口)
+ self.rightDock = QDockWidget("属性", self)
+ self.rightDock.setStyleSheet("""
+ QDockWidget {
+ background-color: #19191b;
+ color: #ffffff;
+ border: none;
+ }
+ QDockWidget::title {
+ background-color: #19191b;
+ padding: 8px 10px;
+ border-bottom: none;
+ text-align: left;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 14px;
+ font-weight: 500;
+ }
+ QDockWidget::close-button {
+ background-color: rgba(89, 100, 113, 0.3);
+ border: 1px solid rgba(184, 211, 241, 0.2);
+ icon-size: 10px;
+ border-radius: 2px;
+ width: 16px;
+ height: 16px;
+ right: 6px;
+ top: 6px;
+ }
+ QDockWidget::float-button {
+ background-color: rgba(89, 100, 113, 0.3);
+ border: 1px solid rgba(184, 211, 241, 0.2);
+ icon-size: 10px;
+ border-radius: 2px;
+ width: 16px;
+ height: 16px;
+ right: 26px;
+ top: 6px;
+ }
+ QDockWidget::close-button:hover, QDockWidget::float-button:hover {
+ background-color: rgba(89, 100, 113, 0.5);
+ border: 1px solid rgba(184, 211, 241, 0.3);
+ }
+ QDockWidget::close-button:pressed, QDockWidget::float-button:pressed {
+ background-color: #3067c0;
+ border: 1px solid rgba(48, 103, 192, 0.6);
+ }
+ QScrollArea {
+ background-color: #19191b;
+ border: none;
+ }
+ QWidget#PropertyContainer {
+ background-color: #19191b;
+ }
+ QLabel {
+ color: #ebebeb;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ }
+
+ /* 现代化的输入控件样式 */
+ QLineEdit {
+ background-color: rgba(89, 100, 113, 0.15);
+ color: #ebebeb;
+ border: 1px solid rgba(76, 92, 110, 0.4);
+ border-radius: 4px;
+ padding: 6px 8px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ 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);
+ }
+
+ QDoubleSpinBox, QSpinBox {
+ background-color: rgba(89, 100, 113, 0.15);
+ color: #ebebeb;
+ border: 1px solid rgba(76, 92, 110, 0.4);
+ border-radius: 4px;
+ padding: 6px 8px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ min-height: 16px;
+ }
+ QDoubleSpinBox:focus, QSpinBox:focus {
+ border: 1px solid #4d74bd;
+ background-color: rgba(77, 116, 189, 0.1);
+ }
+ QDoubleSpinBox:hover, QSpinBox:hover {
+ border: 1px solid rgba(77, 116, 189, 0.6);
+ background-color: rgba(89, 100, 113, 0.2);
+ }
+ QDoubleSpinBox::up-button, QSpinBox::up-button {
+ background-color: rgba(77, 116, 189, 0.2);
+ border: none;
+ border-radius: 2px;
+ width: 16px;
+ subcontrol-origin: border;
+ subcontrol-position: top right;
+ }
+ QDoubleSpinBox::down-button, QSpinBox::down-button {
+ background-color: rgba(77, 116, 189, 0.2);
+ border: none;
+ border-radius: 2px;
+ width: 16px;
+ subcontrol-origin: border;
+ subcontrol-position: bottom right;
+ }
+ QDoubleSpinBox::up-button:hover, QSpinBox::up-button:hover,
+ QDoubleSpinBox::down-button:hover, QSpinBox::down-button:hover {
+ background-color: rgba(77, 116, 189, 0.4);
+ }
+ QDoubleSpinBox::up-arrow, QSpinBox::up-arrow {
+ image: url(icons/up_arrows.png);
+ width: 12px;
+ height: 12px;
+ }
+ QDoubleSpinBox::down-arrow, QSpinBox::down-arrow {
+ image: url(icons/down_arrows.png);
+ width: 12px;
+ height: 12px;
+ }
+ QDoubleSpinBox::up-arrow:hover, QSpinBox::up-arrow:hover {
+ image: url(icons/up_arrows.png);
+ }
+ QDoubleSpinBox::down-arrow:hover, QSpinBox::down-arrow:hover {
+ image: url(icons/down_arrows.png);
+ }
+
+ QComboBox {
+ background-color: rgba(89, 100, 113, 0.15);
+ color: #ebebeb;
+ border: 1px solid rgba(76, 92, 110, 0.4);
+ border-radius: 4px;
+ padding: 6px 8px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ min-height: 16px;
+ }
+ QComboBox:focus {
+ border: 1px solid #4d74bd;
+ background-color: rgba(77, 116, 189, 0.1);
+ }
+ QComboBox:hover {
+ border: 1px solid rgba(77, 116, 189, 0.6);
+ background-color: rgba(89, 100, 113, 0.2);
+ }
+ QComboBox::drop-down {
+ border: none;
+ border-left: 1px solid rgba(76, 92, 110, 0.4);
+ background-color: rgba(77, 116, 189, 0.2);
+ border-radius: 0 4px 4px 0;
+ width: 20px;
+ }
+ QComboBox::drop-down:hover {
+ background-color: rgba(77, 116, 189, 0.4);
+ }
+ QComboBox::down-arrow {
+ image: url(icons/down_arrows.png);
+ width: 12px;
+ height: 12px;
+ }
+ QComboBox QAbstractItemView {
+ background-color: #2a2a2e;
+ color: #ebebeb;
+ selection-background-color: rgba(77, 116, 189, 0.4);
+ border: 1px solid rgba(76, 92, 110, 0.6);
+ border-radius: 4px;
+ padding: 4px;
+ }
+
+ 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: 4px;
+ padding: 8px 12px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 400;
+ min-height: 16px;
+ }
+ 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);
+ }
+
+ QCheckBox {
+ color: #ebebeb;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ spacing: 8px;
+ }
+ QCheckBox::indicator {
+ width: 16px;
+ height: 16px;
+ border: 1px solid rgba(76, 92, 110, 0.6);
+ border-radius: 3px;
+ background-color: rgba(89, 100, 113, 0.15);
+ }
+ QCheckBox::indicator:hover {
+ border: 1px solid rgba(77, 116, 189, 0.6);
+ background-color: rgba(89, 100, 113, 0.2);
+ }
+ QCheckBox::indicator:checked {
+ background-color: #4d74bd;
+ border: 1px solid #4d74bd;
+ image: none;
+ }
+ QCheckBox::indicator:checked:hover {
+ background-color: rgba(77, 116, 189, 1.0);
+ }
+
+ QGroupBox {
+ /* 减少上边距,使相邻组更紧凑 */
+ margin-top: 0px;
+
+ /* 移除边框 */
+ border: none;
+
+ /* 设置内边距,为标题和内容留出空间,左右内边距与主标题对齐 */
+ padding-top: 20px; /* 为标题留出足够空间 */
+ padding-left: 0px; /* Align subtitle with dock title baseline */
+ padding-right: 0px; /* 右内边距与主标题分隔线对齐 */
+
+ /* 减少下边距,使相邻组更紧凑 */
+ background-color: transparent;
+ margin-bottom: 6px;
+
+ /* 使用边框创建样式线,与主标题分隔线保持相同的8px左右间距 */
+ border-top: 1px solid rgba(77, 116, 189, 0.4);
+ margin-left: 0px;
+ margin-right: 8px;
+ }
+
+ QGroupBox::title {
+ /* 将标题定位到控件内部,在样条线下方 */
+ subcontrol-origin: padding;
+ subcontrol-position: top left;
+
+ /* 标题样式,左边距为0因为已经通过padding-left处理了对齐 */
+ padding: 8px 3px 8px 0px; /* 左边距设为0,与内容对齐 */
+ margin-top: 0px; /* 标题紧贴内边距顶部 */
+
+ /* 移除标题上的边框 */
+ border: none;
+
+ /* 字体和颜色样式 */
+ color: rgba(255, 255, 255, 0.8);
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 12px;
+ font-weight: 500;
+ }
+ QGroupBox[groupRole="first"] {
+ padding-top: 16px;
+ margin-top: 0px;
+ border-top: none; /* 明确移除顶部边框 */
+ }
+ QGroupBox[groupRole="first"]::title {
+ padding: 6px 3px 8px 0px;
+ }
+ """)
+
+ # 创建属性面板的主容器和布局
+ self.propertyContainer = QWidget()
+ self.propertyContainer.setObjectName("PropertyContainer")
+
+ try:
+ self.propertyLayout = QVBoxLayout(self.propertyContainer)
+ # Keep subtitles aligned with the dock title left padding
+ self.propertyLayout.setContentsMargins(10, 0, 10, 12)
+ self.propertyLayout.setSpacing(12)
+ print(f"✓ 属性布局创建完成: {self.propertyLayout}")
+
+ # 添加初始提示信息
+ tipLabel = QLabel("选择对象以查看属性")
+ tipLabel.setStyleSheet("color: gray; padding: 10px 0px;") # 使用灰色字体
+ self.propertyLayout.addWidget(tipLabel)
+ print("✓ 提示标签添加完成")
+
+ # 创建滚动区域并设置属性
+ self.scrollArea = QScrollArea()
+ self.scrollArea.setWidgetResizable(True)
+ self.scrollArea.setWidget(self.propertyContainer)
+ print("✓ 滚动区域设置完成")
+
+ # 创建包装容器,添加标题下方的分隔线
+ rightDockContainer = QWidget()
+ rightDockContainer.setStyleSheet("QWidget { background-color: #19191b; }")
+ rightDockLayout = QVBoxLayout(rightDockContainer)
+ rightDockLayout.setContentsMargins(0, 0, 0, 0)
+ rightDockLayout.setSpacing(0)
+
+ # 添加带左右间距的分隔线
+ rightSeparator = QFrame()
+ rightSeparator.setFrameShape(QFrame.HLine)
+ rightSeparator.setFrameShadow(QFrame.Plain)
+ rightSeparator.setStyleSheet("""
+ QFrame {
+ background-color: rgba(77, 116, 189, 0.4);
+ max-height: 1px;
+ margin-left: 8px;
+ margin-right: 8px;
+ border: none;
+ }
+ """)
+ rightDockLayout.addWidget(rightSeparator)
+ rightDockLayout.addWidget(self.scrollArea)
+
+ # 设置包装容器为停靠窗口的主部件
+ self.rightDock.setWidget(rightDockContainer)
+ self.rightDock.setMinimumWidth(300)
+ self.addDockWidget(Qt.RightDockWidgetArea, self.rightDock)
+ print("✓ 右侧停靠窗口添加完成")
+
+ # 设置属性面板到世界对象
+ if hasattr(self.world, 'setPropertyLayout'):
+ print("开始设置属性布局到世界对象...")
+ self.world.setPropertyLayout(self.propertyLayout)
+ print("✓ 属性布局设置完成")
+ else:
+ print("⚠ 世界对象没有 setPropertyLayout 方法")
+
+ except Exception as e:
+ print(f"✗ 设置属性面板时出错: {e}")
+ import traceback
+ traceback.print_exc()
+
+ # 创建基本的属性面板作为后备方案
+ fallback_widget = QLabel("属性面板初始化失败\n请查看控制台日志")
+ fallback_widget.setStyleSheet("color: red; background-color: white; padding: 10px;")
+ self.rightDock.setWidget(fallback_widget)
+ self.addDockWidget(Qt.RightDockWidgetArea, self.rightDock)
+
+ # 创建脚本管理停靠窗口
+ self.scriptDock = QDockWidget("脚本管理", self)
+ self.scriptDock.setStyleSheet("""
+ QDockWidget {
+ background-color: #19191b;
+ color: #ffffff;
+ border: none;
+ }
+ QDockWidget::title {
+ background-color: #19191b;
+ padding: 8px 10px;
+ border-bottom: none;
+ text-align: left;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 14px;
+ font-weight: 500;
+ }
+ QDockWidget::close-button {
+ background-color: rgba(89, 100, 113, 0.3);
+ border: 1px solid rgba(184, 211, 241, 0.2);
+ icon-size: 10px;
+ border-radius: 2px;
+ width: 16px;
+ height: 16px;
+ right: 6px;
+ top: 6px;
+ }
+ QDockWidget::float-button {
+ background-color: rgba(89, 100, 113, 0.3);
+ border: 1px solid rgba(184, 211, 241, 0.2);
+ icon-size: 10px;
+ border-radius: 2px;
+ width: 16px;
+ height: 16px;
+ right: 26px;
+ top: 6px;
+ }
+ QDockWidget::close-button:hover, QDockWidget::float-button:hover {
+ background-color: rgba(89, 100, 113, 0.5);
+ border: 1px solid rgba(184, 211, 241, 0.3);
+ }
+ QDockWidget::close-button:pressed, QDockWidget::float-button:pressed {
+ background-color: #3067c0;
+ border: 1px solid rgba(48, 103, 192, 0.6);
+ }
+ QScrollArea {
+ background-color: #19191b;
+ border: none;
+ }
+ QWidget#ScriptContainer {
+ background-color: #19191b;
+ }
+ QLabel {
+ color: #ebebeb;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ }
+ QGroupBox {
+ /* 减少上边距,使相邻组更紧凑 */
+ margin-top: 0px;
+
+ /* 移除边框 */
+ border: none;
+
+ /* 设置内边距,为标题和内容留出空间,同时与主标题左侧对齐 */
+ padding-top: 20px; /* 为标题留出足够空间 */
+ padding-left: 0px;
+ padding-right: 8px; /* 右内边距与主标题分隔线对齐 */
+
+ /* 减少下边距,使相邻组更紧凑 */
+ background-color: transparent;
+ margin-bottom: 6px;
+
+ /* 使用边框创建样式线,与主标题分隔线保持相同的8px右侧间距 */
+ border-top: 1px solid rgba(77, 116, 189, 0.4);
+ margin-left: 0px;
+ margin-right: 8px;
+ }
+
+ QGroupBox::title {
+ /* 将标题定位到控件内部,在样条线下方 */
+ subcontrol-origin: padding;
+ subcontrol-position: top left;
+
+ /* 标题样式,左边距为0因为已经通过padding-left处理了对齐 */
+ padding: 8px 3px 8px 0px; /* 左边距设为0,与内容对齐 */
+ margin-top: 0px; /* 标题紧贴内边距顶部 */
+
+ /* 移除标题上的边框 */
+ border: none;
+
+ /* 字体和颜色样式 */
+ color: rgba(255, 255, 255, 0.8);
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 12px;
+ font-weight: 500;
+ }
+ QGroupBox[groupRole="first"] {
+ padding-top: 16px;
+ margin-top: 0px;
+ border-top: none; /* 明确移除顶部边框 */
+ }
+ QGroupBox[groupRole="first"]::title {
+ padding: 6px 3px 8px 0px;
+ }
+ """)
+
+ # 创建脚本面板的主容器和布局(与属性面板相同结构)
+ self.scriptContainer = QWidget()
+ # 设置脚本容器的背景色
+ self.scriptContainer.setStyleSheet("""
+ QWidget#ScriptContainer {
+ background-color: #19191b;
+ }
+ """)
+ self.scriptContainer.setObjectName("ScriptContainer")
+ self.scriptLayout = QVBoxLayout(self.scriptContainer)
+ # Match the dock title horizontal padding for consistent alignment
+ self.scriptLayout.setContentsMargins(10, 0, 10, 12)
+ self.scriptLayout.setSpacing(12)
+
+ # 创建滚动区域并设置属性
+ self.scriptScrollArea = QScrollArea()
+ self.scriptScrollArea.setWidgetResizable(True)
+ self.scriptScrollArea.setWidget(self.scriptContainer)
+
+ # 创建包装容器,添加标题下方的分隔线
+ scriptDockContainer = QWidget()
+ scriptDockContainer.setStyleSheet("QWidget { background-color: #19191b; }")
+ scriptDockLayout = QVBoxLayout(scriptDockContainer)
+ scriptDockLayout.setContentsMargins(0, 0, 0, 0)
+ scriptDockLayout.setSpacing(0)
+
+ # 添加带左右间距的分隔线
+ scriptSeparator = QFrame()
+ scriptSeparator.setFrameShape(QFrame.HLine)
+ scriptSeparator.setFrameShadow(QFrame.Plain)
+ scriptSeparator.setStyleSheet("""
+ QFrame {
+ background-color: rgba(77, 116, 189, 0.4);
+ max-height: 1px;
+ margin-left: 8px;
+ margin-right: 8px;
+ border: none;
+ }
+ """)
+ scriptDockLayout.addWidget(scriptSeparator)
+ scriptDockLayout.addWidget(self.scriptScrollArea)
+
+ # 设置包装容器为停靠窗口的主部件
+ self.scriptDock.setWidget(scriptDockContainer)
+ self.scriptDock.setMinimumWidth(300)
+
+ # 设置脚本面板内容 - 这里添加调用
+ self.setupScriptPanel(self.scriptLayout)
+ self.addDockWidget(Qt.RightDockWidgetArea, self.scriptDock)
+
+
+ # 创建底部停靠窗口(资源窗口)
+ self.bottomDock = QDockWidget("资源", self)
+ self.bottomDock.setStyleSheet("""
+ QDockWidget {
+ background-color: #19191b;
+ color: #ffffff;
+ border: none;
+ }
+ QDockWidget::title {
+ background-color: #19191b;
+ padding: 8px 10px;
+ border-bottom: none;
+ text-align: left;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 14px;
+ font-weight: 500;
+ }
+ QDockWidget::close-button {
+ background-color: rgba(89, 100, 113, 0.3);
+ border: 1px solid rgba(184, 211, 241, 0.2);
+ icon-size: 10px;
+ border-radius: 2px;
+ width: 16px;
+ height: 16px;
+ right: 6px;
+ top: 6px;
+ }
+ QDockWidget::float-button {
+ background-color: rgba(89, 100, 113, 0.3);
+ border: 1px solid rgba(184, 211, 241, 0.2);
+ icon-size: 10px;
+ border-radius: 2px;
+ width: 16px;
+ height: 16px;
+ right: 26px;
+ top: 6px;
+ }
+ QDockWidget::close-button:hover, QDockWidget::float-button:hover {
+ background-color: rgba(89, 100, 113, 0.5);
+ border: 1px solid rgba(184, 211, 241, 0.3);
+ }
+ QDockWidget::close-button:pressed, QDockWidget::float-button:pressed {
+ background-color: #3067c0;
+ border: 1px solid rgba(48, 103, 192, 0.6);
+ }
+ """)
+
+ self.fileView = CustomAssetsTreeWidget(self.world)
+ # 为资源树添加样式
+ self.fileView.setStyleSheet("""
+ QTreeWidget {
+ background-color: #19191b;
+ color: #ebebeb;
+ border: none;
+ alternate-background-color: #19191b;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 400;
+ outline: none;
+ }
+ QTreeWidget::item {
+ padding: 3px 0px;
+ border: none;
+ }
+ QTreeWidget::item:hover {
+ background-color: rgba(89, 100, 113, 0.3);
+ }
+ QTreeWidget::item:selected {
+ background-color: rgba(48, 103, 192, 0.4);
+ color: #ffffff;
+ }
+ QTreeWidget::branch {
+ background-color: #19191b;
+ }
+ QTreeWidget::branch:has-children:!has-siblings:closed,
+ QTreeWidget::branch:closed:has-children:has-siblings {
+ image: url(icons/solid_right_arrows.png);
+ width: 12px;
+ height: 12px;
+ }
+ QTreeWidget::branch:open:has-children:!has-siblings,
+ QTreeWidget::branch:open:has-children:has-siblings {
+ image: url(icons/solid_down_arrows.png);
+ width: 12px;
+ height: 12px;
+ }
+ QHeaderView::section {
+ background-color: #19191b;
+ color: #ebebeb;
+ border: none;
+ border-bottom: 1px solid #4c5c6e;
+ padding: 6px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 12px;
+ font-weight: 500;
+ }
+ QGroupBox[groupRole="first"] {
+ border-top: none;
+ margin-top: 0px;
+ padding-top: 8px;
+ }
+ QGroupBox[groupRole="first"]::title {
+ padding: 8px 5px 8px 0px;
+ }
+ """)
+
+ # 创建包装容器,添加标题下方的分隔线
+ bottomDockContainer = QWidget()
+ bottomDockContainer.setStyleSheet("QWidget { background-color: #19191b; }")
+ bottomDockLayout = QVBoxLayout(bottomDockContainer)
+ bottomDockLayout.setContentsMargins(0, 0, 0, 0)
+ bottomDockLayout.setSpacing(0)
+
+ # 添加带左右间距的分隔线
+ bottomSeparator = QFrame()
+ bottomSeparator.setFrameShape(QFrame.HLine)
+ bottomSeparator.setFrameShadow(QFrame.Plain)
+ bottomSeparator.setStyleSheet("""
+ QFrame {
+ background-color: rgba(77, 116, 189, 0.4);
+ max-height: 1px;
+ margin-left: 8px;
+ margin-right: 8px;
+ border: none;
+ }
+ """)
+ bottomDockLayout.addWidget(bottomSeparator)
+
+ # Wrap the resource view to add consistent left padding
+ resourceContentWrapper = QWidget()
+ resourceContentLayout = QVBoxLayout(resourceContentWrapper)
+ resourceContentLayout.setContentsMargins(8, 0, 0, 0)
+ resourceContentLayout.setSpacing(0)
+ resourceContentLayout.addWidget(self.fileView)
+ bottomDockLayout.addWidget(resourceContentWrapper)
+
+ self.bottomDock.setWidget(bottomDockContainer)
+ self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock)
+
+ # 创建底部停靠控制台
+ self.consoleDock = QDockWidget("控制台", self)
+ self.consoleDock.setStyleSheet("""
+ QDockWidget {
+ background-color: #19191b;
+ color: #ffffff;
+ border: none;
+ }
+ QDockWidget::title {
+ background-color: #19191b;
+ padding: 8px 10px;
+ border-bottom: none;
+ text-align: left;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 14px;
+ font-weight: 500;
+ }
+ QDockWidget::close-button {
+ background-color: rgba(89, 100, 113, 0.3);
+ border: 1px solid rgba(184, 211, 241, 0.2);
+ icon-size: 10px;
+ border-radius: 2px;
+ width: 16px;
+ height: 16px;
+ right: 6px;
+ top: 6px;
+ }
+ QDockWidget::float-button {
+ background-color: rgba(89, 100, 113, 0.3);
+ border: 1px solid rgba(184, 211, 241, 0.2);
+ icon-size: 10px;
+ border-radius: 2px;
+ width: 16px;
+ height: 16px;
+ right: 26px;
+ top: 6px;
+ }
+ QDockWidget::close-button:hover, QDockWidget::float-button:hover {
+ background-color: rgba(89, 100, 113, 0.5);
+ border: 1px solid rgba(184, 211, 241, 0.3);
+ }
+ QDockWidget::close-button:pressed, QDockWidget::float-button:pressed {
+ background-color: #3067c0;
+ border: 1px solid rgba(48, 103, 192, 0.6);
+ }
+ """)
+ self.consoleView = CustomConsoleDockWidget(self.world)
+ # 为控制台添加样式
+ self.consoleView.setStyleSheet("""
+ QTextEdit {
+ background-color: #19191b;
+ color: #ebebeb;
+ border: none;
+ font-family: 'Consolas', 'Monaco', 'Microsoft YaHei', monospace;
+ font-size: 10px;
+ font-weight: 300;
+ }
+ """)
+
+ # 创建包装容器,添加标题下方的分隔线
+ consoleDockContainer = QWidget()
+ consoleDockContainer.setStyleSheet("QWidget { background-color: #19191b; }")
+ consoleDockLayout = QVBoxLayout(consoleDockContainer)
+ consoleDockLayout.setContentsMargins(0, 0, 0, 0)
+ consoleDockLayout.setSpacing(0)
+
+ # 添加带左右间距的分隔线
+ consoleSeparator = QFrame()
+ consoleSeparator.setFrameShape(QFrame.HLine)
+ consoleSeparator.setFrameShadow(QFrame.Plain)
+ consoleSeparator.setStyleSheet("""
+ QFrame {
+ background-color: rgba(77, 116, 189, 0.4);
+ max-height: 1px;
+ margin-left: 8px;
+ margin-right: 8px;
+ border: none;
+ }
+ """)
+ consoleDockLayout.addWidget(consoleSeparator)
+ consoleDockLayout.addWidget(self.consoleView)
+
+ self.consoleDock.setWidget(consoleDockContainer)
+ self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock)
+
+ # 配置Dock区域的角落归属
+ # 使右侧和下侧的Dock面板可以占据右下角区域
+ # setCorner(角落位置, 归属的Dock区域)
+ self.setCorner(Qt.TopRightCorner, Qt.RightDockWidgetArea)
+ self.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea)
+ # self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea)
+ # self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)
+
+ # 设置Dock标签栏位置为上方(North)
+ self.setTabPosition(Qt.RightDockWidgetArea, QTabWidget.North)
+ self.setTabPosition(Qt.LeftDockWidgetArea, QTabWidget.North)
+ self.setTabPosition(Qt.TopDockWidgetArea, QTabWidget.North)
+ self.setTabPosition(Qt.BottomDockWidgetArea, QTabWidget.North)
+
+ # 将右侧的属性面板和脚本管理面板合并为标签页
+ # self.tabifyDockWidget(self.rightDock, self.scriptDock)
+
+ # 设定默认显示的标签(属性面板在前)
+ self.rightDock.raise_()
+
+ # 确保其他面板也正常显示
+ self.bottomDock.raise_()
+ self.consoleDock.raise_()
+ self.leftDock.raise_()
+ # =========================================================================
+ # ↓↓↓ 为停靠窗口的标签栏(QTabBar)设置统一样式(根据Figma设计)↓↓↓
+ # =========================================================================
+ # 这段样式会应用到主窗口内的所有 QTabBar,特别是停靠区域的标签栏。
+ tab_bar_style = """
+ /* QTabBar 的整体样式 */
+ QTabBar {
+ qproperty-drawBase: 0;
+ qproperty-expanding: 0;
+ background-color: #19191b;
+ border: none;
+ }
+
+ /* 标签的基础样式 - 未选中状态 */
+ QTabBar::tab {
+ background-color: #394560;
+ color: rgba(255, 255, 255, 0.7);
+ border: none;
+ border-radius: 2px;
+ padding: 7px 16px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-weight: 500;
+ font-size: 12px;
+ margin-right: 4px;
+ min-height: 20px;
+ max-height: 20px;
+ min-width: 80px; /* 增加一个最小宽度,根据您的标签文字长度调整 */
+ }
+
+ /* 鼠标悬停在标签上时的样式 */
+ QTabBar::tab:hover {
+ background-color: #4a5568;
+ }
+
+ /* 当前选中的标签页的样式 */
+ QTabBar::tab:selected {
+ background-color: #3067c0;
+ color: #ffffff;
+ font-weight: 500;
+ }
+
+ /* 未选中的标签 */
+ QTabBar::tab:!selected {
+ margin-top: 0px;
+ }
+
+ /* 标签栏的对齐方式 */
+ QTabBar::tab-bar {
+ alignment: left;
+ }
+
+ /* 标签栏的关闭按钮(如果有) */
+ QTabBar::close-button {
+ image: none;
+ subcontrol-position: right;
+ }
+
+ /* 移除标签栏的滚动按钮样式冲突 */
+ QTabBar::scroller {
+ width: 0px;
+ }
+ """
+
+ # 获取主窗口现有的样式表,并附加我们新的样式规则
+ # 这样可以避免覆盖掉其他可能存在的全局样式
+ existing_style = self.styleSheet()
+ self.setStyleSheet(existing_style + tab_bar_style)
+
+ def setupToolbar(self):
+ """创建工具栏"""
+ self.toolbar = self.addToolBar('工具栏')
+
+ # 创建工具按钮组
+ self.toolGroup = QButtonGroup()
+
+ # 选择工具
+ self.selectTool = QToolButton()
+ self.selectTool.setText("选择")
+ self.selectTool.setCheckable(True)
+ self.toolGroup.addButton(self.selectTool)
+ self.toolbar.addWidget(self.selectTool)
+
+ # 旋转工具
+ self.rotateTool = QToolButton()
+ self.rotateTool.setText("旋转")
+ self.rotateTool.setCheckable(True)
+ self.toolGroup.addButton(self.rotateTool)
+ self.toolbar.addWidget(self.rotateTool)
+
+ # 缩放工具
+ self.scaleTool = QToolButton()
+ self.scaleTool.setText("缩放")
+ self.scaleTool.setCheckable(True)
+ self.toolGroup.addButton(self.scaleTool)
+ self.toolbar.addWidget(self.scaleTool)
+
+ # 添加分隔符
+ self.toolbar.addSeparator()
+
+ # GUI创建工具
+ self.createButtonTool = QToolButton()
+ self.createButtonTool.setText("创建按钮")
+ self.toolbar.addWidget(self.createButtonTool)
+
+ self.createLabelTool = QToolButton()
+ self.createLabelTool.setText("创建标签")
+ self.toolbar.addWidget(self.createLabelTool)
+
+ self.create3DTextTool = QToolButton()
+ self.create3DTextTool.setText("3D文本")
+ self.toolbar.addWidget(self.create3DTextTool)
+
+ self.create3DImageTool = QToolButton()
+ self.create3DImageTool.setText("3D图片")
+ self.toolbar.addWidget(self.create3DImageTool)
+
+ self.create2DImageTool = QToolButton()
+ self.create2DImageTool.setText("2D图片")
+ self.toolbar.addWidget(self.create2DImageTool)
+
+ self.createSpotLight = QToolButton()
+ self.createSpotLight.setText("聚光灯")
+ self.toolbar.addWidget(self.createSpotLight)
+
+ self.createPointLight = QToolButton()
+ self.createPointLight.setText("点光灯")
+ self.toolbar.addWidget(self.createPointLight)
+
+ # # Cesium 工具按钮
+ # self.cesiumViewTool = QToolButton()
+ # self.cesiumViewTool.setText("地图视图")
+ # self.cesiumViewTool.clicked.connect(self.onCreateCesiumView)
+ # self.toolbar.addWidget(self.cesiumViewTool)
+ #
+ # self.refreshCesiumTool = QToolButton()
+ # self.refreshCesiumTool.setText("刷新地图")
+ # self.refreshCesiumTool.clicked.connect(self.onRefreshCesiumView)
+ # self.toolbar.addWidget(self.refreshCesiumTool)
+ #
+ # self.addModelTool = QToolButton()
+ # self.addModelTool.setText("添加模型")
+ # self.addModelTool.clicked.connect(self.onAddModelClicked)
+ # self.toolbar.addWidget(self.addModelTool)
+
+ #地形
+ # 地形工具
+ self.createFlatTerrainTool = QToolButton()
+ self.createFlatTerrainTool.setText("平面地形")
+ self.toolbar.addWidget(self.createFlatTerrainTool)
+
+ self.createHeightmapTerrainTool = QToolButton()
+ self.createHeightmapTerrainTool.setText("高度图地形")
+ self.toolbar.addWidget(self.createHeightmapTerrainTool)
+
+ # 默认选择"选择"工具
+ self.selectTool.setChecked(True)
+ self.world.setCurrentTool("选择")
+
+ def setupScriptPanel(self, layout):
+ """创建脚本管理面板"""
+ # 脚本状态组
+ statusGroup = QGroupBox("脚本系统状态")
+
+ statusGroup.setProperty("groupRole", "first")
+ statusLayout = QVBoxLayout()
+
+ # 脚本系统状态行
+ scriptSystemLayout = QHBoxLayout()
+ scriptSystemLabel = QLabel("脚本系统:")
+ scriptSystemLabel.setStyleSheet("""
+ QLabel {
+ color: #ebebeb;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ letter-spacing: 0.5px;
+ }
+ """)
+ scriptSystemLayout.addWidget(scriptSystemLabel)
+
+ self.scriptStatusLabel = QLabel("已启动")
+ self.scriptStatusLabel.setStyleSheet("""
+ QLabel {
+ background-color: rgba(45, 255, 196, 0.17);
+ border: 1px solid #2dffc4;
+ color: #2dffc4;
+ border-radius: 2px;
+ padding: 2px 8px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ letter-spacing: 0.5px;
+ }
+ """)
+ scriptSystemLayout.addWidget(self.scriptStatusLabel)
+ scriptSystemLayout.addStretch()
+ statusLayout.addLayout(scriptSystemLayout)
+
+ # 热重载状态行
+ hotReloadLayout = QHBoxLayout()
+ hotReloadLabel = QLabel("热重载:")
+ hotReloadLabel.setStyleSheet("""
+ QLabel {
+ color: #ebebeb;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ letter-spacing: 0.5px;
+ }
+ """)
+ hotReloadLayout.addWidget(hotReloadLabel)
+
+ self.hotReloadLabel = QLabel("已启用")
+ self.hotReloadLabel.setStyleSheet("""
+ QLabel {
+ background-color: rgba(45, 136, 255, 0.17);
+ border: 1px solid #289eff;
+ color: #289eff;
+ border-radius: 2px;
+ padding: 2px 8px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ letter-spacing: 0.5px;
+ }
+ """)
+ hotReloadLayout.addWidget(self.hotReloadLabel)
+ hotReloadLayout.addStretch()
+ statusLayout.addLayout(hotReloadLayout)
+
+ statusGroup.setLayout(statusLayout)
+ layout.addWidget(statusGroup)
+
+ # 脚本创建组
+ createGroup = QGroupBox("创建脚本")
+ createLayout = QVBoxLayout()
+
+ # 脚本名称输入
+ nameLayout = QHBoxLayout()
+ nameLabel = QLabel("脚本名称:")
+ nameLabel.setStyleSheet("""
+ QLabel {
+ color: #ebebeb;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ letter-spacing: 0.5px;
+ min-width: 60px;
+ }
+ """)
+ nameLayout.addWidget(nameLabel)
+
+ self.scriptNameEdit = QLineEdit()
+ self.scriptNameEdit.setPlaceholderText("输入脚本名称...")
+ self.scriptNameEdit.setStyleSheet("""
+ QLineEdit {
+ background-color: rgba(89, 100, 113, 0.2);
+ color: #ebebeb;
+ border: 1px solid rgba(76, 92, 110, 0.6);
+ border-radius: 2px;
+ padding: 4px 8px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ letter-spacing: 0.5px;
+ min-height: 18px;
+ }
+ QLineEdit::placeholder {
+ color: rgba(235, 235, 235, 0.7);
+ }
+ QLineEdit:focus {
+ border: 1px solid #289eff;
+ }
+ """)
+ nameLayout.addWidget(self.scriptNameEdit)
+ createLayout.addLayout(nameLayout)
+
+ # 模板选择
+ templateLayout = QHBoxLayout()
+ templateLabel = QLabel("模板:")
+ templateLabel.setStyleSheet("""
+ QLabel {
+ color: #ebebeb;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ letter-spacing: 0.5px;
+ min-width: 60px;
+ }
+ """)
+ templateLayout.addWidget(templateLabel)
+
+ self.templateCombo = QComboBox()
+ self.templateCombo.addItems(["basic", "movement", "animation"])
+ self.templateCombo.setStyleSheet("""
+ QComboBox {
+ background-color: rgba(89, 100, 113, 0.2);
+ color: #ebebeb;
+ border: 1px solid rgba(76, 92, 110, 0.6);
+ border-radius: 2px;
+ padding: 4px 8px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ letter-spacing: 0.5px;
+ min-height: 18px;
+ }
+ QComboBox::drop-down {
+ border: none;
+ border-left: 1px solid rgba(76, 92, 110, 0.6);
+ background-color: rgba(89, 100, 113, 0.3);
+ width: 16px;
+ }
+ QComboBox::down-arrow {
+ image: url(icons/down_arrows.png);
+ width: 10px;
+ height: 10px;
+ }
+ QComboBox QAbstractItemView {
+ background-color: #596471;
+ color: #ebebeb;
+ selection-background-color: rgba(48, 103, 192, 0.4);
+ border: 1px solid rgba(76, 92, 110, 0.6);
+ }
+ """)
+ templateLayout.addWidget(self.templateCombo)
+ createLayout.addLayout(templateLayout)
+
+ # 创建按钮
+ self.createScriptBtn = QPushButton("创建脚本")
+ self.createScriptBtn.clicked.connect(self.onCreateScript)
+ self.createScriptBtn.setStyleSheet("""
+ QPushButton {
+ background-color: rgba(89, 98, 118, 0.4);
+ color: #ebebeb;
+ border: none;
+ padding: 6px 12px;
+ border-radius: 2px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-weight: 300;
+ font-size: 10px;
+ letter-spacing: 0.5px;
+ min-height: 18px;
+ }
+ QPushButton:hover {
+ background-color: #3067c0;
+ color: #ffffff;
+ }
+ QPushButton:pressed {
+ background-color: #2556a0;
+ }
+ QPushButton:disabled {
+ background-color: #394560;
+ color: rgba(235, 235, 235, 0.5);
+ }
+ """)
+ createLayout.addWidget(self.createScriptBtn)
+
+ createGroup.setLayout(createLayout)
+ layout.addWidget(createGroup)
+
+ # 可用脚本组
+ scriptsGroup = QGroupBox("可用脚本")
+ scriptsLayout = QVBoxLayout()
+
+ # 脚本列表
+ self.scriptsList = QListWidget()
+ self.scriptsList.setStyleSheet("""
+ QListWidget {
+ background-color: rgba(89, 100, 113, 0.15);
+ color: #ebebeb;
+ border: 1px solid rgba(76, 92, 110, 0.6);
+ border-radius: 2px;
+ alternate-background-color: rgba(89, 100, 113, 0.15);
+ selection-background-color: rgba(48, 103, 192, 0.4);
+ selection-color: #ffffff;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ letter-spacing: 0.5px;
+ outline: none;
+ min-height: 120px;
+ max-height: 120px;
+ }
+ QListWidget::item {
+ padding: 6px 8px;
+ border: none;
+ border-bottom: 1px solid rgba(76, 92, 110, 0.3);
+ }
+ QListWidget::item:last {
+ border-bottom: none;
+ }
+ QListWidget::item:hover {
+ background-color: rgba(89, 100, 113, 0.3);
+ }
+ QListWidget::item:selected {
+ background-color: rgba(48, 103, 192, 0.4);
+ color: #ffffff;
+ }
+ """)
+ self.scriptsList.itemDoubleClicked.connect(self.onScriptDoubleClick)
+ scriptsLayout.addWidget(self.scriptsList)
+
+ # 脚本操作按钮
+ scriptButtonsLayout = QHBoxLayout()
+
+ self.loadScriptBtn = QPushButton("加载脚本")
+ self.loadScriptBtn.clicked.connect(self.onLoadScript)
+ self.loadScriptBtn.setStyleSheet("""
+ QPushButton {
+ background-color: rgba(89, 98, 118, 0.4);
+ color: #ebebeb;
+ border: none;
+ padding: 6px 12px;
+ border-radius: 2px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-weight: 300;
+ font-size: 10px;
+ letter-spacing: 0.5px;
+ min-height: 18px;
+ }
+ QPushButton:hover {
+ background-color: #3067c0;
+ color: #ffffff;
+ }
+ QPushButton:pressed {
+ background-color: #2556a0;
+ }
+ """)
+ scriptButtonsLayout.addWidget(self.loadScriptBtn)
+
+ self.reloadAllBtn = QPushButton("重载全部")
+ self.reloadAllBtn.clicked.connect(self.onReloadAllScripts)
+ self.reloadAllBtn.setStyleSheet("""
+ QPushButton {
+ background-color: rgba(89, 98, 118, 0.4);
+ color: #ebebeb;
+ border: none;
+ padding: 6px 12px;
+ border-radius: 2px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-weight: 300;
+ font-size: 10px;
+ letter-spacing: 0.5px;
+ min-height: 18px;
+ }
+ QPushButton:hover {
+ background-color: #3067c0;
+ color: #ffffff;
+ }
+ QPushButton:pressed {
+ background-color: #2556a0;
+ }
+ """)
+ scriptButtonsLayout.addWidget(self.reloadAllBtn)
+
+ scriptsLayout.addLayout(scriptButtonsLayout)
+ scriptsGroup.setLayout(scriptsLayout)
+ layout.addWidget(scriptsGroup)
+
+ # 脚本挂载组
+ mountGroup = QGroupBox("脚本挂载")
+ mountLayout = QVBoxLayout()
+
+ # 当前选中对象显示
+ self.selectedObjectLabel = QLabel("未选择对象")
+ self.selectedObjectLabel.setStyleSheet("""
+ QLabel {
+ color: #2dffc4;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ letter-spacing: 0.5px;
+ padding: 2px 0px;
+ }
+ """)
+ mountLayout.addWidget(self.selectedObjectLabel)
+
+ # 脚本选择和挂载
+ mountControlLayout = QHBoxLayout()
+
+ self.mountScriptCombo = QComboBox()
+ self.mountScriptCombo.setEnabled(False)
+ self.mountScriptCombo.setStyleSheet("""
+ QComboBox {
+ background-color: rgba(89, 100, 113, 0.2);
+ color: #ebebeb;
+ border: 1px solid rgba(76, 92, 110, 0.6);
+ border-radius: 2px;
+ padding: 4px 8px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ letter-spacing: 0.5px;
+ min-height: 18px;
+ }
+ QComboBox:disabled {
+ background-color: rgba(89, 100, 113, 0.1);
+ color: rgba(235, 235, 235, 0.5);
+ }
+ QComboBox::drop-down {
+ border: none;
+ border-left: 1px solid rgba(76, 92, 110, 0.6);
+ background-color: rgba(89, 100, 113, 0.3);
+ width: 16px;
+ }
+ QComboBox::down-arrow {
+ image: url(icons/down_arrows.png);
+ width: 10px;
+ height: 10px;
+ }
+ QComboBox QAbstractItemView {
+ background-color: #596471;
+ color: #ebebeb;
+ selection-background-color: rgba(48, 103, 192, 0.4);
+ border: 1px solid rgba(76, 92, 110, 0.6);
+ }
+ """)
+ mountControlLayout.addWidget(self.mountScriptCombo)
+
+ self.mountBtn = QPushButton("挂载")
+ self.mountBtn.setEnabled(False)
+ self.mountBtn.clicked.connect(self.onMountScript)
+ self.mountBtn.setStyleSheet("""
+ QPushButton {
+ background-color: rgba(89, 98, 118, 0.4);
+ color: #ebebeb;
+ border: none;
+ padding: 6px 12px;
+ border-radius: 2px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-weight: 300;
+ font-size: 10px;
+ letter-spacing: 0.5px;
+ min-height: 18px;
+ }
+ QPushButton:hover {
+ background-color: #3067c0;
+ color: #ffffff;
+ }
+ QPushButton:pressed {
+ background-color: #2556a0;
+ }
+ QPushButton:disabled {
+ background-color: #394560;
+ color: rgba(235, 235, 235, 0.5);
+ }
+ """)
+ mountControlLayout.addWidget(self.mountBtn)
+
+ mountLayout.addLayout(mountControlLayout)
+
+ # 已挂载脚本列表
+ self.mountedScriptsList = QListWidget()
+ self.mountedScriptsList.setStyleSheet("""
+ QListWidget {
+ background-color: rgba(89, 100, 113, 0.15);
+ color: #ebebeb;
+ border: 1px solid rgba(76, 92, 110, 0.6);
+ border-radius: 2px;
+ alternate-background-color: rgba(89, 100, 113, 0.15);
+ selection-background-color: rgba(48, 103, 192, 0.4);
+ selection-color: #ffffff;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ letter-spacing: 0.5px;
+ outline: none;
+ min-height: 80px;
+ max-height: 80px;
+ }
+ QListWidget::item {
+ padding: 4px 8px;
+ border: none;
+ border-bottom: 1px solid rgba(76, 92, 110, 0.3);
+ }
+ QListWidget::item:last {
+ border-bottom: none;
+ }
+ QListWidget::item:hover {
+ background-color: rgba(89, 100, 113, 0.3);
+ }
+ QListWidget::item:selected {
+ background-color: rgba(48, 103, 192, 0.4);
+ color: #ffffff;
+ }
+ """)
+ self.mountedScriptsList.setMaximumHeight(100)
+
+ mountedLabel = QLabel("已挂载脚本:")
+ mountedLabel.setStyleSheet("""
+ QLabel {
+ color: #ebebeb;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ letter-spacing: 0.5px;
+ padding: 2px 0px;
+ }
+ """)
+ mountLayout.addWidget(mountedLabel)
+ mountLayout.addWidget(self.mountedScriptsList)
+
+ # 卸载按钮
+ self.unmountBtn = QPushButton("卸载选中脚本")
+ self.unmountBtn.clicked.connect(self.onUnmountScript)
+ self.unmountBtn.setStyleSheet("""
+ QPushButton {
+ background-color: rgba(89, 98, 118, 0.4);
+ color: #ebebeb;
+ border: none;
+ padding: 6px 12px;
+ border-radius: 2px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-weight: 300;
+ font-size: 10px;
+ letter-spacing: 0.5px;
+ min-height: 18px;
+ }
+ QPushButton:hover {
+ background-color: #3067c0;
+ color: #ffffff;
+ }
+ QPushButton:pressed {
+ background-color: #2556a0;
+ }
+ """)
+ mountLayout.addWidget(self.unmountBtn)
+
+ mountGroup.setLayout(mountLayout)
+ layout.addWidget(mountGroup)
+
+ # 添加拉伸以填充剩余空间
+ layout.addStretch()
+
+ # 初始化脚本列表
+ self.refreshScriptsList()
+
+ def connectEvents(self):
+ """连接事件信号"""
+ # 导入项目管理功能函数
+ from main import createNewProject, saveProject, openProject, buildPackage
+
+ # 连接文件菜单事件
+ self.newAction.triggered.connect(lambda: createNewProject(self))
+ self.openAction.triggered.connect(lambda: openProject(self))
+ self.saveAction.triggered.connect(lambda: saveProject(self))
+ self.buildAction.triggered.connect(lambda: buildPackage(self))
+ self.exitAction.triggered.connect(QApplication.instance().quit)
+
+ #添加保存项目快捷键盘
+ self.saveAction.setShortcut(QKeySequence.Save)
+
+ # 连接工具事件
+ self.sunsetAction.triggered.connect(lambda: self.world.setCurrentTool("光照编辑"))
+ self.pluginAction.triggered.connect(lambda: self.world.setCurrentTool("图形编辑"))
+
+ # 连接GUI编辑模式事件
+ #self.guiEditModeAction.triggered.connect(lambda: self.world.toggleGUIEditMode())
+
+ # 连接创建事件 - 使用菜单动作而不是不存在的工具栏按钮
+ # self.createSpotLightAction.triggered.connect(lambda: self.world.createSpotLight())
+ # self.createPointLightAction.triggered.connect(lambda: self.world.createPointLight())
+ # self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton())
+ # self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel())
+ # self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry())
+ # self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText())
+ # self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen())
+ # self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView)
+ # self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView)
+ # self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView)
+
+ # 连接地形创建事件
+ self.createFlatTerrainAction.triggered.connect(self.onCreateFlatTerrain)
+ self.createHeightmapTerrainAction.triggered.connect(self.onCreateHeightmapTerrain)
+ # self.terrainEditModeAction.triggered.connect(self.onTerrainEditMode)
+
+ # 连接树节点点击信号
+ self.treeWidget.itemSelectionChanged.connect(
+ lambda: self.world.onTreeItemClicked(self.treeWidget.currentItem(), 0))
+ print("已连接点击信号")
+
+ self.undoAction.triggered.connect(self.onUndo)
+ self.redoAction.triggered.connect(self.onRedo)
+ self.cutAction.triggered.connect(self.onCut)
+ self.copyAction.triggered.connect(self.onCopy)
+ self.pasteAction.triggered.connect(self.onPaste)
+
+ self.undoAction.setShortcut(QKeySequence.Undo)
+ self.redoAction.setShortcut(QKeySequence.Redo)
+ self.cutAction.setShortcut(QKeySequence.Cut)
+ self.copyAction.setShortcut(QKeySequence.Copy)
+ self.pasteAction.setShortcut(QKeySequence.Paste)
+
+ #连接视图菜单事件
+ #self.setupViewMenuActions()
+
+ # 连接工具切换信号
+ #self.toolGroup.buttonClicked.connect(self.onToolChanged)
+
+ # 连接脚本菜单事件
+ # self.createScriptAction.triggered.connect(self.onCreateScriptDialog)
+ # self.loadScriptAction.triggered.connect(self.onLoadScriptFile)
+ # self.loadAllScriptsAction.triggered.connect(self.onReloadAllScripts)
+ # self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload)
+ # self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager)
+
+ # 连接VR菜单事件
+ self.enterVRAction.triggered.connect(self.onEnterVR)
+ self.exitVRAction.triggered.connect(self.onExitVR)
+ self.vrStatusAction.triggered.connect(self.onShowVRStatus)
+ self.vrSettingsAction.triggered.connect(self.onShowVRSettings)
+
+ # 连接VR调试菜单事件
+ self.vrDebugToggleAction.triggered.connect(self.onToggleVRDebug)
+ self.vrShowPerformanceAction.triggered.connect(self.onShowVRPerformance)
+ self.vrDebugBriefAction.triggered.connect(lambda: self.onSetVRDebugMode('brief'))
+ self.vrDebugDetailedAction.triggered.connect(lambda: self.onSetVRDebugMode('detailed'))
+ self.vrPerformanceMonitorAction.triggered.connect(self.onToggleVRPerformanceMonitor)
+ self.vrGpuTimingAction.triggered.connect(self.onToggleVRGpuTiming)
+ self.vrPipelineMonitorAction.triggered.connect(self.onToggleVRPipelineMonitor)
+ self.vrPoseRenderCallbackAction.triggered.connect(lambda: self.onSetVRPoseStrategy('render_callback'))
+ self.vrPoseUpdateTaskAction.triggered.connect(lambda: self.onSetVRPoseStrategy('update_task'))
+ self.vrTestPipelineAction.triggered.connect(self.onTestVRPipeline)
+ self.vrTestModeAction.triggered.connect(self.onToggleVRTestMode)
+
+ # 连接VR测试模式调试菜单事件
+ self.vrTestSubmitTextureAction.triggered.connect(self.onToggleVRTestSubmitTexture)
+ self.vrTestWaitPosesAction.triggered.connect(self.onToggleVRTestWaitPoses)
+ self.vrTestStep1Action.triggered.connect(lambda: self.onSetVRTestStep(1))
+ self.vrTestStep2Action.triggered.connect(lambda: self.onSetVRTestStep(2))
+ self.vrTestStep3Action.triggered.connect(lambda: self.onSetVRTestStep(3))
+ self.vrTestResetAction.triggered.connect(lambda: self.onSetVRTestStep(0))
+
+ self.vrDebugSettingsAction.triggered.connect(self.onShowVRDebugSettings)
+
+ def onCopy(self):
+ """复制操作"""
+ try:
+ selected_item = self.treeWidget.currentItem()
+ if not selected_item:
+ UniversalMessageDialog.show_warning(
+ self,
+ "提示",
+ "请先选择要复制的节点",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ return
+
+ # 获取选中的节点
+ selected_node = getattr(selected_item, 'node_path', None)
+ if not selected_node:
+ selected_node = getattr(selected_item, 'node', None)
+
+ if not selected_node and hasattr(self.world, 'selection'):
+ selected_node = getattr(self.world.selection, 'selectedNode', None)
+
+ if not selected_node or selected_node.isEmpty():
+ UniversalMessageDialog.show_warning(
+ self,
+ "错误",
+ "无法获取选中节点",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ return
+
+ # 检查是否是根节点
+ if selected_node.getName() == "render":
+ UniversalMessageDialog.show_warning(
+ self,
+ "错误",
+ "不能复制根节点",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ return
+
+ # 序列化节点数据
+ node_data = self.world.scene_manager.serializeNodeForCopy(selected_node)
+ if not node_data:
+ UniversalMessageDialog.show_warning(
+ self,
+ "错误",
+ "无法序列化选中节点",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ return
+
+ # 存储到剪切板
+ self.clipboard = [node_data]
+ self.clipboard_mode = "copy"
+
+ UniversalMessageDialog.show_success(
+ self,
+ "成功",
+ "节点已复制到剪切板",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+
+ except Exception as e:
+ UniversalMessageDialog.show_error(
+ self,
+ "错误",
+ f"复制操作失败: {str(e)}",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+
+ def onCut(self):
+ """剪切操作"""
+ try:
+ selected_item = self.treeWidget.currentItem()
+ if not selected_item:
+ UniversalMessageDialog.show_warning(
+ self,
+ "提示",
+ "请先选择要剪切的节点",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ return
+
+ # 获取选中的节点
+ selected_node = getattr(selected_item, 'node_path', None)
+ if not selected_node:
+ selected_node = getattr(selected_item, 'node', None)
+
+ if not selected_node and hasattr(self.world, 'selection'):
+ selected_node = getattr(self.world.selection, 'selectedNode', None)
+
+ if not selected_node or selected_node.isEmpty():
+ UniversalMessageDialog.show_warning(
+ self,
+ "错误",
+ "无法获取选中节点",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ return
+
+ # 检查是否是根节点或特殊节点
+ if selected_node.getName() in ["render", "camera", "ambientLight", "directionalLight"]:
+ UniversalMessageDialog.show_warning(
+ self,
+ "错误",
+ "不能剪切根节点或系统节点",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ return
+
+ # 序列化节点数据
+ node_data = self.world.scene_manager.serializeNodeForCopy(selected_node)
+ if not node_data:
+ UniversalMessageDialog.show_warning(
+ self,
+ "错误",
+ "无法序列化选中节点",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ return
+
+ # 存储到剪切板
+ self.clipboard = [node_data]
+ self.clipboard_mode = "cut"
+
+ # 删除原节点
+ self.treeWidget.delete_items([selected_item])
+
+ UniversalMessageDialog.show_success(
+ self,
+ "成功",
+ "节点已剪切到剪切板",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+
+ except Exception as e:
+ UniversalMessageDialog.show_error(
+ self,
+ "错误",
+ f"剪切操作失败: {str(e)}",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+
+ def onPaste(self):
+ """粘贴操作"""
+ try:
+ if not self.clipboard:
+ UniversalMessageDialog.show_warning(
+ self,
+ "提示",
+ "剪切板为空",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ return
+
+ # 获取粘贴目标节点
+ parent_item = self.treeWidget.currentItem()
+ parent_node = None
+
+ # 如果选中了节点,将其作为父节点
+ if parent_item:
+ parent_node = getattr(parent_item, 'node_path', None)
+ if not parent_node:
+ parent_node = getattr(parent_item, 'node', None)
+
+ # 确保获取到有效的父节点
+ if parent_node and not parent_node.isEmpty():
+ print(f"将粘贴到选中的节点: {parent_node.getName()}")
+ else:
+ parent_node = None
+
+ # 如果没有选中有效节点,默认粘贴到render节点下
+ if not parent_node:
+ print("未选中有效节点,将粘贴到根节点下")
+ # 查找render节点
+ for i in range(self.treeWidget.topLevelItemCount()):
+ item = self.treeWidget.topLevelItem(i)
+ if item.text(0) == "render":
+ parent_item = item
+ break
+
+ # 如果找到了render节点项,获取对应的节点
+ if parent_item:
+ parent_node = getattr(parent_item, 'node_path', None)
+ if not parent_node:
+ parent_node = getattr(parent_item, 'node', None)
+
+ # 如果仍然没有找到父节点项,直接使用world.render
+ if not parent_node:
+ parent_node = self.world.render
+
+ # 检查父节点有效性
+ if not parent_node or parent_node.isEmpty():
+ UniversalMessageDialog.show_warning(
+ self,
+ "错误",
+ "无法获取有效的父节点",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ return
+
+ # 检查目标节点是否为允许的父节点类型
+ parent_name = parent_node.getName()
+ if parent_name in ["camera", "ambientLight", "directionalLight"]:
+ UniversalMessageDialog.show_warning(
+ self,
+ "错误",
+ "不能粘贴到该类型节点下",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ return
+
+ # 粘贴节点
+ pasted_nodes = []
+ for node_data in self.clipboard:
+ print(f"正在粘贴节点数据:{node_data.get('name','Unknown')}")
+ new_node = self.world.scene_manager.recreateNodeFromData(node_data, parent_node)
+ if new_node:
+ pasted_nodes.append(new_node)
+ print(f"成功粘贴节点: {new_node.getName()}")
+ else:
+ print(f"粘贴节点失败: {node_data.get('name', 'Unknown')}")
+
+ # 如果是剪切操作,清空剪切板
+ if self.clipboard_mode == "cut":
+ self.clipboard.clear()
+ self.clipboard_mode = None
+
+ UniversalMessageDialog.show_success(
+ self,
+ "成功",
+ f"已粘贴 {len(pasted_nodes)} 个节点",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+
+ except Exception as e:
+ UniversalMessageDialog.show_error(
+ self,
+ "错误",
+ f"粘贴操作失败: {str(e)}",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+
+ def _serializeNode(self, node):
+ """序列化节点数据"""
+ try:
+ if not node or node.isEmpty():
+ return None
+
+ node_data = {
+ 'name': node.getName(),
+ 'type': type(node.node()).__name__,
+ 'pos': (node.getX(), node.getY(), node.getZ()),
+ 'hpr': (node.getH(), node.getP(), node.getR()),
+ 'scale': (node.getSx(), node.getSy(), node.getSz()),
+ 'tags': {},
+ 'children': []
+ }
+
+ # 保存所有标签
+ try:
+ # 使用更安全的方式获取标签
+ if hasattr(node, 'getTagKeys'):
+ for tag_key in node.getTagKeys():
+ node_data['tags'][tag_key] = node.getTag(tag_key)
+ except Exception as e:
+ print(f"获取标签时出错: {e}")
+
+ # 递归序列化子节点(跳过辅助节点)
+ try:
+ if hasattr(node, 'getChildren'):
+ for child in node.getChildren():
+ # 跳过辅助节点
+ child_name = child.getName() if hasattr(child, 'getName') else ""
+ if not child_name.startswith(('gizmo', 'selectionBox', 'grid')):
+ child_data = self._serializeNode(child)
+ if child_data:
+ node_data['children'].append(child_data)
+ except Exception as e:
+ print(f"序列化子节点时出错: {e}")
+
+ return node_data
+
+ except Exception as e:
+ print(f"序列化节点失败: {e}")
+ return None
+
+ def _deserializeNode(self, node_data, parent_node):
+ """反序列化节点数据"""
+ try:
+ if not node_data or not parent_node or parent_node.isEmpty():
+ return None
+
+ # 创建新节点
+ node_name = node_data.get('name', 'node')
+ new_node = parent_node.attachNewNode(node_name)
+
+ # 设置变换
+ try:
+ pos = node_data.get('pos', (0, 0, 0))
+ hpr = node_data.get('hpr', (0, 0, 0))
+ scale = node_data.get('scale', (1, 1, 1))
+
+ new_node.setPos(*pos)
+ new_node.setHpr(*hpr)
+ new_node.setScale(*scale)
+ except Exception as e:
+ print(f"设置变换时出错: {e}")
+
+ # 恢复标签
+ try:
+ for tag_key, tag_value in node_data.get('tags', {}).items():
+ new_node.setTag(tag_key, str(tag_value)) # 确保标签值是字符串
+ except Exception as e:
+ print(f"恢复标签时出错: {e}")
+
+ # 递归创建子节点
+ try:
+ for child_data in node_data.get('children', []):
+ self._deserializeNode(child_data, new_node)
+ except Exception as e:
+ print(f"创建子节点时出错: {e}")
+
+ return new_node
+
+ except Exception as e:
+ print(f"反序列化节点失败: {e}")
+ return None
+
+ def _deleteNode(self, node, tree_item):
+ """删除节点"""
+ try:
+ if not node or node.isEmpty():
+ return
+
+ # 特殊处理选中节点
+ if hasattr(self.world, 'selection') and self.world.selection.selectedNode == node:
+ self.world.selection.clearSelection()
+
+ # 从场景中删除节点
+ node.removeNode()
+
+ # 从树形控件中删除项目
+ if tree_item:
+ try:
+ parent = tree_item.parent()
+ if parent:
+ parent.removeChild(tree_item)
+ else:
+ index = self.treeWidget.indexOfTopLevelItem(tree_item)
+ if index >= 0:
+ self.treeWidget.takeTopLevelItem(index)
+ except Exception as e:
+ print(f"从树形控件删除项目时出错: {e}")
+
+ except Exception as e:
+ print(f"删除节点失败: {e}")
+
+ # 添加撤销/重做功能的基础实现
+ def onUndo(self):
+ """撤销操作"""
+ if hasattr(self.world,'command_manager'):
+ if self.world.command_manager.can_undo():
+ success = self.world.command_manager.undo()
+ if success:
+ print("成功操作")
+ else:
+ print("撤销失败")
+ UniversalMessageDialog.show_warning(
+ self,
+ "提示",
+ "撤销操作失败",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ else:
+ print("没有可撤销的操作")
+ UniversalMessageDialog.show_warning(
+ self,
+ "提示",
+ "没有可撤销的操作",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ else:
+ print("命令管理器未初始化")
+ UniversalMessageDialog.show_warning(
+ self,
+ "提示",
+ "命令系统未初始化",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+
+ def onRedo(self):
+ """重做操作"""
+ if hasattr(self.world,'command_manager'):
+ if self.world.command_manager.can_redo():
+ success = self.world.command_manager.redo()
+ if success:
+ print("成功重做")
+ else:
+ print("重做失败")
+ UniversalMessageDialog.show_warning(
+ self,
+ "提示",
+ "重做操作失败",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ else:
+ print("没有可重做的操作")
+ UniversalMessageDialog.show_warning(
+ self,
+ "提示",
+ "没有可重做的操作",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ else:
+ print("命令管理器未初始化")
+ UniversalMessageDialog.show_warning(
+ self,
+ "提示",
+ "命令系统未初始化",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+
+ def onCreateCesiumView(self):
+ if hasattr(self.world,'gui_manager') and self.world.gui_manager:
+ self.world.gui_manager.createCesiumView()
+ else:
+ UniversalMessageDialog.show_warning(
+ self,
+ "错误",
+ "GUI管理器不可用",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ def onToggleCesiumView(self):
+ """切换 Cesium 视图显示状态"""
+ if hasattr(self.world, 'gui_manager') and self.world.gui_manager:
+ self.world.gui_manager.toggleCesiumView()
+ else:
+ UniversalMessageDialog.show_warning(
+ self,
+ "错误",
+ "GUI 管理器不可用",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ def onRefreshCesiumView(self):
+ """刷新 Cesium 视图"""
+ if hasattr(self.world, 'gui_manager') and self.world.gui_manager:
+ self.world.gui_manager.refreshCesiumView()
+ else:
+ UniversalMessageDialog.show_warning(
+ self,
+ "错误",
+ "GUI 管理器不可用",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ def onUpdateCesiumURL(self):
+ """更新 Cesium URL"""
+ dialog = self.createStyledInputDialog(
+ self,
+ "更新 Cesium URL",
+ "输入新的 URL:",
+ QLineEdit.Normal,
+ "http://localhost:8080/Apps/HelloWorld.html"
+ )
+ if dialog.exec_() == QDialog.Accepted:
+ url = dialog.textValue()
+ if url:
+ if hasattr(self.world, 'gui_manager') and self.world.gui_manager:
+ self.world.gui_manager.updateCesiumURL(url)
+ else:
+ UniversalMessageDialog.show_warning(
+ self,
+ "错误",
+ "GUI 管理器不可用",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+
+
+ def onAddModelClicked(self):
+ """处理加入模型按钮点击事件"""
+ # 检查 Cesium 视图是否存在
+ cesium_view_exists = False
+ if hasattr(self.world, 'gui_manager') and self.world.gui_manager:
+ for element in self.world.gui_manager.gui_elements:
+ if hasattr(element, 'objectName') and element.objectName() == "CesiumView":
+ cesium_view_exists = True
+ break
+
+ if not cesium_view_exists:
+ result = UniversalMessageDialog.show_info(
+ self,
+ "提示",
+ "Cesium 地图视图尚未打开,是否先打开地图视图?",
+ show_cancel=True,
+ confirm_text="打开视图",
+ cancel_text="取消"
+ )
+
+ if result == QDialog.Accepted:
+ self.onCreateCesiumView()
+ # 给一点时间让 Cesium 视图加载
+ QTimer.singleShot(1000, self.showAddModelDialog)
+ return
+ else:
+ return
+
+ self.showAddModelDialog()
+
+ def showAddModelDialog(self):
+ """显示添加模型对话框"""
+ # 打开文件选择对话框
+ dialog = self.createStyledFileDialog(
+ self,
+ "选择 3D 模型文件",
+ "",
+ "3D 模型文件 (*.glb *.gltf *.obj);;所有文件 (*)"
+ )
+
+ if dialog.exec_() == QDialog.Accepted:
+ file_path = dialog.selectedFiles()[0]
+ if file_path:
+ # 获取模型位置信息
+ coords, ok = self.getModelCoordinates()
+ if ok:
+ longitude, latitude, height, scale = coords
+
+ # 生成唯一的模型 ID
+ import uuid
+ model_id = f"model_{uuid.uuid4().hex[:8]}"
+
+ try:
+ # 添加模型到 Cesium
+ if hasattr(self.world, 'gui_manager') and self.world.gui_manager:
+ success = self.world.gui_manager.addModelToCesium(
+ model_id,
+ file_path,
+ longitude,
+ latitude,
+ height,
+ scale
+ )
+
+ if success:
+ UniversalMessageDialog.show_success(
+ self,
+ "成功",
+ f"模型已成功添加到地图!\n模型ID: {model_id}",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ else:
+ UniversalMessageDialog.show_warning(
+ self,
+ "失败",
+ "添加模型失败,请检查控制台输出",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ except Exception as e:
+ UniversalMessageDialog.show_error(
+ self,
+ "错误",
+ f"添加模型时发生错误:\n{str(e)}",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+
+
+ def getModelCoordinates(self):
+ """获取模型坐标信息的对话框"""
+ # 创建对话框
+ dialog = QDialog(self)
+ dialog.setWindowTitle("设置模型位置")
+ dialog.setModal(True)
+ dialog.resize(300, 200)
+
+ layout = QVBoxLayout(dialog)
+
+ # 经度
+ lon_layout = QHBoxLayout()
+ lon_layout.addWidget(QLabel("经度:"))
+ lon_spin = QDoubleSpinBox()
+ lon_spin.setRange(-180, 180)
+ lon_spin.setValue(116.3975) # 默认北京位置
+ lon_layout.addWidget(lon_spin)
+ layout.addLayout(lon_layout)
+
+ # 纬度
+ lat_layout = QHBoxLayout()
+ lat_layout.addWidget(QLabel("纬度:"))
+ lat_spin = QDoubleSpinBox()
+ lat_spin.setRange(-90, 90)
+ lat_spin.setValue(39.9085) # 默认北京位置
+ lat_layout.addWidget(lat_spin)
+ layout.addLayout(lat_layout)
+
+ # 高度
+ height_layout = QHBoxLayout()
+ height_layout.addWidget(QLabel("高度(米):"))
+ height_spin = QDoubleSpinBox()
+ height_spin.setRange(-10000, 100000)
+ height_spin.setValue(0)
+ height_layout.addWidget(height_spin)
+ layout.addLayout(height_layout)
+
+ # 缩放
+ scale_layout = QHBoxLayout()
+ scale_layout.addWidget(QLabel("缩放:"))
+ scale_spin = QDoubleSpinBox()
+ scale_spin.setRange(0.001, 100000)
+ scale_spin.setValue(1.0)
+ scale_spin.setSingleStep(0.1)
+ scale_layout.addWidget(scale_spin)
+ layout.addLayout(scale_layout)
+
+ # 按钮
+ button_layout = QHBoxLayout()
+ ok_button = QPushButton("确定")
+ cancel_button = QPushButton("取消")
+ button_layout.addWidget(ok_button)
+ button_layout.addWidget(cancel_button)
+ layout.addLayout(button_layout)
+
+ # 连接信号
+ ok_button.clicked.connect(dialog.accept)
+ cancel_button.clicked.connect(dialog.reject)
+
+ # 显示对话框
+ if dialog.exec_() == QDialog.Accepted:
+ return (
+ lon_spin.value(),
+ lat_spin.value(),
+ height_spin.value(),
+ scale_spin.value()
+ ), True
+ else:
+ return None, False
+
+ def onLoadCesiumTileset(self):
+ """加载 Cesium 3D Tiles"""
+ fields = [
+ {
+ "name": "url",
+ "label": "Tileset URL",
+ "type": "text",
+ "value": "https://assets.ion.cesium.com/96128/tileset.json",
+ "placeholder": "输入 tileset.json 地址",
+ "label_width": 110
+ },
+ {
+ "name": "longitude",
+ "label": "经度(°)",
+ "type": "double",
+ "min": -180.0,
+ "max": 180.0,
+ "step": 0.1,
+ "decimals": 6,
+ "value": 0.0,
+ "label_width": 110
+ },
+ {
+ "name": "latitude",
+ "label": "纬度(°)",
+ "type": "double",
+ "min": -90.0,
+ "max": 90.0,
+ "step": 0.1,
+ "decimals": 6,
+ "value": 0.0,
+ "label_width": 110
+ },
+ {
+ "name": "height",
+ "label": "高度(米)",
+ "type": "double",
+ "min": -10000.0,
+ "max": 10000.0,
+ "step": 1.0,
+ "decimals": 2,
+ "value": 0.0,
+ "label_width": 110
+ }
+ ]
+
+ dialog = StyledTerrainDialog(self, "加载 Cesium 3D Tiles", fields, primary_text="加载", secondary_text="取消")
+
+ if dialog.exec_() == QDialog.Accepted:
+ url = dialog.get_value("url")
+ if not url or not url.strip():
+ UniversalMessageDialog.show_warning(
+ self,
+ "提示",
+ "请输入有效的 tileset URL",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ return
+
+ longitude = dialog.get_value("longitude")
+ latitude = dialog.get_value("latitude")
+ height = dialog.get_value("height")
+
+ try:
+ import uuid
+ tileset_name = f"tileset_{uuid.uuid4().hex[:8]}"
+
+ if hasattr(self.world, 'addCesiumTileset'):
+ success = self.world.addCesiumTileset(
+ tileset_name,
+ url.strip(),
+ (longitude, latitude, height)
+ )
+ if success:
+ UniversalMessageDialog.show_success(
+ self,
+ "成功",
+ f"Cesium 3D Tiles 已加载到场景中!\n名称: {tileset_name}",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ else:
+ UniversalMessageDialog.show_warning(
+ self,
+ "失败",
+ "加载 Cesium 3D Tiles 失败",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ except Exception as e:
+ UniversalMessageDialog.show_error(
+ self,
+ "错误",
+ f"加载 Cesium 3D Tiles 时发生错误:\n{str(e)}",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+
+ def onToolChanged(self, button):
+ """工具切换事件处理"""
+ if button.isChecked():
+ tool_name = button.text().strip() # 添加strip()去除空格
+ if tool_name: # 确保工具名称不为空
+ self.world.setCurrentTool(tool_name)
+ print(f"工具栏: 选择了 {tool_name} 工具")
+ else:
+ print("工具栏: 选择了空工具名称")
+ else:
+ self.world.setCurrentTool(None)
+ print("工具栏: 取消选择工具")
+
+ def openWebBrowser_win(self):
+ """打开Web浏览器面板(使用独立进程避免与Panda3D冲突)"""
+ try:
+ from PyQt5.QtWidgets import QDockWidget, QWidget, QVBoxLayout
+ from PyQt5.QtCore import QProcess, Qt
+ import subprocess
+ import sys
+
+ main_window = self.world.main_window
+
+ # 尝试获取主窗口引用
+ if main_window is None:
+ print("🔍 尝试获取主窗口引用...")
+ if hasattr(self.world, 'interface_manager'):
+ print(f" - interface_manager 存在: {self.world.interface_manager}")
+ if hasattr(self.world.interface_manager, 'main_window'):
+ main_window = self.world.interface_manager.main_window
+ print(f" - interface_manager.main_window: {main_window}")
+
+ if main_window is None and hasattr(self.world, 'main_window'):
+ main_window = self.world.main_window
+ print(f" - world.main_window: {main_window}")
+
+ if main_window is None and self.world.treeWidget:
+ try:
+ main_window = self.world.treeWidget.window()
+ print(f" - 从 treeWidget 获取窗口: {main_window}")
+ except:
+ pass
+
+ if main_window is None:
+ print("✗ 无法获取主窗口引用")
+ UniversalMessageDialog.show_warning(
+ self, "错误", "无法获取主窗口引用", False, "确定"
+ )
+ return None
+ else:
+ print(f"✅ 使用传入的主窗口引用: {main_window}")
+
+ # 检查主窗口是否有效
+ if not hasattr(main_window, 'addDockWidget'):
+ print(f"✗ 主窗口引用无效,缺少 addDockWidget 方法")
+ return None
+
+ # 检查是否已经存在浏览器视图
+ for element in self.world.gui_elements:
+ if hasattr(element, 'objectName') and element.objectName() == "WebBrowserView":
+ print("⚠ 浏览器视图已经存在")
+ element.show()
+ element.raise_()
+ return element
+
+ print(f"🔧 创建浏览器停靠窗口(独立进程模式),父窗口: {main_window}")
+
+ # 创建停靠窗口
+ browser_dock = QDockWidget("信息面板", main_window)
+ browser_dock.setObjectName("WebBrowserView")
+
+ # 创建容器 Widget
+ container_widget = QWidget()
+ container_layout = QVBoxLayout(container_widget)
+ container_layout.setContentsMargins(0, 0, 0, 0)
+
+ # 创建一个占位 Widget,用于嵌入外部进程窗口
+ embed_widget = QWidget()
+ embed_widget.setMinimumSize(400, 300)
+ container_layout.addWidget(embed_widget)
+
+ browser_dock.setWidget(container_widget)
+
+ # 添加到主窗口
+ print("📍 将浏览器视图添加到主窗口")
+ main_window.addDockWidget(Qt.RightDockWidgetArea, browser_dock)
+
+ # 获取嵌入窗口的 ID
+ embed_window_id = int(embed_widget.winId())
+ print(f"🔑 嵌入窗口 ID: {embed_window_id}")
+
+ # 启动独立进程
+ print("🚀 启动独立 WebEngine 进程...")
+ #web_process_script = os.path.join(os.path.dirname(__file__), "web_browser_process.py")
+ import tempfile
+
+ temp_dir = tempfile.gettempdir()
+ web_process_script = os.path.join(temp_dir, "web_browser_process_temp.py")
+
+ with open(web_process_script, "w", encoding="utf-8") as f:
+ f.write(WEB_BROWSER_PROCESS_CODE)
+
+ # -----------------------------------------------------
+ # 创建 QProcess(注意:必须在 start 之前)
+ # -----------------------------------------------------
+ # -----------------------------------------------------
+ # 创建 QProcess(注意:必须在 start 之前)
+ # -----------------------------------------------------
+ process = QProcess(browser_dock)
+ browser_dock.web_process = process # 保存进程引用
+
+ # -----------------------------------------------------
+ # 输出信号
+ # -----------------------------------------------------
+ def on_process_output():
+ output = bytes(process.readAllStandardOutput()).decode('utf-8', errors='ignore')
+ if output.strip():
+ print(f"[WebProcess] {output.strip()}")
+
+ def on_process_error():
+ error = bytes(process.readAllStandardError()).decode('utf-8', errors='ignore')
+ if error.strip():
+ print(f"[WebProcess Error] {error.strip()}")
+
+ def on_process_finished(exit_code, exit_status):
+ print(f"⚠️ WebEngine 进程退出: {exit_code} {exit_status}")
+
+ process.readyReadStandardOutput.connect(on_process_output)
+ process.readyReadStandardError.connect(on_process_error)
+ process.finished.connect(on_process_finished)
+
+ # -----------------------------------------------------
+ # 写入临时脚本
+ # -----------------------------------------------------
+ import tempfile
+ temp_dir = tempfile.gettempdir()
+ web_process_script = os.path.join(temp_dir, "web_browser_process_temp.py")
+
+ with open(web_process_script, "w", encoding="utf-8") as f:
+ f.write(WEB_BROWSER_PROCESS_CODE)
+
+ # -----------------------------------------------------
+ # 启动子进程(这里顺序不能错)
+ # -----------------------------------------------------
+ python_exe = sys.executable
+ url = "https://baidu.com" # 更可控的测试站点
+
+ print(f"🚀 启动: {python_exe} {web_process_script} {url} {embed_window_id}")
+ process.start(python_exe, [web_process_script, url, str(embed_window_id)])
+
+ if not process.waitForStarted(3000):
+ print("✗ 启动 WebEngine 失败")
+ return None
+
+ print("✓ WebEngine 进程已启动")
+
+ # 添加到GUI元素列表以便管理
+ self.world.gui_elements.append(browser_dock)
+
+ # 添加清理函数
+ def cleanup_process():
+ if hasattr(browser_dock, 'web_process') and browser_dock.web_process:
+ print("🧹 清理 WebEngine 进程...")
+ browser_dock.web_process.terminate()
+ if not browser_dock.web_process.waitForFinished(2000):
+ browser_dock.web_process.kill()
+
+ browser_dock.destroyed.connect(cleanup_process)
+
+ print("✓ 网页浏览器视图已创建并集成到项目中(独立进程模式)")
+ return browser_dock
+
+ except Exception as e:
+ print(f"✗ 创建浏览器视图失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ UniversalMessageDialog.show_error(
+ self, "错误",
+ f"创建浏览器视图失败:\n{str(e)}",
+ False, "确定"
+ )
+ return None
+
+ def openWebBrowser(self):
+ if not WEB_ENGINE_AVAILABLE:
+ return None
+ try:
+ # from PyQt5.QtWebEngineWidgets import QWebEngineView
+ from PyQt5.QtWidgets import QDockWidget
+ from PyQt5.QtCore import QUrl
+ import os
+
+ main_window = self.world.main_window
+
+ # 尝试获取主窗口引用
+ if main_window is None:
+ print("🔍 尝试获取主窗口引用...")
+
+ # 检查各种可能的主窗口引用
+ if hasattr(self.world, 'interface_manager'):
+ print(f" - interface_manager 存在: {self.world.interface_manager}")
+ if hasattr(self.world.interface_manager, 'main_window'):
+ main_window = self.world.interface_manager.main_window
+ print(f" - interface_manager.main_window: {main_window}")
+
+ if main_window is None and hasattr(self.world, 'main_window'):
+ main_window = self.world.main_window
+ print(f" - world.main_window: {main_window}")
+
+ # 如果仍然没有主窗口,尝试从树形控件获取
+ if main_window is None and self.world.treeWidget:
+ try:
+ main_window = self.world.treeWidget.window()
+ print(f" - 从 treeWidget 获取窗口: {main_window}")
+ except:
+ pass
+
+ if main_window is None:
+ print("✗ 无法获取主窗口引用")
+ return None
+ else:
+ print(f"✅ 使用传入的主窗口引用: {main_window}")
+
+ # 检查主窗口是否有效
+ if not hasattr(main_window, 'addDockWidget'):
+ print(f"✗ 主窗口引用无效,缺少 addDockWidget 方法")
+ return None
+
+ # 检查是否已经存在浏览器视图
+ for element in self.world.gui_elements:
+ if hasattr(element, 'objectName') and element.objectName() == "WebBrowserView":
+ print("⚠ 浏览器视图已经存在")
+ # 将其前置显示
+ element.show()
+ element.raise_()
+ return element
+
+ # 创建停靠窗口
+ print(f"🔧 创建浏览器停靠窗口,父窗口: {main_window}")
+ browser_dock = QDockWidget("信息面板", main_window)
+ browser_dock.setObjectName("WebBrowserView")
+
+ # 创建 Web 视图
+ self.web_view = QWebEngineView()
+
+ # 加载百度网页
+ # print("🌐 加载百度网页: https://www.baidu.com")
+ # self.web_view.load(QUrl("https://www.bootstrapmb.com/item/15762/preview"))
+ self.web_view.load(QUrl("https://www.baidu.com"))
+
+ # 设置内容
+ browser_dock.setWidget(self.web_view)
+
+ # 添加到主窗口
+ print("📍 将浏览器视图添加到主窗口")
+ main_window.addDockWidget(Qt.RightDockWidgetArea, browser_dock)
+
+ # 添加到GUI元素列表以便管理
+ self.world.gui_elements.append(browser_dock)
+
+ print("✓ 网页浏览器视图已创建并集成到项目中")
+ return browser_dock
+
+ except Exception as e:
+ print(f"✗ 创建浏览器视图失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ return None
+
+
+ def getSampleWeatherData(self):
+ """获取示例天气数据"""
+ try:
+ from datetime import datetime
+ import random
+
+ # 模拟天气数据
+ cities = ["北京", "上海", "广州", "深圳", "杭州", "成都", "武汉", "西安"]
+ conditions = ["晴天", "多云", "阴天", "小雨", "雷阵雨", "雪", "雾"]
+
+ city = random.choice(cities)
+ condition = random.choice(conditions)
+ temp = random.randint(-5, 35)
+ humidity = random.randint(30, 90)
+ wind_speed = round(random.uniform(0, 20), 1)
+ pressure = random.randint(980, 1030)
+
+ current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
+
+ return f"城市: {city}\n天气状况: {condition}\n温度: {temp}°C\n湿度: {humidity}%\n风速: {wind_speed} m/s\n气压: {pressure} hPa\n更新时间: {current_time}"
+ except Exception as e:
+ return f"获取示例数据失败: {str(e)}"
+
+ def onCreate3DSampleInfoPanel(self):
+ """创建3D示例天气信息面板(修复透明度问题)"""
+ try:
+ # 获取中文字体
+ from panda3d.core import TextNode
+ font = self.world.getChineseFont() if self.world.getChineseFont() else None
+
+ # 创建面板
+ info_manager = self.world.info_panel_manager
+ info_manager.setParent(self.world.render)
+
+ # 使用唯一的面板ID
+ import time
+ unique_id = f"weather_info_3d_{int(time.time())}"
+
+ # 创建3D示例面板 - 修复透明度问题
+ weather_panel = info_manager.create3DInfoPanel(
+ panel_id=unique_id,
+ position=(2, 0, 2), # 调整Z坐标避免与其他对象重叠
+ size=(5, 5),
+ bg_color=(0.15, 0.25, 0.35, 0.85), # 设置合适的透明度值
+ border_color=(0.3, 0.5, 0.7, 1.0),
+ title_color=(0.7, 0.9, 1.0, 1.0),
+ content_color=(0.95, 0.95, 0.95, 1.0),
+ font=font
+ )
+ weather_panel.setTag("name",unique_id)
+
+ # 更新面板标题
+ info_manager.update3DPanelContent(unique_id, title="3D北京天气")
+
+ # 添加到场景树
+ self.addInfoPanelToTree(weather_panel, "3D天气信息面板")
+
+ # 显示加载中信息
+ info_manager.update3DPanelContent(unique_id, content="正在获取天气数据...")
+
+ info_manager.registerDataSource(unique_id, info_manager.getRealWeatherData, update_interval=5.0)
+
+ print("✓ 3D示例天气信息面板已创建")
+
+ except Exception as e:
+ print(f"✗ 创建3D示例天气信息面板失败: {e}")
+ import traceback
+ traceback.print_exc()
+ UniversalMessageDialog.show_error(self, "错误", f"创建3D示例天气信息面板时出错: {str(e)}")
+
+ # 更新 addInfoPanelToTree 方法以支持3D面板
+ def addInfoPanelToTree(self, panel, panel_name):
+ """
+ 将信息面板添加到场景树控件中
+ """
+ if panel and self.treeWidget:
+ # 找到场景根节点
+ scene_root = None
+ for i in range(self.treeWidget.topLevelItemCount()):
+ item = self.treeWidget.topLevelItem(i)
+ if item.text(0) == "render":
+ scene_root = item
+ break
+
+ # 如果找不到场景根节点,使用第一个顶级节点
+ if not scene_root and self.treeWidget.topLevelItemCount() > 0:
+ scene_root = self.treeWidget.topLevelItem(0)
+
+ if scene_root:
+ # 根据面板类型确定节点类型
+ node_type = "INFO_PANEL_3D" if "3d" in panel_name.lower() else "INFO_PANEL"
+
+ tree_item = self.treeWidget.add_node_to_tree_widget(
+ node=panel,
+ parent_item=scene_root,
+ node_type=node_type
+ )
+ if tree_item:
+ self.treeWidget.setCurrentItem(tree_item)
+ self.treeWidget.update_selection_and_properties(panel, tree_item)
+ print(f"✓ {panel_name}节点已添加到场景树")
+ return True
+ else:
+ print(f"⚠️ {panel_name}节点添加到场景树失败")
+ else:
+ print("❌ 未找到场景根节点")
+ return False
+
+ def onCreateSystemStatusPanel(self):
+ """创建系统状态信息面板"""
+ try:
+ # 获取中文字体
+ from panda3d.core import TextNode
+ font = self.world.getChineseFont() if self.world.getChineseFont() else None
+
+ # 创建面板
+ info_manager = self.world.info_panel_manager
+ info_manager.setParent(aspect2d)
+
+
+ panel = info_manager.createInfoPanel(
+ panel_id="system_status",
+ position=(1.4, 0.2),
+ size=(0.8, 1.2),
+ bg_color=(0.25, 0.15, 0.15, 0.95), # 红色背景
+ border_color=(0.7, 0.3, 0.3, 1.0), # 红色边框
+ title_color=(1.0, 0.5, 0.5, 1.0), # 浅红色标题
+ content_color=(0.95, 0.95, 0.95, 1.0),
+ font=font
+ )
+
+ # 添加到场景树
+ self.addInfoPanelToTree(panel, "系统状态信息面板")
+
+ # 立即显示初始数据
+ initial_data = self.getSystemStatusData()
+ info_manager.updatePanelContent("system_status", content=initial_data)
+
+ # 注册数据源,每5秒更新一次
+ info_manager.registerDataSource("system_status", self.getSystemStatusData, update_interval=5.0)
+
+ except Exception as e:
+ print(f"✗ 创建系统状态信息面板失败: {e}")
+ import traceback
+ traceback.print_exc()
+ UniversalMessageDialog.show_error(self, "错误", f"创建系统状态信息面板时出错: {str(e)}")
+
+ def getSystemStatusData(self):
+ """
+ 获取系统状态数据的回调函数
+ """
+ try:
+ import psutil
+ import time
+ from datetime import datetime
+
+ # 获取系统信息
+ cpu_percent = psutil.cpu_percent(interval=0.1)
+ memory = psutil.virtual_memory()
+ memory_mb = round(memory.used / (1024 * 1024), 1)
+ memory_total_mb = round(memory.total / (1024 * 1024), 1)
+ memory_percent = memory.percent
+
+ # 网络状态
+ net_io = psutil.net_io_counters()
+ bytes_sent = round(net_io.bytes_sent / (1024 * 1024), 2) # MB
+ bytes_recv = round(net_io.bytes_recv / (1024 * 1024), 2) # MB
+
+ # 磁盘使用情况
+ disk = psutil.disk_usage('/')
+ disk_used_gb = round(disk.used / (1024 ** 3), 2)
+ disk_total_gb = round(disk.total / (1024 ** 3), 2)
+ disk_percent = round((disk.used / disk.total) * 100, 1)
+
+ # 时间戳
+ timestamp = datetime.now().strftime("%H:%M:%S")
+
+ return f"CPU使用率: {cpu_percent}%\n内存使用: {memory_mb}MB / {memory_total_mb}MB ({memory_percent}%)\n磁盘使用: {disk_used_gb}GB / {disk_total_gb}GB ({disk_percent}%)\n网络发送: {bytes_sent}MB\n网络接收: {bytes_recv}MB\n更新时间: {timestamp}"
+ except Exception as e:
+ return f"获取系统状态失败: {str(e)}"
+
+ def onCreateSensorDataPanel(self):
+ """创建传感器数据信息面板"""
+ try:
+ # 获取中文字体
+ from panda3d.core import TextNode
+ font = self.world.getChineseFont() if self.world.getChineseFont() else None
+
+ # 创建面板
+ info_manager = self.world.info_panel_manager
+ info_manager.setParent(aspect2d)
+
+ panel = info_manager.createInfoPanel(
+ panel_id="sensor_data",
+ position=(0.8, -0.2),
+ size=(0.8, 0.6),
+ bg_color=(0.15, 0.25, 0.15, 0.95), # 绿色背景
+ border_color=(0.3, 0.7, 0.3, 1.0), # 绿色边框
+ title_color=(0.5, 1.0, 0.5, 1.0), # 浅绿色标题
+ content_color=(0.95, 0.95, 0.95, 1.0),
+ font=font
+ )
+
+ # 添加到场景树
+ self.addInfoPanelToTree(panel, "传感器数据信息面板")
+
+ # 立即显示初始数据
+ initial_data = self.getSensorData()
+ info_manager.updatePanelContent("sensor_data", content=initial_data)
+
+ # 注册数据源,每2秒更新一次
+ info_manager.registerDataSource("sensor_data", self.getSensorData, update_interval=2.0)
+
+ # 绑定键盘事件
+ info_manager.accept("F3", info_manager.togglePanel, ["sensor_data"])
+
+ print("✓ 传感器数据信息面板已创建(按 F3 切换显示)")
+
+ except Exception as e:
+ print(f"✗ 创建传感器数据信息面板失败: {e}")
+ import traceback
+ traceback.print_exc()
+ UniversalMessageDialog.show_error(self, "错误", f"创建传感器数据信息面板时出错: {str(e)}")
+
+ def getSensorData(self):
+ """
+ 获取传感器数据的回调函数(模拟数据)
+ """
+ try:
+ import random
+ from datetime import datetime
+
+ # 模拟传感器数据
+ temperature = round(random.uniform(20, 35), 1)
+ humidity = round(random.uniform(30, 70), 1)
+ pressure = round(random.uniform(990, 1030), 1)
+ light_level = round(random.uniform(0, 1000), 1)
+
+ # 时间戳
+ timestamp = datetime.now().strftime("%H:%M:%S")
+
+ return f"温度: {temperature}°C\n湿度: {humidity}%\n气压: {pressure} hPa\n光照: {light_level} lux\n更新时间: {timestamp}"
+ except Exception as e:
+ return f"获取传感器数据失败: {str(e)}"
+
+ def onCreateSceneInfoPanel(self):
+ """创建场景信息面板"""
+ try:
+ # 获取中文字体
+ from panda3d.core import TextNode
+ font = self.world.getChineseFont() if self.world.getChineseFont() else None
+
+ # 创建面板
+ info_manager = self.world.info_panel_manager
+ info_manager.setParent(aspect2d)
+
+ panel = info_manager.createInfoPanel(
+ panel_id="scene_info",
+ position=(-0.8, 0.5),
+ size=(0.8, 0.6),
+ bg_color=(0.12, 0.12, 0.12, 0.95), # 深灰色背景
+ border_color=(0.4, 0.4, 0.4, 1.0), # 灰色边框
+ title_color=(0.2, 0.8, 1.0, 1.0), # 蓝色标题
+ content_color=(0.9, 0.9, 0.9, 1.0),
+ font=font
+ )
+
+ # 添加到场景树
+ self.addInfoPanelToTree(panel, "场景信息面板")
+
+ # 立即显示初始数据
+ initial_data = self.getSceneInfoData()
+ info_manager.updatePanelContent("scene_info", content=initial_data)
+
+ # 注册数据源,每3秒更新一次
+ info_manager.registerDataSource("scene_info", self.getSceneInfoData, update_interval=3.0)
+
+ # 绑定键盘事件
+ info_manager.accept("F4", info_manager.togglePanel, ["scene_info"])
+
+ print("✓ 场景信息面板已创建(按 F4 切换显示)")
+
+ except Exception as e:
+ print(f"✗ 创建场景信息面板失败: {e}")
+ import traceback
+ traceback.print_exc()
+ UniversalMessageDialog.show_error(self, "错误", f"创建场景信息面板时出错: {str(e)}")
+
+ def getSceneInfoData(self):
+ """
+ 获取场景信息数据的回调函数
+ """
+ try:
+ # 获取场景信息
+ node_count = 0
+ texture_count = 0
+ light_count = 0
+
+ # 如果有场景管理器,获取实际数据
+ if hasattr(self.world, 'scene_graph'):
+ # 这里可以根据实际的场景结构来统计节点数
+ node_count = len([node for node in self.world.scene_graph.nodes]) if hasattr(self.world.scene_graph,
+ 'nodes') else 0
+
+ # 统计光源数量
+ if hasattr(self.world, 'lights'):
+ light_count = len(self.world.lights)
+
+ # 统计纹理数量
+ if hasattr(self.world, 'textures'):
+ texture_count = len(self.world.textures)
+
+ # 当前时间
+ from datetime import datetime
+ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+
+ return f"场景节点数: {node_count}\n纹理数量: {texture_count}\n光源数量: {light_count}\nFPS: {self.world.clock.getAverageFrameRate():.1f}\n更新时间: {current_time}"
+ except Exception as e:
+ return f"获取场景信息失败: {str(e)}"
+
+
+ # ==================== 脚本管理事件处理 ====================
+
+ def refreshScriptsList(self):
+ """刷新脚本列表"""
+ self.scriptsList.clear()
+ self.mountScriptCombo.clear()
+
+ available_scripts = self.world.getAvailableScripts()
+ for script_name in available_scripts:
+ self.scriptsList.addItem(script_name)
+ self.mountScriptCombo.addItem(script_name)
+
+ def updateScriptPanel(self):
+ """更新脚本面板状态"""
+ # 更新热重载状态
+ hot_reload_enabled = self.world.script_manager.hot_reload_enabled
+
+ # 更新热重载标签文本和样式
+ if hot_reload_enabled:
+ self.hotReloadLabel.setText("已启用")
+ self.hotReloadLabel.setStyleSheet("""
+ QLabel {
+ background-color: rgba(243, 157, 120, 0.15);
+ border: 1px solid #f39d78 ;
+ color: #f39d78 ;
+ border-radius: 2px;
+ padding: 2px 8px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ letter-spacing: 0.5px;
+ }
+ """)
+ else:
+ self.hotReloadLabel.setText("已禁用")
+ self.hotReloadLabel.setStyleSheet("""
+ QLabel {
+ background-color: rgba(45, 136, 255, 0.17) ;
+ border: 1px solid #289eff ;
+ color: #289eff ;
+ border-radius: 2px;
+ padding: 2px 8px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ letter-spacing: 0.5px;
+ }
+ """)
+
+ # 更新热重载菜单状态
+ self.toggleHotReloadAction.setChecked(hot_reload_enabled)
+
+ # 更新脚本系统状态(确保使用正确的样式)
+ self.scriptStatusLabel.setText("已启动")
+ self.scriptStatusLabel.setStyleSheet("""
+ QLabel {
+ background-color: rgba(45, 255, 196, 0.17);
+ border: 1px solid #2dffc4;
+ color: #2dffc4;
+ border-radius: 2px;
+ padding: 2px 8px;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ letter-spacing: 0.5px;
+ }
+ """)
+
+ # 更新选中对象信息
+ selected_object = getattr(self.world.selection, 'selectedObject', None)
+ if selected_object:
+ self.selectedObjectLabel.setText(f"选中对象:{selected_object.getName()}")
+ self.selectedObjectLabel.setStyleSheet("""
+ QLabel {
+ color: #2dffc4;
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ letter-spacing: 0.5px;
+ padding: 2px 0px;
+ }
+ """)
+ self.mountScriptCombo.setEnabled(True)
+ self.mountBtn.setEnabled(True)
+
+ # 更新已挂载脚本列表
+ self.updateMountedScriptsList(selected_object)
+ else:
+ self.selectedObjectLabel.setText("未选择对象")
+ self.selectedObjectLabel.setStyleSheet("""
+ QLabel {
+ color: rgba(235, 235, 235, 0.5);
+ font-family: 'Microsoft YaHei', 'Inter', sans-serif;
+ font-size: 10px;
+ font-weight: 300;
+ letter-spacing: 0.5px;
+ padding: 2px 0px;
+ font-style: italic;
+ }
+ """)
+ self.mountScriptCombo.setEnabled(False)
+ self.mountBtn.setEnabled(False)
+ self.mountedScriptsList.clear()
+
+ def updateMountedScriptsList(self, game_object):
+ """更新已挂载脚本列表"""
+ # 保存当前选中项的脚本名(去除状态前缀)
+ current_item = self.mountedScriptsList.currentItem()
+ selected_script_name = None
+ if current_item:
+ # 提取脚本名(移除 "✓ " 或 "✗ " 前缀)
+ selected_script_name = current_item.text()[2:]
+
+ # 清空并重新填充列表
+ self.mountedScriptsList.clear()
+ scripts = self.world.getScripts(game_object)
+ for script_component in scripts:
+ script_name = script_component.script_name
+ enabled = "✓" if script_component.enabled else "✗"
+ item_text = f"{enabled} {script_name}"
+ self.mountedScriptsList.addItem(item_text)
+
+ # 恢复选中状态(根据脚本名匹配)
+ if selected_script_name:
+ for i in range(self.mountedScriptsList.count()):
+ item = self.mountedScriptsList.item(i)
+ # 提取当前项的脚本名进行比较
+ current_script_name = item.text()[2:]
+ if current_script_name == selected_script_name:
+ self.mountedScriptsList.setCurrentItem(item)
+ break
+
+ def onCreateScript(self):
+ """创建脚本按钮事件"""
+ script_name = self.scriptNameEdit.text().strip()
+ if not script_name:
+ UniversalMessageDialog.show_warning(
+ self, "警告", "请输入脚本名称!", show_cancel=False, confirm_text="确认"
+ )
+ return
+
+ template = self.templateCombo.currentText()
+
+ try:
+ success = self.world.createScript(script_name, template)
+ if success:
+ UniversalMessageDialog.show_success(
+ self,
+ "成功",
+ f"脚本 '{script_name}' 创建成功!",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ self.scriptNameEdit.clear()
+ self.refreshScriptsList()
+ else:
+ UniversalMessageDialog.show_warning(
+ self,
+ "警告",
+ f"脚本 '{script_name}' 创建失败!",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ except Exception as e:
+ UniversalMessageDialog.show_error(
+ self,
+ "错误",
+ f"创建脚本时发生错误: {str(e)}",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+
+ def onCreateScriptDialog(self):
+ """菜单创建脚本事件"""
+ dialog = self.createStyledInputDialog(self, "创建脚本", "请输入脚本名称:")
+
+ if dialog.exec_() == QDialog.Accepted:
+ script_name = dialog.textValue()
+ if script_name.strip():
+ try:
+ success = self.world.createScript(script_name.strip(), "basic")
+ if success:
+ UniversalMessageDialog.show_success(
+ self,
+ "成功",
+ f"脚本 '{script_name}' 创建成功!",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ self.refreshScriptsList()
+ else:
+ UniversalMessageDialog.show_warning(
+ self,
+ "警告",
+ f"脚本 '{script_name}' 创建失败!",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ except Exception as e:
+ UniversalMessageDialog.show_error(
+ self,
+ "错误",
+ f"创建脚本时发生错误: {str(e)}",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+
+ def onLoadScript(self):
+ """加载脚本按钮事件"""
+ current_item = self.scriptsList.currentItem()
+ if not current_item:
+ UniversalMessageDialog.show_warning(
+ self, "警告", "请选择要加载的脚本!", show_cancel=False, confirm_text="确认"
+ )
+ return
+
+ script_name = current_item.text()
+ try:
+ success = self.world.reloadScript(script_name)
+ if success:
+ UniversalMessageDialog.show_success(
+ self, "成功", f"脚本 '{script_name}' 加载成功!", show_cancel=False, confirm_text="确认"
+ )
+ else:
+ UniversalMessageDialog.show_warning(
+ self, "警告", f"脚本 '{script_name}' 加载失败!", show_cancel=False, confirm_text="确认"
+ )
+ except Exception as e:
+ UniversalMessageDialog.show_error(
+ self, "错误", f"加载脚本时发生错误: {str(e)}", show_cancel=False, confirm_text="确认"
+ )
+
+ def onLoadScriptFile(self):
+ """加载脚本文件菜单事件"""
+ dialog = self.createStyledFileDialog(
+ self,
+ "选择脚本文件",
+ "",
+ "Python文件 (*.py)"
+ )
+
+ if dialog.exec_() == QDialog.Accepted:
+ file_path = dialog.selectedFiles()[0]
+ if file_path:
+ try:
+ success = self.world.loadScript(file_path)
+ if success:
+ UniversalMessageDialog.show_success(
+ self, "成功", "脚本文件加载成功!", show_cancel=False, confirm_text="确认"
+ )
+ self.refreshScriptsList()
+ else:
+ UniversalMessageDialog.show_warning(
+ self, "警告", "脚本文件加载失败!", show_cancel=False, confirm_text="确认"
+ )
+ except Exception as e:
+ UniversalMessageDialog.show_error(
+ self, "错误", f"加载脚本文件时发生错误: {str(e)}", show_cancel=False, confirm_text="确认"
+ )
+
+ def onReloadAllScripts(self):
+ """重新加载所有脚本事件"""
+ try:
+ scripts_loaded = self.world.loadAllScripts()
+ UniversalMessageDialog.show_success(
+ self,
+ "成功",
+ f"更新完成,共加载 {len(scripts_loaded)} 个脚本!",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ self.refreshScriptsList()
+ except Exception as e:
+ UniversalMessageDialog.show_error(
+ self,
+ "错误",
+ f"加载脚本时发生错误: {str(e)}",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+
+ def onToggleHotReload(self):
+ """切换热更新状态"""
+ enabled = self.toggleHotReloadAction.isChecked()
+ self.world.enableHotReload(enabled)
+ status = "已开启" if enabled else "已关闭"
+ UniversalMessageDialog.show_info(
+ self,
+ "脚本热更",
+ f"脚本热更{status}",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+
+ def onOpenScriptsManager(self):
+ """打开脚本管理器"""
+ # 显示脚本管理停靠窗口
+ self.scriptDock.show()
+ self.scriptDock.raise_()
+
+ def onScriptDoubleClick(self, item):
+ """脚本列表双击事件"""
+ script_name = item.text()
+ UniversalMessageDialog.show_info(
+ self,
+ "提示",
+ f"双击脚本: {script_name}\n\n请使用外部编辑器编辑脚本文件。",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+
+ def onMountScript(self):
+ """挂载脚本事件"""
+ selected_object = getattr(self.world.selection, 'selectedObject', None)
+ if not selected_object:
+ UniversalMessageDialog.show_warning(
+ self, "警告", "请选择一个对象!", show_cancel=False, confirm_text="确认"
+ )
+ return
+
+ script_name = self.mountScriptCombo.currentText()
+ if not script_name:
+ UniversalMessageDialog.show_warning(
+ self, "警告", "请选择要挂载的脚本!", show_cancel=False, confirm_text="确认"
+ )
+ return
+
+ try:
+ success = self.world.addScript(selected_object, script_name)
+ if success:
+ UniversalMessageDialog.show_success(
+ self, "成功", f"脚本 '{script_name}' 已挂载到对象!", show_cancel=False, confirm_text="确认"
+ )
+ self.updateMountedScriptsList(selected_object)
+
+ if self.treeWidget and self.treeWidget.currentItem():
+ self.world.updatePropertyPanel(self.treeWidget.currentItem())
+ else:
+ UniversalMessageDialog.show_warning(
+ self, "警告", "挂载脚本失败!", show_cancel=False, confirm_text="确认"
+ )
+ except Exception as e:
+ UniversalMessageDialog.show_error(
+ self, "错误", f"挂载脚本时发生错误: {str(e)}", show_cancel=False, confirm_text="确认"
+ )
+
+ def onUnmountScript(self):
+ """卸载脚本事件"""
+ selected_object = getattr(self.world.selection, 'selectedObject', None)
+ if not selected_object:
+ UniversalMessageDialog.show_warning(
+ self,
+ "警告",
+ "请选择一个对象!",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ return
+
+ current_item = self.mountedScriptsList.currentItem()
+ if not current_item:
+ UniversalMessageDialog.show_warning(
+ self,
+ "警告",
+ "请选择要卸载的脚本!",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ return
+
+ item_text = current_item.text()
+ script_name = item_text[2:]
+
+ try:
+ success = self.world.removeScript(selected_object, script_name)
+ if success:
+ UniversalMessageDialog.show_success(
+ self,
+ "成功",
+ f"脚本 '{script_name}' 已从对象卸载!",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ self.updateMountedScriptsList(selected_object)
+
+ if self.treeWidget and self.treeWidget.currentItem():
+ self.world.updatePropertyPanel(self.treeWidget.currentItem())
+ else:
+ UniversalMessageDialog.show_warning(
+ self,
+ "警告",
+ "卸载脚本失败!",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ except Exception as e:
+ UniversalMessageDialog.show_error(
+ self,
+ "错误",
+ f"卸载脚本时发生错误: {str(e)}",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+
+ def onOpenIconManager(self):
+ """打开图标管理器"""
+ try:
+ from ui.icon_manager_gui import show_icon_manager
+ self.icon_manager_dialog = show_icon_manager(self)
+ print("🎨 图标管理器已打开")
+ except Exception as e:
+ print(f"❌ 打开图标管理器失败: {e}")
+ UniversalMessageDialog.show_warning(self, "警告", f"打开图标管理器失败: {str(e)}", show_cancel=False, confirm_text="确认")
+
+ def closeEvent(self, event):
+ """处理窗口关闭事件"""
+ try:
+ print("🔄 正在关闭应用程序...")
+
+ # 关闭拆装交互相关的弹窗
+ if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction:
+ print("🧹 关闭拆装交互弹窗...")
+ if hasattr(self.world.assembly_interaction, 'step_dialog') and self.world.assembly_interaction.step_dialog:
+ self.world.assembly_interaction.step_dialog.close()
+ self.world.assembly_interaction.step_dialog = None
+ # 停止交互模式
+ if self.world.assembly_interaction.is_active:
+ self.world.assembly_interaction.stop_interaction_mode()
+
+ # 清理工具管理器中的进程
+ if hasattr(self.world, 'tool_manager') and self.world.tool_manager:
+ print("🧹 清理工具管理器进程...")
+ if hasattr(self.world.tool_manager, 'cleanup_processes'):
+ self.world.tool_manager.cleanup_processes()
+ else:
+ print("✓ 工具管理器无需清理进程")
+
+ # 停止更新定时器
+ if hasattr(self, 'updateTimer') and self.updateTimer:
+ self.updateTimer.stop()
+ print("⏹️ 更新定时器已停止")
+
+ # 清理VR资源
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ print("🧹 清理VR资源...")
+ self.world.vr_manager.cleanup()
+
+ # 清理Panda3D资源
+ if hasattr(self, 'pandaWidget') and self.pandaWidget:
+ print("🧹 清理Panda3D资源...")
+ self.pandaWidget.cleanup()
+
+ print("✅ 应用程序清理完成")
+ event.accept()
+
+ except Exception as e:
+ print(f"⚠️ 关闭应用程序时出错: {e}")
+ event.accept() # 即使出错也要关闭
+
+ def onCreateFlatTerrain(self):
+ """创建平面地形"""
+ fields = [
+ {
+ "name": "width",
+ "label": "宽度:",
+ "type": "double",
+ "min": 0.0,
+ "max": 10000.0,
+ "step": 0.1,
+ "decimals": 2,
+ "value": 0.30,
+ "label_width": 60
+ },
+ {
+ "name": "height",
+ "label": "高度:",
+ "type": "double",
+ "min": 0.0,
+ "max": 10000.0,
+ "step": 0.1,
+ "decimals": 2,
+ "value": 0.30,
+ "label_width": 60
+ },
+ {
+ "name": "resolution",
+ "label": "分辨率:",
+ "type": "int",
+ "min": 16,
+ "max": 2048,
+ "step": 16,
+ "value": 256,
+ "label_width": 60
+ },
+ ]
+ dialog = StyledTerrainDialog(self, "创建平面地形", fields, primary_text="创建", secondary_text="取消")
+
+ if dialog.exec_() == QDialog.Accepted:
+ width = dialog.get_value("width")
+ height = dialog.get_value("height")
+ resolution = int(dialog.get_value("resolution"))
+
+ terrain_info = self.world.createFlatTerrain((width, height), resolution)
+ if terrain_info:
+ UniversalMessageDialog.show_success(
+ self,
+ "成功",
+ "平面地形创建成功!",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ else:
+ UniversalMessageDialog.show_warning(
+ self,
+ "警告",
+ "平面地形创建失败!",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+
+ def onCreateHeightmapTerrain(self):
+ """从高度图创建地形"""
+ file_dialog = self.createStyledFileDialog(
+ self,
+ "选择高度图文件",
+ "",
+ "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga);;所有文件 (*)"
+ )
+
+ if file_dialog.exec_() == QDialog.Accepted:
+ file_path = file_dialog.selectedFiles()[0]
+ if file_path:
+ fields = [
+ {
+ "name": "x_scale",
+ "label": "X缩放:",
+ "type": "double",
+ "min": 0.1,
+ "max": 1000.0,
+ "step": 0.1,
+ "decimals": 2,
+ "value": 0.30,
+ "label_width": 70
+ },
+ {
+ "name": "y_scale",
+ "label": "Y缩放:",
+ "type": "double",
+ "min": 0.1,
+ "max": 1000.0,
+ "step": 0.1,
+ "decimals": 2,
+ "value": 0.30,
+ "label_width": 70
+ },
+ {
+ "name": "z_scale",
+ "label": "Z缩放:",
+ "type": "double",
+ "min": 0.1,
+ "max": 1000.0,
+ "step": 1.0,
+ "decimals": 2,
+ "value": 50.0,
+ "label_width": 70
+ },
+ ]
+
+ params_dialog = StyledTerrainDialog(self, "设置地形参数", fields, primary_text="创建", secondary_text="取消")
+
+ if params_dialog.exec_() == QDialog.Accepted:
+ x_scale = params_dialog.get_value("x_scale")
+ y_scale = params_dialog.get_value("y_scale")
+ z_scale = params_dialog.get_value("z_scale")
+
+ terrain_info = self.world.createTerrainFromHeightMap(
+ file_path,
+ (x_scale, y_scale, z_scale)
+ )
+ if terrain_info:
+ UniversalMessageDialog.show_success(
+ self,
+ "成功",
+ "高度图地形创建成功!",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+ else:
+ UniversalMessageDialog.show_warning(
+ self,
+ "警告",
+ "高度图地形创建失败!",
+ show_cancel=False,
+ confirm_text="确认"
+ )
+
+ def onOpenAssemblyDisassemblyConfig(self):
+ """打开拆装配置界面"""
+ try:
+ from ui.assembly_disassembly_config_simple import AssemblyDisassemblyConfigDialog
+ config_dialog = AssemblyDisassemblyConfigDialog(self, self.world)
+ config_dialog.show()
+ except Exception as e:
+ UniversalMessageDialog.show_error(self, "错误", f"打开拆装配置界面失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
+
+ def onStartAssemblyInteraction(self):
+ """开始拆装交互"""
+ try:
+ # 显示模式选择对话框
+ mode_dialog = AssemblyModeSelectionDialog(self)
+ if mode_dialog.exec_() != QDialog.Accepted:
+ return
+
+ selected_mode = mode_dialog.get_selected_mode()
+ print(f"🎯 用户选择的拆装模式: {selected_mode}")
+
+ from core.assembly_interaction import AssemblyInteractionManager
+
+ # 检查是否已有交互管理器实例
+ if not hasattr(self.world, 'assembly_interaction'):
+ self.world.assembly_interaction = AssemblyInteractionManager(self.world)
+
+ # 启动交互模式,传递模式参数
+ self.world.assembly_interaction.start_interaction_mode(mode=selected_mode)
+
+ except Exception as e:
+ UniversalMessageDialog.show_error(self, "错误", f"启动拆装交互失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
+
+ def onOpenMaintenanceSystem(self):
+ """打开维修系统"""
+ try:
+ # 导入简化的登录界面
+ from ui.simple_maintenance_login import SimpleMaintenanceLoginDialog
+ from ui.maintenance_system import MaintenanceSubjectDialog, MaintenanceSystemManager
+
+ print("🔧 启动维修系统...")
+
+ # 显示登录界面
+ login_dialog = SimpleMaintenanceLoginDialog(self)
+
+ if login_dialog.exec_() == QDialog.Accepted:
+ print("✅ 登录成功,显示科目选择界面")
+
+ # 获取当前项目路径
+ project_path = None
+ if hasattr(self.world, 'project_manager') and self.world.project_manager:
+ project_path = self.world.project_manager.getCurrentProjectPath()
+
+ # 显示科目选择界面
+ subject_dialog = MaintenanceSubjectDialog(project_path, self)
+
+ def on_subject_selected(subject_path, mode):
+ """处理科目选择"""
+ try:
+ print(f"🎯 启动维修科目: {subject_path}")
+ print(f"📝 模式: {mode}")
+
+ # 加载科目配置
+ import json
+ with open(subject_path, 'r', encoding='utf-8') as f:
+ subject_config = json.load(f)
+
+ # 初始化拆装交互系统(如果还没有)
+ from core.assembly_interaction import AssemblyInteractionManager
+
+ if not hasattr(self.world, 'assembly_interaction') or not self.world.assembly_interaction:
+ print("🔧 初始化拆装交互系统...")
+ self.world.assembly_interaction = AssemblyInteractionManager(self.world)
+
+ # 设置配置并启动交互模式
+ self.world.assembly_interaction.config_data = subject_config
+
+ # 启动交互模式
+ success = self.world.assembly_interaction.start_interaction_mode(mode=mode)
+
+ if success:
+ print(f"✅ 维修科目启动成功")
+ else:
+ print("❌ 维修科目启动失败")
+ UniversalMessageDialog.show_warning(self, "错误", "维修科目启动失败",False,"确认")
+
+ except Exception as e:
+ print(f"❌ 启动维修科目失败: {e}")
+ UniversalMessageDialog.show_error(self, "错误", f"启动维修科目失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
+
+ subject_dialog.subject_selected.connect(on_subject_selected)
+ subject_dialog.exec_()
+
+ else:
+ print("ℹ️ 用户取消了登录")
+
+ except Exception as e:
+ print(f"❌ 打开维修系统失败: {e}")
+ UniversalMessageDialog.show_error(self, "错误", f"打开维修系统失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
+
+
+
+ # ==================== VR事件处理 ====================
+
+ def onEnterVR(self):
+ """进入VR模式"""
+ try:
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ success = self.world.vr_manager.enable_vr()
+ if success:
+ # 更新菜单状态
+ self.enterVRAction.setEnabled(False)
+ self.exitVRAction.setEnabled(True)
+ UniversalMessageDialog.show_info(
+ self, "成功", "VR模式已启用!\n请确保您的VR头显已正确连接。"
+ , False, "确认")
+ else:
+ UniversalMessageDialog.show_warning(
+ self, "错误", "无法启用VR模式!\n请检查:\n1. SteamVR是否正在运行\n2. VR头显是否已连接\n3. OpenVR库是否已正确安装"
+ , False, "确认")
+ else:
+ UniversalMessageDialog.show_warning(
+ self, "错误", "VR管理器不可用!"
+ , False, "确认")
+ except Exception as e:
+ UniversalMessageDialog.show_error(
+ self, "错误", f"启用VR模式时发生错误:\n{str(e)}"
+ , False, "确认")
+ def onExitVR(self):
+ """退出VR模式"""
+ try:
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ self.world.vr_manager.disable_vr()
+ # 更新菜单状态
+ self.enterVRAction.setEnabled(True)
+ self.exitVRAction.setEnabled(False)
+ UniversalMessageDialog.show_info(self, "成功", "已退出VR模式"
+ , False, "确认")
+ else:
+ UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!"
+ , False, "确认")
+ except Exception as e:
+ UniversalMessageDialog.show_error(self, "错误", f"退出VR模式时发生错误:\n{str(e)}"
+ , False, "确认")
+
+ def onShowVRStatus(self):
+ """显示VR状态"""
+ try:
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ status = self.world.vr_manager.get_vr_status()
+
+ status_text = f"""VR系统状态:
+
+可用性: {'✅ 可用' if status['available'] else '❌ 不可用'}
+初始化: {'✅ 已初始化' if status['initialized'] else '❌ 未初始化'}
+启用状态: {'✅ 已启用' if status['enabled'] else '❌ 未启用'}
+渲染分辨率: {status['eye_resolution'][0]}x{status['eye_resolution'][1]}
+追踪设备数: {status['device_count']}
+
+提示:
+- 如果VR不可用,请确保已安装SteamVR并连接VR头显
+- 如果OpenVR库未安装,请运行:pip install openvr
+"""
+ UniversalMessageDialog.show_info(self, "VR状态", status_text
+ , False, "确认")
+ else:
+ UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!"
+ , False, "确认")
+ except Exception as e:
+ UniversalMessageDialog.show_error(self, "错误", f"获取VR状态时发生错误:\n{str(e)}"
+ , False, "确认")
+
+ def onShowVRSettings(self):
+ """显示VR设置对话框"""
+ try:
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ dialog = self.createVRSettingsDialog()
+ dialog.exec_()
+ else:
+ UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!"
+ , False, "确认")
+ except Exception as e:
+ UniversalMessageDialog.show_error(self, "错误", f"打开VR设置时发生错误:\n{str(e)}"
+ , False, "确认")
+
+ def createVRSettingsDialog(self):
+ """创建VR设置对话框(统一为 NewProjectDialog 风格)"""
+ dialog = QDialog(self)
+ dialog.setWindowTitle("VR设置")
+ dialog.setModal(True)
+ dialog.resize(508, 420)
+ dialog.setObjectName("newProjectDialog")
+ dialog.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
+ dialog.setAttribute(Qt.WA_TranslucentBackground, True)
+
+ dialog.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 0 0; 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: 0 0 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; }
+ QLabel[role="fieldLabel"] { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 12px; font-weight: 400; letter-spacing: 0.6px; }
+ QLabel { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; letter-spacing: 0.55px; }
+ QComboBox, QSpinBox, QDoubleSpinBox { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; }
+ QCheckBox { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; }
+ 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); }
+ QTextEdit { background-color: rgba(89,100,113,0.1); color: #EBEBEB; border: 1px solid rgba(76,92,110,0.4); border-radius: 4px; font-size: 11px; }
+ 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; }
+ """)
+
+ main_layout = QVBoxLayout(dialog)
+ 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)
+
+ # Title bar
+ title_bar = QWidget()
+ title_bar.setObjectName('titleBar')
+ tb_layout = QHBoxLayout(title_bar)
+ tb_layout.setContentsMargins(8, 0, 8, 0)
+ tb_layout.setSpacing(6)
+ control_buttons = QWidget()
+ control_buttons.setObjectName('controlButtons')
+ cb_layout = QHBoxLayout(control_buttons)
+ cb_layout.setContentsMargins(0, 0, 0, 0)
+ cb_layout.setSpacing(0)
+ close_btn = QPushButton()
+ close_btn.setObjectName('closeButton')
+ try:
+ close_btn.setIcon(get_icon('close_icon', QSize(18, 18)))
+ close_btn.setIconSize(QSize(18, 18))
+ except Exception:
+ pass
+ close_btn.clicked.connect(dialog.reject)
+ cb_layout.addWidget(close_btn)
+ left_placeholder = QWidget()
+ left_placeholder.setFixedWidth(control_buttons.sizeHint().width())
+ tb_layout.addWidget(left_placeholder)
+ title_label = QLabel("VR设置")
+ title_label.setObjectName('titleLabel')
+ title_label.setAlignment(Qt.AlignCenter)
+ tb_layout.addWidget(title_label, 1)
+ tb_layout.addWidget(control_buttons)
+ base_layout.addWidget(title_bar)
+
+ # drag handlers
+ dragging_state = {'dragging': False, 'pos': QPoint()}
+ def _tb_press(event):
+ if event.button() == Qt.LeftButton:
+ dragging_state['dragging'] = True
+ dragging_state['pos'] = event.globalPos() - dialog.frameGeometry().topLeft()
+ event.accept()
+ else:
+ event.ignore()
+ def _tb_move(event):
+ if event.buttons() & Qt.LeftButton and dragging_state['dragging']:
+ dialog.move(event.globalPos() - dragging_state['pos'])
+ event.accept()
+ else:
+ event.ignore()
+ def _tb_release(event):
+ if event.button() == Qt.LeftButton:
+ dragging_state['dragging'] = False
+ event.accept()
+ else:
+ event.ignore()
+ title_bar.mousePressEvent = _tb_press
+ title_bar.mouseMoveEvent = _tb_move
+ title_bar.mouseReleaseEvent = _tb_release
+
+ # Content
+ 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)
+ container_layout = QVBoxLayout(content_container)
+ container_layout.setContentsMargins(15, 10, 15, 10)
+ container_layout.setSpacing(10)
+
+ # 左侧对齐辅助:统一字段列宽,并让小标题与控件左侧对齐
+ label_column_width = 110
+ def add_section_title(text):
+ title = QLabel(text)
+ title.setProperty('role', 'sectionTitle')
+ container_layout.addWidget(title)
+
+ # VR 状态
+ add_section_title("VR状态")
+ status_widget = QWidget()
+ status_vlayout = QVBoxLayout(status_widget)
+ status_vlayout.setContentsMargins(0, 0, 0, 0)
+ status_vlayout.setSpacing(6)
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ status = self.world.vr_manager.get_vr_status()
+ available_label = QLabel(f"VR可用性: {'是' if status['available'] else '否'}")
+ available_label.setStyleSheet(f"color: {'#2dffc4' if status['available'] else 'red'};")
+ status_vlayout.addWidget(available_label)
+ enabled_label = QLabel(f"VR状态: {'已启用' if status['enabled'] else '未启用'}")
+ enabled_label.setStyleSheet(f"color: {'#2dffc4' if status['enabled'] else 'gray'};")
+ status_vlayout.addWidget(enabled_label)
+ resolution_label = QLabel(f"渲染分辨率: {status['eye_resolution'][0]}x{status['eye_resolution'][1]}")
+ status_vlayout.addWidget(resolution_label)
+ container_layout.addWidget(status_widget)
+
+ # 渲染模式
+ add_section_title("渲染模式")
+ render_mode_button_group = QButtonGroup(dialog)
+ row_mode1 = QWidget(); row_mode1_layout = QHBoxLayout(row_mode1); row_mode1_layout.setContentsMargins(0,0,0,0); row_mode1_layout.setSpacing(10)
+ normal_render_radio = QRadioButton("普通渲染模式")
+ normal_render_radio.setStyleSheet("""
+ QRadioButton {
+ color: #EBEBEB;
+ font-family: 'Inter', 'Microsoft YaHei', sans-serif;
+ font-size: 12px;
+ font-weight: 400;
+ letter-spacing: 0.6px;
+ }
+ """)
+ row_mode1_layout.addWidget(normal_render_radio)
+ row_mode1_layout.addStretch(); container_layout.addWidget(row_mode1)
+ row_mode2 = QWidget(); row_mode2_layout = QHBoxLayout(row_mode2); row_mode2_layout.setContentsMargins(0,0,0,0); row_mode2_layout.setSpacing(10)
+ pipeline_render_radio = QRadioButton("RenderPipeline高级渲染(推荐)")
+ pipeline_render_radio.setStyleSheet("""
+ QRadioButton {
+ color: #EBEBEB;
+ font-family: 'Inter', 'Microsoft YaHei', sans-serif;
+ font-size: 12px;
+ font-weight: 400;
+ letter-spacing: 0.6px;
+ }
+ """)
+ row_mode2_layout.addWidget(pipeline_render_radio)
+ row_mode2_layout.addStretch(); container_layout.addWidget(row_mode2)
+ render_mode_button_group.addButton(normal_render_radio, 0)
+ render_mode_button_group.addButton(pipeline_render_radio, 1)
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ try:
+ from core.vr import VRRenderMode
+ current_mode = self.world.vr_manager.get_vr_render_mode()
+ if current_mode == VRRenderMode.RENDER_PIPELINE:
+ pipeline_render_radio.setChecked(True)
+ else:
+ normal_render_radio.setChecked(True)
+ except Exception:
+ normal_render_radio.setChecked(True)
+ else:
+ normal_render_radio.setChecked(True)
+ info_text = QTextEdit()
+ info_text.setReadOnly(True)
+ info_text.setMaximumHeight(60)
+ info_text.setPlainText("• 普通渲染:性能最优,适合低配置\n• RenderPipeline:高级图形效果(阴影、AO等),需要较高性能")
+ container_layout.addWidget(info_text)
+ dialog.render_mode_button_group = render_mode_button_group
+
+ # 载入配置
+ vr_config = {}
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager and self.world.vr_manager.config_manager:
+ vr_config = self.world.vr_manager.config_manager.load_config()
+
+ # 渲染设置
+ add_section_title("渲染设置")
+ row_quality = QWidget()
+ row_quality_layout = QHBoxLayout(row_quality)
+ row_quality_layout.setContentsMargins(0,0,0,0)
+ row_quality_layout.setSpacing(10)
+ quality_label = QLabel("渲染质量:")
+ quality_label.setProperty('role','fieldLabel')
+ quality_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
+ row_quality_layout.addWidget(quality_label)
+ quality_combo = QComboBox()
+ quality_combo.addItems(["低","中","高"])
+ quality_preset = vr_config.get("quality_preset", "balanced")
+ quality_map={"performance":"低","balanced":"中","quality":"高"}
+ quality_combo.setCurrentText(quality_map.get(quality_preset,"中"))
+ row_quality_layout.addWidget(quality_combo,1)
+ container_layout.addWidget(row_quality)
+ row_aa = QWidget(); row_aa_layout = QHBoxLayout(row_aa)
+ row_aa_layout.setContentsMargins(0,0,0,0)
+ row_aa_layout.setSpacing(10)
+ aa_label = QLabel("抗锯齿:")
+ aa_label.setProperty('role','fieldLabel')
+ aa_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
+ row_aa_layout.addWidget(aa_label)
+ aa_combo = QComboBox()
+ aa_combo.addItems(["无","2x","4x","8x"])
+ aa_combo.setCurrentText(vr_config.get("anti_aliasing","4x"))
+ row_aa_layout.addWidget(aa_combo,1)
+ container_layout.addWidget(row_aa)
+
+ # 性能设置
+ add_section_title("性能设置")
+ row_refresh = QWidget()
+ row_refresh_layout = QHBoxLayout(row_refresh)
+ row_refresh_layout.setContentsMargins(0,0,0,0)
+ row_refresh_layout.setSpacing(10)
+ refresh_label = QLabel("刷新率:")
+ refresh_label.setProperty('role','fieldLabel')
+ refresh_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
+ row_refresh_layout.addWidget(refresh_label)
+ refresh_combo = QComboBox()
+ refresh_combo.addItems(["72Hz","90Hz","120Hz","144Hz"])
+ refresh_combo.setCurrentText(vr_config.get("refresh_rate","90Hz"))
+ row_refresh_layout.addWidget(refresh_combo,1)
+ container_layout.addWidget(row_refresh)
+ row_async = QWidget()
+ row_async_layout = QHBoxLayout(row_async)
+ row_async_layout.setContentsMargins(0,0,0,0)
+ row_async_layout.setSpacing(10)
+ async_check = QCheckBox("启用异步重投影")
+ async_check.setChecked(vr_config.get("async_reprojection",True))
+ row_async_layout.addWidget(async_check)
+ row_async_layout.addStretch()
+ container_layout.addWidget(row_async)
+
+ # Save refs
+ dialog.quality_combo = quality_combo
+ dialog.aa_combo = aa_combo
+ dialog.refresh_combo = refresh_combo
+ dialog.async_check = async_check
+
+ # Separator before buttons
+ separator_buttons = QFrame()
+ separator_buttons.setFrameShape(QFrame.HLine)
+ separator_buttons.setFrameShadow(QFrame.Plain)
+ separator_buttons.setFixedHeight(1)
+ separator_buttons.setStyleSheet("background-color: #2C2F36; border: none;")
+ container_layout.addWidget(separator_buttons)
+
+ # Buttons
+ button_layout = QHBoxLayout()
+ apply_button = QPushButton("应用")
+ ok_button = QPushButton("确定")
+ # ok_button.setObjectName("primaryButton")
+ cancel_button = QPushButton("取消")
+ button_layout.addWidget(apply_button)
+ button_layout.addStretch()
+ button_layout.addWidget(ok_button)
+ button_layout.addWidget(cancel_button)
+ container_layout.addLayout(button_layout)
+
+ content_layout.addWidget(content_container, 0, Qt.AlignTop)
+ base_layout.addWidget(content_widget)
+ main_layout.addWidget(base_frame)
+
+ # signals
+ apply_button.clicked.connect(lambda: self.applyVRSettings(dialog))
+ ok_button.clicked.connect(lambda: self.onVRSettingsOK(dialog))
+ cancel_button.clicked.connect(dialog.reject)
+
+ return dialog
+
+ def onVRSettingsOK(self, dialog):
+ """确定按钮 - 应用设置并关闭对话框"""
+ # 先应用设置
+ self.applyVRSettings(dialog)
+ # 关闭对话框
+ dialog.accept()
+
+ def applyVRSettings(self, dialog):
+ """应用VR设置"""
+ try:
+ if not hasattr(self.world, 'vr_manager') or not self.world.vr_manager:
+ UniversalMessageDialog.show_warning(dialog, "错误", "VR管理器不可用!",False,"确定")
+ return
+
+ if not self.world.vr_manager.config_manager:
+ UniversalMessageDialog.show_warning(dialog, "错误", "VR配置管理器不可用!",False,"确定")
+ return
+
+ # 1️⃣ 读取所有UI控件的值
+ # 渲染模式
+ selected_mode_id = dialog.render_mode_button_group.checkedId()
+ new_mode = "render_pipeline" if selected_mode_id == 1 else "normal"
+ mode_name = "RenderPipeline高级渲染" if selected_mode_id == 1 else "普通渲染"
+
+ # 渲染质量
+ quality_text = dialog.quality_combo.currentText()
+ quality_map_reverse = {"低": "performance", "中": "balanced", "高": "quality"}
+ quality_preset = quality_map_reverse.get(quality_text, "balanced")
+
+ # 其他设置
+ anti_aliasing = dialog.aa_combo.currentText()
+ refresh_rate = dialog.refresh_combo.currentText()
+ async_reprojection = dialog.async_check.isChecked()
+
+ # 2️⃣ 加载当前配置
+ config = self.world.vr_manager.config_manager.load_config()
+
+ # 3️⃣ 更新配置
+ config["quality_preset"] = quality_preset
+ config["anti_aliasing"] = anti_aliasing
+ config["refresh_rate"] = refresh_rate
+ config["async_reprojection"] = async_reprojection
+
+ # 4️⃣ 检查渲染模式是否改变
+ from core.vr import VRRenderMode
+ current_mode = self.world.vr_manager.get_vr_render_mode()
+ mode_changed = (current_mode.value != new_mode)
+
+ # 5️⃣ 如果渲染模式改变,询问用户确认
+ if mode_changed:
+ reply = UniversalMessageDialog.show_info(dialog, "确认切换", f"确定要切换到{mode_name}模式吗?\n\n注意:切换渲染模式将重新创建VR缓冲区,可能需要几秒钟。",True,"取消","确定")
+
+ if reply == QDialog.Rejected:
+ # 用户取消渲染模式切换,但仍然保存其他设置
+ self.world.vr_manager.config_manager.save_config(config)
+ UniversalMessageDialog.show_info(dialog, "提示", "已保存其他设置(未切换渲染模式)",
+ False, "确定")
+ return
+
+ # 应用渲染模式切换
+ success = self.world.vr_manager.set_vr_render_mode(new_mode)
+
+ if not success:
+ UniversalMessageDialog.show_warning(dialog, "失败", f"切换到{mode_name}模式失败!\n请查看控制台输出了解详情。",
+ False, "确定")
+ return
+
+ # 6️⃣ 保存配置(如果模式改变,set_vr_render_mode已经保存了,但我们需要确保其他设置也被保存)
+ self.world.vr_manager.config_manager.save_config(config)
+
+ # 7️⃣ 应用质量预设到VR管理器
+ if hasattr(self.world.vr_manager, 'current_quality_preset'):
+ self.world.vr_manager.current_quality_preset = quality_preset
+
+ # 8️⃣ 显示成功消息
+ if mode_changed:
+ UniversalMessageDialog.show_info(dialog, "成功", f"VR设置已应用!\n• 渲染模式: {mode_name}\n• 渲染质量: {quality_text}\n配置已自动保存。",
+ False, "确定")
+ else:
+ UniversalMessageDialog.show_info(dialog, "成功", f"VR设置已保存!\n• 渲染质量: {quality_text}",
+ False, "确定")
+
+ except Exception as e:
+ UniversalMessageDialog.show_critical(dialog, "错误", f"应用VR设置时发生错误:\n{str(e)}",
+ False, "确定")
+ import traceback
+ traceback.print_exc()
+
+ # ==================== VR调试事件处理 ====================
+
+ def onToggleVRDebug(self):
+ """切换VR调试输出"""
+ try:
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ enabled = self.world.vr_manager.toggle_debug_output()
+ self.vrDebugToggleAction.setChecked(enabled)
+
+ status = "启用" if enabled else "禁用"
+ UniversalMessageDialog.show_info(self, "提示", f"已{status}VR调试输出",
+ False, "确定")
+ else:
+ UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!",
+ False, "确定")
+ except Exception as e:
+ UniversalMessageDialog.show_error(self, "错误", f"切换VR调试时发生错误:\n{str(e)}",
+ False, "确定")
+
+ def onShowVRPerformance(self):
+ """立即显示VR性能报告"""
+ try:
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ if self.world.vr_manager.vr_enabled:
+ self.world.vr_manager.force_performance_report()
+ UniversalMessageDialog.show_info(self, "提示", "已立即显示VR性能报告",
+ False, "确定")
+ else:
+ UniversalMessageDialog.show_warning(self, "提示", "请先启用VR模式",
+ False, "确定")
+ else:
+ UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!",
+ False, "确定")
+ except Exception as e:
+ UniversalMessageDialog.show_error(self, "错误", f"显示VR性能报告时发生错误:\n{str(e)}",
+ False, "确定")
+
+ def onSetVRDebugMode(self, mode):
+ """设置VR调试模式"""
+ try:
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ self.world.vr_manager.set_debug_mode(mode)
+
+ # 更新菜单状态
+ if mode == 'brief':
+ self.vrDebugBriefAction.setChecked(True)
+ self.vrDebugDetailedAction.setChecked(False)
+ else:
+ self.vrDebugBriefAction.setChecked(False)
+ self.vrDebugDetailedAction.setChecked(True)
+
+ mode_name = "简短" if mode == 'brief' else "详细"
+ UniversalMessageDialog.show_info(self, "提示", f"已设置VR调试模式为:{mode_name}",
+ False, "确定")
+ else:
+ UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!",
+ False, "确定")
+ except Exception as e:
+ UniversalMessageDialog.show_error(self, "错误", f"设置VR调试模式时发生错误:\n{str(e)}",
+ False, "确定")
+
+ def onToggleVRPerformanceMonitor(self):
+ """切换VR性能监控"""
+ try:
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ enabled = self.vrPerformanceMonitorAction.isChecked()
+ if enabled:
+ self.world.vr_manager.enable_performance_monitoring()
+ else:
+ self.world.vr_manager.disable_performance_monitoring()
+
+ status = "启用" if enabled else "禁用"
+ UniversalMessageDialog.show_info(self, "提示", f"已{status}VR性能监控",
+ False, "确定")
+ else:
+ UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!",
+ False, "确定")
+ self.vrPerformanceMonitorAction.setChecked(False)
+ except Exception as e:
+ UniversalMessageDialog.show_error(self, "错误", f"切换VR性能监控时发生错误:\n{str(e)}",
+ False, "确定")
+
+ def onToggleVRGpuTiming(self):
+ """切换VR GPU时间监控"""
+ try:
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ enabled = self.vrGpuTimingAction.isChecked()
+ if enabled:
+ self.world.vr_manager.enable_gpu_timing_monitoring()
+ else:
+ self.world.vr_manager.disable_gpu_timing_monitoring()
+
+ status = "启用" if enabled else "禁用"
+ UniversalMessageDialog.show_info(self, "提示", f"已{status}VR GPU时间监控",
+ False, "确定")
+ else:
+ UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!",
+ False, "确定")
+ self.vrGpuTimingAction.setChecked(False)
+ except Exception as e:
+ UniversalMessageDialog.show_error(self, "错误", f"切换VR GPU时间监控时发生错误:\n{str(e)}",
+ False, "确定")
+
+ def onToggleVRPipelineMonitor(self):
+ """切换VR管线监控"""
+ try:
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ enabled = self.vrPipelineMonitorAction.isChecked()
+ self.world.vr_manager.enable_pipeline_monitoring = enabled
+ status = "启用" if enabled else "禁用"
+ print(f"✓ VR管线监控已{status}")
+ else:
+ UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!",
+ False, "确定")
+ self.vrPipelineMonitorAction.setChecked(False)
+ except Exception as e:
+ UniversalMessageDialog.show_error(self, "错误", f"切换VR管线监控时发生错误:\n{str(e)}",
+ False, "确定")
+
+ def onSetVRPoseStrategy(self, strategy):
+ """设置VR姿态策略"""
+ try:
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ success = self.world.vr_manager.set_pose_strategy(strategy)
+ if success:
+ strategy_names = {
+ 'render_callback': '渲染回调策略',
+ 'update_task': '更新任务策略'
+ }
+ UniversalMessageDialog.show_info(self, "提示", f"已设置VR姿态策略为:{strategy_names.get(strategy, strategy)}",
+ False, "确定")
+ else:
+ UniversalMessageDialog.show_warning(self, "错误", "设置VR姿态策略失败!",
+ False, "确定")
+ else:
+ UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!",
+ False, "确定")
+ except Exception as e:
+ UniversalMessageDialog.show_error(self, "错误", f"设置VR姿态策略时发生错误:\n{str(e)}",
+ False, "确定")
+
+ def onTestVRPipeline(self):
+ """测试VR管线监控功能"""
+ try:
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ self.world.vr_manager.test_pipeline_monitoring()
+ UniversalMessageDialog.show_info(self, "提示", "VR管线监控测试已开始,请稍等...",
+ False, "确定")
+ else:
+ UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!",
+ False, "确定")
+ except Exception as e:
+ UniversalMessageDialog.show_error(self, "错误", f"测试VR管线监控时发生错误:\n{str(e)}",
+ False, "确定")
+
+ def onShowVRDebugSettings(self):
+ """显示VR调试设置对话框"""
+ try:
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ dialog = self.createVRDebugSettingsDialog()
+ dialog.exec_()
+ else:
+ UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!",
+ False, "确定")
+ except Exception as e:
+ UniversalMessageDialog.show_error(self, "错误", f"打开VR调试设置时发生错误:\n{str(e)}",
+ False, "确定")
+
+ def createVRDebugSettingsDialog(self):
+ """创建VR调试设置对话框(统一为 NewProjectDialog 风格)"""
+ from PyQt5.QtWidgets import QCheckBox, QSlider
+ from PyQt5.QtCore import Qt
+
+ dialog = QDialog(self)
+ dialog.setWindowTitle("VR调试设置")
+ dialog.setModal(True)
+ dialog.resize(508, 460)
+ dialog.setObjectName("newProjectDialog")
+ dialog.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
+ dialog.setAttribute(Qt.WA_TranslucentBackground, True)
+
+ dialog.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 0 0; 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: 0 0 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; }
+ QLabel[role="fieldLabel"] { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 12px; font-weight: 400; letter-spacing: 0.6px; }
+ QLabel { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; letter-spacing: 0.55px; }
+ QComboBox, QSpinBox, QDoubleSpinBox { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; }
+ QCheckBox { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; }
+ 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; }
+ """)
+
+ main_layout = QVBoxLayout(dialog)
+ 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)
+
+ title_bar = QWidget()
+ title_bar.setObjectName('titleBar')
+ tb_layout = QHBoxLayout(title_bar)
+ tb_layout.setContentsMargins(8,0,8,0)
+ tb_layout.setSpacing(6)
+ control_buttons = QWidget()
+ control_buttons.setObjectName('controlButtons')
+ cb_layout = QHBoxLayout(control_buttons)
+ cb_layout.setContentsMargins(0,0,0,0)
+ cb_layout.setSpacing(0)
+ close_btn = QPushButton()
+ close_btn.setObjectName('closeButton')
+ try:
+ close_btn.setIcon(get_icon('close_icon', QSize(18, 18)))
+ close_btn.setIconSize(QSize(18, 18))
+ except Exception: pass
+ close_btn.clicked.connect(dialog.reject)
+ cb_layout.addWidget(close_btn)
+ left_placeholder = QWidget()
+ left_placeholder.setFixedWidth(control_buttons.sizeHint().width())
+ tb_layout.addWidget(left_placeholder)
+ title_label = QLabel("VR调试设置")
+ title_label.setObjectName('titleLabel')
+ title_label.setAlignment(Qt.AlignCenter)
+ tb_layout.addWidget(title_label,1)
+ tb_layout.addWidget(control_buttons)
+ base_layout.addWidget(title_bar)
+
+ dragging_state={'dragging':False,'pos':QPoint()}
+ def _tb_press(e):
+ if e.button()==Qt.LeftButton: dragging_state['dragging']=True; dragging_state['pos']=e.globalPos()-dialog.frameGeometry().topLeft(); e.accept()
+ else: e.ignore()
+ def _tb_move(e):
+ if e.buttons() & Qt.LeftButton and dragging_state['dragging']: dialog.move(e.globalPos()-dragging_state['pos']); e.accept()
+ else: e.ignore()
+ def _tb_release(e):
+ if e.button()==Qt.LeftButton: dragging_state['dragging']=False; e.accept()
+ else: e.ignore()
+ title_bar.mousePressEvent=_tb_press; title_bar.mouseMoveEvent=_tb_move; title_bar.mouseReleaseEvent=_tb_release
+
+ 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)
+ container_layout = QVBoxLayout(content_container)
+ container_layout.setContentsMargins(15,10,15,10)
+ container_layout.setSpacing(10)
+
+ # 获取当前设置
+ vr_manager = self.world.vr_manager
+ debug_status = vr_manager.get_debug_status()
+ perf_config = vr_manager.get_performance_monitoring_config()
+
+
+ # 左侧对齐辅助:统一字段列宽,并让小标题与控件左侧对齐
+ label_column_width = 110
+ def add_section_title(text):
+ title = QLabel(text)
+ title.setProperty('role','sectionTitle')
+ container_layout.addWidget(title)
+
+ # 调试状态
+ add_section_title("调试状态")
+ status_widget = QWidget()
+ status_layout = QVBoxLayout(status_widget)
+ status_layout.setContentsMargins(0,0,0,0)
+ status_layout.setSpacing(6)
+ debug_enabled_label = QLabel(f"调试输出: {'启用' if debug_status['debug_enabled'] else '禁用'}")
+ debug_enabled_label.setStyleSheet(f"color: {'#2dffc4' if debug_status['debug_enabled'] else 'red'};")
+ status_layout.addWidget(debug_enabled_label)
+ debug_mode_label = QLabel(f"调试模式: {debug_status['debug_mode']}")
+ status_layout.addWidget(debug_mode_label)
+ performance_label = QLabel(f"性能监控: {'启用' if debug_status['performance_monitoring'] else '禁用'}")
+ performance_label.setStyleSheet(f"color: {'#2dffc4' if debug_status['performance_monitoring'] else 'red'};")
+ status_layout.addWidget(performance_label)
+ container_layout.addWidget(status_widget)
+
+ # 报告设置
+ add_section_title("报告设置")
+ row_interval = QWidget()
+ row_interval_layout = QHBoxLayout(row_interval)
+ row_interval_layout.setContentsMargins(0,0,0,0)
+ row_interval_layout.setSpacing(10)
+ interval_text_label = QLabel("报告间隔:")
+ interval_text_label.setProperty('role','fieldLabel')
+ interval_text_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
+ row_interval_layout.addWidget(interval_text_label)
+ interval_slider = QSlider(Qt.Horizontal)
+ interval_slider.setMinimum(5)
+ interval_slider.setMaximum(120)
+ interval_slider.setValue(int(debug_status['report_interval_seconds']))
+ interval_slider.setTickPosition(QSlider.TicksBelow)
+ interval_slider.setTickInterval(15)
+ interval_label = QLabel(f"{int(debug_status['report_interval_seconds'])}秒")
+ interval_slider.valueChanged.connect(lambda v: interval_label.setText(f"{v}秒"))
+ inner = QHBoxLayout()
+ inner.setContentsMargins(0,0,0,0)
+ inner.setSpacing(6)
+ inner.addWidget(interval_slider)
+ inner.addWidget(interval_label)
+ row_interval_layout.addLayout(inner,1)
+ container_layout.addWidget(row_interval)
+
+ row_check = QWidget()
+ row_check_layout = QHBoxLayout(row_check)
+ row_check_layout.setContentsMargins(0,0,0,0)
+ row_check_layout.setSpacing(10)
+ check_label = QLabel("性能检查间隔:")
+ check_label.setProperty('role','fieldLabel')
+ check_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
+ row_check_layout.addWidget(check_label)
+ check_interval_combo = QComboBox()
+ check_interval_combo.addItems(["0.1秒","0.5秒","1.0秒","2.0秒"])
+ current_check_interval = perf_config['check_interval']
+ if current_check_interval == 0.1:
+ check_interval_combo.setCurrentIndex(0)
+ elif current_check_interval == 0.5:
+ check_interval_combo.setCurrentIndex(1)
+ elif current_check_interval == 1.0:
+ check_interval_combo.setCurrentIndex(2)
+ else:
+ check_interval_combo.setCurrentIndex(3)
+ row_check_layout.addWidget(check_interval_combo,1)
+ container_layout.addWidget(row_check)
+
+ row_hist = QWidget(); row_hist_layout = QHBoxLayout(row_hist)
+ row_hist_layout.setContentsMargins(0,0,0,0)
+ row_hist_layout.setSpacing(10)
+ hist_label = QLabel("帧时间历史:")
+ hist_label.setProperty('role','fieldLabel')
+ hist_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
+ row_hist_layout.addWidget(hist_label)
+ frame_history_spin = QSpinBox()
+ frame_history_spin.setMinimum(10)
+ frame_history_spin.setMaximum(1000)
+ frame_history_spin.setValue(perf_config['frame_history_size'])
+ frame_history_spin.setSuffix(" 帧")
+ row_hist_layout.addWidget(frame_history_spin,1)
+ container_layout.addWidget(row_hist)
+
+ # 监控项目
+ add_section_title("监控项目")
+ monitor_widget = QWidget()
+ monitor_layout = QVBoxLayout(monitor_widget)
+ monitor_layout.setContentsMargins(0,0,0,0)
+ monitor_layout.setSpacing(6)
+ cpu_check = QCheckBox("CPU使用率")
+ cpu_check.setChecked(perf_config['psutil_available'])
+ cpu_check.setEnabled(perf_config['psutil_available'])
+ monitor_layout.addWidget(cpu_check)
+ memory_check = QCheckBox("内存使用率")
+ memory_check.setChecked(perf_config['psutil_available'])
+ memory_check.setEnabled(perf_config['psutil_available'])
+ monitor_layout.addWidget(memory_check)
+ gpu_check = QCheckBox("GPU使用率")
+ gpu_check.setChecked(perf_config['gputil_available'] or perf_config['nvidia_ml_available'])
+ gpu_check.setEnabled(perf_config['gputil_available'] or perf_config['nvidia_ml_available'])
+ monitor_layout.addWidget(gpu_check)
+ frame_time_check = QCheckBox("帧时间统计")
+ frame_time_check.setChecked(True)
+ monitor_layout.addWidget(frame_time_check)
+ container_layout.addWidget(monitor_widget)
+
+ separator_buttons = QFrame()
+ separator_buttons.setFrameShape(QFrame.HLine)
+ separator_buttons.setFrameShadow(QFrame.Plain)
+ separator_buttons.setFixedHeight(1)
+ separator_buttons.setStyleSheet("background-color: #2C2F36; border: none;")
+ container_layout.addWidget(separator_buttons)
+ button_layout = QHBoxLayout()
+ apply_button = QPushButton("应用")
+ reset_button = QPushButton("重置计数器")
+ ok_button = QPushButton("确定")
+ # ok_button.setObjectName("primaryButton")
+ cancel_button = QPushButton("取消")
+ button_layout.addWidget(apply_button)
+ button_layout.addWidget(reset_button)
+ button_layout.addStretch()
+ button_layout.addWidget(ok_button)
+ button_layout.addWidget(cancel_button)
+ container_layout.addLayout(button_layout)
+
+ content_layout.addWidget(content_container, 0, Qt.AlignTop)
+ base_layout.addWidget(content_widget); main_layout.addWidget(base_frame)
+
+ def apply_settings():
+ try:
+ new_interval_seconds = interval_slider.value()
+ new_interval_frames = int(new_interval_seconds * 60)
+ vr_manager.set_performance_report_interval(new_interval_frames)
+ check_intervals = [0.1, 0.5, 1.0, 2.0]
+ new_check_interval = check_intervals[check_interval_combo.currentIndex()]
+ vr_manager.set_performance_check_interval(new_check_interval)
+ vr_manager.set_frame_time_history_size(frame_history_spin.value())
+ UniversalMessageDialog.show_success(dialog, "成功", "VR调试设置已应用!", False, "确定")
+ except Exception as e:
+ UniversalMessageDialog.show_error(dialog, "错误", f"应用设置时发生错误:\n{str(e)}", False, "确定")
+ def reset_counters():
+ try:
+ vr_manager.reset_performance_counters()
+ UniversalMessageDialog.show_success(dialog, "成功", "性能计数器已重置!", False, "确定")
+ except Exception as e:
+ UniversalMessageDialog.show_error(dialog, "错误", f"重置计数器时发生错误:\n{str(e)}", False, "确定")
+
+ apply_button.clicked.connect(apply_settings)
+ reset_button.clicked.connect(reset_counters)
+ ok_button.clicked.connect(lambda: (apply_settings(), dialog.accept()))
+ cancel_button.clicked.connect(dialog.reject)
+ return dialog
+
+ # ==================== VR测试模式事件处理 ====================
+
+ def onToggleVRTestMode(self):
+ """切换VR测试模式"""
+ try:
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ if self.vrTestModeAction.isChecked():
+ # 启用VR测试模式
+ success = self.world.vr_manager.enable_vr_test_mode(display_mode='stereo')
+ if success:
+ UniversalMessageDialog.show_info(self, "VR测试模式",
+ "VR测试模式已启用!\n\n现在VR渲染内容将直接显示在PC屏幕上,无需VR头显。\n\n特点:\n- 显示VR左右眼视图\n- 实时性能监控HUD\n- 复用完整VR渲染管线\n- 可测量纯渲染性能",
+ False, "确定")
+ print("✅ VR测试模式已启用")
+
+ # 可选:自动开启性能测试
+ self.world.vr_manager.run_vr_performance_test(duration_seconds=10)
+ else:
+ self.vrTestModeAction.setChecked(False)
+ UniversalMessageDialog.show_warning(self, "错误", "启用VR测试模式失败!",
+ False, "确定")
+ else:
+ # 禁用VR测试模式
+ self.world.vr_manager.disable_vr_test_mode()
+ UniversalMessageDialog.show_info(self, "VR测试模式", "VR测试模式已禁用!",
+ False, "确定")
+ print("✅ VR测试模式已禁用")
+ else:
+ self.vrTestModeAction.setChecked(False)
+ UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!",
+ False, "确定")
+ except Exception as e:
+ self.vrTestModeAction.setChecked(False)
+ UniversalMessageDialog.show_error(self, "错误", f"切换VR测试模式时发生错误:\n{str(e)}",
+ False, "确定")
+
+ def onToggleVRTestSubmitTexture(self):
+ """切换VR测试模式纹理提交功能"""
+ try:
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ enabled = self.vrTestSubmitTextureAction.isChecked()
+ self.world.vr_manager.set_test_mode_features(submit_texture=enabled)
+ except Exception as e:
+ UniversalMessageDialog.show_error(self, "错误", f"设置纹理提交功能时发生错误:\n{str(e)}",
+ False, "确定")
+
+ def onToggleVRTestWaitPoses(self):
+ """切换VR测试模式姿态等待功能"""
+ try:
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ enabled = self.vrTestWaitPosesAction.isChecked()
+ self.world.vr_manager.set_test_mode_features(wait_poses=enabled)
+ except Exception as e:
+ UniversalMessageDialog.show_error(self, "错误", f"设置姿态等待功能时发生错误:\n{str(e)}",
+ False, "确定")
+
+ def onSetVRTestStep(self, step):
+ """设置VR测试步骤"""
+ try:
+ if hasattr(self.world, 'vr_manager') and self.world.vr_manager:
+ if step == 0: # 重置
+ self.world.vr_manager.set_test_mode_features(submit_texture=False, wait_poses=False)
+ self.vrTestSubmitTextureAction.setChecked(False)
+ self.vrTestWaitPosesAction.setChecked(False)
+ UniversalMessageDialog.show_info(self, "VR测试", "已重置为基线状态:两个功能都禁用",
+ False, "确定")
+ elif step == 1: # 只启用纹理提交
+ self.world.vr_manager.set_test_mode_features(submit_texture=True, wait_poses=False)
+ self.vrTestSubmitTextureAction.setChecked(True)
+ self.vrTestWaitPosesAction.setChecked(False)
+ UniversalMessageDialog.show_info(self, "VR测试", "步骤1:只启用纹理提交\n观察FPS变化来判断submit_texture是否影响性能",
+ False, "确定")
+ elif step == 2: # 只启用姿态等待
+ self.world.vr_manager.set_test_mode_features(submit_texture=False, wait_poses=True)
+ self.vrTestSubmitTextureAction.setChecked(False)
+ self.vrTestWaitPosesAction.setChecked(True)
+ UniversalMessageDialog.show_info(self, "VR测试", "步骤2:只启用姿态等待\n观察FPS变化来判断waitGetPoses是否影响性能",
+ False, "确定")
+ elif step == 3: # 同时启用两者
+ self.world.vr_manager.set_test_mode_features(submit_texture=True, wait_poses=True)
+ self.vrTestSubmitTextureAction.setChecked(True)
+ self.vrTestWaitPosesAction.setChecked(True)
+ UniversalMessageDialog.show_info(self, "VR测试", "步骤3:同时启用两者\n这应该完全复现普通VR模式的36FPS问题",
+ False, "确定")
+ except Exception as e:
+ UniversalMessageDialog.show_error(self, "错误", f"设置VR测试步骤时发生错误:\n{str(e)}",
+ False, "确定")
+
+
+class PluginConfigDialog(QDialog):
+ def __init__(self,parent=None):
+ super().__init__(parent)
+ self.setWindowTitle("渲染管线插件配置")
+ self.setGeometry(200,200,400,500)
+ self.plugins_config_path = os.path.join("RenderPipelineFile", "config", "plugins.yaml")
+ self.init_ui()
+ self.load_plugins_config()
+
+ def init_ui(self):
+ layout = QVBoxLayout()
+
+ # 标题
+ title_label = QLabel("启用的插件:")
+ layout.addWidget(title_label)
+
+ # 插件列表
+ self.plugins_list = QListWidget()
+ layout.addWidget(self.plugins_list)
+
+ # 按钮布局
+ button_layout = QHBoxLayout()
+
+ # 全选/全不选按钮
+ self.select_all_btn = QPushButton("全选")
+ self.select_all_btn.clicked.connect(self.select_all_plugins)
+ button_layout.addWidget(self.select_all_btn)
+
+ self.deselect_all_btn = QPushButton("全不选")
+ self.deselect_all_btn.clicked.connect(self.deselect_all_plugins)
+ button_layout.addWidget(self.deselect_all_btn)
+
+ button_layout.addStretch()
+
+ # 对话框按钮
+ self.button_box = QDialogButtonBox(
+ QDialogButtonBox.Ok | QDialogButtonBox.Cancel | QDialogButtonBox.Apply
+ )
+ self.button_box.accepted.connect(self.accept)
+ self.button_box.rejected.connect(self.reject)
+ self.button_box.button(QDialogButtonBox.Apply).clicked.connect(self.apply_changes)
+
+ button_layout.addWidget(self.button_box)
+ layout.addLayout(button_layout)
+
+ self.setLayout(layout)
+
+ def load_plugins_config(self):
+ """加载插件配置"""
+ try:
+ # 检查文件是否存在
+ if not os.path.exists(self.plugins_config_path):
+ print(f"插件配置文件不存在: {self.plugins_config_path}")
+ return
+
+ with open(self.plugins_config_path, 'r', encoding='utf-8') as f:
+ config = yaml.safe_load(f)
+
+ enabled_plugins = config.get('enabled', [])
+
+ # 清空列表
+ self.plugins_list.clear()
+
+ # 添加启用的插件
+ for plugin in enabled_plugins:
+ item = QListWidgetItem(plugin)
+ item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
+ item.setCheckState(Qt.Checked)
+ self.plugins_list.addItem(item)
+
+ except Exception as e:
+ print(f"加载插件配置失败: {e}")
+
+ def select_all_plugins(self):
+ """全选所有插件"""
+ for i in range(self.plugins_list.count()):
+ item = self.plugins_list.item(i)
+ item.setCheckState(Qt.Checked)
+
+ def deselect_all_plugins(self):
+ """取消选择所有插件"""
+ for i in range(self.plugins_list.count()):
+ item = self.plugins_list.item(i)
+ item.setCheckState(Qt.Unchecked)
+
+ def apply_changes(self):
+ """应用更改到配置文件,保留原有配置结构"""
+ try:
+ # 检查文件是否存在
+ if not os.path.exists(self.plugins_config_path):
+ print(f"插件配置文件不存在: {self.plugins_config_path}")
+ return
+
+ # 读取完整配置
+ with open(self.plugins_config_path, 'r', encoding='utf-8') as f:
+ config = yaml.safe_load(f) or {}
+
+ # 获取选中的插件
+ enabled_plugins = []
+ for i in range(self.plugins_list.count()):
+ item = self.plugins_list.item(i)
+ if item.checkState() == Qt.Checked:
+ enabled_plugins.append(item.text())
+
+ # 只更新enabled部分,保留其他配置
+ config['enabled'] = enabled_plugins
+
+ # 保存配置时保留原有格式(尽可能)
+ with open(self.plugins_config_path, 'w', encoding='utf-8') as f:
+ yaml.dump(config, f, default_flow_style=False, allow_unicode=True,
+ sort_keys=False, indent=4)
+
+ print("插件配置已保存")
+
+ except Exception as e:
+ print(f"保存插件配置失败: {e}")
+
+ def accept(self):
+ """点击确定按钮时保存并关闭"""
+ self.apply_changes()
+ super().accept()
+
+def setup_main_window(world,path = None):
+ """设置主窗口的便利函数"""
+ app = QApplication.instance()
+ if app is None:
+ app = QApplication(sys.argv)
+
+ main_window = MainWindow(world)
+ main_window.show()
+ from main import openProjectForPath
+ if path:
+ openProjectForPath(path,main_window)
+
+ return app, main_window
+
+class AssemblyModeSelectionDialog(QDialog):
+ """拆装模式选择对话框"""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.selected_mode = "training" # 默认选择训练模式
+ self.setupUI()
+
+ def setupUI(self):
+ self.setWindowTitle("选择拆装模式")
+ self.setFixedSize(400, 300)
+ self.setModal(True)
+
+ layout = QVBoxLayout(self)
+
+ # 标题
+ title_label = QLabel("请选择拆装交互模式")
+ title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #2E86C1; margin: 10px;")
+ title_label.setAlignment(Qt.AlignCenter)
+ layout.addWidget(title_label)
+
+ # 模式选择组
+ mode_group = QGroupBox("模式选择")
+ mode_layout = QVBoxLayout(mode_group)
+
+ # 训练模式
+ self.training_radio = QRadioButton("训练模式")
+ self.training_radio.setChecked(True) # 默认选中
+ self.training_radio.setStyleSheet("font-size: 14px; margin: 5px;")
+ mode_layout.addWidget(self.training_radio)
+
+ training_desc = QTextEdit()
+ training_desc.setMaximumHeight(60)
+ training_desc.setReadOnly(True)
+ training_desc.setPlainText("• 显示详细的步骤描述和操作提示\n• 提供工具选择的正确性提示\n• 播放语音指导")
+ training_desc.setStyleSheet("background-color: #f0f8ff; border: 1px solid #ccc; margin-left: 20px;")
+ mode_layout.addWidget(training_desc)
+
+ # 考核模式
+ self.exam_radio = QRadioButton("考核模式")
+ self.exam_radio.setStyleSheet("font-size: 14px; margin: 5px;")
+ mode_layout.addWidget(self.exam_radio)
+
+ exam_desc = QTextEdit()
+ exam_desc.setMaximumHeight(60)
+ exam_desc.setReadOnly(True)
+ exam_desc.setPlainText("• 不显示步骤描述\n• 工具选择错误时不提示,直接扣分\n• 不播放语音指导")
+ exam_desc.setStyleSheet("background-color: #fff5f5; border: 1px solid #ccc; margin-left: 20px;")
+ mode_layout.addWidget(exam_desc)
+
+ layout.addWidget(mode_group)
+
+ # 按钮
+ button_layout = QHBoxLayout()
+
+ self.ok_button = QPushButton("开始")
+ self.ok_button.setStyleSheet("""
+ QPushButton {
+ background-color: #27AE60;
+ color: white;
+ font-size: 14px;
+ font-weight: bold;
+ padding: 8px 20px;
+ border: none;
+ border-radius: 4px;
+ }
+ QPushButton:hover {
+ background-color: #2ECC71;
+ }
+ """)
+ self.ok_button.clicked.connect(self.accept)
+
+ self.cancel_button = QPushButton("取消")
+ self.cancel_button.setStyleSheet("""
+ QPushButton {
+ background-color: #95A5A6;
+ color: white;
+ font-size: 14px;
+ padding: 8px 20px;
+ border: none;
+ border-radius: 4px;
+ }
+ QPushButton:hover {
+ background-color: #BDC3C7;
+ }
+ """)
+ self.cancel_button.clicked.connect(self.reject)
+
+ button_layout.addStretch()
+ button_layout.addWidget(self.ok_button)
+ button_layout.addWidget(self.cancel_button)
+
+ layout.addLayout(button_layout)
+
+ # 连接单选按钮信号
+ self.training_radio.toggled.connect(self.on_mode_changed)
+ self.exam_radio.toggled.connect(self.on_mode_changed)
+
+ def on_mode_changed(self):
+ """模式改变时的处理"""
+ if self.training_radio.isChecked():
+ self.selected_mode = "training"
+ elif self.exam_radio.isChecked():
+ self.selected_mode = "exam"
+ print(f"🔄 模式选择改变: {self.selected_mode}")
+
+ def get_selected_mode(self):
+ """获取选中的模式"""
+ return self.selected_mode
+
+
+def setup_main_window(world,path = None):
+ """设置主窗口的便利函数"""
+ app = QApplication.instance()
+ if app is None:
+ # 修复 Windows 下 WebEngine 与 OpenGL 上下文共享问题
+ QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts)
+ if sys.platform == 'win32':
+ QCoreApplication.setAttribute(Qt.AA_UseDesktopOpenGL)
+
+ # 添加 Chromium 参数禁用硬件加速,避免与 Panda3D 的 OpenGL 冲突
+ sys.argv.extend([
+ '--disable-gpu',
+ '--disable-software-rasterizer',
+ '--disable-gpu-compositing',
+ '--disable-accelerated-2d-canvas',
+ '--num-raster-threads=1'
+ ])
+ app = QApplication(sys.argv)
+ print("✓ QApplication 已创建,QtWebEngine 硬件加速已禁用")
+
+ main_window = MainWindow(world)
+ main_window.show()
+ from main import openProjectForPath
+ if path:
+ openProjectForPath(path,main_window)
+
+ return app, main_window
diff --git a/ui/widgets.py b/ui/widgets.py
new file mode 100644
index 00000000..c667635b
--- /dev/null
+++ b/ui/widgets.py
@@ -0,0 +1,4319 @@
+"""
+自定义Qt部件模块
+
+包含所有自定义的Qt界面组件:
+- NewProjectDialog: 新建项目对话框
+- CustomPanda3DWidget: 自定义Panda3D显示部件
+- CustomFileView: 自定义文件浏览器
+- CustomTreeWidget: 自定义场景树部件
+"""
+
+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
+
+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;
+ }
+ """)
+
+ # 创建主布局
+ 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)
+
+ # 创建内容区域
+ 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)
+
+ 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)
+
+ 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)
+
+ 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)
+
+ 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)
+
+ 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
+
+
+