MetaWhiz/scripts/ui/main_window.py
2025-04-17 13:00:39 +08:00

648 lines
26 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.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Tool - Main Window UI
Provides the main UI window for the Tool plugin
"""
import os
import sys
import maya.cmds as cmds
import maya.OpenMayaUI as omui
# According to the import logic in Main.py, use the global variable QT_VERSION
# We don't try to import here, but use the modules already imported in Main.py
# If running this file directly (not through Main.py), you need to import manually
if 'QtCore' not in globals():
import maya.cmds as cmds
import maya.mel as mel
# Get Maya version
MAYA_VERSION = int(cmds.about(version=True).split()[0])
# Choose appropriate PySide version based on Maya version
if MAYA_VERSION >= 2025:
try:
from PySide6 import QtCore, QtGui, QtWidgets
from shiboken6 import wrapInstance
print(f"Using PySide6 (Maya {MAYA_VERSION})")
except ImportError as e:
print(f"PySide6 import error: {str(e)}")
try:
from PySide2 import QtCore, QtGui, QtWidgets
from shiboken2 import wrapInstance
print(f"Using PySide2 (fallback mode, Maya {MAYA_VERSION})")
except ImportError:
cmds.error("Cannot import PySide6 or PySide2, please make sure they are installed")
elif MAYA_VERSION >= 2022:
try:
from PySide2 import QtCore, QtGui, QtWidgets
from shiboken2 import wrapInstance
print(f"使用PySide2 (Maya {MAYA_VERSION})")
except ImportError:
try:
from PySide6 import QtCore, QtGui, QtWidgets
from shiboken6 import wrapInstance
print(f"Using PySide6 (non-standard configuration, Maya {MAYA_VERSION})")
except ImportError:
cmds.error("Cannot import PySide2 or PySide6, please make sure they are installed")
else:
try:
from PySide2 import QtCore, QtGui, QtWidgets
from shiboken2 import wrapInstance
print(f"使用PySide2 (Maya {MAYA_VERSION})")
except ImportError:
try:
from PySide import QtCore, QtGui, QtWidgets
from shiboken import wrapInstance
print(f"Using PySide (Maya {MAYA_VERSION})")
except ImportError:
cmds.error("Cannot import PySide or PySide2, please make sure they are installed")
# Import configuration
import config
# Import UI modules
from ui.dna_editor import DNAEditorWidget
from ui.calibration import CalibrationWidget
from ui.binding import BindingWidget
from ui.blendshape import BlendShapeWidget
from ui.settings_dialog import SettingsDialog
# Import utility modules
from utils.settings_utils import SettingsUtils
def maya_main_window():
"""Get the Maya main window as a QWidget"""
main_window_ptr = omui.MQtUtil.mainWindow()
if main_window_ptr:
return wrapInstance(int(main_window_ptr), QtWidgets.QWidget)
return None
class ToolWindow(QtWidgets.QMainWindow):
"""Main window for the Tool plugin"""
def __init__(self, parent=maya_main_window()):
"""Initialize the main window"""
super(ToolWindow, self).__init__(parent)
self.setWindowTitle(f"{config.TOOL_NAME} {config.TOOL_VERSION}")
self.setMinimumSize(900, 600)
self.setWindowFlags(QtCore.Qt.Window)
# Initialize settings utility
self.settings_utils = SettingsUtils()
self.settings = self.settings_utils.get_all_settings()
# Set window icon
icon_path = os.path.join(config.ICONS_PATH, "Logo", "Tool_logo.png")
if os.path.exists(icon_path):
self.setWindowIcon(QtGui.QIcon(icon_path))
# Initialize UI
self._create_widgets()
self._create_layouts()
self._create_connections()
self._load_stylesheet()
# Apply user settings
self.apply_settings(self.settings)
def _create_widgets(self):
"""Create main window widgets"""
# Create header frame
self.header_frame = QtWidgets.QFrame()
self.header_frame.setObjectName("headerFrame")
self.header_frame.setMinimumHeight(60)
self.header_frame.setMaximumHeight(60)
# 创建标题布局
header_layout = QtWidgets.QHBoxLayout(self.header_frame)
header_layout.setContentsMargins(10, 5, 10, 5)
# 创建Logo标签
self.logo_label = QtWidgets.QLabel()
self.logo_label.setObjectName("Logo")
# 根据当前主题选择Logo
theme = self.settings_utils.get_setting("ui", "theme", "dark")
logo_filename = "logo_dark.png" if theme == "light" else "logo.png"
logo_path = os.path.join(config.ICONS_PATH, "Logo", logo_filename)
# 如果特定主题的Logo不存在尝试使用默认Logo
if not os.path.exists(logo_path):
logo_path = os.path.join(config.ICONS_PATH, "logo.png")
if os.path.exists(logo_path):
# 使用Qt版本兼容的方式缩放图像
if hasattr(QtCore.Qt, "KeepAspectRatio"):
logo_pixmap = QtGui.QPixmap(logo_path).scaled(
40, 40,
QtCore.Qt.KeepAspectRatio,
QtCore.Qt.SmoothTransformation
)
else:
# 兼容PySide6的枚举变化
logo_pixmap = QtGui.QPixmap(logo_path).scaled(
40, 40,
QtCore.Qt.AspectRatioMode.KeepAspectRatio,
QtCore.Qt.TransformationMode.SmoothTransformation
)
self.logo_label.setPixmap(logo_pixmap)
self.logo_label.setMinimumSize(40, 40)
self.logo_label.setMaximumSize(40, 40)
# 创建标题标签
self.title_label = QtWidgets.QLabel(f"{config.TOOL_NAME}")
self.title_label.setObjectName("welcomeLabel")
# 创建版本标签
self.version_label = QtWidgets.QLabel(f"v{config.TOOL_VERSION}")
self.version_label.setObjectName("versionLabel")
# 添加控件到标题布局
header_layout.addWidget(self.logo_label)
header_layout.addWidget(self.title_label)
header_layout.addStretch()
header_layout.addWidget(self.version_label)
# 创建主标签控件
self.tab_widget = QtWidgets.QTabWidget()
self.tab_widget.setTabPosition(QtWidgets.QTabWidget.North)
self.tab_widget.setDocumentMode(True)
self.tab_widget.setMovable(True)
# 创建标签页控件
try:
self.dna_editor_widget = DNAEditorWidget(self)
self.calibration_widget = CalibrationWidget(self)
self.binding_widget = BindingWidget(self)
self.blendshape_widget = BlendShapeWidget(self)
# 添加标签页
self.tab_widget.addTab(self.dna_editor_widget, "DNA 编辑器")
self.tab_widget.addTab(self.calibration_widget, "骨骼校准")
self.tab_widget.addTab(self.binding_widget, "自定义绑定")
self.tab_widget.addTab(self.blendshape_widget, "BlendShape 编辑")
except Exception as e:
print(f"创建标签页控件出错: {str(e)}")
import traceback
traceback.print_exc()
# 创建底部状态栏
self.status_bar = QtWidgets.QStatusBar()
self.status_bar.setObjectName("footerFrame")
self.setStatusBar(self.status_bar)
self.status_bar.showMessage(f"{config.TOOL_NAME} {config.TOOL_VERSION} 已加载")
# 创建菜单栏
self._create_menu_bar()
def _create_menu_bar(self):
"""Create menu bar for the main window"""
# Create menu bar
self.menu_bar = QtWidgets.QMenuBar()
self.setMenuBar(self.menu_bar)
# File menu
self.file_menu = self.menu_bar.addMenu("文件")
# Load DNA action
self.load_dna_action = QtWidgets.QAction("加载 DNA 文件", self)
self.load_dna_action.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "open.png")))
self.file_menu.addAction(self.load_dna_action)
# Save DNA action
self.save_dna_action = QtWidgets.QAction("保存 DNA 文件", self)
self.save_dna_action.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "save.png")))
self.file_menu.addAction(self.save_dna_action)
self.file_menu.addSeparator()
# Export FBX action
self.export_fbx_action = QtWidgets.QAction("导出 FBX", self)
self.export_fbx_action.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "export.png")))
self.file_menu.addAction(self.export_fbx_action)
self.file_menu.addSeparator()
# Exit action
self.exit_action = QtWidgets.QAction("退出", self)
self.exit_action.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "exit.png")))
self.file_menu.addAction(self.exit_action)
# Tools menu
self.tools_menu = self.menu_bar.addMenu("工具")
# Create MetaHuman Rig action
self.create_rig_action = QtWidgets.QAction("创建 MetaHuman 骨架", self)
self.tools_menu.addAction(self.create_rig_action)
# Batch Export BlendShapes action
self.batch_export_blendshapes_action = QtWidgets.QAction("批量导出 BlendShape", self)
self.tools_menu.addAction(self.batch_export_blendshapes_action)
# Settings menu
self.settings_menu = self.menu_bar.addMenu("设置")
# Preferences action
self.preferences_action = QtWidgets.QAction("首选项", self)
self.preferences_action.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "settings.png")))
self.settings_menu.addAction(self.preferences_action)
# Reset settings action
self.reset_settings_action = QtWidgets.QAction("重置设置", self)
self.reset_settings_action.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "reset.png")))
self.settings_menu.addAction(self.reset_settings_action)
# Help menu
self.help_menu = self.menu_bar.addMenu("帮助")
# About action
self.about_action = QtWidgets.QAction("关于", self)
self.help_menu.addAction(self.about_action)
# Documentation action
self.documentation_action = QtWidgets.QAction("文档", self)
self.help_menu.addAction(self.documentation_action)
def _create_layouts(self):
"""Create layouts for the main window"""
# Create central widget
central_widget = QtWidgets.QWidget()
self.setCentralWidget(central_widget)
# Create main layout
main_layout = QtWidgets.QVBoxLayout(central_widget)
main_layout.setContentsMargins(5, 5, 5, 5)
main_layout.setSpacing(5)
# Add tab widget to main layout
main_layout.addWidget(self.tab_widget)
def _create_connections(self):
"""创建信号/槽连接"""
try:
# 文件菜单连接
self.load_dna_action.triggered.connect(self._on_load_dna)
self.save_dna_action.triggered.connect(self._on_save_dna)
self.export_fbx_action.triggered.connect(self._on_export_fbx)
self.exit_action.triggered.connect(self.close)
# 工具菜单连接
self.create_rig_action.triggered.connect(self._on_create_rig)
self.batch_export_blendshapes_action.triggered.connect(self._on_batch_export_blendshapes)
# 设置菜单连接
self.preferences_action.triggered.connect(self._on_preferences)
self.reset_settings_action.triggered.connect(self._on_reset_settings)
# Help menu connections
self.about_action.triggered.connect(self._on_about)
self.documentation_action.triggered.connect(self._on_documentation)
# Tab change connections
self.tab_widget.currentChanged.connect(self._on_tab_changed)
print("UI signal/slot connections created")
except Exception as e:
print(f"Error creating signal/slot connections: {str(e)}")
import traceback
traceback.print_exc()
def _load_stylesheet(self, stylesheet_name="dark_theme.qss"):
"""Load main window stylesheet"""
try:
# Try to load specified stylesheet from style directory
stylesheet_path = os.path.join(config.STYLE_PATH, stylesheet_name)
if os.path.exists(stylesheet_path):
with open(stylesheet_path, 'r', encoding='utf-8') as f:
stylesheet = f.read()
# Adjust stylesheet for Qt version
qt_version = None
if 'QT_VERSION' in globals():
qt_version = globals()['QT_VERSION']
if qt_version == "PySide6":
# Adjust stylesheet for PySide6
# Replace known incompatible selectors and properties
stylesheet = stylesheet.replace("::item:hover", ":item:hover")
stylesheet = stylesheet.replace("::item:selected", ":item:selected")
stylesheet = stylesheet.replace("QTabBar::tab:hover:!selected", "QTabBar::tab:hover")
# Add PySide6-specific selectors
stylesheet += "\nQTabBar::tab:selected:hover { background-color: #2D2D30; }\n"
self.setStyleSheet(stylesheet)
print(f"Stylesheet loaded: {stylesheet_path}")
# Update status bar message
if hasattr(self, "status_bar"):
theme_name = "Dark" if "dark" in stylesheet_name else "Light"
self.status_bar.showMessage(f"{theme_name} theme applied")
# If specified stylesheet does not exist, try to load default stylesheet
elif hasattr(config, 'TOOL_STYLE_FILE') and os.path.exists(config.TOOL_STYLE_FILE):
with open(config.TOOL_STYLE_FILE, 'r', encoding='utf-8') as f:
self.setStyleSheet(f.read())
print(f"Default stylesheet loaded: {config.TOOL_STYLE_FILE}")
else:
print("Stylesheet file not found, using built-in style")
except Exception as e:
print(f"Error loading stylesheet: {str(e)}")
import traceback
traceback.print_exc()
def _on_load_dna(self):
"""Handle load DNA action"""
file_path, _ = QtWidgets.QFileDialog.getOpenFileName(
self,
"Select DNA File",
config.DNA_FILE_PATH,
"DNA Files (*.dna);;All Files (*.*)"
)
if file_path:
# Import DNA utils here to avoid circular imports
from utils.dna_utils import DNAUtils
dna_utils = DNAUtils()
try:
result = dna_utils.load_dna(file_path)
if result:
self.status_bar.showMessage(f"DNA file loaded: {os.path.basename(file_path)}")
else:
self.status_bar.showMessage(f"Failed to load DNA file: {os.path.basename(file_path)}")
except Exception as e:
self.status_bar.showMessage(f"Error loading DNA file: {str(e)}")
def _on_save_dna(self):
"""Handle save DNA action"""
file_path, _ = QtWidgets.QFileDialog.getSaveFileName(
self,
"Save DNA File",
config.DNA_FILE_PATH,
"DNA Files (*.dna);;All Files (*.*)"
)
if file_path:
# Import DNA utils here to avoid circular imports
from utils.dna_utils import DNAUtils
dna_utils = DNAUtils()
try:
result = dna_utils.save_dna(file_path)
if result:
self.status_bar.showMessage(f"DNA file saved: {os.path.basename(file_path)}")
else:
self.status_bar.showMessage(f"Failed to save DNA file: {os.path.basename(file_path)}")
except Exception as e:
self.status_bar.showMessage(f"Error saving DNA file: {str(e)}")
def _on_export_fbx(self):
"""Handle export FBX action"""
file_path, _ = QtWidgets.QFileDialog.getSaveFileName(
self,
"Export FBX File",
"",
"FBX Files (*.fbx);;All Files (*.*)"
)
if file_path:
# Import Maya utils here to avoid circular imports
from utils.maya_utils import MayaUtils
maya_utils = MayaUtils()
try:
result = maya_utils.export_fbx(file_path)
if result:
self.status_bar.showMessage(f"FBX file exported: {os.path.basename(file_path)}")
else:
self.status_bar.showMessage(f"Failed to export FBX file: {os.path.basename(file_path)}")
except Exception as e:
self.status_bar.showMessage(f"Error exporting FBX file: {str(e)}")
def _on_create_rig(self):
"""Handle create MetaHuman rig action"""
# Import joint utils here to avoid circular imports
from utils.joint_utils import JointUtils
joint_utils = JointUtils()
try:
# Get selected mesh
selection = cmds.ls(selection=True)
if not selection:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"Please select a mesh first"
)
return
mesh = selection[0]
# Create rig
result = joint_utils.create_metahuman_skeleton(mesh)
if result:
self.status_bar.showMessage(f"MetaHuman skeleton created for {mesh}")
else:
self.status_bar.showMessage(f"Failed to create MetaHuman skeleton for {mesh}")
except Exception as e:
self.status_bar.showMessage(f"Error creating MetaHuman skeleton: {str(e)}")
def _on_batch_export_blendshapes(self):
"""Handle batch export blendshapes action"""
dir_path = QtWidgets.QFileDialog.getExistingDirectory(
self,
"Select Export Directory",
""
)
if dir_path:
# Import blendshape utils here to avoid circular imports
from utils.blendshape_utils import BlendShapeUtils
blendshape_utils = BlendShapeUtils()
try:
result = blendshape_utils.batch_export(dir_path)
if result:
self.status_bar.showMessage(f"Blendshapes exported to: {dir_path}")
else:
self.status_bar.showMessage(f"Failed to export blendshapes to: {dir_path}")
except Exception as e:
self.status_bar.showMessage(f"Error exporting blendshapes: {str(e)}")
def _on_about(self):
"""Handle about action"""
try:
# Get Maya and Python version information
import maya.cmds as cmds
import platform
maya_version = cmds.about(version=True)
python_version = platform.python_version()
os_name = platform.system()
about_text = f"""
<h2>{config.TOOL_NAME} {config.TOOL_VERSION}</h2>
<p><b>Author:</b> {config.TOOL_AUTHOR}</p>
<p><b>Maya Version:</b> {maya_version}</p>
<p><b>Python Version:</b> {python_version}</p>
<p><b>Operating System:</b> {os_name}</p>
<p><b>Description:</b> A Maya plugin for working with MetaHuman models, providing DNA editing, skeleton calibration, custom binding, and blendshape editing features.</p>
<p><b>Copyright 2025</b></p>
"""
# Create custom about dialog
about_dialog = QtWidgets.QDialog(self)
about_dialog.setWindowTitle(f"About {config.TOOL_NAME}")
about_dialog.setMinimumSize(400, 300)
about_dialog.setWindowFlags(about_dialog.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
# Create layout
layout = QtWidgets.QVBoxLayout(about_dialog)
# Add logo
logo_label = QtWidgets.QLabel()
logo_label.setAlignment(QtCore.Qt.AlignCenter)
logo_path = os.path.join(config.ICONS_PATH, "Logo.png")
if os.path.exists(logo_path):
logo_pixmap = QtGui.QPixmap(logo_path).scaled(100, 100, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
logo_label.setPixmap(logo_pixmap)
layout.addWidget(logo_label)
# Add text information
info_label = QtWidgets.QLabel()
info_label.setText(about_text)
info_label.setTextFormat(QtCore.Qt.RichText)
info_label.setWordWrap(True)
info_label.setAlignment(QtCore.Qt.AlignCenter)
layout.addWidget(info_label)
# Add OK button
button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok)
button_box.accepted.connect(about_dialog.accept)
layout.addWidget(button_box)
# Show dialog
about_dialog.exec_()
except Exception as e:
print(f"Error showing about dialog: {str(e)}")
# Fall back to simple message box
QtWidgets.QMessageBox.about(
self,
f"About {config.TOOL_NAME}",
f"{config.TOOL_NAME} {config.TOOL_VERSION}\nAuthor: {config.TOOL_AUTHOR}"
)
def _on_documentation(self):
"""Handle documentation action"""
import webbrowser
webbrowser.open("https://epicgames.github.io/MetaHuman-DNA-Calibration/index.html")
def _on_tab_changed(self, index):
"""Handle tab change"""
tab_name = self.tab_widget.tabText(index)
self.status_bar.showMessage(f"Current module: {tab_name}")
def _on_preferences(self):
"""Handle preferences action"""
try:
# Create settings dialog
settings_dialog = SettingsDialog(self)
settings_dialog.exec_()
except Exception as e:
print(f"Error opening settings dialog: {str(e)}")
import traceback
traceback.print_exc()
def _on_reset_settings(self):
"""Handle reset settings action"""
try:
# Ask user if they want to reset settings
reply = QtWidgets.QMessageBox.question(
self,
"Reset Settings",
"Are you sure you want to reset all settings to default values?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
QtWidgets.QMessageBox.No
)
if reply == QtWidgets.QMessageBox.Yes:
# Reset settings
self.settings_utils.reset_settings()
self.settings = self.settings_utils.get_all_settings()
# Apply settings
self.apply_settings(self.settings)
# Show confirmation message
self.status_bar.showMessage("All settings have been reset to default values")
except Exception as e:
print(f"Error resetting settings: {str(e)}")
import traceback
traceback.print_exc()
def apply_settings(self, settings):
"""Apply settings"""
try:
self.settings = settings
# Apply UI settings
font_size = settings["ui"]["font_size"]
font = self.font()
font.setPointSize(font_size)
self.setFont(font)
# Apply theme
theme = settings["ui"]["theme"]
if theme == "dark":
self._load_stylesheet("dark_theme.qss")
else:
self._load_stylesheet("light_theme.qss")
# Apply exit confirmation setting
self.confirm_on_exit = settings["ui"]["confirm_on_exit"]
# Apply tooltip settings
QtWidgets.QToolTip.setEnabled(settings["ui"]["show_tooltips"])
# Update status bar message
self.status_bar.showMessage("Settings applied")
print("Settings applied")
except Exception as e:
print(f"Error applying settings: {str(e)}")
import traceback
traceback.print_exc()
def closeEvent(self, event):
"""Handle close event"""
try:
# Check if exit confirmation is needed
if hasattr(self, "confirm_on_exit") and self.confirm_on_exit:
# Ask user if they want to exit
reply = QtWidgets.QMessageBox.question(
self,
"Confirm Exit",
f"Are you sure you want to exit {config.TOOL_NAME}?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
QtWidgets.QMessageBox.No
)
if reply == QtWidgets.QMessageBox.Yes:
# Save settings (if needed)
print(f"{config.TOOL_NAME} 正在关闭...")
event.accept()
else:
event.ignore()
else:
# No confirmation needed, close directly
print(f"{config.TOOL_NAME} 正在关闭...")
event.accept()
except Exception as e:
print(f"Error handling close event: {str(e)}")
event.accept()