EG/ui/widgets.py
2025-10-27 14:59:34 +08:00

4310 lines
162 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
自定义Qt部件模块
包含所有自定义的Qt界面组件
- NewProjectDialog: 新建项目对话框
- CustomPanda3DWidget: 自定义Panda3D显示部件
- CustomFileView: 自定义文件浏览器
- CustomTreeWidget: 自定义场景树部件
"""
import os
import re
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout,
QLineEdit, QPushButton, QLabel,
QTreeView, QTreeWidget, QTreeWidgetItem, QWidget,
QFileDialog, QMessageBox, QAbstractItemView, QMenu, QDockWidget, QButtonGroup, QToolButton, QFrame, QSizePolicy)
from PyQt5.QtCore import Qt, QUrl, QMimeData, QPoint, QSize
from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush, QFont
from PyQt5.sip import wrapinstance
from direct.showbase.ShowBaseGlobal import aspect2d
from panda3d.core import ModelRoot, NodePath, CollisionNode
from QMeta3D.QMeta3DWidget import QMeta3DWidget
from scene import util
from ui.icon_manager import get_icon_manager
class NewProjectDialog(QDialog):
"""新建项目对话框"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("新建项目")
self.setObjectName("newProjectDialog")
self.resize(508, 274)
self.setMinimumSize(508, 274)
self.setModal(True)
# 移除默认窗口装饰,使用自定义顶部栏
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground, True)
# 拖拽和窗口状态
self.dragging = False
self.drag_position = QPoint()
self.icon_manager = get_icon_manager()
self._title_icon_size = QSize(18, 18)
self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size)
# 设置严格按照Figma设计的样式
self.setStyleSheet("""
QDialog#newProjectDialog {
background-color: transparent;
color: #EBEBEB;
border: none;
}
QFrame#baseFrame {
background-color: #000000;
border: 1px solid #3E3E42;
border-radius: 5px;
}
QWidget#titleBar {
background-color: transparent;
border: none;
border-radius: 5px 5px 0px 0px;
min-height: 32px;
max-height: 32px;
}
QWidget#titleBar QWidget {
background-color: transparent;
border: none;
}
QLabel#titleLabel {
color: #FFFFFF;
font-family: 'Inter', 'Microsoft YaHei', sans-serif;
font-size: 14px;
font-weight: 500;
letter-spacing: 0.7px;
}
QWidget#controlButtons QPushButton {
background-color: transparent;
border: none;
color: #EBEBEB;
font-size: 14px;
min-width: 18px;
max-width: 18px;
min-height: 18px;
max-height: 18px;
padding: 0px;
border-radius: 3px;
}
QWidget#controlButtons QPushButton:hover {
background-color: #2A2D2E;
color: #FFFFFF;
}
QPushButton#closeButton {
border-radius: 0px 5px 0px 0px;
}
QPushButton#closeButton:hover {
background-color: #2A2D2E;
color: #FFFFFF;
}
QWidget#contentWidget {
background-color: transparent;
border-radius: 0px 0px 5px 5px;
}
QFrame#contentContainer {
background-color: #19191B;
border: 1px solid #2C2F36;
border-radius: 5px;
}
QLabel[role="sectionTitle"] {
color: #EBEBEB;
font-family: 'Inter', 'Microsoft YaHei', sans-serif;
font-size: 12px;
font-weight: 500;
letter-spacing: 0.6px;
padding: 0px;
margin-bottom: 0px;
}
QLabel[role="hint"] {
color: rgba(235, 235, 235, 0.6);
font-family: 'Inter', 'Microsoft YaHei', sans-serif;
font-size: 11px;
font-weight: 300;
letter-spacing: 0.55px;
margin-top: 0px;
padding: 0px;
}
QLineEdit {
background-color: rgba(89, 100, 113, 0.2);
color: #EBEBEB;
border: 1px solid rgba(76, 92, 110, 0.6);
border-radius: 2px;
padding: 6px 10px;
font-family: 'Inter', 'Microsoft YaHei', sans-serif;
font-size: 11px;
font-weight: 300;
letter-spacing: 0.55px;
min-height: 14px;
max-height: 30px;
}
QLineEdit:focus {
border: 1px solid #3067C0;
background-color: rgba(48, 103, 192, 0.1);
}
QLineEdit:hover {
border: 1px solid #3067C0;
background-color: rgba(89, 100, 113, 0.3);
}
QLineEdit:disabled {
background-color: rgba(89, 100, 113, 0.1);
color: rgba(235, 235, 235, 0.4);
border: 1px solid rgba(76, 92, 110, 0.2);
}
QPushButton {
background-color: rgba(89, 98, 118, 0.5);
color: #EBEBEB;
border: none;
border-radius: 2px;
padding: 0px 0px;
font-family: 'Inter', 'Microsoft YaHei', sans-serif;
font-weight: 300;
font-size: 10px;
letter-spacing: 0.5px;
min-width: 90px;
min-height: 30px;
max-height: 30px;
}
QPushButton:hover {
background-color: #3067C0;
color: #FFFFFF;
}
QPushButton:pressed {
background-color: #2556A0;
color: #FFFFFF;
}
QPushButton:disabled {
background-color: rgba(89, 98, 118, 0.3);
color: rgba(235, 235, 235, 0.4);
}
QPushButton#primaryButton {
background-color: rgba(89, 98, 118, 0.5);
border: none;
color: #EBEBEB;
font-weight: 300;
min-width: 120px;
max-width: 120px;
}
QPushButton#primaryButton:hover {
background-color: #2556A0;
}
QPushButton#primaryButton:pressed {
background-color: #1E4A8C;
}
QPushButton#secondaryButton {
background-color: rgba(89, 98, 118, 0.5);
border: none;
color: #EBEBEB;
min-width: 120px;
max-width: 120px;
}
QPushButton#secondaryButton:hover {
background-color: #3067C0;
color: #FFFFFF;
}
QPushButton#secondaryButton:pressed {
background-color: #2556A0;
color: #FFFFFF;
}
QPushButton#browseButton {
min-width: 90px;
max-width: 90px;
min-height: 28px;
max-height: 28px;
}
""")
# 创建主布局
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
# 创建基础容器,负责绘制圆角和边框
base_frame = QFrame()
base_frame.setObjectName('baseFrame')
base_frame.setFrameShape(QFrame.NoFrame)
base_frame.setAttribute(Qt.WA_StyledBackground, True)
base_layout = QVBoxLayout(base_frame)
base_layout.setContentsMargins(0, 0, 0, 0)
base_layout.setSpacing(0)
# 创建自定义顶部栏
self.createTitleBar()
base_layout.addWidget(self.title_bar)
# 创建内容区域
content_widget = QWidget()
content_widget.setObjectName('contentWidget')
content_layout = QVBoxLayout(content_widget)
content_layout.setContentsMargins(10, 10, 10, 10)
content_layout.setSpacing(0)
content_container = QFrame()
content_container.setObjectName('contentContainer')
content_container.setFrameShape(QFrame.NoFrame)
content_container.setAttribute(Qt.WA_StyledBackground, True)
content_container.setFixedWidth(488)
content_container.setMinimumHeight(213)
container_layout = QVBoxLayout(content_container)
container_layout.setContentsMargins(15, 10, 15, 10)
container_layout.setSpacing(10)
pathLabel = QLabel('项目路径')
pathLabel.setProperty('role', 'sectionTitle')
container_layout.addWidget(pathLabel)
path_row_widget = QWidget()
path_row_layout = QHBoxLayout(path_row_widget)
path_row_layout.setContentsMargins(0, 0, 0, 0)
path_row_layout.setSpacing(10)
path_row_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.pathEdit = QLineEdit()
self.pathEdit.setReadOnly(True)
self.pathEdit.setPlaceholderText('请选择项目路径')
self.pathEdit.setMinimumWidth(358)
self.pathEdit.setFixedHeight(30)
path_row_layout.addWidget(self.pathEdit)
browseButton = QPushButton('浏览...')
browseButton.setObjectName('browseButton')
browseButton.clicked.connect(self.browsePath)
path_row_layout.addWidget(browseButton)
container_layout.addWidget(path_row_widget)
nameLabel = QLabel('项目名称')
nameLabel.setProperty('role', 'sectionTitle')
container_layout.addWidget(nameLabel)
self.nameEdit = QLineEdit()
self.nameEdit.setText('新项目')
self.nameEdit.setPlaceholderText('请输入项目名称')
self.nameEdit.selectAll()
self.nameEdit.setFixedHeight(30)
container_layout.addWidget(self.nameEdit)
self.tipLabel = QLabel('项目名称只能包含字母、数字、下划线、中划线和中文')
self.tipLabel.setProperty('role', 'hint')
container_layout.addWidget(self.tipLabel)
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setFrameShadow(QFrame.Plain)
separator.setFixedHeight(1)
separator.setStyleSheet('background-color: #2C2F36; border: none;')
container_layout.addWidget(separator)
button_row_widget = QWidget()
button_row_layout = QHBoxLayout(button_row_widget)
button_row_layout.setContentsMargins(0, 0, 0, 0)
button_row_layout.setSpacing(10)
button_row_layout.addStretch()
button_row_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.cancelButton = QPushButton('取消')
self.cancelButton.setObjectName('secondaryButton')
self.cancelButton.clicked.connect(self.reject)
self.cancelButton.setFixedSize(120, 30)
button_row_layout.addWidget(self.cancelButton)
self.confirmButton = QPushButton('确认')
self.confirmButton.setObjectName('primaryButton')
self.confirmButton.clicked.connect(self.validate)
self.confirmButton.setFixedSize(120, 30)
button_row_layout.addWidget(self.confirmButton)
container_layout.addWidget(button_row_widget)
self.confirmButton.setDefault(True)
self.confirmButton.setAutoDefault(True)
self.cancelButton.setAutoDefault(False)
content_layout.addWidget(content_container, 0, Qt.AlignTop)
base_layout.addWidget(content_widget)
main_layout.addWidget(base_frame)
self.projectPath = ""
self.projectName = ""
def createTitleBar(self):
"""创建自定义顶部栏"""
self.title_bar = QFrame()
self.title_bar.setObjectName("titleBar")
title_layout = QHBoxLayout(self.title_bar)
title_layout.setContentsMargins(12, 0, 12, 0)
title_layout.setSpacing(0)
# Control buttons area
controls = QWidget()
controls.setObjectName("controlButtons")
controls_layout = QHBoxLayout(controls)
controls_layout.setContentsMargins(0, 0, 0, 0)
controls_layout.setSpacing(0)
self.close_button = QPushButton()
self.close_button.setObjectName("closeButton")
self.close_button.clicked.connect(self.reject)
self.close_button.setFocusPolicy(Qt.NoFocus)
controls_layout.addWidget(self.close_button)
self._applyTitleBarIcons()
# Reserve left space equal to control buttons to keep title centered
left_placeholder = QWidget()
left_placeholder.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
title_layout.addWidget(left_placeholder)
self.title_label = QLabel(self.windowTitle())
self.title_label.setObjectName("titleLabel")
self.title_label.setAlignment(Qt.AlignCenter)
title_layout.addWidget(self.title_label, 1)
title_layout.addWidget(controls)
left_placeholder.setFixedWidth(controls.sizeHint().width())
def _applyTitleBarIcons(self):
"""为标题栏按钮应用图标"""
if self._icon_close:
self.close_button.setIcon(self._icon_close)
self.close_button.setIconSize(self._title_icon_size)
self.close_button.setText("")
self.close_button.setToolTip("关闭")
def toggleMaximize(self):
"""切换窗口最大化/还原"""
if self.isMaximized():
self.showNormal()
else:
self.showMaximized()
def setWindowTitle(self, title):
super().setWindowTitle(title)
if hasattr(self, "title_label"):
self.title_label.setText(title)
def mousePressEvent(self, event):
"""鼠标按下事件 - 用于拖拽窗口"""
if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()):
if self.isMaximized():
self.showNormal()
self.dragging = True
self.drag_position = event.globalPos() - self.frameGeometry().topLeft()
event.accept()
super().mousePressEvent(event)
def mouseDoubleClickEvent(self, event):
"""双击标题栏切换最大化"""
if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()):
self.toggleMaximize()
event.accept()
return
super().mouseDoubleClickEvent(event)
def mouseMoveEvent(self, event):
"""鼠标移动事件 - 用于拖拽窗口"""
if event.buttons() == Qt.LeftButton and self.dragging:
self.move(event.globalPos() - self.drag_position)
event.accept()
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
"""鼠标释放事件 - 停止拖拽"""
if event.button() == Qt.LeftButton:
self.dragging = False
super().mouseReleaseEvent(event)
def browsePath(self):
"""浏览选择项目路径"""
path = QFileDialog.getExistingDirectory(self, "选择项目路径")
if path:
self.pathEdit.setText(path)
def validate(self):
"""验证输入并关闭对话框"""
# 获取并验证路径
self.projectPath = self.pathEdit.text()
if not self.projectPath:
UniversalMessageDialog.show_warning(self, "错误", "请选择项目路径!", show_cancel=False, confirm_text="确认")
return
# 获取并验证项目名称
self.projectName = self.nameEdit.text().strip()
if not self.projectName:
UniversalMessageDialog.show_warning(self, "错误", "请输入项目名称!", show_cancel=False, confirm_text="确认")
return
# 验证项目名称格式
if not re.match(r'^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$', self.projectName):
UniversalMessageDialog.show_warning(self, "错误",
"项目名称只能包含字母、数字、下划线、中划线和中文!", show_cancel=False, confirm_text="确认")
return
# 检查项目是否已存在
full_path = os.path.join(self.projectPath, self.projectName)
if os.path.exists(full_path):
UniversalMessageDialog.show_warning(self, "错误", "项目已存在!", show_cancel=False, confirm_text="确认")
return
self.accept()
class CustomMeta3DWidget(QMeta3DWidget):
"""自定义Panda3D显示部件"""
def __init__(self, world, parent=None):
if parent is None:
parent = wrapinstance(0, QWidget)
super().__init__(world, parent)
self.world = world
self.setFocusPolicy(Qt.StrongFocus) # 允许接收键盘焦点
self.setAcceptDrops(True) # 允许接收拖放
self.setMouseTracking(True) # 启用鼠标追踪
# 让world引用这个widget以便获取准确的尺寸
if hasattr(world, 'setQtWidget'):
world.setQtWidget(self)
def getActualSize(self):
"""获取Qt部件的实际渲染尺寸"""
return (self.width(), self.height())
def dragEnterEvent(self, event):
"""处理拖拽进入事件"""
# 检查是否是文件拖拽
if event.mimeData().hasUrls():
# 检查是否包含支持的模型文件
for url in event.mimeData().urls():
filepath = url.toLocalFile()
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
event.acceptProposedAction()
return
event.ignore()
def dragMoveEvent(self, event):
"""处理拖拽移动事件"""
if event.mimeData().hasUrls():
event.acceptProposedAction()
else:
event.ignore()
def dropEvent(self, event):
"""处理拖放事件"""
if event.mimeData().hasUrls():
for url in event.mimeData().urls():
filepath = url.toLocalFile()
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
# 使用关键字参数确保兼容性
self.world.importModel(filepath)
event.acceptProposedAction()
else:
event.ignore()
def wheelEvent(self, event):
"""处理滚轮事件"""
if event.angleDelta().y() > 0:
# 滚轮向前滚动
self.world.wheelForward()
else:
# 滚轮向后滚动
self.world.wheelBackward()
event.accept()
def mousePressEvent(self, event):
"""处理 Qt 鼠标按下事件"""
if event.button() == Qt.LeftButton:
self.world.mousePressEventLeft({
'x': event.x(),
'y': event.y()
})
elif event.button() == Qt.RightButton:
self.world.mousePressEventRight({
'x': event.x(),
'y': event.y()
})
elif event.button() == Qt.MiddleButton: # 添加滑轮按下事件处理
self.world.mousePressEventMiddle({
'x': event.x(),
'y': event.y()
})
event.accept()
def mouseReleaseEvent(self, event):
"""处理 Qt 鼠标释放事件"""
if event.button() == Qt.LeftButton:
self.world.mouseReleaseEventLeft({
'x': event.x(),
'y': event.y()
})
elif event.button() == Qt.RightButton:
self.world.mouseReleaseEventRight({
'x': event.x(),
'y': event.y()
})
elif event.button() == Qt.MiddleButton: # 添加滑轮释放事件处理
self.world.mouseReleaseEventMiddle({
'x': event.x(),
'y': event.y()
})
event.accept()
def mouseMoveEvent(self, event):
"""处理 Qt 鼠标移动事件"""
self.world.mouseMoveEvent({
'x': event.x(),
'y': event.y()
})
event.accept()
def cleanup(self):
"""清理Panda3D资源"""
try:
print("🧹 清理CustomPanda3DWidget资源...")
# 清理world资源
if hasattr(self, 'world') and self.world:
# 如果world有cleanup方法调用它
if hasattr(self.world, 'cleanup'):
self.world.cleanup()
# 清理world引用
self.world = None
# 调用父类的清理方法(如果存在)
if hasattr(super(), 'cleanup'):
super().cleanup()
print("✅ CustomPanda3DWidget资源清理完成")
except Exception as e:
print(f"⚠️ 清理CustomPanda3DWidget资源时出错: {e}")
class CustomFileView(QTreeView):
"""自定义文件浏览器"""
def __init__(self, world, parent=None):
if parent is None:
parent = wrapinstance(0, QWidget)
super().__init__(parent)
base_font = self.font()
base_font.setFamily("Inter")
base_font.setPointSize(10)
base_font.setWeight(QFont.Normal)
self.setFont(base_font)
if self.model():
self.model().rowsInserted.connect(self._handle_rows_inserted)
self.world = world
self.setupUI()
self.setupDragDrop()
def setupUI(self):
"""初始化UI设置"""
self.setHeaderHidden(True)
# 启用多选和拖拽
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.setDropIndicatorShown(True) # 启用拖放指示线
def setupDragDrop(self):
"""设置拖拽功能"""
# 使用自定义拖拽模式
self.setDragDropMode(QTreeView.DragOnly) # 只允许拖出,不允许拖入
self.setDefaultDropAction(Qt.DropAction.MoveAction)
self.setDragEnabled(True)
self.setAcceptDrops(True)
def startDrag(self, supportedActions):
"""开始拖拽操作"""
# 获取选中的文件
indexes = self.selectedIndexes()
if not indexes:
return
# 只处理文件名列
indexes = [idx for idx in indexes if idx.column() == 0]
# 创建 MIME 数据
mimeData = self.model().mimeData(indexes)
# 检查是否包含支持的模型文件
urls = []
for index in indexes:
filepath = self.model().filePath(index)
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
urls.append(QUrl.fromLocalFile(filepath))
if not urls:
return
# 设置 URL 列表
mimeData.setUrls(urls)
# 创建拖拽对象
drag = QDrag(self)
drag.setMimeData(mimeData)
# 设置拖拽图标(可选)
pixmap = QPixmap(32, 32)
pixmap.fill(Qt.transparent)
painter = QPainter(pixmap)
painter.drawText(pixmap.rect(), Qt.AlignCenter, str(len(urls)))
painter.end()
drag.setPixmap(pixmap)
# 执行拖拽
drag.exec_(supportedActions)
def mouseDoubleClickEvent(self, event):
"""双击标题栏切换最大化"""
index = self.indexAt(event.pos())
if index.isValid():
model = self.model()
filepath = model.filePath(index)
# 检查是否是模型文件
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
self.world.importModel(filepath)
else:
print("不支持的文件类型")
super().mouseDoubleClickEvent(event)
from PyQt5.QtCore import QFileSystemWatcher,QTimer
import os
class CustomAssetsTreeWidget(QTreeWidget):
def __init__(self, world, parent=None):
if parent is None:
parent = wrapinstance(0, QWidget)
super().__init__(parent)
self.world = world
self.root_path = None
self.setupUI()
self.setupDragDrop()
#添加文件系统监控器
self.file_watcher = QFileSystemWatcher()
self.file_watcher.directoryChanged.connect(self.onDirectoryChanged)
self.file_watcher.fileChanged.connect(self.onFileChanged)
self.refresh_timer = QTimer()
self.refresh_timer.setSingleShot(True)
self.refresh_timer.timeout.connect(self.refreshView)
#存储监控的目录
self.watched_directories = set()
# 默认加载项目根路径
self.load_file_tree()
# 设置右键菜单
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self.showContextMenu)
def showContextMenu(self, position):
"""显示右键菜单"""
item = self.itemAt(position)
if not item:
return
filepath = item.data(0, Qt.UserRole)
is_folder = item.data(0, Qt.UserRole + 1)
menu = QMenu(self)
if is_folder:
# 文件夹右键菜单
menu.addAction("📁 新建文件夹", lambda: self.createNewFolder(item))
menu.addAction("📄 新建文件", lambda: self.createNewFile(item))
menu.addSeparator()
menu.addAction("✏️ 重命名", lambda: self.renameItem(item))
menu.addAction("🗑️ 删除", lambda: self.deleteItem(item))
menu.addSeparator()
menu.addAction("📋 复制路径", lambda: self.copyPath(filepath))
menu.addAction("🔍 查看属性", lambda: self.showProperties(item))
else:
# 文件右键菜单
menu.addAction("📂 打开文件", lambda: self.openFile(filepath))
menu.addSeparator()
menu.addAction("✏️ 重命名", lambda: self.renameItem(item))
menu.addAction("🗑️ 删除", lambda: self.deleteItem(item))
menu.addSeparator()
menu.addAction("📋 复制路径", lambda: self.copyPath(filepath))
menu.addAction("🔍 查看属性", lambda: self.showProperties(item))
# 显示菜单
menu.exec_(self.mapToGlobal(position))
def _saveExpandedState(self):
"""保存展开状态"""
expanded_paths = set()
def collectExpanded(item):
if item.isExpanded():
path = item.data(0, Qt.UserRole)
if path:
expanded_paths.add(path)
for i in range(item.childCount()):
collectExpanded(item.child(i))
# 遍历所有顶级项目
for i in range(self.topLevelItemCount()):
collectExpanded(self.topLevelItem(i))
return expanded_paths
def _restoreExpandedState(self, expanded_paths):
"""恢复展开状态"""
def restoreExpanded(item):
path = item.data(0, Qt.UserRole)
if path in expanded_paths:
item.setExpanded(True)
for i in range(item.childCount()):
restoreExpanded(item.child(i))
# 遍历所有顶级项目
for i in range(self.topLevelItemCount()):
restoreExpanded(self.topLevelItem(i))
def _refreshWithStatePreservation(self):
"""刷新树形视图并保持状态"""
# 保存当前状态
expanded_paths = self._saveExpandedState()
# 刷新树形结构
self.load_file_tree()
# 恢复展开状态
self._restoreExpandedState(expanded_paths)
def createNewFolder(self, parent_item):
"""新建文件夹"""
import os
parent_path = parent_item.data(0, Qt.UserRole)
dialog = StyledTextInputDialog(self, "新建文件夹", "文件夹名称", "请输入文件夹名称")
if dialog.exec_() != QDialog.Accepted:
return
folder_name = dialog.text()
if not folder_name:
return
new_folder_path = os.path.join(parent_path, folder_name)
try:
os.makedirs(new_folder_path, exist_ok=True)
self._refreshWithStatePreservation()
print(f"新建文件夹: {new_folder_path}")
except OSError as e:
print(f"新建文件夹失败: {e}")
def createNewFile(self, parent_item):
"""新建文件"""
import os
parent_path = parent_item.data(0, Qt.UserRole)
dialog = StyledTextInputDialog(self, "新建文件", "文件名称", "请输入文件名称")
if dialog.exec_() != QDialog.Accepted:
return
file_name = dialog.text()
if not file_name:
return
new_file_path = os.path.join(parent_path, file_name)
try:
with open(new_file_path, "w", encoding="utf-8") as f:
f.write("")
self._refreshWithStatePreservation()
print(f"新建文件: {new_file_path}")
except OSError as e:
print(f"新建文件失败: {e}")
def renameItem(self, item):
"""重命名文件或文件夹"""
import os
old_path = item.data(0, Qt.UserRole)
old_name = os.path.basename(old_path)
dialog = StyledTextInputDialog(self, "重命名", "新名称", "请输入新名称", old_name)
if dialog.exec_() != QDialog.Accepted:
return
new_name = dialog.text()
if not new_name or new_name == old_name:
return
parent_dir = os.path.dirname(old_path)
new_path = os.path.join(parent_dir, new_name)
try:
os.rename(old_path, new_path)
item.setText(0, new_name)
item.setData(0, Qt.UserRole, new_path)
self._refreshWithStatePreservation()
except OSError as e:
print(f"重命名失败: {e}")
def deleteItem(self, item):
"""删除文件或文件夹"""
import os
import shutil
filepath = item.data(0, Qt.UserRole)
is_folder = item.data(0, Qt.UserRole + 1)
item_type = "文件夹" if is_folder else "文件"
result = UniversalMessageDialog.show_warning(
self,
"确认删除",
f"确定要删除这个{item_type}吗?\n{filepath}",
show_cancel=True,
confirm_text="删除",
cancel_text="取消"
)
if result == QDialog.Accepted:
try:
if is_folder:
shutil.rmtree(filepath)
else:
os.remove(filepath)
self._refreshWithStatePreservation()
print(f"删除{item_type}: {filepath}")
except OSError as e:
print(f"删除{item_type}失败: {e}")
UniversalMessageDialog.show_error(
self,
"错误",
f"删除{item_type}失败: {e}",
show_cancel=False,
confirm_text="确认"
)
def copyPath(self, filepath):
"""复制路径到剪贴板"""
from PyQt5.QtWidgets import QApplication
clipboard = QApplication.clipboard()
clipboard.setText(filepath)
print(f"已复制路径: {filepath}")
def openFile(self, filepath):
"""打开文件"""
import os
import subprocess
import platform
try:
system = platform.system()
if system == "Windows":
os.startfile(filepath)
elif system == "Darwin": # macOS
subprocess.run(["open", filepath])
else: # Linux
subprocess.run(["xdg-open", filepath])
print(f"打开文件: {filepath}")
except Exception as e:
print(f"打开文件失败: {e}")
def showProperties(self, item):
"""显示属性面板"""
import os
filepath = item.data(0, Qt.UserRole)
is_folder = item.data(0, Qt.UserRole + 1)
try:
stat = os.stat(filepath)
size = stat.st_size
modified = os.path.getmtime(filepath)
import datetime
modified_str = datetime.datetime.fromtimestamp(modified).strftime('%Y-%m-%d %H:%M:%S')
item_type = "文件夹" if is_folder else "文件"
size_str = f"{size} 字节" if not is_folder else "文件夹"
properties = f"""
路径: {filepath}
类型: {item_type}
大小: {size_str}
修改时间: {modified_str}
"""
UniversalMessageDialog.show_info(
self,
"属性",
properties.strip(),
show_cancel=False,
confirm_text="确认"
)
except OSError as e:
UniversalMessageDialog.show_warning(
self,
"错误",
f"无法获取属性: {e}",
show_cancel=False,
confirm_text="确认"
)
# def mouseDoubleClickEvent(self, event):
# """处理双击事件"""
# item = self.itemAt(event.pos())
# if item:
# filepath = item.data(0, Qt.UserRole)
# is_folder = item.data(0, Qt.UserRole + 1)
#
# if is_folder:
# # 文件夹:展开/折叠
# item.setExpanded(not item.isExpanded())
# else:
# # 文件:检查是否是模型文件
# if filepath and filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
# self.world.importModel(filepath)
# else:
# # 其他文件:用系统默认程序打开
# self.openFile(filepath)
#
# super().mouseDoubleClickEvent(event)
def setupUI(self):
"""初始化UI设置"""
self.setHeaderHidden(True)
# 启用多选和拖拽
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.setDropIndicatorShown(True) # 启用拖放指示线
def setupDragDrop(self):
"""设置拖拽功能"""
# 使用InternalMove模式以正确显示插入指示线
self.setDragDropMode(QAbstractItemView.InternalMove)
self.setDefaultDropAction(Qt.DropAction.MoveAction)
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.setDropIndicatorShown(True)
def getProjectRootPath(self):
"""获取项目根路径下的Resources文件夹考虑跨平台"""
import os
# 获取当前文件所在目录,然后向上查找项目根目录
current_dir = os.path.dirname(os.path.abspath(__file__))
# 向上查找直到找到项目根目录(包含特定标识文件或文件夹)
project_root = current_dir
max_depth = 10 # 限制向上查找的深度
depth = 0
while depth < max_depth:
# 检查是否是项目根目录(可以根据实际情况调整判断条件)
if (os.path.exists(os.path.join(project_root, "main.py")) or
os.path.exists(os.path.join(project_root, "setup.py")) or
os.path.exists(os.path.join(project_root, ".git"))):
break
parent_dir = os.path.dirname(project_root)
if parent_dir == project_root: # 已经到达文件系统根目录
# 回退到使用当前工作目录
project_root = os.getcwd()
break
project_root = parent_dir
depth += 1
# 构建Resources文件夹路径跨平台
resources_path = os.path.join(project_root, "Resources")
# 如果Resources文件夹不存在创建它
if not os.path.exists(resources_path):
try:
os.makedirs(resources_path, exist_ok=True)
print(f"创建Resources文件夹: {resources_path}")
except OSError as e:
print(f"无法创建Resources文件夹: {e}")
# 如果无法创建,回退到项目根路径
return project_root
return resources_path
# def getProjectRootPath(self):
# """获取项目根路径下的Resources文件夹考虑跨平台"""
# import os
#
# # 获取项目根路径
# project_root = os.getcwd()
#
# # 构建Resources文件夹路径跨平台
# resources_path = os.path.join(project_root, "Resources")
#
# # 如果Resources文件夹不存在创建它
# if not os.path.exists(resources_path):
# try:
# os.makedirs(resources_path, exist_ok=True)
# print(f"创建Resources文件夹: {resources_path}")
# except OSError as e:
# print(f"无法创建Resources文件夹: {e}")
# # 如果无法创建,回退到项目根路径
# return project_root
#
# return resources_path
def load_file_tree(self):
"""加载树形视图"""
self.clear()
self.current_path = self.getProjectRootPath()
try:
# 创建当前目录的根节点
root_name = os.path.basename(self.current_path) or self.current_path
root_item = QTreeWidgetItem([f"📁 {root_name}"])
root_item.setData(0, Qt.UserRole, self.current_path)
root_item.setData(0, Qt.UserRole + 1, True)
font = QFont("Microsoft YaHei", 10, QFont.Light) # 假设您希望字体大小为10
root_item.setFont(0, font)
self.addTopLevelItem(root_item)
# 加载当前目录内容
self.load_directory_tree(self.current_path, root_item)
#添加目录到监控器
self.addWatchedDirectory(self.current_path)
# 展开根节点
root_item.setExpanded(True)
except PermissionError:
error_item = QTreeWidgetItem(["❌ 无权限访问此目录"])
self.addTopLevelItem(error_item)
def addWatchedDirectory(self,directory):
"""添加监控目录"""
if os.path.exists(directory) and directory not in self.watched_directories:
if self.file_watcher.addPath(directory):
self.watched_directories.add(directory)
#print(f"开始监控目录:{directory}")
try:
for item in os.listdir(directory):
item_path = os.path.join(directory,item)
if os.path.isdir(item_path):
self.addWatchedDirectory(item_path)
except Exception as e:
pass
else:
print(f"无法监控目录:{directory}")
def removeWatchedDirectory(self,directory):
"""移除监控目录"""
if directory in self.watched_directories:
if self.file_watcher.removePath(directory):
self.watched_directories.discard(directory)
print(f"停止监控目录:{directory}")
else:
print(f"无法停止监控目录:{directory}")
def onDirectoryChanged(self,path):
"""目录发生变化时处理"""
print(f"目录变化{path}")
if not self.refresh_timer.isActive():
self.refresh_timer.start(1000)
def onFileChanged(self,path):
"""目录发生变化时的处理"""
print(f"目录变化{path}")
if not self.refresh_timer.isActive():
self.refresh_timer.start(1000)
def refreshView(self):
"""刷新视图"""
print("刷新资源视图...")
try:
expanded_paths = self._saveExpandedState()
self.load_file_tree()
self._restoreExpandedState(expanded_paths)
print("资源视图刷新完成")
except Exception as e:
print(f"刷新资源视图失败{e}")
def load_directory_tree(self, path, parent_item, max_depth=3, current_depth=0):
"""递归加载目录树(类似左侧导航面板)"""
if current_depth >= max_depth:
return
try:
items = os.listdir(path)
items.sort()
# 分别处理文件夹和文件
folders = []
files = []
for item in items:
# 跳过隐藏文件和系统文件
if item.startswith('.') or item.startswith('__'):
continue
item_path = os.path.join(path, item)
if os.path.isdir(item_path):
folders.append(item)
elif os.path.isfile(item_path):
files.append(item)
# 先添加文件夹
for folder in folders:
folder_path = os.path.join(path, folder)
folder_item = self.create_simple_tree_item(folder, folder_path, True)
parent_item.addChild(folder_item)
# 递归加载子目录(限制深度)
if current_depth < max_depth - 1:
self.load_directory_tree(folder_path, folder_item, max_depth, current_depth + 1)
# 再添加文件(显示重要文件类型,包括图片和模型)
important_extensions = {
# 编程文件
'.py', '.js', '.html', '.css', '.java', '.cpp', '.c', '.php', '.rb', '.go', '.rs',
# 文档文件
'.md', '.txt', '.pdf', '.doc', '.docx', '.rtf',
# 配置和数据文件
'.json', '.xml', '.yaml', '.yml', '.ini', '.cfg', '.toml',
# 图片文件
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.ico', '.webp', '.tiff', '.tif',
# 3D模型文件
'.fbx', '.obj', '.3ds', '.max', '.blend', '.dae', '.gltf', '.glb', '.stl', '.ply',
# 音视频文件
'.mp3', '.wav', '.mp4', '.avi', '.mov', '.wmv', '.flac', '.aac',
# 压缩文件
'.zip', '.rar', '.7z', '.tar', '.gz',
# 材质和纹理
'.mtl', '.mat', '.exr', '.hdr', '.hdri', '.dds',
# CAD文件
'.dwg', '.dxf', '.step', '.stp', '.iges', '.sldprt', '.sldasm',
# 其他重要文件
'.exe', '.dll', '.bat', '.sh', '.log'
}
# 重要文件名(不依赖扩展名)
important_files = {'requirements.txt', 'README.md', 'main.py', 'LICENSE', 'CHANGELOG.md', 'INSTALL.md'}
for file in files:
file_ext = os.path.splitext(file)[1].lower()
if file_ext in important_extensions or file in important_files:
file_path = os.path.join(path, file)
file_item = self.create_simple_tree_item(file, file_path, False)
parent_item.addChild(file_item)
except (OSError, PermissionError):
pass
def create_simple_tree_item(self, name, path, is_folder):
"""创建简单的树形项目"""
try:
if is_folder:
# 文件夹项目,展开/折叠图标由CSS样式控制
item = QTreeWidgetItem([f"📁 {name}"])
item.setData(0, Qt.UserRole + 1, True) # 标记为文件夹
else:
icon = self.get_file_icon(name)
item = QTreeWidgetItem([f"{icon} {name}"])
item.setData(0, Qt.UserRole + 1, False) # 标记为文件
item.setData(0, Qt.UserRole, path)
return item
except (OSError, PermissionError):
item = QTreeWidgetItem([name])
item.setData(0, Qt.UserRole, path)
return item
def get_file_icon(self, filename):
"""
根据文件扩展名获取图标
这个函数根据文件的扩展名返回对应的Unicode图标字符。
如果文件类型不在映射表中,返回默认的文档图标。
参数:
filename (str): 文件名(包含扩展名)
返回值:
str: 对应的Unicode图标字符
"""
# 获取文件扩展名并转换为小写
ext = os.path.splitext(filename)[1].lower()
# 文件扩展名到图标的映射表
icon_map = {
# 编程语言文件
'.py': '🐍', # Python文件
'.js': '', # JavaScript文件
'.html': '🌐', # HTML文件
'.css': '🎨', # CSS样式文件
'.java': '', # Java文件
'.cpp': '⚙️', # C++文件
'.c': '⚙️', # C文件
'.h': '📋', # 头文件
'.php': '🐘', # PHP文件
'.rb': '💎', # Ruby文件
'.go': '🐹', # Go文件
'.rs': '🦀', # Rust文件
'.swift': '🐦', # Swift文件
'.kt': '🎯', # Kotlin文件
# 文档文件
'.txt': '📄', # 纯文本文件
'.md': '📝', # Markdown文档
'.rst': '📝', # reStructuredText文档
'.pdf': '📕', # PDF文档
'.doc': '📘', # Word文档
'.docx': '📘', # Word文档
'.rtf': '📄', # RTF文档
'.odt': '📄', # OpenDocument文本
# 数据文件
'.json': '📋', # JSON数据文件
'.xml': '📋', # XML文件
'.yaml': '📋', # YAML文件
'.yml': '📋', # YAML文件
'.csv': '📊', # CSV表格文件
'.xls': '📗', # Excel表格
'.xlsx': '📗', # Excel表格
'.ods': '📊', # OpenDocument表格
# 图像文件
'.jpg': '🖼️', # JPEG图像
'.jpeg': '🖼️', # JPEG图像
'.png': '🖼️', # PNG图像
'.gif': '🖼️', # GIF图像
'.bmp': '🖼️', # BMP图像
'.svg': '🎨', # SVG矢量图
'.ico': '🎯', # 图标文件
'.webp': '🖼️', # WebP图像
'.tiff': '🖼️', # TIFF图像
'.tif': '🖼️', # TIFF图像
# 音视频文件
'.mp4': '🎬', # MP4视频
'.avi': '🎬', # AVI视频
'.mov': '🎬', # MOV视频
'.wmv': '🎬', # WMV视频
'.flv': '🎬', # FLV视频
'.webm': '🎬', # WebM视频
'.mp3': '🎵', # MP3音频
'.wav': '🎵', # WAV音频
'.flac': '🎵', # FLAC音频
'.aac': '🎵', # AAC音频
'.ogg': '🎵', # OGG音频
# 压缩文件
'.zip': '📦', # ZIP压缩包
'.rar': '📦', # RAR压缩包
'.7z': '📦', # 7Z压缩包
'.tar': '📦', # TAR归档
'.gz': '📦', # GZIP压缩
'.bz2': '📦', # BZIP2压缩
'.xz': '📦', # XZ压缩
# 可执行文件
'.exe': '⚙️', # Windows可执行文件
'.msi': '📦', # Windows安装包
'.deb': '📦', # Debian包
'.rpm': '📦', # RPM包
'.dmg': '💿', # macOS磁盘镜像
'.app': '📱', # macOS应用程序
# 系统文件
'.dll': '🔧', # 动态链接库
'.so': '🔧', # 共享库
'.dylib': '🔧', # macOS动态库
'.lib': '📚', # 静态库
# 脚本文件
'.bat': '📜', # Windows批处理
'.cmd': '📜', # Windows命令脚本
'.sh': '📜', # Shell脚本
'.ps1': '💙', # PowerShell脚本
'.vbs': '📜', # VBScript脚本
# 配置文件
'.ini': '⚙️', # INI配置文件
'.cfg': '⚙️', # 配置文件
'.conf': '⚙️', # 配置文件
'.config': '⚙️', # 配置文件
'.toml': '⚙️', # TOML配置文件
# 3D模型文件
'.fbx': '🎭', # FBX模型文件
'.obj': '🎭', # OBJ模型文件
'.3ds': '🎭', # 3DS Max模型
'.max': '🎭', # 3DS Max场景
'.blend': '🎭', # Blender模型
'.dae': '🎭', # COLLADA模型
'.gltf': '🎭', # glTF模型
'.glb': '🎭', # glTF二进制模型
'.x3d': '🎭', # X3D模型
'.ply': '🎭', # PLY模型
'.stl': '🎭', # STL模型3D打印
'.off': '🎭', # OFF模型
'.3mf': '🎭', # 3MF模型
'.amf': '🎭', # AMF模型
'.x': '🎭', # DirectX模型
'.md2': '🎭', # Quake II模型
'.md3': '🎭', # Quake III模型
'.mdl': '🎭', # Source引擎模型
'.mesh': '🎭', # OGRE模型
'.scene': '🎭', # OGRE场景
'.ac': '🎭', # AC3D模型
'.ase': '🎭', # ASCII Scene Export
'.assbin': '🎭', # Assimp二进制
'.b3d': '🎭', # Blitz3D模型
'.bvh': '🎭', # BioVision层次
'.csm': '🎭', # CharacterStudio Motion
'.hmp': '🎭', # 3D GameStudio模型
'.irr': '🎭', # Irrlicht场景
'.irrmesh': '🎭', # Irrlicht网格
'.lwo': '🎭', # LightWave对象
'.lws': '🎭', # LightWave场景
'.ms3d': '🎭', # MilkShape 3D
'.nff': '🎭', # Neutral文件格式
'.q3o': '🎭', # Quick3D对象
'.q3s': '🎭', # Quick3D场景
'.raw': '🎭', # RAW三角形
'.smd': '🎭', # Valve SMD
'.ter': '🎭', # Terragen地形
'.uc': '🎭', # Unreal模型
'.vta': '🎭', # Valve VTA
'.xgl': '🎭', # XGL模型
'.zgl': '🎭', # ZGL模型
# 纹理和材质文件
'.mtl': '🎨', # OBJ材质文件
'.mat': '🎨', # 材质文件
'.sbsar': '🎨', # Substance Archive
'.sbs': '🎨', # Substance Designer
'.sbsm': '🎨', # Substance材质
'.exr': '🖼️', # OpenEXR高动态范围图像
'.hdr': '🖼️', # HDR图像
'.hdri': '🖼️', # HDRI环境贴图
'.dds': '🖼️', # DirectDraw Surface
'.ktx': '🖼️', # Khronos纹理
'.astc': '🖼️', # ASTC纹理
'.pvr': '🖼️', # PowerVR纹理
'.etc1': '🖼️', # ETC1纹理
'.etc2': '🖼️', # ETC2纹理
# 动画文件
'.anim': '🎬', # 动画文件
'.fbx': '🎬', # FBX动画也可以是模型
'.bip': '🎬', # Character Studio Biped
'.cal3d': '🎬', # Cal3D动画
'.motion': '🎬', # 动作文件
'.mocap': '🎬', # 动作捕捉数据
# 其他常见文件
'.log': '📋', # 日志文件
'.tmp': '🗂️', # 临时文件
'.bak': '💾', # 备份文件
'.old': '📦', # 旧文件
}
# 返回对应的图标,如果找不到则返回默认文档图标
return icon_map.get(ext, '📄')
def startDrag(self, supportedActions):
"""开始拖拽操作"""
selected_items = self.selectedItems()
if not selected_items:
return
# 创建 MIME 数据
mimeData = QMimeData()
# 收集文件路径用于向外拖拽
urls = []
internal_paths = []
for item in selected_items:
filepath = item.data(0, Qt.UserRole)
if filepath:
internal_paths.append(filepath)
# 检查是否是模型文件(用于向外拖拽)
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
urls.append(QUrl.fromLocalFile(filepath))
# 设置内部拖拽数据
mimeData.setText('\n'.join(internal_paths))
# 设置向外拖拽数据
if urls:
mimeData.setUrls(urls)
# 创建拖拽对象
drag = QDrag(self)
drag.setMimeData(mimeData)
# 设置拖拽图标
pixmap = QPixmap(32, 32)
pixmap.fill(Qt.transparent)
painter = QPainter(pixmap)
painter.drawText(pixmap.rect(), Qt.AlignCenter, str(len(selected_items)))
painter.end()
drag.setPixmap(pixmap)
# 执行拖拽
drag.exec_(supportedActions)
def dragEnterEvent(self, event):
"""处理拖拽进入事件"""
# 检查是否是内部拖拽
if event.mimeData().hasText():
event.acceptProposedAction()
else:
event.ignore()
def dragMoveEvent(self, event):
"""处理拖拽移动事件"""
if not event.mimeData().hasText():
event.ignore()
return
# 获取目标项
target_item = self.itemAt(event.pos())
selected_items = self.selectedItems()
# 检查是否拖拽到多选区域内的项目
if target_item and target_item in selected_items:
event.ignore()
return
# 检查是否拖拽到自己的子级
if target_item:
for selected_item in selected_items:
if self._isChildOf(target_item, selected_item):
event.ignore()
return
# 如果拖到文件夹,允许拖到文件夹内部
if target_item:
is_folder = target_item.data(0, Qt.UserRole + 1)
if is_folder:
# 接受拖放,显示指示框
event.acceptProposedAction()
return
# 调用父类方法处理插入指示线
event.accept()
super().dragMoveEvent(event)
def _isChildOf(self, potential_child, potential_parent):
"""检查 potential_child 是否是 potential_parent 的子级"""
current = potential_child.parent()
while current:
if current == potential_parent:
return True
current = current.parent()
return False
def dropEvent(self, event):
"""处理拖放事件"""
if not event.mimeData().hasText():
event.ignore()
return
drag_paths = event.mimeData().text().split('\n')
if not drag_paths:
event.ignore()
return
# 获取目标项
target_item = self.itemAt(event.pos())
if not target_item:
# 如果拖到空白处,默认放到根目录
target_path = self.current_path
else:
# 如果是文件夹,就放到里面
is_folder = target_item.data(0, Qt.UserRole + 1)
if is_folder:
target_path = target_item.data(0, Qt.UserRole)
else:
# 如果是文件,则放到其父目录
parent_item = target_item.parent()
if parent_item:
target_path = parent_item.data(0, Qt.UserRole)
else:
target_path = self.current_path
# 执行移动
self._moveFiles(drag_paths, target_path)
event.acceptProposedAction()
# 让 Qt 更新界面
super().dropEvent(event)
def _moveFiles(self, source_paths, target_dir):
"""移动文件到目标目录"""
import os
import shutil
from PyQt5.QtWidgets import QMessageBox
moved_files = []
failed_files = []
for source_path in source_paths:
if not source_path or not os.path.exists(source_path):
continue
if os.path.isdir(source_path) and target_dir.startswith(source_path):
failed_files.append(f"{source_path} (不能移动到子目录)")
continue
if os.path.dirname(source_path) == target_dir:
continue
filename = os.path.basename(source_path)
target_path = os.path.join(target_dir, filename)
if os.path.exists(target_path):
failed_files.append(f"{filename} (目标已存在)")
continue
try:
shutil.move(source_path, target_path)
moved_files.append(filename)
print(f"移动文件: {source_path} -> {target_path}")
except Exception as e:
failed_files.append(f"{filename} ({str(e)})")
# 使用状态保持刷新
if moved_files:
self._refreshWithStatePreservation()
if failed_files:
StyledMessageBox.warning(
self,
"移动失败",
f"以下文件移动失败:\n" + "\n".join(failed_files)
)
if moved_files:
print(f"成功移动 {len(moved_files)} 个文件")
# def mouseDoubleClickEvent(self, event):
# """处理双击事件"""
# item = self.itemAt(event.pos())
# if item:
# filepath = item.data(0, Qt.UserRole)
# is_folder = item.data(0, Qt.UserRole + 1)
#
# if is_folder:
# # 文件夹:展开/折叠
# item.setExpanded(not item.isExpanded())
# else:
# # 文件:检查是否是模型文件
# if filepath and filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
# self.world.importModel(filepath)
# else:
# # 其他文件:用系统默认程序打开
# self.openFile(filepath)
#
# super().mouseDoubleClickEvent(event)
class CustomConsoleDockWidget(QWidget):
"""自定义控制台停靠部件"""
def __init__(self, world, parent=None):
if parent is None:
parent = wrapinstance(0, QWidget)
super().__init__(parent)
self.world = world
self.setupUI()
self.setupConsoleRedirect()
def setupUI(self):
"""初始化控制台UI"""
layout = QVBoxLayout(self)
# 控制台工具栏
toolbar = QHBoxLayout()
# 清空按钮
self.clearBtn = QPushButton("清空")
self.clearBtn.setStyleSheet("""
QPushButton {
background-color: rgba(84, 89, 98, 0.5);
color: #ffffff;
border: none;
padding: 4px 12px;
border-radius: 2px;
font-family: 'Microsoft YaHei', 'Inter', sans-serif;
font-size: 12px;
font-weight: 300;
letter-spacing: 0.7px;
}
QPushButton:hover {
background-color: rgba(84, 89, 98, 0.7);
}
QPushButton:pressed {
background-color: rgba(84, 89, 98, 0.9);
}
""")
self.clearBtn.clicked.connect(self.clearConsole)
toolbar.addWidget(self.clearBtn)
# 自动滚动开关
self.autoScrollBtn = QPushButton("自动滚动")
self.autoScrollBtn.setCheckable(True)
self.autoScrollBtn.setChecked(True)
self.autoScrollBtn.setStyleSheet("""
QPushButton {
background-color: rgba(84, 89, 98, 0.5);
color: #ffffff;
border: none;
padding: 4px 12px;
border-radius: 2px;
font-family: 'Microsoft YaHei', 'Inter', sans-serif;
font-size: 12px;
font-weight: 300;
letter-spacing: 0.7px;
}
QPushButton:checked {
background-color: #3067c0;
color: #ffffff;
}
QPushButton:hover {
background-color: rgba(84, 89, 98, 0.7);
}
QPushButton:pressed {
background-color: rgba(84, 89, 98, 0.9);
}
""")
toolbar.addWidget(self.autoScrollBtn)
# 时间戳开关
self.timestampBtn = QPushButton("显示时间")
self.timestampBtn.setCheckable(True)
self.timestampBtn.setChecked(True)
self.timestampBtn.setStyleSheet("""
QPushButton {
background-color: rgba(84, 89, 98, 0.5);
color: #ffffff;
border: none;
padding: 4px 12px;
border-radius: 2px;
font-family: 'Microsoft YaHei', 'Inter', sans-serif;
font-size: 12px;
font-weight: 300;
letter-spacing: 0.7px;
}
QPushButton:checked {
background-color: #3067c0;
color: #ffffff;
}
QPushButton:hover {
background-color: rgba(84, 89, 98, 0.7);
}
QPushButton:pressed {
background-color: rgba(84, 89, 98, 0.9);
}
""")
toolbar.addWidget(self.timestampBtn)
self.fpsLabel = QLabel("FPS:0.0")
self.fpsLabel.setStyleSheet("""
QLabel {
background-color: #3067c0;
color: #ffffff;
border: none;
padding: 4px 12px;
border-radius: 2px;
font-family: 'Microsoft YaHei', 'Inter', sans-serif;
font-size: 12px;
font-weight: 300;
letter-spacing: 0.7px;
}
""")
self.fpsLabel.setMinimumWidth(100)
self.fpsLabel.setAlignment(Qt.AlignCenter)
toolbar.addWidget(self.fpsLabel)
# 帧率更新定时器
self.fpsTimer = QTimer()
self.fpsTimer.timeout.connect(self.updateFPS)
self.fpsTimer.start(105) # 每秒更新一次
toolbar.addStretch()
layout.addLayout(toolbar)
# 控制台文本区域
from PyQt5.QtWidgets import QTextEdit
self.consoleText = QTextEdit()
self.consoleText.setReadOnly(True)
self.consoleText.setStyleSheet("""
QTextEdit {
background-color: #19191b;
color: #ebebeb;
font-family: 'Consolas', 'Monaco', 'Microsoft YaHei', monospace;
font-size: 10px;
font-weight: 300;
border: none;
letter-spacing: 0.5px;
line-height: 12px;
}
""")
layout.addWidget(self.consoleText)
# # 命令输入区域
# inputLayout = QHBoxLayout()
# inputLayout.addWidget(QLabel(">>> "))
#
# self.commandInput = QLineEdit()
# self.commandInput.setStyleSheet("""
# QLineEdit {
# background-color: #2d2d2d;
# color: #ffffff;
# font-family: 'Consolas', 'Monaco', monospace;
# font-size: 10pt;
# border: 1px solid #3e3e3e;
# padding: 5px;
# }
# """)
# self.commandInput.returnPressed.connect(self.executeCommand)
# inputLayout.addWidget(self.commandInput)
#
# layout.addLayout(inputLayout)
# 添加欢迎信息
self.addMessage("🎮 编辑器控制台已启动", "INFO")
def updateFPS(self):
try:
if hasattr(self.world,'clock'):
fps = self.world.clock.getAverageFrameRate()
self.fpsLabel.setText(f"FPS:{fps:.1f}")
# 根据帧率设置颜色
if fps >= 50:
color = "#80ff80" # 绿色 - 优秀
elif fps >= 30:
color = "#ffff80" # 黄色 - 一般
else:
color = "#ff8080" # 红色 - 较差
self.fpsLabel.setStyleSheet(f"""
QLabel {{
background-color: #3067c0;
color: {color};
border: 1px solid #3a3a4a;
padding: 6px 12px;
border-radius: 4px;
font-weight: 500;
font-family: 'Consolas', 'Monaco', monospace;
}}
""")
except Exception as e:
pass # 静默处理错误,避免影响控制台功能
def setupConsoleRedirect(self):
"""设置控制台重定向"""
import sys
# 保存原始的stdout和stderr
self.original_stdout = sys.stdout
self.original_stderr = sys.stderr
# 创建自定义输出流
sys.stdout = ConsoleRedirect(self, "STDOUT")
sys.stderr = ConsoleRedirect(self, "STDERR")
def addMessage(self, message, msg_type="INFO"):
"""添加消息到控制台"""
import datetime
# 获取当前时间
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
# 根据消息类型设置颜色
color_map = {
"INFO": "#ffffff", # 白色
"WARNING": "#ffaa00", # 橙色
"ERROR": "#ff4444", # 红色
"SUCCESS": "#44ff44", # 绿色
"STDOUT": "#cccccc", # 浅灰色
"STDERR": "#ff6666", # 浅红色
}
color = color_map.get(msg_type, "#ffffff")
# 构建HTML格式的消息
if self.timestampBtn.isChecked():
html_message = f'<span style="color: #888888;">[{timestamp}]</span> <span style="color: {color};">{message}</span>'
else:
html_message = f'<span style="color: {color};">{message}</span>'
# 添加到控制台
self.consoleText.append(html_message)
# 自动滚动到底部
if self.autoScrollBtn.isChecked():
scrollbar = self.consoleText.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
def clearConsole(self):
"""清空控制台"""
self.consoleText.clear()
self.addMessage("控制台已清空", "INFO")
def executeCommand(self):
"""执行命令"""
command = self.commandInput.text().strip()
if not command:
return
# 显示输入的命令
self.addMessage(f">>> {command}", "INFO")
self.commandInput.clear()
try:
# 执行Python命令
if hasattr(self.world, 'executeCommand'):
result = self.world.executeCommand(command)
if result:
self.addMessage(str(result), "SUCCESS")
else:
# 简单的eval执行
result = eval(command)
if result is not None:
self.addMessage(str(result), "SUCCESS")
except Exception as e:
self.addMessage(f"错误: {str(e)}", "ERROR")
def cleanup(self):
"""清理资源"""
import sys
# 恢复原始的stdout和stderr
if hasattr(self, 'original_stdout'):
sys.stdout = self.original_stdout
if hasattr(self, 'original_stderr'):
sys.stderr = self.original_stderr
class ConsoleRedirect:
"""控制台重定向类"""
def __init__(self, console_widget, stream_type):
self.console_widget = console_widget
self.stream_type = stream_type
def write(self, text):
"""重定向写入"""
if text.strip(): # 忽略空行
self.console_widget.addMessage(text.strip(), self.stream_type)
def flush(self):
"""刷新缓冲区"""
pass
class StyledMessageBox:
"""统一样式的消息框辅助类"""
MESSAGEBOX_STYLE = """
QMessageBox {
background-color: #1e1e1f;
color: #ebebeb;
border: 1px solid rgba(77, 116, 189, 0.3);
border-radius: 8px;
padding: 20px;
min-width: 300px;
}
QMessageBox QLabel {
color: #ffffff;
font-family: 'Microsoft YaHei', 'Inter', sans-serif;
font-size: 13px;
font-weight: 400;
line-height: 1.4;
padding: 10px 0px;
}
QMessageBox QPushButton {
background-color: rgba(89, 100, 113, 0.4);
color: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(76, 92, 110, 0.4);
border-radius: 6px;
padding: 8px 16px;
font-family: 'Microsoft YaHei', 'Inter', sans-serif;
font-weight: 400;
font-size: 11px;
min-width: 80px;
min-height: 28px;
}
QMessageBox QPushButton:hover {
background-color: rgba(89, 100, 113, 0.6);
border: 1px solid rgba(77, 116, 189, 0.6);
color: #ffffff;
}
QMessageBox QPushButton:pressed, QMessageBox QPushButton:checked {
background-color: rgba(77, 116, 189, 0.8);
border: 1px solid #4d74bd;
color: #ffffff;
}
QMessageBox QPushButton:disabled {
background-color: rgba(89, 100, 113, 0.2);
color: rgba(235, 235, 235, 0.4);
border: 1px solid rgba(76, 92, 110, 0.2);
}
QMessageBox QPushButton:default {
background-color: rgba(77, 116, 189, 0.8);
border: 1px solid #4d74bd;
color: #ffffff;
font-weight: 500;
}
QMessageBox QPushButton:default:hover {
background-color: rgba(77, 116, 189, 1.0);
border: 1px solid rgba(77, 116, 189, 1.0);
}
QMessageBox QIcon {
padding: 0px 10px 0px 0px;
}
"""
INPUTDIALOG_STYLE = """
QInputDialog {
background-color: #1e1e1f;
color: #ebebeb;
border: 1px solid rgba(77, 116, 189, 0.3);
border-radius: 8px;
padding: 20px;
min-width: 350px;
}
QLabel {
color: #ffffff;
font-family: 'Microsoft YaHei', 'Inter', sans-serif;
font-weight: 500;
font-size: 13px;
padding: 0px 0px 10px 0px;
}
QLineEdit {
background-color: rgba(89, 100, 113, 0.15);
color: #ebebeb;
border: 1px solid rgba(76, 92, 110, 0.4);
border-radius: 6px;
padding: 10px 12px;
font-family: 'Microsoft YaHei', 'Inter', sans-serif;
font-size: 11px;
font-weight: 400;
min-height: 16px;
}
QLineEdit:focus {
border: 1px solid #4d74bd;
background-color: rgba(77, 116, 189, 0.1);
}
QLineEdit:hover {
border: 1px solid rgba(77, 116, 189, 0.6);
background-color: rgba(89, 100, 113, 0.2);
}
QPushButton {
background-color: rgba(89, 100, 113, 0.4);
color: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(76, 92, 110, 0.4);
border-radius: 6px;
padding: 8px 16px;
font-family: 'Microsoft YaHei', 'Inter', sans-serif;
font-weight: 400;
font-size: 11px;
min-width: 80px;
min-height: 28px;
}
QPushButton:hover {
background-color: rgba(89, 100, 113, 0.6);
border: 1px solid rgba(77, 116, 189, 0.6);
color: #ffffff;
}
QPushButton:pressed, QPushButton:checked {
background-color: rgba(77, 116, 189, 0.8);
border: 1px solid #4d74bd;
color: #ffffff;
}
QPushButton:disabled {
background-color: rgba(89, 100, 113, 0.2);
color: rgba(235, 235, 235, 0.4);
border: 1px solid rgba(76, 92, 110, 0.2);
}
QPushButton:default {
background-color: rgba(77, 116, 189, 0.8);
border: 1px solid #4d74bd;
color: #ffffff;
font-weight: 500;
}
"""
BUTTON_STYLE = """
QPushButton {
background-color: rgba(89, 100, 113, 0.4);
color: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(76, 92, 110, 0.4);
border-radius: 6px;
padding: 8px 16px;
font-family: 'Microsoft YaHei', 'Inter', sans-serif;
font-weight: 400;
font-size: 11px;
min-width: 80px;
min-height: 28px;
}
QPushButton:hover {
background-color: rgba(89, 100, 113, 0.6);
border: 1px solid rgba(77, 116, 189, 0.6);
color: #ffffff;
}
QPushButton:pressed, QPushButton:checked {
background-color: rgba(77, 116, 189, 0.8);
border: 1px solid #4d74bd;
color: #ffffff;
}
QPushButton:disabled {
background-color: rgba(89, 100, 113, 0.2);
color: rgba(235, 235, 235, 0.4);
border: 1px solid rgba(76, 92, 110, 0.2);
}
"""
@staticmethod
def information(parent, title, message):
"""显示信息提示框"""
msg = QMessageBox(QMessageBox.Information, title, message, QMessageBox.Ok, parent)
msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE)
# 强制设置所有按钮的样式
for button in msg.buttons():
button.setStyleSheet(StyledMessageBox.BUTTON_STYLE)
return msg.exec_()
@staticmethod
def question(parent, title, message, buttons=QMessageBox.Yes | QMessageBox.No, defaultButton=QMessageBox.No):
"""显示确认对话框"""
msg = QMessageBox(QMessageBox.Question, title, message, buttons, parent)
msg.setDefaultButton(defaultButton)
msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE)
# 强制设置所有按钮的样式
for button in msg.buttons():
button.setStyleSheet(StyledMessageBox.BUTTON_STYLE)
return msg.exec_()
@staticmethod
def warning(parent, title, message):
"""显示警告对话框"""
msg = QMessageBox(QMessageBox.Warning, title, message, QMessageBox.Ok, parent)
msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE)
# 强制设置所有按钮的样式
for button in msg.buttons():
button.setStyleSheet(StyledMessageBox.BUTTON_STYLE)
return msg.exec_()
@staticmethod
def critical(parent, title, message):
"""显示错误对话框"""
msg = QMessageBox(QMessageBox.Critical, title, message, QMessageBox.Ok, parent)
msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE)
# 强制设置所有按钮的样式
for button in msg.buttons():
button.setStyleSheet(StyledMessageBox.BUTTON_STYLE)
return msg.exec_()
@staticmethod
def getText(parent, title, label, text=""):
"""显示样式统一的文本输入对话框"""
from PyQt5.QtWidgets import QInputDialog
dialog = QInputDialog(parent)
dialog.setWindowTitle(title)
dialog.setLabelText(label)
dialog.setTextValue(text)
dialog.setStyleSheet(StyledMessageBox.INPUTDIALOG_STYLE)
# 强制设置所有按钮的样式
for button in dialog.findChildren(QPushButton):
button.setStyleSheet(StyledMessageBox.BUTTON_STYLE)
ok = dialog.exec_()
return dialog.textValue(), ok
class UniversalMessageDialog(QDialog):
"""通用消息对话框类 - 支持不同图标和按钮配置"""
# 消息类型枚举
SUCCESS = "success_icon"
WARNING = "warning_icon"
ERROR = "fail_icon"
INFO = "info"
def __init__(self, parent, title, message, message_type=INFO, show_cancel=True,
confirm_text="确认", cancel_text="取消", icon_size=QSize(20, 20)):
"""
初始化通用消息对话框
Args:
parent: 父窗口
title: 对话框标题
message: 消息内容
message_type: 消息类型 (SUCCESS, WARNING, ERROR, INFO)
show_cancel: 是否显示取消按钮
confirm_text: 确认按钮文字
cancel_text: 取消按钮文字
icon_size: 图标尺寸
"""
super().__init__(parent)
self.setWindowTitle(title)
self.setObjectName("universalMessageDialog")
self.setModal(True)
self.resize(508, 134)
self.setMinimumSize(508, 134)
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground, True)
# 对话框配置
self.message_type = message_type
self.show_cancel = show_cancel
self.confirm_text = confirm_text
self.cancel_text = cancel_text
self.icon_size = icon_size
# 拖拽相关
self.dragging = False
self.drag_position = QPoint()
# 图标管理
self.icon_manager = get_icon_manager()
self._title_icon_size = QSize(18, 18)
self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size)
# 根据消息类型获取对应图标
self._message_icon = self._get_message_icon()
# 设置样式
self._setup_styles()
# 创建UI
self._create_ui(message)
def _get_message_icon(self):
"""根据消息类型获取对应图标"""
icon_map = {
self.SUCCESS: 'success_icon',
self.WARNING: 'warning_icon',
self.ERROR: 'fail_icon',
self.INFO: 'success_icon' # 默认使用成功图标
}
icon_name = icon_map.get(self.message_type, 'success_icon')
return self.icon_manager.get_icon(icon_name, self.icon_size)
def _setup_styles(self):
"""设置对话框样式"""
self.setStyleSheet("""
QDialog#universalMessageDialog {
background-color: transparent;
border: none;
}
QFrame#baseFrame {
background-color: #19191B;
border: 1px solid #3E3E42;
border-radius: 5px;
}
QWidget#titleBar {
background-color: transparent;
border: none;
border-radius: 5px 5px 0px 0px;
min-height: 32px;
max-height: 32px;
}
QWidget#titleBar QWidget {
background-color: transparent;
border: none;
}
QLabel#titleLabel {
color: #FFFFFF;
font-family: 'Inter', 'Microsoft YaHei', sans-serif;
font-size: 14px;
font-weight: 500;
letter-spacing: 0.7px;
}
QWidget#controlButtons QPushButton {
background-color: transparent;
border: none;
color: #EBEBEB;
font-size: 14px;
min-width: 18px;
max-width: 18px;
min-height: 18px;
max-height: 18px;
padding: 0px;
border-radius: 3px;
}
QWidget#controlButtons QPushButton:hover {
background-color: #2A2D2E;
color: #FFFFFF;
}
QPushButton#closeButton {
border-radius: 0px 5px 0px 0px;
}
QPushButton#closeButton:hover {
background-color: #2A2D2E;
color: #FFFFFF;
}
QFrame#titleSeparator {
background-color: #3E3E42;
border: none;
min-height: 1px;
max-height: 1px;
}
QWidget#contentWidget {
background-color: transparent;
border: none;
}
QLabel#messageLabel {
color: #EBEBEB;
font-family: 'Inter', 'Microsoft YaHei', sans-serif;
font-size: 13px;
font-weight: 400;
letter-spacing: 0.6px;
line-height: 1.4;
padding: 0px;
margin: 0px;
}
QPushButton {
background-color: rgba(89, 98, 118, 0.5);
color: #EBEBEB;
border: none;
border-radius: 2px;
padding: 0px 12px;
font-family: 'Inter', 'Microsoft YaHei', sans-serif;
font-weight: 300;
font-size: 10px;
letter-spacing: 0.5px;
min-width: 90px;
max-width: 90px;
min-height: 28px;
max-height: 28px;
}
QPushButton:hover {
background-color: #3067C0;
color: #FFFFFF;
}
QPushButton:pressed {
background-color: #2556A0;
color: #FFFFFF;
}
QPushButton#primaryButton {
background-color: rgba(89, 98, 118, 0.5);
border: none;
color: #EBEBEB;
font-weight: 300;
}
QPushButton#primaryButton:hover {
background-color: #2556A0;
}
QPushButton#primaryButton:pressed {
background-color: #1E4A8C;
}
QPushButton#secondaryButton {
background-color: rgba(89, 98, 118, 0.5);
border: none;
color: #EBEBEB;
}
QPushButton#secondaryButton:hover {
background-color: #3067C0;
color: #FFFFFF;
}
QPushButton#secondaryButton:pressed {
background-color: #2556A0;
color: #FFFFFF;
}
""")
def _create_ui(self, message):
"""构建用户界面"""
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
base_frame = QFrame()
base_frame.setObjectName('baseFrame')
base_frame.setFrameShape(QFrame.NoFrame)
base_frame.setAttribute(Qt.WA_StyledBackground, True)
base_layout = QVBoxLayout(base_frame)
base_layout.setContentsMargins(25, 14, 25, 14)
base_layout.setSpacing(0)
self._create_title_bar()
base_layout.addWidget(self.title_bar)
base_layout.addSpacing(4)
title_separator = QFrame()
title_separator.setObjectName('titleSeparator')
title_separator.setFrameShape(QFrame.HLine)
title_separator.setFrameShadow(QFrame.Plain)
base_layout.addWidget(title_separator)
base_layout.addSpacing(10)
content_widget = QWidget()
content_widget.setObjectName('contentWidget')
content_layout = QVBoxLayout(content_widget)
content_layout.setContentsMargins(0, 0, 0, 0)
content_layout.setSpacing(10)
message_area = QHBoxLayout()
message_area.setContentsMargins(0, 0, 0, 0)
message_area.setSpacing(10)
# 用一个垂直布局包裹icon_label确保顶部对齐
icon_vbox = QVBoxLayout()
icon_vbox.setContentsMargins(0, 0, 0, 0)
icon_vbox.setSpacing(0)
icon_label = QLabel()
if self._message_icon and not self._message_icon.isNull():
icon_label.setPixmap(self._message_icon.pixmap(self.icon_size))
icon_label.setAlignment(Qt.AlignTop | Qt.AlignLeft)
icon_label.setFixedSize(self.icon_size)
icon_vbox.addWidget(icon_label, alignment=Qt.AlignTop)
icon_vbox.addStretch()
message_area.addLayout(icon_vbox)
self.message_label = QLabel(message)
self.message_label.setObjectName('messageLabel')
self.message_label.setWordWrap(True)
self.message_label.setAlignment(Qt.AlignTop | Qt.AlignLeft)
self.message_label.setMinimumHeight(self.icon_size.height())
self.message_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
message_area.addWidget(self.message_label, 1)
message_area.addStretch()
content_layout.addLayout(message_area)
base_layout.addWidget(content_widget)
base_layout.addSpacing(10)
button_widget = QWidget()
button_layout = QHBoxLayout(button_widget)
button_layout.setContentsMargins(0, 0, 0, 0)
button_layout.setSpacing(10)
button_layout.addStretch()
if self.show_cancel:
self.cancel_button = QPushButton(self.cancel_text)
self.cancel_button.setObjectName("secondaryButton")
self.cancel_button.clicked.connect(self.reject)
self.cancel_button.setFixedSize(90, 28)
button_layout.addWidget(self.cancel_button)
self.confirm_button = QPushButton(self.confirm_text)
self.confirm_button.setObjectName("primaryButton")
self.confirm_button.clicked.connect(self.accept)
self.confirm_button.setFixedSize(90, 28)
button_layout.addWidget(self.confirm_button)
self.confirm_button.setDefault(True)
self.confirm_button.setAutoDefault(True)
if self.show_cancel:
self.cancel_button.setAutoDefault(False)
base_layout.addWidget(button_widget)
main_layout.addWidget(base_frame)
def _create_title_bar(self):
"""创建自定义标题栏"""
self.title_bar = QFrame()
self.title_bar.setObjectName("titleBar")
title_layout = QHBoxLayout(self.title_bar)
title_layout.setContentsMargins(0, 0, 0, 0)
title_layout.setSpacing(0)
self.title_label = QLabel(self.windowTitle())
self.title_label.setObjectName("titleLabel")
self.title_label.setAlignment(Qt.AlignVCenter | Qt.AlignLeft)
title_layout.addWidget(self.title_label, 1)
title_layout.addStretch()
controls = QWidget()
controls.setObjectName("controlButtons")
controls_layout = QHBoxLayout(controls)
controls_layout.setContentsMargins(0, 0, 0, 0)
controls_layout.setSpacing(0)
self.close_button = QPushButton()
self.close_button.setObjectName("closeButton")
self.close_button.clicked.connect(self.reject)
self.close_button.setFocusPolicy(Qt.NoFocus)
controls_layout.addWidget(self.close_button)
self._apply_title_bar_icons()
title_layout.addWidget(controls)
def _apply_title_bar_icons(self):
"""应用标题栏图标"""
if self._icon_close:
self.close_button.setIcon(self._icon_close)
self.close_button.setIconSize(self._title_icon_size)
self.close_button.setText("")
self.close_button.setToolTip("关闭")
def setWindowTitle(self, title):
"""设置窗口标题"""
super().setWindowTitle(title)
if hasattr(self, "title_label"):
self.title_label.setText(title)
def mousePressEvent(self, event):
"""鼠标按下事件 - 用于拖拽窗口"""
if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()):
self.dragging = True
self.drag_position = event.globalPos() - self.frameGeometry().topLeft()
event.accept()
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
"""鼠标移动事件 - 用于拖拽窗口"""
if event.buttons() == Qt.LeftButton and self.dragging:
self.move(event.globalPos() - self.drag_position)
event.accept()
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
"""鼠标释放事件 - 停止拖拽"""
if event.button() == Qt.LeftButton:
self.dragging = False
super().mouseReleaseEvent(event)
@staticmethod
def show_success(parent, title, message, show_cancel=False, confirm_text="确认"):
"""显示成功消息对话框"""
dialog = UniversalMessageDialog(
parent, title, message,
UniversalMessageDialog.SUCCESS,
show_cancel, confirm_text
)
return dialog.exec_()
@staticmethod
def show_warning(parent, title, message, show_cancel=True, confirm_text="确认", cancel_text="取消"):
"""显示警告消息对话框"""
dialog = UniversalMessageDialog(
parent, title, message,
UniversalMessageDialog.WARNING,
show_cancel, confirm_text, cancel_text
)
return dialog.exec_()
@staticmethod
def show_error(parent, title, message, show_cancel=False, confirm_text="确认"):
"""显示错误消息对话框"""
dialog = UniversalMessageDialog(
parent, title, message,
UniversalMessageDialog.ERROR,
show_cancel, confirm_text
)
return dialog.exec_()
@staticmethod
def show_info(parent, title, message, show_cancel=True, confirm_text="确认", cancel_text="取消"):
"""显示信息消息对话框"""
dialog = UniversalMessageDialog(
parent, title, message,
UniversalMessageDialog.INFO,
show_cancel, confirm_text, cancel_text
)
return dialog.exec_()
class StyledTextInputDialog(QDialog):
"""与新建项目样式一致的文本输入对话框"""
def __init__(self, parent, title, label_text="", placeholder="", default_text=""):
super().__init__(parent)
self.setWindowTitle(title)
self.setObjectName("styledTextInputDialog")
self.setModal(True)
self.resize(420, 186)
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground, True)
self.dragging = False
self.drag_position = QPoint()
self.icon_manager = get_icon_manager()
self._title_icon_size = QSize(18, 18)
self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size)
self.setStyleSheet("""
QDialog#styledTextInputDialog {
background-color: transparent;
border: none;
}
QFrame#baseFrame {
background-color: #000000;
border: 1px solid #3E3E42;
border-radius: 5px;
}
QWidget#titleBar {
background-color: transparent;
border: none;
border-radius: 5px 5px 0px 0px;
min-height: 32px;
max-height: 32px;
}
QWidget#titleBar QWidget {
background-color: transparent;
border: none;
}
QLabel#titleLabel {
color: #FFFFFF;
font-family: 'Inter', 'Microsoft YaHei', sans-serif;
font-size: 14px;
font-weight: 500;
letter-spacing: 0.7px;
}
QWidget#controlButtons QPushButton {
background-color: transparent;
border: none;
color: #EBEBEB;
font-size: 14px;
min-width: 18px;
max-width: 18px;
min-height: 18px;
max-height: 18px;
padding: 0px;
border-radius: 3px;
}
QWidget#controlButtons QPushButton:hover {
background-color: #2A2D2E;
color: #FFFFFF;
}
QPushButton#closeButton {
border-radius: 0px 5px 0px 0px;
}
QPushButton#closeButton:hover {
background-color: #2A2D2E;
color: #FFFFFF;
}
QWidget#contentWidget {
background-color: transparent;
border-radius: 0px 0px 5px 5px;
}
QFrame#contentContainer {
background-color: #19191B;
border: 1px solid #2C2F36;
border-radius: 5px;
}
QLabel[role="fieldLabel"] {
color: #EBEBEB;
font-family: 'Inter', 'Microsoft YaHei', sans-serif;
font-size: 12px;
font-weight: 400;
letter-spacing: 0.6px;
}
QLineEdit {
background-color: rgba(89, 100, 113, 0.2);
color: #EBEBEB;
border: 1px solid rgba(76, 92, 110, 0.6);
border-radius: 2px;
padding: 0px 10px;
font-family: 'Inter', 'Microsoft YaHei', sans-serif;
font-size: 11px;
font-weight: 300;
letter-spacing: 0.55px;
min-height: 30px;
max-height: 30px;
}
QLineEdit:focus {
border: 1px solid #3067C0;
background-color: rgba(48, 103, 192, 0.1);
}
QLineEdit:hover {
border: 1px solid #3067C0;
background-color: rgba(89, 100, 113, 0.3);
}
QPushButton {
background-color: rgba(89, 98, 118, 0.5);
color: #EBEBEB;
border: none;
border-radius: 2px;
padding: 0px 12px;
font-family: 'Inter', 'Microsoft YaHei', sans-serif;
font-weight: 300;
font-size: 10px;
letter-spacing: 0.5px;
min-width: 120px;
min-height: 30px;
max-height: 30px;
}
QPushButton:hover {
background-color: #3067C0;
color: #FFFFFF;
}
QPushButton:pressed {
background-color: #2556A0;
color: #FFFFFF;
}
""")
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
base_frame = QFrame()
base_frame.setObjectName('baseFrame')
base_frame.setFrameShape(QFrame.NoFrame)
base_frame.setAttribute(Qt.WA_StyledBackground, True)
base_layout = QVBoxLayout(base_frame)
base_layout.setContentsMargins(0, 0, 0, 0)
base_layout.setSpacing(0)
self._createTitleBar()
base_layout.addWidget(self.title_bar)
base_layout.addSpacing(10)
content_widget = QWidget()
content_widget.setObjectName('contentWidget')
content_layout = QVBoxLayout(content_widget)
content_layout.setContentsMargins(10, 0, 10, 10)
content_layout.setSpacing(0)
content_container = QFrame()
content_container.setObjectName('contentContainer')
content_container.setFrameShape(QFrame.NoFrame)
content_container.setAttribute(Qt.WA_StyledBackground, True)
content_container.setFixedWidth(400)
container_layout = QVBoxLayout(content_container)
container_layout.setContentsMargins(15, 10, 15, 10)
container_layout.setSpacing(10)
if label_text:
label = QLabel(label_text)
label.setProperty('role', 'fieldLabel')
container_layout.addWidget(label)
self.line_edit = QLineEdit()
self.line_edit.setPlaceholderText(placeholder)
self.line_edit.setText(default_text)
container_layout.addWidget(self.line_edit)
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setFrameShadow(QFrame.Plain)
separator.setFixedHeight(1)
separator.setStyleSheet("background-color: #2C2F36; border: none;")
container_layout.addWidget(separator)
button_row = QHBoxLayout()
button_row.setContentsMargins(0, 0, 0, 0)
button_row.setSpacing(10)
button_row.addStretch()
self.confirmButton = QPushButton("确认")
self.confirmButton.setObjectName("primaryButton")
self.confirmButton.clicked.connect(self._onAccept)
button_row.addWidget(self.confirmButton)
self.cancelButton = QPushButton("取消")
self.cancelButton.setObjectName("secondaryButton")
self.cancelButton.clicked.connect(self.reject)
button_row.addWidget(self.cancelButton)
container_layout.addLayout(button_row)
content_layout.addWidget(content_container, 0, Qt.AlignTop)
base_layout.addWidget(content_widget)
main_layout.addWidget(base_frame)
self.confirmButton.setDefault(True)
self.confirmButton.setAutoDefault(True)
self.line_edit.selectAll()
self.line_edit.setFocus()
def _createTitleBar(self):
self.title_bar = QFrame()
self.title_bar.setObjectName("titleBar")
title_layout = QHBoxLayout(self.title_bar)
title_layout.setContentsMargins(12, 6, 12, 6)
title_layout.setSpacing(0)
controls = QWidget()
controls.setObjectName("controlButtons")
controls_layout = QHBoxLayout(controls)
controls_layout.setContentsMargins(0, 0, 0, 0)
controls_layout.setSpacing(0)
self.close_button = QPushButton()
self.close_button.setObjectName("closeButton")
self.close_button.clicked.connect(self.reject)
self.close_button.setFocusPolicy(Qt.NoFocus)
self.close_button.setFixedSize(18, 18)
controls_layout.addWidget(self.close_button)
self._applyTitleBarIcons()
left_placeholder = QWidget()
left_placeholder.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
title_layout.addWidget(left_placeholder)
self.title_label = QLabel(self.windowTitle())
self.title_label.setObjectName("titleLabel")
self.title_label.setAlignment(Qt.AlignCenter)
title_layout.addWidget(self.title_label, 1)
title_layout.addWidget(controls)
left_placeholder.setFixedWidth(controls.sizeHint().width())
def _applyTitleBarIcons(self):
if self._icon_close:
self.close_button.setIcon(self._icon_close)
self.close_button.setIconSize(self._title_icon_size)
self.close_button.setText("")
self.close_button.setToolTip("关闭")
def setWindowTitle(self, title):
super().setWindowTitle(title)
if hasattr(self, "title_label"):
self.title_label.setText(title)
def _onAccept(self):
if self.line_edit.text().strip():
self.accept()
def text(self):
return self.line_edit.text().strip()
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()):
self.dragging = True
self.drag_position = event.globalPos() - self.frameGeometry().topLeft()
event.accept()
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if event.buttons() == Qt.LeftButton and self.dragging:
self.move(event.globalPos() - self.drag_position)
event.accept()
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
self.dragging = False
super().mouseReleaseEvent(event)
class CustomTreeWidget(QTreeWidget):
"""自定义场景树部件"""
def __init__(self, world, parent=None):
if parent is None:
parent = wrapinstance(0, QWidget)
super().__init__(parent)
self.world = world
# self.selectedItems = None
self.initData()
self.setupUI() # 初始化界面
self.setupContextMenu() # 初始化右键菜单
self.setupDragDrop() # 设置拖拽功能
self.original_scales={}
def initData(self):
"""初始化变量"""
# 定义2D GUI元素类型
self.gui_2d_types = {
"GUI_BUTTON", # GUI 按钮
"GUI_LABEL", # GUI 标签
"GUI_ENTRY", # GUI 输入框
"GUI_IMAGE", # GUI 图片
"GUI_2D_VIDEO_SCREEN", # GUI 2D视频
"GUI_SPHERICAL_VIDEO", # GUI 3D球形视频
"GUI_NODE" # 其他2D GUI容器
}
# 定义3D GUI元素类型
self.gui_3d_types = {
"GUI_3DTEXT", # 3D 文本节点
"GUI_3DIMAGE", # 3D 图片节点
"GUI_VIRTUAL_SCREEN", # 3D视频
"GUI_VirtualScreen" # 3D虚拟视频
}
# 定义3D场景节点类型可以接受3D GUI元素和其他3D场景元素
self.scene_3d_types = {
"SCENE_ROOT",
"SCENE_NODE",
"LIGHT_NODE", # 灯节点
"CAMERA_NODE",
"IMPORTED_MODEL_NODE", # 导入模型节点
"MODEL_NODE",
"TERRAIN_NODE", # 地形节点
"CESIUM_TILESET_NODE" # 3D Tileset
}
# 这是一个最佳实践,它让代码的意图变得非常清晰。
self.valid_3d_parent_types = self.scene_3d_types.union(self.gui_3d_types)
def setupUI(self):
"""初始化UI设置"""
self.setHeaderHidden(True)
# 启用多选和拖拽
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.setDropIndicatorShown(True) # 启用拖放指示线
def setupDragDrop(self):
"""设置拖拽功能"""
# 使用自定义拖拽模式
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) # 或者使用 DragDrop
self.setDefaultDropAction(Qt.DropAction.MoveAction)
self.setDragEnabled(True)
self.setAcceptDrops(True)
def setupContextMenu(self):
"""设置右键菜单"""
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self.showContextMenu)
def showContextMenu(self, position):
"""显示右键菜单 - 复用主菜单的创建动作"""
if self.selectedItems():
item = self.selectedItems()[0]
print(f"为项目 '{item.text(0)}' 显示右键菜单")
# 创建右键菜单
menu = QMenu(self)
# 获取主窗口的创建菜单动作 - 关键修改
if hasattr(self.world, 'main_window'):
main_window = self.world.main_window
create_actions = main_window.getCreateMenuActions()
# 创建子菜单 - 复用主菜单结构
createMenu = menu.addMenu('创建')
# 基础对象
createMenu.addAction(create_actions['createEmpty'])
# 3D对象菜单
create3dObjectMenu = createMenu.addMenu('3D对象')
# 3D GUI菜单
create3dGUIMenu = createMenu.addMenu('3D GUI')
create3dGUIMenu.addAction(create_actions['create3DText'])
create3dGUIMenu.addAction(create_actions['create3DImage'])
# GUI菜单
createGUIMenu = createMenu.addMenu('GUI')
createGUIMenu.addAction(create_actions['createButton'])
createGUIMenu.addAction(create_actions['createLabel'])
createGUIMenu.addAction(create_actions['createEntry'])
createGUIMenu.addAction(create_actions['createImage'])
createGUIMenu.addSeparator()
createGUIMenu.addAction(create_actions['createVideoScreen'])
createGUIMenu.addAction(create_actions['createSphericalVideo'])
createGUIMenu.addSeparator()
createGUIMenu.addAction(create_actions['createVirtualScreen'])
# 光源菜单
createLightMenu = createMenu.addMenu('光源')
createLightMenu.addAction(create_actions['createSpotLight'])
createLightMenu.addAction(create_actions['createPointLight'])
else:
# 备用方案:如果无法获取主窗口动作,显示提示
createMenu = menu.addMenu('创建')
noActionItem = createMenu.addAction('功能不可用')
noActionItem.setEnabled(False)
# 添加删除选项
menu.addSeparator()
deleteAction = menu.addAction('删除')
if hasattr(self, 'delete_items'):
deleteAction.triggered.connect(lambda: self.delete_items(self.selectedItems()))
# 显示菜单
global_pos = self.mapToGlobal(position)
action = menu.exec_(global_pos)
print(f"右键菜单已显示在位置: {global_pos}")
if action:
print(f"用户选择了动作: {action.text()}")
else:
print("用户取消了菜单选择")
# 在 CustomTreeWidget 类的 dropEvent 方法中替换缩放处理部分
def dropEvent(self, event):
# 1. 获取所有被拖拽的项
dragged_items = self.selectedItems()
if not dragged_items:
event.ignore()
return
# 2. 在执行Qt的默认拖拽前记录所有拖拽项的原始状态
drag_info = []
for item in dragged_items:
panda_node = item.data(0, Qt.UserRole)
if not panda_node or panda_node.is_empty():
continue # 跳过无效节点
drag_info.append({
"item": item,
"node": panda_node,
"old_parent_node": item.parent().data(0, Qt.UserRole) if item.parent() else None
})
# 3. 执行Qt的默认拖拽让UI树先行更新
# 这一步会自动处理移动或复制,并将项目从旧父节点移除,添加到新父节点
super().dropEvent(event)
# 4. 遍历记录下的信息同步每一个Panda3D节点的状态
try:
for info in drag_info:
dragged_item = info["item"]
dragged_node = info["node"]
old_parent_node = info["old_parent_node"]
# 获取拖拽后的新父节点
new_parent_item = dragged_item.parent()
new_parent_node = new_parent_item.data(0, Qt.UserRole) if new_parent_item else None
# 仅当父节点实际发生变化时才执行重新父化
if old_parent_node != new_parent_node:
print(f"跨层级拖拽:从 {old_parent_node} 移动到 {new_parent_node}")
# # 保存世界坐标位置
# world_pos = dragged_node.getPos(self.world.render)
# world_hpr = dragged_node.getHpr(self.world.render)
# world_scale = dragged_node.getScale(self.world.render)
# 检查是否是2D GUI元素
dragged_type = dragged_item.data(0, Qt.UserRole + 1)
is_2d_gui = dragged_type in self.gui_2d_types
# 重新父化到新的父节点
if new_parent_node:
if is_2d_gui:
# 2D GUI元素需要特殊处理
if hasattr(new_parent_node, 'getTag') and new_parent_node.getTag("is_gui_element") == "1":
# 目标是GUI元素直接重新父化
dragged_node.wrtReparentTo(new_parent_node)
else:
# 目标是3D节点保持GUI特性重新父化到aspect2d
# from direct.showbase.ShowBase import aspect2d
dragged_node.wrtReparentTo(self.world.aspect2d)
print(f"2D GUI元素 {dragged_item.text(0)} 保持在aspect2d下")
else:
# 非GUI元素正常重新父化
dragged_node.wrtReparentTo(new_parent_node)
else:
# 如果新父节点为None根据元素类型决定父节点
if is_2d_gui:
# from direct.showbase.ShowBase import aspect2d
dragged_node.wrtReparentTo(self.world.aspect2d)
print(f"2D GUI元素 {dragged_item.text(0)} 重新父化到aspect2d")
else:
dragged_node.wrtReparentTo(self.world.render)
# # 恢复世界坐标位置对于2D GUI可能需要调整
# if is_2d_gui:
# # 2D GUI元素使用屏幕坐标系可能需要特殊处理
# dragged_node.setPos(world_pos)
# dragged_node.setHpr(world_hpr)
# dragged_node.setScale(world_scale)
# else:
# dragged_node.setPos(self.world.render, world_pos)
# dragged_node.setHpr(self.world.render, world_hpr)
# dragged_node.setScale(self.world.render, world_scale)
print(f"✅ Panda3D父子关系已更新")
else:
print(f"同层级移动父节点未变化跳过Panda3D重新父化")
except Exception as e:
print(f"⚠️ 同步Panda3D场景图失败: {e}")
# 不影响Qt树的更新继续执行
# 事后验证:确保节点仍在"场景"根节点下
self._ensureUnderSceneRoot(dragged_item)
self.world.property_panel._syncEffectiveVisibility(dragged_node)
event.accept()
def _ensureUnderSceneRoot(self, item):
"""确保节点在场景根节点下,如果不是则自动修正"""
if not item:
return
# 检查是否成为了顶级节点
if not item.parent():
# 通过数据标识判断是否是场景根节点
scene_root_marker = item.data(0, Qt.UserRole + 1)
if scene_root_marker != "SCENE_ROOT":
print(f"⚠️ 检测到节点 {item.text(0)} 意外成为顶级节点,正在修正...")
# 找到场景根节点
scene_root = None
for i in range(self.topLevelItemCount()):
top_item = self.topLevelItem(i)
if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT":
scene_root = top_item
break
if scene_root:
# 将节点移回场景根节点下
self.takeTopLevelItem(self.indexOfTopLevelItem(item))
scene_root.addChild(item)
print(f"✅ 已将节点 {item.text(0)} 移回场景根节点下")
def isValidParentChild(self, dragged_item, target_item):
"""检查是否是有效的父子关系(防止循环)+ GUI类型验证"""
# 1. 禁止拖放到自身
if dragged_item == target_item:
return False
# 2. 禁止拖到根节点之外(根节点本身除外)
target_root = self._getRootNode(target_item)
if not target_root or target_root.data(0, Qt.UserRole + 1) != "SCENE_ROOT":
print(f"❌ 目标节点 {target_item.text(0)} 不在场景下")
return False
# 3. 禁止拖拽场景根节点
dragged_root = self._getRootNode(dragged_item)
if (dragged_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT" or
not dragged_root or dragged_root.data(0, Qt.UserRole + 1) != "SCENE_ROOT"):
print(f"❌ 禁止拖拽场景根节点或根节点外的节点")
return False
# 4. Qt 树循环检查
current = target_item
while current:
if current == dragged_item:
print(f"❌ Qt 树检测:{target_item.text(0)}{dragged_item.text(0)} 的后代")
return False
current = current.parent()
# 5. GUI元素类型验证 - 新增功能
if not self._validateGUITypeCompatibility(dragged_item, target_item):
return False
return True
def _validateGUITypeCompatibility(self, dragged_item, target_item):
"""验证GUI元素类型兼容性"""
try:
# 获取节点类型标识
dragged_type = dragged_item.data(0, Qt.UserRole + 1)
target_type = target_item.data(0, Qt.UserRole + 1)
# 定义2D GUI元素类型
gui_2d_types = self.gui_2d_types
# 定义3D GUI元素类型
gui_3d_types = self.gui_3d_types
# 定义3D场景节点类型可以接受3D GUI元素和其他3D场景元素
scene_3d_types = self.scene_3d_types
# 检查拖拽元素的类型
is_dragged_2d_gui = dragged_type in gui_2d_types
is_dragged_3d_gui = dragged_type in gui_3d_types
is_dragged_3d_scene = dragged_type in scene_3d_types
# 检查目标的类型
is_target_2d_gui = target_type in gui_2d_types
is_target_3d_gui = target_type in gui_3d_types
is_target_3d_scene = target_type in scene_3d_types
# === 严格的类型隔离验证逻辑 ===
# 1. 2D GUI元素的拖拽限制 - 只能拖拽到其他2D GUI元素下
if is_dragged_2d_gui:
if target_type == "SCENE_ROOT":
return True
elif is_target_2d_gui:
print(f"✅ 2D GUI元素 {dragged_item.text(0)} 可以拖拽到2D GUI父节点 {target_item.text(0)}")
return True
elif is_target_3d_gui:
print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到3D GUI元素 {target_item.text(0)}")
print(" 💡 提示: 2D GUI元素只能作为其他2D GUI元素的子节点")
return False
elif is_target_3d_scene:
print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到3D场景节点 {target_item.text(0)}")
print(" 💡 提示: 2D GUI元素应该保持在2D GUI层级结构中")
return False
else:
print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)}")
return False
# 2. 3D GUI元素的拖拽限制 - 只能拖拽到3D场景节点下
elif is_dragged_3d_gui:
if is_target_3d_scene:
print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}")
return False
elif is_target_2d_gui:
print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)}")
print(" 💡 提示: 3D GUI元素不能与2D GUI元素建立父子关系")
return False
elif is_target_3d_gui:
print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到其他3D GUI元素 {target_item.text(0)}")
print(" 💡 提示: 允许3D GUI元素之间建立父子关系")
return True
else:
print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)}")
return False
# 3. 3D场景元素的拖拽限制 - 只能拖拽到3D场景节点或3D GUI元素下
elif is_dragged_3d_scene:
if is_target_3d_scene:
print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}")
return True
elif is_target_2d_gui:
print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)}")
print(" 💡 提示: 3D场景元素不能与2D GUI元素建立父子关系")
print(" 💡 建议: 将3D场景元素拖拽到其他3D场景节点或场景根节点下")
return False
elif is_target_3d_gui:
print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D GUI元素 {target_item.text(0)}")
print(" 💡 提示: 允许3D场景元素挂载在3D GUI元素下")
return False
else:
print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)}")
return False
# 4. 其他未分类元素 - 严格禁止拖拽到2D GUI下
else:
if is_target_2d_gui:
print(f"❌ 元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)}")
print(" 💡 提示: 只有2D GUI元素才能作为其他2D GUI元素的子节点")
return False
elif is_target_3d_gui or is_target_3d_scene:
print(f"✅ 允许元素 {dragged_item.text(0)} 拖拽到3D节点 {target_item.text(0)}")
return True
else:
print(f"❌ 元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)}")
return False
# 默认情况(理论上不应该到达这里)
print(f"⚠️ GUI类型验证遇到未处理的情况: {dragged_type} -> {target_type}")
return False
except Exception as e:
print(f"❌ GUI类型兼容性验证失败: {str(e)}")
# 出错时采用保守策略,禁止拖拽
return False
def _getRootNode(self, item):
"""获取树中节点的根节点项"""
if not item:
return None
current = item
while current.parent():
current = current.parent()
return current
def dragEnterEvent(self, event):
"""处理拖入事件"""
if event.source() == self:
event.accept()
else:
event.ignore()
def dragMoveEvent(self, event):
"""处理拖动事件"""
indicator_pos = self.dropIndicatorPosition()
indicator_str = "Unknown"
if indicator_pos == QAbstractItemView.DropIndicatorPosition.OnItem:
indicator_str = "OnItem"
elif indicator_pos == QAbstractItemView.DropIndicatorPosition.AboveItem:
indicator_str = "AboveItem"
elif indicator_pos == QAbstractItemView.DropIndicatorPosition.BelowItem:
indicator_str = "BelowItem"
elif indicator_pos == QAbstractItemView.DropIndicatorPosition.OnViewport:
indicator_str = "OnViewport"
#print(f'indicator pos: {indicator_str} (value: {int(indicator_pos)})')
if event.source() != self:
event.ignore()
return
# 获取当前拖拽的项目和目标位置
target_item = self.itemAt(event.pos())
selected_items = self.selectedItems()
# 检查是否拖拽到多选区域内的项目
if target_item and target_item in selected_items:
event.ignore()
return
# 检查其他禁止条件
if target_item and selected_items:
for dragged_item in selected_items:
if not self.isValidParentChild(dragged_item, target_item):
event.ignore()
return
super().dragMoveEvent(event)
event.accept()
def keyPressEvent(self, event):
"""处理键盘按键事件"""
if event.key() == Qt.Key_Delete:
# currentItem = self.currentItem()
# if currentItem and currentItem.parent():
# # 检查是否是模型节点或其子节点
# if self.world.interface_manager.isModelOrChild(currentItem):
# nodePath = currentItem.data(0, Qt.UserRole)
# if nodePath:
# print("正在删除节点...")
# self.world.interface_manager.deleteNode(nodePath, currentItem)
# print("删除完成")
selected_items = self.selectedItems()
if selected_items:
# 执行删除操作
# if selected_items.data(0, Qt.UserRole + 1) == "LIGHT_NODE":
# self._preprocess_light_items_for_deletion(selected_items)
self.delete_items(selected_items)
else:
# 没有选中任何项目,执行默认操作
super().keyPressEvent(event)
else:
super().keyPressEvent(event)
def _preprocess_light_items_for_deletion(self, selected_items):
"""预处理灯光节点删除,特别处理最后一个灯光节点的问题"""
if not selected_items:
return selected_items
# 检查选中的项目中是否包含灯光节点
light_items = []
for item in selected_items:
node_type = item.data(0, Qt.UserRole + 1)
if node_type == "LIGHT_NODE":
light_items.append(item)
# 如果没有灯光节点,直接返回
if not light_items:
return selected_items
# 检查是否只有最后一个灯光节点被选中
processed_items = list(selected_items) # 创建副本
for item in light_items:
panda_node = item.data(0, Qt.UserRole)
if not panda_node:
continue
# 获取灯光类型
if hasattr(panda_node, 'getTag'):
light_type = panda_node.getTag("light_type")
# 检查是否是最后一个Spotlight
if (light_type == "spot_light" and hasattr(self.world, 'Spotlight') and
self.world.Spotlight and self.world.Spotlight[-1] == panda_node and
len(self.world.Spotlight) > 1):
print(f"⚠️ 检测到选中最后一个Spotlight节点: {item.text(0)}")
# 这里可以添加特殊处理逻辑,比如提示用户或阻止删除
# 检查是否是最后一个Pointlight
elif (light_type == "point_light" and hasattr(self.world, 'Pointlight') and
self.world.Pointlight and self.world.Pointlight[-1] == panda_node and
len(self.world.Pointlight) > 1):
print(f"⚠️ 检测到选中最后一个Pointlight节点: {item.text(0)}")
# 这里可以添加特殊处理逻辑,比如提示用户或阻止删除
return processed_items
def delete_items(self, selected_items):
"""删除选中的item - 简化版本"""
if not selected_items:
return
# 1. 过滤掉不能删除的节点
deletable_items = []
for item in selected_items:
node_type = item.data(0, Qt.UserRole + 1)
panda_node = item.data(0, Qt.UserRole)
# 跳过场景根节点和主相机
if (node_type == "SCENE_ROOT" or
(panda_node and hasattr(self.world, 'cam') and panda_node == self.world.cam)):
continue
deletable_items.append(item)
if not deletable_items:
StyledMessageBox.information(self, "提示", "没有可删除的节点")
return
# 2. 确认删除
item_count = len(deletable_items)
if item_count == 1:
message = f"确定要删除节点 \"{deletable_items[0].text(0)}\" 吗?"
else:
message = f"确定要删除 {item_count} 个节点吗?"
dialog = UniversalMessageDialog(
self,
"确认删除",
message,
message_type=UniversalMessageDialog.WARNING,
show_cancel=True,
confirm_text="确认",
cancel_text="取消"
)
if dialog.exec_() != QDialog.Accepted:
return
# 默认选中场景根节点,通常是第一个顶级节点
#next_item_to_select = self.topLevelItem(0)
# 3. 执行删除循环
deleted_count = 0
for item in deletable_items:
try:
# 在删除前,记录其父节点,作为删除后的新选择
# 选择最后一个被删除项的父节点作为新的焦点
if item.parent():
next_item_to_select = item.parent()
panda_node = item.data(0, Qt.UserRole)
if panda_node:
# 清理选择状态
if (hasattr(self.world, 'selection') and
hasattr(self.world.selection, 'selectedNode') and
self.world.selection.selectedNode == panda_node):
self.world.selection.updateSelection(None)
# 清理特殊资源参考interface_manager.py的逻辑
if hasattr(self.world, 'property_panel'):
self.world.property_panel.removeActorForModel(panda_node)
# 清理灯光
if hasattr(panda_node, 'getPythonTag'):
light_object = panda_node.getPythonTag('rp_light_object')
if light_object and hasattr(self.world, 'render_pipeline'):
self.world.render_pipeline.remove_light(light_object)
# 从world列表中移除
if hasattr(self.world, 'gui_elements') and panda_node in self.world.gui_elements:
self.world.gui_elements.remove(panda_node)
if hasattr(self.world, 'models') and panda_node in self.world.models:
self.world.models.remove(panda_node)
if hasattr(self.world, 'Spotlight') and panda_node in self.world.Spotlight:
self.world.Spotlight.remove(panda_node)
if hasattr(self.world, 'Pointlight') and panda_node in self.world.Pointlight:
self.world.Pointlight.remove(panda_node)
if hasattr(self.world, 'terrains') and panda_node in self.world.terrains:
self.world.terrains.remove(panda_node)
if hasattr(self.world, 'tilesets') and panda_node in self.world.tilesets:
# self.world.tilesets.remove(panda_node)
# 从 tilesets 列表中移除
if hasattr(self.world, 'scene_manager'):
tilesets_to_remove = []
for i, tileset_info in enumerate(self.world.scene_manager.tilesets):
if tileset_info['node'] == panda_node:
tilesets_to_remove.append(i)
# 从后往前删除,避免索引问题
for i in reversed(tilesets_to_remove):
del self.world.scene_manager.tilesets[i]
# 从Panda3D场景中移除
try:
if not panda_node.isEmpty():
panda_node.removeNode()
except Exception as e:
print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}")
# 从Qt树中移除
parent_item = item.parent()
if parent_item:
parent_item.removeChild(item)
else:
index = self.indexOfTopLevelItem(item)
if index >= 0:
self.takeTopLevelItem(index)
deleted_count += 1
print(f"✅ 删除节点: {item.text(0)}")
except Exception as e:
print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}")
import traceback
traceback.print_exc()
# 最终清理
# if hasattr(self.world, 'property_panel'):
# self.world.property_panel.clearPropertyPanel()
# 4. 删除操作完成后更新UI ---
if deleted_count > 0:
print(f"🎉 成功删除 {deleted_count} 个节点。正在更新UI...")
self.update_selection_and_properties(None, None)
def delete_item(self, panda_node):
"""删除指定节点 panda3Dnode- 优化和修复版本"""
if not panda_node or panda_node.is_empty():
print(" 尝试删除一个空的或无效的节点,操作取消。")
return
# #如果有命令管理系统,则使用命令系统
# if hasattr(self.world,'command_manager') and self.world.command_manager:
# from core.Command_System import DeleteNodeCommand
# parent_node = panda_node.getParent()
# command = DeleteNodeCommand(panda_node,parent_node)
# self.world.command_manager.execute_command(command)
# return
# --- 关键修复:在操作前,安全地获取节点名字 ---
node_name_for_logging = panda_node.getName()
# 1. 寻找对应的Qt Item
item = self.world.interface_manager.findTreeItem(panda_node, self._findSceneRoot())
# 场景清理无论是否找到item都应该执行
self._cleanup_panda_node_resources(panda_node)
panda_node.removeNode()
# 如果没有找到item说明UI已经移除或不同步清理完Panda3D资源后即可退出
if not item:
print(f"✅ Panda3D节点 '{node_name_for_logging}' 已清理并移除。UI树中未找到对应项。")
return
try:
# 2. 过滤受保护节点
node_type = item.data(0, Qt.UserRole + 1)
if node_type == "SCENE_ROOT": # 相机检查已包含在panda_node判空中
print(f" 节点 {item.text(0)} 是受保护节点,无法删除。")
return
# 3. 从UI树中移除
parent_for_next_selection = item.parent()
if item.parent():
item.parent().removeChild(item)
else:
index = self.indexOfTopLevelItem(item)
if index >= 0:
self.takeTopLevelItem(index)
print(f"✅ 成功删除节点: {node_name_for_logging}")
# 4. 更新UI
print(f"🔄 正在更新UI...")
if parent_for_next_selection and self.indexFromItem(parent_for_next_selection).isValid():
new_selection_item = parent_for_next_selection
else:
new_selection_item = self.topLevelItem(0)
if new_selection_item:
self.setCurrentItem(new_selection_item)
new_panda_node_to_select = new_selection_item.data(0, Qt.UserRole)
self.update_selection_and_properties(new_panda_node_to_select, new_selection_item)
else:
self.update_selection_and_properties(None, None)
except Exception as e:
print(f"❌ 删除节点 {node_name_for_logging} 时发生意外错误: {str(e)}")
import traceback
traceback.print_exc()
def clear_tree(self):
"""清空UI树"""
print("Clear")
self.clear()
# 创建场景根节点
sceneRoot = QTreeWidgetItem(self, ['场景'])
sceneRoot.setData(0, Qt.UserRole, self.world.render)
sceneRoot.setData(0, Qt.UserRole + 1, "SCENE_ROOT")
self._apply_font_to_item(sceneRoot)
# 添加相机节点
cameraItem = QTreeWidgetItem(sceneRoot, ['相机'])
cameraItem.setData(0, Qt.UserRole, self.world.cam)
cameraItem.setData(0, Qt.UserRole + 1, "CAMERA_NODE")
self._apply_font_to_item(cameraItem)
# 添加地板节点
if hasattr(self.world, 'ground') and self.world.ground:
groundItem = QTreeWidgetItem(sceneRoot, ['地板'])
groundItem.setData(0, Qt.UserRole, self.world.ground)
groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE")
self._apply_font_to_item(groundItem)
def _apply_font_to_item(self, item):
"""根据节点层级设置字体大小"""
if not item:
return
font = QFont(self.font())
marker = item.data(0, Qt.UserRole + 1)
if item.parent() is None and (marker == "SCENE_ROOT" or item.text(0) == '场景'):
font = QFont("Microsoft YaHei", 12)
font.setWeight(QFont.Light)
else:
font = QFont("Microsoft YaHei", 10)
font.setWeight(QFont.Light)
item.setFont(0, font)
def _apply_font_recursively(self, item):
"""递归应用字体样式"""
if not item:
return
self._apply_font_to_item(item)
for index in range(item.childCount()):
self._apply_font_recursively(item.child(index))
def _handle_rows_inserted(self, parent_index, first, last):
"""在插入新节点时自动调整字体"""
model = self.model()
if not model:
return
for row in range(first, last + 1):
index = model.index(row, 0, parent_index)
item = self.itemFromIndex(index)
if item:
self._apply_font_recursively(item)
def _cleanup_panda_node_resources(self, panda_node):
"""一个集中的辅助函数用于清理与Panda3D节点相关的所有资源。"""
if not panda_node or panda_node.is_empty():
return
try:
# 清理选择状态
if hasattr(self.world, 'selection') and self.world.selection.selectedNode == panda_node:
self.world.selection.updateSelection(None)
# 清理属性面板
if hasattr(self.world, 'property_panel'):
self.world.property_panel.removeActorForModel(panda_node)
# 清理灯光
if hasattr(panda_node, 'getPythonTag'):
light_object = panda_node.getPythonTag('rp_light_object')
if light_object and hasattr(self.world, 'render_pipeline'):
self.world.render_pipeline.remove_light(light_object)
# 从各种world管理列表中移除
lists_to_check = ['gui_elements', 'models', 'Spotlight', 'Pointlight', 'terrains']
for list_name in lists_to_check:
if hasattr(self.world, list_name):
world_list = getattr(self.world, list_name)
if panda_node in world_list:
world_list.remove(panda_node)
# 特殊处理tilesets
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'tilesets'):
tilesets_to_remove = [i for i, info in enumerate(self.world.scene_manager.tilesets) if
info.get('node') == panda_node]
for i in reversed(tilesets_to_remove):
del self.world.scene_manager.tilesets[i]
print(f"🧹 已清理节点 {panda_node.getName()} 的所有关联资源。")
except Exception as e:
# 即便这里出错,也要打印信息,但不要让整个删除流程中断
print(f"⚠️ 清理节点 {panda_node.getName()} 资源时出错: {e}")
# def mousePressEvent(self, event):
# """鼠标按下事件"""
# if event.button() == Qt.LeftButton:
# if self.currentItem():
# print(f"self.currentItem() = {self.currentItem()}")
# else:
# print(f"self.currentItem() = None")
#
# # 调用父类处理其他事件
# super().mousePressEvent(event)
def update_item_name(self, text, item):
""" 树节点名字 """
if not item:
return
try:
# 正确的代码
node = item.data(0, Qt.UserRole)
item.setText(0, text)
node.setName(text)
except Exception as e:
print(e)
def _findSceneRoot(self):
"""查找场景根节点"""
for i in range(self.topLevelItemCount()):
top_item = self.topLevelItem(i)
if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT":
return top_item
return None
def create_model_items(self, model: NodePath):
"""
【此函数保持不变】
创建模型项。
只寻找模型下一层带有 'is_scene_element' 标签的子节点作为分支的根,
然后完整地展示这些分支。
"""
if not model:
print("传入的参数model为空")
return
# 找到场景树的根节点,我们将把模型节点添加到这里
root_item = self._findSceneRoot()
if not root_item:
print("错误:未能找到场景根节点项")
return
# 1. 在模型的第一层子节点中进行筛选
for child_node in model.getChildren():
if child_node.hasTag("is_scene_element"):
print(f"找到带标签的根节点:{child_node.getName()}")
if (child_node.hasTag("gui_type")and
child_node.getTag("gui_type") in ["3d_text","3d_image","video_screen"]):
print(f"跳过3dGUI节点{child_node.getName()}")
continue
# 为这个带标签的节点创建一个树项
child_item = QTreeWidgetItem(root_item)
child_item.setText(0, child_node.getName() or "Unnamed Tagged Node")
child_item.setData(0, Qt.UserRole, child_node)
child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type"))
# self._add_node_info(child_item, child_node) # 可选信息
# 2. 对这个节点的所有后代进行“无条件”递归添加 (但会跳过碰撞体)
self._add_all_children_unconditionally(child_item, child_node)
def _add_all_children_unconditionally(self, parent_item: QTreeWidgetItem, node_path: NodePath):
"""
【此函数已更新】
无条件地、递归地添加一个节点下的所有子节点,但会跳过碰撞节点。
"""
for child_node in node_path.getChildren():
# 新增:检查节点是否为碰撞节点
if isinstance(child_node.node(), CollisionNode):
# print(f"跳过碰撞节点: {child_node.getName()}") # 用于调试
continue # 如果是,则跳过此节点及其所有子节点
# 创建子项
child_item = QTreeWidgetItem(parent_item)
child_item.setText(0, child_node.getName() or "Unnamed Child")
child_item.setData(0, Qt.UserRole, child_node)
child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type"))
# self._add_node_info(child_item, child_node) # 可选信息
# 继续无条件地递归
if not child_node.is_empty():
self._add_all_children_unconditionally(child_item, child_node)
# ==================== 辅助方法 ====================
def _findSceneRoot(self):
"""查找场景根节点"""
for i in range(self.topLevelItemCount()):
top_item = self.topLevelItem(i)
if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT":
return top_item
return
def _generateUniqueNodeName(self, base_name, parent_node):
"""生成唯一的节点名称"""
# 获取父节点下所有子节点的名称
existing_names = set()
for child in parent_node.getChildren():
existing_names.add(child.getName())
# 如果基础名称不存在,直接使用
if base_name not in existing_names:
return base_name
# 否则添加数字后缀
counter = 1
while f"{base_name}_{counter}" in existing_names:
counter += 1
return f"{base_name}_{counter}"
def add_node_to_tree_widget(self, node, parent_item, node_type):
"""将node元素添加到树形控件"""
if hasattr(node, 'getTag'):
if node.hasTag('tree_item_type'):
print(f"node0: {node.getName()},{node.getTag('tree_item_type')}")
tree_type = node.getTag('tree_item_type')
else:
node.setTag('tree_item_type', node_type)
else:
print(f"node2: {node.getName()},{node_type}")
tree_type = node_type
# BLACK_LIST 和依赖项导入保持不变
BLACK_LIST = {'', '**', 'temp', 'collision'}
from panda3d.core import CollisionNode, ModelRoot
from PyQt5.QtWidgets import QTreeWidgetItem
from PyQt5.QtCore import Qt
# 1. 修改内部函数,让它返回创建的节点
def addNodeToTree(node, parentItem, force=False):
"""内部递归函数,现在会返回创建的顶级节点项"""
if not force and should_skip(node):
return None # 如果跳过返回None
nodeItem = QTreeWidgetItem(parentItem, [node.getName()])
nodeItem.setData(0, Qt.UserRole, node)
nodeItem.setData(0, Qt.UserRole + 1, tree_type)
for child in node.getChildren():
# 递归调用但我们只关心顶级的nodeItem
addNodeToTree(child, nodeItem, force=False)
return nodeItem # <-- 新增返回创建的QTreeWidgetItem
def should_skip(node):
name = node.getName()
return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(), CollisionNode) or isinstance(
node.node(), ModelRoot) or name == ""
# 使用一个变量来确保无论哪个分支都有返回值
new_qt_item = None
node_name = ""
try:
if tree_type == "IMPORTED_MODEL_NODE":
# getTag('file') 可能是你自己设置的tag这里假设它存在
node_name = node.getTag("file") if hasattr(node, 'getTag') and node.hasTag("file") else node.getName()
# 2. 接收 addNodeToTree 的返回值
new_qt_item = addNodeToTree(node, parent_item, force=True)
else:
node_name = node.getName() if hasattr(node, 'getName') else "node"
new_qt_item = QTreeWidgetItem(parent_item, [node_name])
new_qt_item.setData(0, Qt.UserRole, node)
new_qt_item.setData(0, Qt.UserRole + 1, tree_type)
# 确保 new_qt_item 成功创建后再继续操作
if new_qt_item:
# 展开父节点
if hasattr(parent_item, 'setExpanded'):
parent_item.setExpanded(True)
print(f"✅ Qt树节点添加成功: {node_name}")
return new_qt_item
else:
# 如果 addNodeToTree 因为 should_skip 返回了 None
print(f" 节点 {node_name} 被跳过,未添加到树中。")
return None
except Exception as e:
import traceback
print(f"❌ 添加node到树形控件失败: {str(e)}")
traceback.print_exc() # 打印更详细的错误堆栈,方便调试
return None
def update_selection_and_properties(self, node, qt_item):
"""更新选择状态和属性面板"""
try:
# 更新选择状态
if hasattr(self.world, 'selection'):
self.world.selection.updateSelection(node)
# 更新属性面板
if hasattr(self.world, 'property_panel'):
self.world.property_panel.updatePropertyPanel(qt_item)
elif hasattr(self.world, 'updatePropertyPanel'):
self.world.updatePropertyPanel(qt_item)
print(f"✅ 更新选择和属性面板: {qt_item.text(0)}")
except Exception as e:
print(f"❌ 更新选择和属性面板失败: {str(e)}")
# ==================== 3D辅助方法 ====================
def get_target_parents_for_creation(self):
"""获取创建目标的父节点列表"""
from PyQt5.QtCore import Qt
target_parents = []
try:
selected_items = self.selectedItems()
# 检查选中的项目,找出所有有效的父节点
for item in selected_items:
if self._isValidParentForNewNode(item):
parent_node = item.data(0, Qt.UserRole)
if parent_node:
target_parents.append((item, parent_node))
print(f"📍 找到有效父节点: {item.text(0)}")
# 如果没有有效的选中节点,使用场景根节点
if not target_parents:
print("⚠️ 没有有效选中节点,使用场景根节点")
scene_root = self._findSceneRoot()
if scene_root:
parent_node = scene_root.data(0, Qt.UserRole)
if parent_node:
target_parents.append((scene_root, parent_node))
else:
# 如果没有_findSceneRoot方法使用world.render作为默认父节点
if hasattr(self.world, 'render'):
# 创建一个虚拟的树项目来表示render节点
class MockTreeItem:
def text(self, column):
return "render"
mock_item = MockTreeItem()
target_parents.append((mock_item, self.world.render))
print(f"📊 总共找到 {len(target_parents)} 个目标父节点")
return target_parents
except Exception as e:
print(f"❌ 获取目标父节点失败: {str(e)}")
return []
def _isValidParentForNewNode(self, item):
"""检查节点是否适合作为新节点的父节点-3d"""
if not item:
return False
# 获取节点类型标识
node_type = item.data(0, Qt.UserRole + 1)
# 场景根节点和普通场景节点可以作为父节点
if node_type in self.valid_3d_parent_types:
return True
# # 模型节点也可以作为父节点
# panda_node = item.data(0, Qt.UserRole)
# if panda_node and panda_node.hasTag("is_model_root"):
# return True
#
# # 其他类型的节点也可以,但排除一些特殊情况
# if panda_node:
# # # 排除相机节点
# # if panda_node == self.world.cam:
# # return False
# # 排除碰撞节点
# from panda3d.core import CollisionNode
# if isinstance(panda_node.node(), CollisionNode):
# return False
# return True
return False
def get_target_parents_for_gui_creation(self):
"""获取GUI创建目标的父节点列表 - 支持GUI元素作为父节点"""
from PyQt5.QtCore import Qt
target_parents = []
try:
selected_items = self.selectedItems()
# 检查选中的项目,找出所有有效的父节点
for item in selected_items:
if self.isValidParentForGUI(item):
parent_node = item.data(0, Qt.UserRole)
if parent_node:
target_parents.append((item, parent_node)) # 修复:确保添加到列表
print(f"📍 找到有效GUI父节点: {item.text(0)}")
else:
print(f"⚠️ GUI父节点 {item.text(0)} 的Panda3D数据为空")
# 如果没有有效的选中节点,使用场景根节点
if not target_parents:
print("⚠️ 没有有效选中节点,使用场景根节点")
scene_root = self._findSceneRoot()
if scene_root:
parent_node = scene_root.data(0, Qt.UserRole)
if parent_node:
target_parents.append((scene_root, parent_node))
else:
if hasattr(self.world, 'render'):
class MockTreeItem:
def text(self, column):
return "render"
mock_item = MockTreeItem()
target_parents.append((mock_item, self.world.render))
print(f"📊 总共找到 {len(target_parents)} 个目标父节点")
return target_parents
except Exception as e:
print(f"❌ 获取目标父节点失败: {str(e)}")
return []
def isValidParentForGUI(self, item):
"""检查节点是否适合作为GUI元素的父节点"""
if not item:
return False
# 获取节点类型标识
node_type = item.data(0, Qt.UserRole + 1)
# GUI元素可以作为其他GUI元素的父节点
if node_type in self.gui_2d_types:
return True
# 场景根节点和普通场景节点也可以作为父节点
if node_type in ["SCENE_ROOT"]:
return True
return False
def is_gui_element(self, node):
"""判断节点是否是GUI元素"""
if not node:
return False
# 检查是否有GUI标签
if hasattr(node, 'getTag'):
return node.getTag("is_gui_element") == "1" or node.getTag("gui_type") in ["info_panel", "button", "label", "entry", "3d_text", "virtual_screen"]
# 检查是否是DirectGUI对象
try:
from direct.gui.DirectGuiBase import DirectGuiBase
return isinstance(node, DirectGuiBase)
except ImportError:
return False
def calculate_relative_gui_position(self, pos, parent_gui):
"""计算相对于GUI父节点的位置"""
try:
# 对于GUI子元素使用相对坐标
# 这里使用较小的缩放因子因为是相对于父GUI的位置
relative_pos = (pos[0] * 0.05, 0, pos[2] * 0.05)
print(f"📐 计算GUI相对位置: {pos} -> {relative_pos}")
return relative_pos
except Exception as e:
print(f"❌ 计算GUI相对位置失败: {str(e)}")
# 如果计算失败,返回默认的屏幕坐标
return (pos[0] * 0.1, 0, pos[2] * 0.1)
#---------------------------暂时无用-------------------------------
def add_existing_node(self, panda_node, node_type="SCENE_NODE", parent_item=None):
"""将已存在的Panda3D节点添加到Qt树形控件中
Args:
panda_node: 已创建的Panda3D节点
node_type: 节点类型标识
parent_item: 父Qt项目如果为None则使用当前选中项或根节点
Returns:
QTreeWidgetItem: 创建的Qt树项目
"""
try:
if not panda_node:
print("❌ 传入的Panda3D节点为空")
return None
# 确定父项目 - 保持简单,只处理单个父节点
if parent_item is None:
# 优先使用当前选中的第一个有效节点作为父节点
selected_items = self.selectedItems()
if selected_items:
# 找到第一个有效的父节点
for potential_parent in selected_items:
if self._isValidParentForNewNode(potential_parent):
parent_item = potential_parent
print(f"📍 使用选中节点作为父节点: {parent_item.text(0)}")
break
# 如果没有找到有效的选中节点
if not parent_item:
print("⚠️ 所有选中节点都不适合作为父节点,查找场景根节点")
parent_item = self._findSceneRoot()
else:
# 没有选中任何节点,使用场景根节点
print("📍 没有选中节点,使用场景根节点作为父节点")
parent_item = self._findSceneRoot()
# 如果场景根节点也找不到最后尝试render节点
if not parent_item:
print("📍 场景根节点未找到尝试使用render节点")
parent_item = self._findRenderItem()
if not parent_item:
print("❌ 无法找到合适的父节点")
return None
# 创建Qt树项目
node_name = panda_node.getName()
new_qt_item = QTreeWidgetItem(parent_item, [node_name])
new_qt_item.setData(0, Qt.UserRole, panda_node)
new_qt_item.setData(0, Qt.UserRole + 1, node_type)
# 展开父节点
parent_item.setExpanded(True)
print(f"✅ 成功将现有节点添加到树形控件: {node_name} -> 父节点: {parent_item.text(0)}")
return new_qt_item
except Exception as e:
print(f"❌ 添加现有节点到树形控件失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def _findRenderItem(self):
"""查找render根节点项目"""
try:
root = self.invisibleRootItem()
for i in range(root.childCount()):
item = root.child(i)
if item.text(0) == "render":
return item
# 如果没找到render节点返回第一个子项目
if root.childCount() > 0:
return root.child(0)
return None
except Exception as e:
print(f"查找render节点失败: {e}")
return None
def create_item(self, node_type="empty", selected_items=None):
"""创建不同类型的场景节点
Args:
node_type: 节点类型 ("empty", "spot_light", "point_light")
selected_items: 选中的父节点项目列表如果为None则使用当前选中项或根节点
"""
try:
# 确定父节点
parent_items = self._determineParentItems(selected_items)
if not parent_items:
print("⚠️ 无法确定父节点")
return []
created_nodes = []
for parent_item in parent_items:
# 验证父节点的有效性
if not self._isValidParentForNewNode(parent_item):
print(f"⚠️ 节点 {parent_item.text(0)} 不适合作为父节点")
continue
# 获取父节点的Panda3D对象
parent_node = parent_item.data(0, Qt.UserRole)
if not parent_node:
print(f"⚠️ 父节点 {parent_item.text(0)} 没有对应的Panda3D对象")
continue
# 根据节点类型创建不同的节点
if node_type == "empty":
new_node = self._createEmptyNode(parent_node, parent_item)
elif node_type == "spot_light":
new_node = self._createSpotLightNode(parent_node, parent_item)
elif node_type == "point_light":
new_node = self._createPointLightNode(parent_node, parent_item)
else:
print(f"❌ 不支持的节点类型: {node_type}")
continue
if new_node:
created_nodes.append(new_node)
# 如果只创建了一个节点,自动选中它
if len(created_nodes) == 1:
_, qt_item = created_nodes[0]
self.setCurrentItem(qt_item)
# 更新选择和属性面板
if hasattr(self.world, 'selection'):
self.world.selection.updateSelection(qt_item.data(0, Qt.UserRole))
if hasattr(self.world, 'property_panel'):
self.world.property_panel.updatePropertyPanel(qt_item)
print(f"✅ 总共创建了 {len(created_nodes)}{node_type} 节点")
return created_nodes
except Exception as e:
print(f"❌ 创建 {node_type} 节点失败: {str(e)}")
import traceback
traceback.print_exc()
return []
def _determineParentItems(self, selected_items):
"""确定父节点项目列表"""
if selected_items is not None:
return selected_items
# 使用当前选中的项目
current_selected = self.selectedItems()
if current_selected:
return current_selected
# 如果没有选中任何项目,使用场景根节点
scene_root = self._findSceneRoot()
if scene_root:
return [scene_root]
return []
def _setupNewNodeDefaults(self, node):
"""设置新节点的默认属性"""
# 设置默认位置(相对于父节点)
node.setPos(0, 0, 0)
node.setHpr(0, 0, 0)
node.setScale(1, 1, 1)
# 设置默认可见性
node.show()
# 可以根据需要添加更多默认设置
# 例如:默认材质、碰撞检测等
# ==================== 创建节点 ====================
def _createEmptyNode(self, parent_node, parent_item):
"""创建空节点"""
# 生成唯一的节点名称
node_name = self._generateUniqueNodeName("空节点", parent_node)
# 在Panda3D场景中创建新节点
new_panda_node = parent_node.attachNewNode(node_name)
# 设置新节点的默认属性
self._setupNewNodeDefaults(new_panda_node)
# 设置节点标签
new_panda_node.setTag("is_scene_element", "1")
new_panda_node.setTag("node_type", "empty_node")
new_panda_node.setTag("created_by_user", "1")
# 在Qt树中创建对应的项目
new_qt_item = QTreeWidgetItem(parent_item, [node_name])
new_qt_item.setData(0, Qt.UserRole, new_panda_node)
new_qt_item.setData(0, Qt.UserRole + 1, "SCENE_NODE")
# 展开父节点
parent_item.setExpanded(True)
print(f"✅ 成功创建空节点: {node_name}")
return (new_panda_node, new_qt_item)
def _createSpotLightNode(self, parent_node, parent_item):
"""创建聚光灯节点"""
from RenderPipelineFile.rpcore import SpotLight
from QMeta3D.Meta3DWorld import get_render_pipeline
from panda3d.core import Vec3, NodePath
try:
render_pipeline = get_render_pipeline()
# 生成唯一的节点名称
light_name = self._generateUniqueNodeName(f"Spotlight_{len(self.world.Spotlight)}", parent_node)
# 创建挂载节点
light_np = NodePath(light_name)
light_np.reparentTo(parent_node)
# 创建聚光灯对象
light = SpotLight()
light.direction = Vec3(0, 0, -1)
light.fov = 70
light.set_color_from_temperature(5 * 1000.0)
light.energy = 5000
light.radius = 1000
light.casts_shadows = True
light.shadow_map_resolution = 256
light.setPos(0, 0, 0) # 相对于父节点的位置
# 添加到渲染管线
render_pipeline.add_light(light)
# 设置节点属性和标签
light_np.setTag("light_type", "spot_light")
light_np.setTag("is_scene_element", "1")
light_np.setTag("light_energy", str(light.energy))
light_np.setTag("created_by_user", "1")
# 保存光源对象引用
light_np.setPythonTag("rp_light_object", light)
# 添加到管理列表
self.world.Spotlight.append(light_np)
# 在Qt树中创建对应的项目
new_qt_item = QTreeWidgetItem(parent_item, [light_name])
new_qt_item.setData(0, Qt.UserRole, light_np)
new_qt_item.setData(0, Qt.UserRole + 1, "LIGHT_NODE")
# 展开父节点
parent_item.setExpanded(True)
print(f"✅ 成功创建聚光灯: {light_name}")
except Exception as e:
print(f"❌ 创建聚光灯失败: {str(e)}")
return None
def _createPointLightNode(self, parent_node, parent_item):
"""创建点光源节点"""
from RenderPipelineFile.rpcore import PointLight
from QMeta3D.Meta3DWorld import get_render_pipeline
from panda3d.core import Vec3, NodePath
try:
render_pipeline = get_render_pipeline()
# 生成唯一的节点名称
light_name = self._generateUniqueNodeName(f"Pointlight_{len(self.world.Pointlight)}", parent_node)
# 创建挂载节点
light_np = NodePath(light_name)
light_np.reparentTo(parent_node)
# 创建点光源对象
light = PointLight()
light.setPos(0, 0, 0) # 相对于父节点的位置
light.energy = 5000
light.radius = 1000
light.inner_radius = 0.4
light.set_color_from_temperature(5 * 1000.0)
light.casts_shadows = True
light.shadow_map_resolution = 256
# 添加到渲染管线
render_pipeline.add_light(light)
# 设置节点属性和标签
light_np.setTag("light_type", "point_light")
light_np.setTag("is_scene_element", "1")
light_np.setTag("light_energy", str(light.energy))
light_np.setTag("created_by_user", "1")
# 保存光源对象引用
light_np.setPythonTag("rp_light_object", light)
# 添加到管理列表
self.world.Pointlight.append(light_np)
# 在Qt树中创建对应的项目
new_qt_item = QTreeWidgetItem(parent_item, [light_name])
new_qt_item.setData(0, Qt.UserRole, light_np)
new_qt_item.setData(0, Qt.UserRole + 1, "LIGHT_NODE")
# 展开父节点
parent_item.setExpanded(True)
print(f"✅ 成功创建点光源: {light_name}")
return (light_np, new_qt_item)
except Exception as e:
print(f"❌ 创建点光源失败: {str(e)}")
return None