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 - - -