6725 lines
274 KiB
Python
6725 lines
274 KiB
Python
"""
|
||
主窗口设置模块
|
||
|
||
负责主窗口的界面构建和事件绑定:
|
||
- 菜单栏、工具栏创建
|
||
- 停靠窗口设置
|
||
- 事件连接和信号处理
|
||
"""
|
||
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
|