586 lines
21 KiB
Python
586 lines
21 KiB
Python
"""
|
||
自定义Qt部件模块
|
||
|
||
包含所有自定义的Qt界面组件:
|
||
- NewProjectDialog: 新建项目对话框
|
||
- CustomPanda3DWidget: 自定义Panda3D显示部件
|
||
- CustomFileView: 自定义文件浏览器
|
||
- CustomTreeWidget: 自定义场景树部件
|
||
"""
|
||
|
||
import os
|
||
import re
|
||
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QGroupBox, QHBoxLayout,
|
||
QLineEdit, QPushButton, QLabel, QDialogButtonBox,
|
||
QTreeView, QTreeWidget, QTreeWidgetItem, QWidget,
|
||
QFileDialog, QMessageBox, QAbstractItemView)
|
||
from PyQt5.QtCore import Qt, QUrl
|
||
from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush
|
||
from PyQt5.sip import wrapinstance
|
||
from QPanda3D.QPanda3DWidget import QPanda3DWidget
|
||
|
||
|
||
class NewProjectDialog(QDialog):
|
||
"""新建项目对话框"""
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.setWindowTitle("新建项目")
|
||
self.setMinimumWidth(500)
|
||
|
||
# 创建布局
|
||
layout = QVBoxLayout(self)
|
||
|
||
# 创建路径选择部分
|
||
pathGroup = QGroupBox("项目路径")
|
||
pathLayout = QHBoxLayout()
|
||
self.pathEdit = QLineEdit()
|
||
self.pathEdit.setReadOnly(True)
|
||
browseButton = QPushButton("浏览...")
|
||
browseButton.clicked.connect(self.browsePath)
|
||
pathLayout.addWidget(self.pathEdit)
|
||
pathLayout.addWidget(browseButton)
|
||
pathGroup.setLayout(pathLayout)
|
||
layout.addWidget(pathGroup)
|
||
|
||
# 创建项目名称部分
|
||
nameGroup = QGroupBox("项目名称")
|
||
nameLayout = QVBoxLayout()
|
||
self.nameEdit = QLineEdit()
|
||
self.nameEdit.setText("新项目")
|
||
self.nameEdit.selectAll()
|
||
nameLayout.addWidget(self.nameEdit)
|
||
# 添加提示标签
|
||
self.tipLabel = QLabel("项目名称只能包含字母、数字、下划线、中划线和中文")
|
||
self.tipLabel.setStyleSheet("color: gray;")
|
||
nameLayout.addWidget(self.tipLabel)
|
||
nameGroup.setLayout(nameLayout)
|
||
layout.addWidget(nameGroup)
|
||
|
||
# 添加按钮
|
||
buttonBox = QDialogButtonBox(
|
||
QDialogButtonBox.Ok | QDialogButtonBox.Cancel
|
||
)
|
||
buttonBox.accepted.connect(self.validate)
|
||
buttonBox.rejected.connect(self.reject)
|
||
layout.addWidget(buttonBox)
|
||
|
||
# 存储结果
|
||
self.projectPath = ""
|
||
self.projectName = ""
|
||
|
||
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:
|
||
QMessageBox.warning(self, "错误", "请选择项目路径!")
|
||
return
|
||
|
||
# 获取并验证项目名称
|
||
self.projectName = self.nameEdit.text().strip()
|
||
if not self.projectName:
|
||
QMessageBox.warning(self, "错误", "请输入项目名称!")
|
||
return
|
||
|
||
# 验证项目名称格式
|
||
if not re.match(r'^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$', self.projectName):
|
||
QMessageBox.warning(self, "错误",
|
||
"项目名称只能包含字母、数字、下划线、中划线和中文!")
|
||
return
|
||
|
||
# 检查项目是否已存在
|
||
full_path = os.path.join(self.projectPath, self.projectName)
|
||
if os.path.exists(full_path):
|
||
QMessageBox.warning(self, "错误", "项目已存在!")
|
||
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)
|
||
#self.world.addAnimationPanel(None,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)
|
||
self.world = world
|
||
self.setDragEnabled(True) # 启用拖拽
|
||
self.setSelectionMode(QTreeView.ExtendedSelection) # 允许多选
|
||
self.setDragDropMode(QTreeView.DragOnly) # 只允许拖出,不允许拖入
|
||
|
||
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)
|
||
|
||
|
||
class CustomTreeWidget(QTreeWidget):
|
||
"""自定义场景树部件"""
|
||
|
||
def __init__(self, world, parent=None):
|
||
if parent is None:
|
||
parent = wrapinstance(0, QWidget)
|
||
super().__init__(parent)
|
||
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(QAbstractItemView.DragDropMode.InternalMove) # 或者使用 DragDrop
|
||
self.setDefaultDropAction(Qt.DropAction.MoveAction)
|
||
self.setDragEnabled(True)
|
||
self.setAcceptDrops(True)
|
||
|
||
def dropEvent(self, event):
|
||
dragged_item = self.currentItem()
|
||
target_item = self.itemAt(event.pos())
|
||
|
||
if not dragged_item or not target_item:
|
||
event.ignore()
|
||
return
|
||
|
||
if not self.isValidParentChild(dragged_item, target_item):
|
||
event.ignore()
|
||
return
|
||
|
||
dragged_node = dragged_item.data(0, Qt.UserRole)
|
||
target_node = target_item.data(0, Qt.UserRole)
|
||
|
||
if not dragged_node or not target_node:
|
||
event.ignore()
|
||
return
|
||
|
||
# # 检查是否是有效的父子关系
|
||
# if self.isValidParentChild(dragged_item, target_item):
|
||
# # 保存当前的世界坐标
|
||
# world_pos = dragged_node.getPos(self.world.render)
|
||
#
|
||
# # 更新场景图中的父子关系
|
||
# dragged_node.wrtReparentTo(target_node)
|
||
#
|
||
# # 接受拖放事件,更新树形控件
|
||
# super().dropEvent(event)
|
||
#
|
||
# #self.world.property_panel.updateNodeVisibilityAfterDrag(dragged_item)
|
||
# # 更新属性面板
|
||
# self.world.updatePropertyPanel(dragged_item)
|
||
# self.world.property_panel._syncEffectiveVisibility(dragged_node)
|
||
|
||
|
||
|
||
print(f"dragged_node: {dragged_node}, target_node: {target_node}")
|
||
|
||
# 记录拖拽前的父节点
|
||
old_parent_item = dragged_item.parent()
|
||
old_parent_node = old_parent_item.data(0, Qt.UserRole) if old_parent_item else None
|
||
|
||
# 执行Qt默认拖拽
|
||
super().dropEvent(event)
|
||
|
||
# 检查拖拽后的父节点
|
||
new_parent_item = dragged_item.parent()
|
||
new_parent_node = new_parent_item.data(0, Qt.UserRole) if new_parent_item else None
|
||
|
||
# 同步Panda3D场景图的父子关系
|
||
try:
|
||
# 检查是否是跨层级拖拽(父节点发生变化)
|
||
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)
|
||
|
||
# 重新父化到新的父节点
|
||
if new_parent_node:
|
||
dragged_node.reparentTo(new_parent_node)
|
||
else:
|
||
# 如果新父节点为None,重新父化到render
|
||
dragged_node.reparentTo(self.world.render)
|
||
|
||
# 恢复世界坐标位置
|
||
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()
|
||
|
||
# try:
|
||
# world_pos = dragged_node.getPos(self.world.render)
|
||
#
|
||
# parent_of_dragged = dragged_node.getParent()
|
||
# target_node.wrtReparentTo(parent_of_dragged)
|
||
#
|
||
# # 拖动节点到目标节点下
|
||
# dragged_node.wrtReparentTo(target_node)
|
||
# dragged_node.setPos(self.world.render, world_pos)
|
||
#
|
||
# # 更新 Qt 树控件
|
||
# super().dropEvent(event)
|
||
#
|
||
# # 更新属性面板
|
||
# self.world.updatePropertyPanel(dragged_item)
|
||
#
|
||
# event.accept()
|
||
#
|
||
# except Exception as e:
|
||
# print(f"重设父节点失败: {e}")
|
||
# event.ignore()
|
||
|
||
def _ensureUnderSceneRoot(self, item):
|
||
"""确保节点在场景根节点下,如果不是则自动修正"""
|
||
if not item:
|
||
return
|
||
|
||
# 检查是否成为了顶级节点
|
||
if not item.parent():
|
||
# 如果节点名称不是"场景",说明意外成为了顶级节点
|
||
if item.text(0) != "场景":
|
||
print(f"⚠️ 检测到节点 {item.text(0)} 意外成为顶级节点,正在修正...")
|
||
|
||
# 找到场景根节点
|
||
scene_root = None
|
||
for i in range(self.topLevelItemCount()):
|
||
top_item = self.topLevelItem(i)
|
||
if top_item.text(0) == "场景":
|
||
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):
|
||
"""检查是否是有效的父子关系(防止循环)"""
|
||
|
||
# 1. 禁止拖放到自身
|
||
if dragged_item == target_item:
|
||
return False
|
||
|
||
# 2. 禁止拖到根节点之外(根节点本身除外)
|
||
target_root = self._getRootNode(target_item)
|
||
if target_root != "场景":
|
||
print(f"❌ 目标节点 {target_item.text(0)} 不在场景下")
|
||
return False
|
||
|
||
# 3. 禁止拖拽"场景"根节点
|
||
dragged_root = self._getRootNode(dragged_item)
|
||
if dragged_item.text(0) == "场景" or dragged_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()
|
||
|
||
return True
|
||
|
||
def _getRootNode(self, item):
|
||
"""获取树中节点的根节点文本"""
|
||
current = item
|
||
while current.parent():
|
||
current = current.parent()
|
||
return current.text(0)
|
||
|
||
def dragEnterEvent(self, event):
|
||
"""处理拖入事件"""
|
||
if event.source() == self:
|
||
event.accept()
|
||
else:
|
||
event.ignore()
|
||
|
||
def dragMoveEvent(self, event):
|
||
"""处理拖动事件"""
|
||
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:
|
||
# 执行删除操作
|
||
self.delete_items(selected_items)
|
||
else:
|
||
# 没有选中任何项目,执行默认操作
|
||
super().keyPressEvent(event)
|
||
else:
|
||
super().keyPressEvent(event)
|
||
|
||
def delete_items(self, selected_items):
|
||
"""删除选中的项目"""
|
||
if not selected_items:
|
||
return
|
||
|
||
# 准备确认对话框的内容
|
||
item_count = len(selected_items)
|
||
if item_count == 1:
|
||
item_names = f'"{selected_items[0].text(0)}"'
|
||
title = "确认删除"
|
||
message = f"确定要删除节点 {item_names} 吗?"
|
||
else:
|
||
item_names = "、".join([f'"{item.text(0)}"' for item in selected_items[:3]])
|
||
if item_count > 3:
|
||
item_names += f" 等 {item_count} 个节点"
|
||
title = "确认批量删除"
|
||
message = f"确定要删除以下 {item_count} 个节点吗?\n\n{item_names}"
|
||
|
||
# 创建确认对话框
|
||
reply = QMessageBox.question(
|
||
self,
|
||
title,
|
||
message,
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
QMessageBox.No # 默认选择"取消",防止误删
|
||
)
|
||
|
||
# 只有用户确认后才执行删除
|
||
if reply == QMessageBox.Yes:
|
||
pass
|
||
print(f"✅ 已删除 {item_count} 个节点") |