#!/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"""

{config.TOOL_NAME} {config.TOOL_VERSION}

Author: {config.TOOL_AUTHOR}

Maya Version: {maya_version}

Python Version: {python_version}

Operating System: {os_name}

Description: A Maya plugin for working with MetaHuman models, providing DNA editing, skeleton calibration, custom binding, and blendshape editing features.

Copyright 2025

""" # 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()