EG/ui/widgets.py
2025-08-15 15:32:04 +08:00

430 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""
自定义Qt部件模块
包含所有自定义的Qt界面组件
- NewProjectDialog: 新建项目对话框
- CustomPanda3DWidget: 自定义Panda3D显示部件
- CustomFileView: 自定义文件浏览器
- CustomTreeWidget: 自定义场景树部件
"""
import os
import re
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, 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
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)
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
# 获取节点引用
dragged_node = dragged_item.data(0, Qt.UserRole)
# 如果目标是模型根节点,使用 render 作为新父节点
if target_item.text(0) == "模型":
target_node = self.world.render
else:
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.updatePropertyPanel(dragged_item)
else:
event.ignore()
def isValidParentChild(self, dragged_item, target_item):
"""检查是否是有效的父子关系"""
# 不能拖放到自己上
if dragged_item == target_item:
return False
# 不能拖放到自己的子节点上
parent = target_item
while parent:
if parent == dragged_item:
return False
parent = parent.parent()
# 检查目标项
if target_item.text(0) == "场景":
return False # 不能拖放到场景根节点
# 允许拖放到模型根节点或其他模型节点
if target_item.text(0) == "模型":
return True
# 检查目标项的父节点
target_parent = target_item.parent()
if not target_parent:
return False
# 允许在模型节点下的任何位置调整父子关系
while target_parent:
if target_parent.text(0) == "模型":
return True
target_parent = target_parent.parent()
return False
def dragEnterEvent(self, event):
"""处理拖入事件"""
if event.source() == self:
event.accept()
else:
event.ignore()
def dragMoveEvent(self, event):
"""处理拖动事件"""
if event.source() == self:
event.accept()
else:
event.ignore()
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("删除完成")
else:
super().keyPressEvent(event)