diff --git a/ui/main_window.py b/ui/main_window.py
deleted file mode 100644
index 0c64dff7..00000000
--- a/ui/main_window.py
+++ /dev/null
@@ -1,6724 +0,0 @@
-"""
-主窗口设置模块
-
-负责主窗口的界面构建和事件绑定:
-- 菜单栏、工具栏创建
-- 停靠窗口设置
-- 事件连接和信号处理
-"""
-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
deleted file mode 100644
index c667635b..00000000
--- a/ui/widgets.py
+++ /dev/null
@@ -1,4319 +0,0 @@
-"""
-自定义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
-
-
-