4308 lines
162 KiB
Python
4308 lines
162 KiB
Python
"""
|
||
自定义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 QPanda3D.QPanda3DWidget import QPanda3DWidget
|
||
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 CustomPanda3DWidget(QPanda3DWidget):
|
||
"""自定义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)
|
||
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(1000) # 每秒更新一次
|
||
|
||
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):
|
||
"""删除指定节点 panda3D(node)- 优化和修复版本"""
|
||
if not panda_node or panda_node.is_empty():
|
||
print("ℹ️ 尝试删除一个空的或无效的节点,操作取消。")
|
||
return
|
||
|
||
# #如果有命令管理系统,则使用命令系统
|
||
# if hasattr(self.world,'command_manager') and self.world.command_manager:
|
||
# from core.Command_System import DeleteNodeCommand
|
||
# parent_node = panda_node.getParent()
|
||
# command = DeleteNodeCommand(panda_node,parent_node)
|
||
# self.world.command_manager.execute_command(command)
|
||
# return
|
||
|
||
# --- 关键修复:在操作前,安全地获取节点名字 ---
|
||
node_name_for_logging = panda_node.getName()
|
||
|
||
# 1. 寻找对应的Qt Item
|
||
item = self.world.interface_manager.findTreeItem(panda_node, self._findSceneRoot())
|
||
|
||
# 场景清理(无论是否找到item,都应该执行)
|
||
self._cleanup_panda_node_resources(panda_node)
|
||
panda_node.removeNode()
|
||
|
||
# 如果没有找到item,说明UI已经移除或不同步,清理完Panda3D资源后即可退出
|
||
if not item:
|
||
print(f"✅ Panda3D节点 '{node_name_for_logging}' 已清理并移除。UI树中未找到对应项。")
|
||
return
|
||
try:
|
||
# 2. 过滤受保护节点
|
||
node_type = item.data(0, Qt.UserRole + 1)
|
||
if node_type == "SCENE_ROOT": # 相机检查已包含在panda_node判空中
|
||
print(f"ℹ️ 节点 {item.text(0)} 是受保护节点,无法删除。")
|
||
return
|
||
|
||
# 3. 从UI树中移除
|
||
parent_for_next_selection = item.parent()
|
||
if item.parent():
|
||
item.parent().removeChild(item)
|
||
else:
|
||
index = self.indexOfTopLevelItem(item)
|
||
if index >= 0:
|
||
self.takeTopLevelItem(index)
|
||
|
||
print(f"✅ 成功删除节点: {node_name_for_logging}")
|
||
|
||
# 4. 更新UI
|
||
print(f"🔄 正在更新UI...")
|
||
if parent_for_next_selection and self.indexFromItem(parent_for_next_selection).isValid():
|
||
new_selection_item = parent_for_next_selection
|
||
else:
|
||
new_selection_item = self.topLevelItem(0)
|
||
|
||
if new_selection_item:
|
||
self.setCurrentItem(new_selection_item)
|
||
new_panda_node_to_select = new_selection_item.data(0, Qt.UserRole)
|
||
self.update_selection_and_properties(new_panda_node_to_select, new_selection_item)
|
||
else:
|
||
self.update_selection_and_properties(None, None)
|
||
|
||
except Exception as e:
|
||
print(f"❌ 删除节点 {node_name_for_logging} 时发生意外错误: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
def clear_tree(self):
|
||
"""清空UI树"""
|
||
print("Clear")
|
||
self.clear()
|
||
# 创建场景根节点
|
||
sceneRoot = QTreeWidgetItem(self, ['场景'])
|
||
sceneRoot.setData(0, Qt.UserRole, self.world.render)
|
||
sceneRoot.setData(0, Qt.UserRole + 1, "SCENE_ROOT")
|
||
self._apply_font_to_item(sceneRoot)
|
||
# 添加相机节点
|
||
cameraItem = QTreeWidgetItem(sceneRoot, ['相机'])
|
||
cameraItem.setData(0, Qt.UserRole, self.world.cam)
|
||
cameraItem.setData(0, Qt.UserRole + 1, "CAMERA_NODE")
|
||
self._apply_font_to_item(cameraItem)
|
||
# 添加地板节点
|
||
if hasattr(self.world, 'ground') and self.world.ground:
|
||
groundItem = QTreeWidgetItem(sceneRoot, ['地板'])
|
||
groundItem.setData(0, Qt.UserRole, self.world.ground)
|
||
groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE")
|
||
self._apply_font_to_item(groundItem)
|
||
|
||
def _apply_font_to_item(self, item):
|
||
"""根据节点层级设置字体大小"""
|
||
if not item:
|
||
return
|
||
font = QFont(self.font())
|
||
marker = item.data(0, Qt.UserRole + 1)
|
||
if item.parent() is None and (marker == "SCENE_ROOT" or item.text(0) == '场景'):
|
||
font.setPointSize(12)
|
||
font.setWeight(QFont.Medium)
|
||
else:
|
||
font.setPointSize(10)
|
||
font.setWeight(QFont.Normal)
|
||
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 QPanda3D.Panda3DWorld 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 QPanda3D.Panda3DWorld 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
|
||
|
||
|
||
|