Files
MetaFusion/scripts/ReloadModules.py
2025-05-08 23:57:22 +08:00

708 lines
25 KiB
Python
Raw Permalink 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 - Module Reload Tool
提供插件的模块重载功能,支持热更新
用法说明:
1. 在Maya中直接运行:
import scripts.ReloadModules
scripts.ReloadModules.main()
2. 从命令行重新加载所有模块:
import scripts.ReloadModules
scripts.ReloadModules.reload_all()
3. 重新加载特定模块:
import scripts.ReloadModules
scripts.ReloadModules.ModuleReloader.reload_module('module_name')
4. 在开发过程中使用:
- 当修改了代码后,运行此模块可以热更新所有更改
- 无需重启Maya即可测试新功能
- 支持UI和命令行两种方式
注意事项:
- 某些深度依赖的模块可能需要手动重启Maya
- 如果遇到导入错误,请检查模块路径是否正确
- 重新加载可能不会影响已经创建的对象实例
"""
#===================================== 导入模块 =====================================
import maya.cmds as cmds
import pymel.core as pm
import maya.mel as mel
from maya import OpenMayaUI as omui
import webbrowser
import subprocess
import importlib
import traceback
import sys
import os
import pkgutil
try:
from Qt import QtWidgets, QtCore, QtGui
from Qt.QtCompat import wrapInstance
HAS_QT = True
print("成功导入Qt模块 (使用Qt.py兼容层)")
except ImportError:
HAS_QT = False
print("警告: 无法导入Qt模块UI功能将不可用")
import config
TOOL_NAME = config.TOOL_NAME
#===================================== 公共函数 =====================================
def clean_pycache(root_dir):
"""
删除给定根目录下的所有 __pycache__ 目录和 .pyc 文件
Args:
root_dir (str): 要搜索 __pycache__ 文件夹和 .pyc 文件的根目录
Returns:
int: 已删除的 __pycache__ 目录和 .pyc 文件数量
"""
count = 0
try:
# 首先确保目录存在
if not os.path.exists(root_dir) or not os.path.isdir(root_dir):
print(f"警告: 目录不存在或不是一个目录: {root_dir}")
return count
print(f"清理目录中的 __pycache__: {root_dir}")
# 收集所有需要删除的路径,避免在遍历时修改目录结构
pycache_dirs = []
pyc_files = []
# 首先收集所有 __pycache__ 目录和 .pyc 文件
for root, dirs, files in os.walk(root_dir):
# 收集 __pycache__ 目录
if "__pycache__" in dirs:
pycache_path = os.path.join(root, "__pycache__")
pycache_dirs.append(pycache_path)
# 收集 .pyc 文件
for file in files:
if file.endswith(".pyc"):
file_path = os.path.join(root, file)
pyc_files.append(file_path)
# 删除 __pycache__ 目录和 .pyc 文件
for pycache_path in pycache_dirs:
try:
if os.path.exists(pycache_path):
for root, dirs, files in os.walk(pycache_path, topdown=False):
for file in files:
os.remove(os.path.join(root, file))
for dir in dirs:
os.rmdir(os.path.join(root, dir))
os.rmdir(pycache_path)
count += 1
except Exception as e:
print(f"无法删除 {pycache_path}: {str(e)}")
# 删除 .pyc 文件
for file_path in pyc_files:
try:
if os.path.exists(file_path):
os.remove(file_path)
count += 1
except Exception as e:
print(f"无法删除 {file_path}: {str(e)}")
except Exception as error:
print(f"清理 __pycache__ 目录时出错: {str(error)}")
traceback.print_exc()
return count
# 获取 Maya 主窗口函数
def getMayaMainWindow():
"""获取 Maya 主窗口"""
if HAS_QT:
mainWindowPtr = omui.MQtUtil.mainWindow()
return wrapInstance(int(mainWindowPtr), QtWidgets.QWidget)
return None
#===================================== 导入模块 =====================================
# 本地化
try:
from scripts.ui import localization
LANG = localization.LANG
except ImportError:
LANG = {}
print("警告: 无法导入本地化模块,将使用默认语言")
# 设置工具路径
TOOL_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if TOOL_PATH not in sys.path:
sys.path.insert(0, TOOL_PATH)
# 设置脚本路径
SCRIPTS_PATH = os.path.dirname(os.path.abspath(__file__))
if SCRIPTS_PATH not in sys.path:
sys.path.insert(0, SCRIPTS_PATH)
#===================================== IMPORT MODULES =====================================
# 导入主配置 - 简化版
def import_main_config():
"""尝试导入主配置文件"""
global TOOL_NAME, TOOL_VERSION, TOOL_AUTHOR, TOOL_PATH, SCRIPTS_PATH
global UI_PATH, STYLE_FILE, ICONS_PATH, ASSETS_PATH, DNA_FILE_PATH, DNA_IMG_PATH
global TOOL_YEAR, TOOL_MOD_FILENAME, TOOL_LANG, TOOL_WSCL_NAME, TOOL_HELP_URL
global TOOL_MAIN_SCRIPT, TOOL_COMMAND_ICON, TOOL_ICON
# 尝试导入配置
try:
import config
importlib.reload(config)
TOOL_NAME = config.TOOL_NAME
TOOL_VERSION = config.TOOL_VERSION
TOOL_AUTHOR = config.TOOL_AUTHOR
TOOL_YEAR = config.TOOL_YEAR
TOOL_MOD_FILENAME = config.TOOL_MOD_FILENAME
TOOL_LANG = config.TOOL_LANG
TOOL_WSCL_NAME = config.TOOL_WSCL_NAME
TOOL_HELP_URL = config.TOOL_HELP_URL
TOOL_PATH = config.TOOL_PATH
SCRIPTS_PATH = config.SCRIPTS_PATH
TOOL_MAIN_SCRIPT = config.TOOL_MAIN_SCRIPT
UI_PATH = config.UI_PATH
STYLE_FILE = config.STYLE_FILE
ICONS_PATH = config.ICONS_PATH
TOOL_ICON = config.TOOL_ICON
ASSETS_PATH = config.ASSETS_PATH
DNA_FILE_PATH = config.DNA_FILE_PATH
DNA_IMG_PATH = config.DNA_IMG_PATH
TOOL_COMMAND_ICON = config.TOOL_COMMAND_ICON
print(f"配置导入成功,当前版本: {TOOL_NAME} {TOOL_VERSION}")
return True
except Exception as e:
print(f"配置导入失败: {str(e)}")
return False
# 尝试导入Config模块
try:
from scripts.utils import Config
except ImportError:
# 创建默认Config对象
class DefaultConfig:
DNA_CONFIG = {
'DNA_PATH': os.path.join(TOOL_PATH, 'assets', 'dna'),
'DNA_FILE_PATH': os.path.join(TOOL_PATH, 'assets', 'dna', 'default.dna'),
'DNA_VERSION': 'DNAv4',
'LOD_LEVELS': [0, 1, 2, 3, 4],
'DEFAULT_MESH_INDICES': [0]
}
Config = DefaultConfig()
#===================================== MODULE RELOADER =====================================
class ModuleReloader(object):
"""Class for reloading modules in the Plugin"""
@staticmethod
def get_package_modules(package_name):
"""
获取包中的所有模块
Args:
package_name (str): 包名
Returns:
list: 模块名列表
"""
try:
# 特殊处理 scripts.Main它不是一个包
if package_name == "scripts.Main":
return []
# 尝试导入包
package = __import__(package_name, fromlist=["*"])
# 获取包的路径
package_path = getattr(package, "__path__", None)
# 如果不是包,返回空列表
if package_path is None:
return []
# 获取包中的所有模块
modules = []
for _, name, is_pkg in pkgutil.iter_modules(package_path):
module_name = f"{package_name}.{name}"
modules.append(module_name)
# 如果是子包,递归获取子包中的模块
if is_pkg:
submodules = ModuleReloader.get_package_modules(module_name)
modules.extend(submodules)
return modules
except Exception as e:
print(f"获取包 {package_name} 中的模块时出错: {str(e)}")
return []
@staticmethod
def reload_module(module_name):
"""
重新加载指定模块
Args:
module_name (str): 模块名
Returns:
bool: 成功返回True失败返回False
"""
try:
# 检查是否是scripts.Main的子模块如果是则跳过
if module_name.startswith("scripts.Main.") and module_name != "scripts.Main":
print(f"跳过 {module_name}因为scripts.Main不是一个包")
return False
# 尝试导入模块
try:
# 首先尝试直接导入
module = importlib.import_module(module_name)
except ImportError:
# 如果直接导入失败尝试添加工具路径到sys.path
sys_path_modified = False
if TOOL_PATH not in sys.path:
sys.path.insert(0, TOOL_PATH)
sys_path_modified = True
module = __import__(module_name, fromlist=["*"])
if sys_path_modified:
sys.path.remove(TOOL_PATH)
# 重新加载模块
importlib.reload(module)
print(f"成功重新加载模块: {module_name}")
return True
except Exception as e:
print(f"重新加载模块 {module_name} 时出错: {str(e)}")
traceback.print_exc()
return False
@classmethod
def reload_all_modules(cls):
"""
重新加载所有插件模块
Returns:
dict: 重新加载每个模块的结果
"""
# 首先声明全局变量
global TOOL_NAME, TOOL_VERSION, TOOL_AUTHOR, TOOL_PATH, SCRIPTS_PATH
global UI_PATH, STYLE_FILE, ICONS_PATH, ASSETS_PATH, DNA_FILE_PATH
# 初始化结果字典
results = {}
# 清理缓存文件
clean_pycache(TOOL_PATH)
# 重载配置 - 使用import_main_config函数
try:
if import_main_config():
results['config'] = True
else:
results['config'] = False
except Exception as e:
print(f"重新加载配置时出现未预期的错误: {str(e)}")
traceback.print_exc()
results['config'] = False
# 定义要重新加载的模块
modules_to_reload = [
"scripts.utils",
"scripts.ui",
"scripts.Main"
]
# 获取所有子模块
all_modules = []
for module in modules_to_reload:
all_modules.append(module)
# 跳过scripts.Main的子模块因为它不是一个包
if module != "scripts.Main":
try:
submodules = cls.get_package_modules(module)
if submodules:
all_modules.extend(submodules)
except Exception as e:
print(f"获取模块 {module} 的子模块时出错: {str(e)}")
# 去除重复项并排序
all_modules = sorted(list(set(all_modules)))
# 过滤掉scripts.Main的子模块
all_modules = [m for m in all_modules if not (m.startswith("scripts.Main.") and m != "scripts.Main")]
# 按照依赖关系对模块进行排序
# 确保配置模块先重新加载
priority_modules = []
normal_modules = []
for module in all_modules:
if "config" in module.lower() or module.endswith(".config"):
priority_modules.append(module)
else:
normal_modules.append(module)
# 先重新加载优先级模块,然后是普通模块
for module in priority_modules + normal_modules:
results[module] = cls.reload_module(module)
return results
def reload_all():
"""Reload all Plugin modules and display results"""
print("\n" + "-"*50)
print("Reloading Plugin modules...")
print("-"*50)
# 获取插件根目录 - 使用config中定义的TOOL_PATH
try:
import config
TOOL_PATH = config.TOOL_PATH
except ImportError:
# 如果无法导入config则使用相对路径
TOOL_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
print(f"Plugin root directory: {TOOL_PATH}")
# 清理 __pycache__ 目录
print("Cleaning __pycache__ directories...")
pycache_count = clean_pycache(TOOL_PATH)
print(f"Removed {pycache_count} __pycache__ directories and .pyc files")
# Reload modules
results = ModuleReloader.reload_all_modules()
success_count = sum(1 for success in results.values() if success)
total_count = len(results)
print("\nReload Summary:")
print(f"Successfully reloaded {success_count} of {total_count} modules")
if success_count < total_count:
print("\nFailed modules:")
for module, success in results.items():
if not success:
print(f" - {module}")
print("-"*50)
return results
#===================================== MODULE RELOADER UI =====================================
class ModuleReloaderUI(QtWidgets.QDialog):
"""模块重载工具UI类"""
def __init__(self, parent=None):
"""初始化UI"""
parent = parent or getMayaMainWindow()
super(ModuleReloaderUI, self).__init__(parent)
self.setWindowTitle("模块重载工具")
self.setMinimumWidth(600)
self.setMinimumHeight(500)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
# 创建UI
self.create_ui()
def create_ui(self):
"""创建UI元素"""
# 主布局
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(10, 10, 10, 10)
main_layout.setSpacing(10)
# 标题标签
title_label = QtWidgets.QLabel("模块重载工具")
title_label.setStyleSheet("font-size: 16px; font-weight: bold;")
main_layout.addWidget(title_label)
# 说明标签
desc_label = QtWidgets.QLabel("选择要重载的模块类别,然后点击'重载选定模块'按钮。")
main_layout.addWidget(desc_label)
# 创建模块选择区域
modules_group = QtWidgets.QGroupBox("模块选择")
modules_layout = QtWidgets.QVBoxLayout(modules_group)
# 全选复选框
self.all_modules_cb = QtWidgets.QCheckBox("重载所有模块")
self.all_modules_cb.setChecked(True)
modules_layout.addWidget(self.all_modules_cb)
# 模块类别
self.module_categories = {
"核心模块": ["scripts.Main", "scripts.utils", "scripts.ui"],
"UI模块": ["scripts.ui.main_window", "scripts.ui.toolbar", "scripts.ui.model_editor_panel",
"scripts.ui.joint_calibration_panel", "scripts.ui.blendshape_edit_panel",
"scripts.ui.dna_browser_panel", "scripts.ui.animation_system_panel"],
"工具模块": ["scripts.utils.utils_rigging", "scripts.utils.utils_geometry",
"scripts.utils.utils_definition", "scripts.utils.utils_behaviour",
"scripts.utils.utils_toolbar", "scripts.utils.dna_manager"]
}
# 创建模块类别复选框
self.module_cbs = {}
for category, modules in self.module_categories.items():
category_cb = QtWidgets.QCheckBox(category)
modules_layout.addWidget(category_cb)
# 添加子模块列表
modules_list = QtWidgets.QListWidget()
modules_list.setMaximumHeight(100)
for module in modules:
item = QtWidgets.QListWidgetItem(module)
item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
item.setCheckState(QtCore.Qt.Checked)
modules_list.addItem(item)
modules_layout.addWidget(modules_list)
self.module_cbs[category] = (category_cb, modules)
# 连接类别复选框和模块列表
category_cb.stateChanged.connect(lambda state, lst=modules_list: self.toggle_category(state, lst))
# 连接全选复选框
self.all_modules_cb.stateChanged.connect(self.toggle_all_modules)
main_layout.addWidget(modules_group)
# 结果文本区域
self.results_text = QtWidgets.QTextEdit()
self.results_text.setReadOnly(True)
main_layout.addWidget(self.results_text)
# 按钮区域
buttons_layout = QtWidgets.QHBoxLayout()
# 重载按钮
self.reload_button = QtWidgets.QPushButton("重载选定模块")
self.reload_button.setMinimumHeight(30)
buttons_layout.addWidget(self.reload_button)
# 清理缓存按钮
self.clean_button = QtWidgets.QPushButton("仅清理缓存")
self.clean_button.setMinimumHeight(30)
buttons_layout.addWidget(self.clean_button)
# 关闭按钮
self.close_button = QtWidgets.QPushButton("关闭")
self.close_button.setMinimumHeight(30)
buttons_layout.addWidget(self.close_button)
main_layout.addLayout(buttons_layout)
# 连接按钮信号
self.reload_button.clicked.connect(self.reload_and_update_ui)
self.clean_button.clicked.connect(self.clean_cache_only)
self.close_button.clicked.connect(self.close)
def toggle_all_modules(self, state):
"""切换是否选择所有模块"""
for category, (cb, _) in self.module_cbs.items():
cb.setEnabled(not state)
def toggle_category(self, state, list_widget):
"""切换类别中的所有模块"""
for i in range(list_widget.count()):
item = list_widget.item(i)
item.setCheckState(QtCore.Qt.Checked if state else QtCore.Qt.Unchecked)
def get_selected_modules(self):
"""获取选定的模块"""
if self.all_modules_cb.isChecked():
return None # 表示所有模块
selected_categories = []
for category, (cb, modules) in self.module_cbs.items():
if cb.isChecked():
selected_categories.extend(modules)
return selected_categories
def reload_and_update_ui(self):
"""重载模块并更新UI"""
self.results_text.clear()
self.results_text.append("正在重载模块...\n")
# 获取插件根目录
try:
import config
TOOL_PATH = config.TOOL_PATH
except ImportError:
TOOL_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
self.results_text.append(f"插件根目录: {TOOL_PATH}")
# 清理 __pycache__ 目录
self.results_text.append("正在清理 __pycache__ 目录...")
pycache_count = clean_pycache(TOOL_PATH)
self.results_text.append(f"已删除 {pycache_count} 个 __pycache__ 目录和 .pyc 文件\n")
# 确定要重载的模块
results = {}
selected_modules = self.get_selected_modules()
if selected_modules is None:
# 重载所有模块
results = ModuleReloader.reload_all_modules()
else:
if not selected_modules:
self.results_text.append("未选择任何模块,操作已取消。")
return
# 获取所有模块
all_modules = []
for package in selected_modules:
# 跳过scripts.Main的子模块
if package == "scripts.Main":
all_modules.append(package)
elif package.startswith("scripts."):
try:
submodules = ModuleReloader.get_package_modules(package)
all_modules.extend(submodules)
all_modules.append(package) # 确保包本身也被重载
except Exception as e:
self.results_text.append(f"获取模块 {package} 的子模块时出错: {str(e)}")
all_modules.append(package) # 至少尝试重载包本身
else:
all_modules.append(package)
# 去除重复项并排序
all_modules = sorted(list(set(all_modules)))
# 过滤掉scripts.Main的子模块
all_modules = [m for m in all_modules if not (m.startswith("scripts.Main.") and m != "scripts.Main")]
# 重载选定的模块
results = {}
for module in all_modules:
results[module] = ModuleReloader.reload_module(module)
# 更新UI显示结果
success_count = sum(1 for success in results.values() if success)
total_count = len(results)
self.results_text.append(f"成功重载 {success_count}/{total_count} 个模块\n")
# 显示成功重载的模块
self.results_text.append("成功重载的模块:")
for module, success in results.items():
if success:
self.results_text.append(f" - {module}")
# 显示失败的模块(如果有)
if success_count < total_count:
self.results_text.append("\n重载失败的模块:")
for module, success in results.items():
if not success:
self.results_text.append(f" - {module}")
# 提示用户重载完成
self.results_text.append("\n重载操作已完成!")
def clean_cache_only(self):
"""仅清理缓存"""
self.results_text.clear()
self.results_text.append("仅清理缓存...\n")
# 获取插件根目录
try:
import config
TOOL_PATH = config.TOOL_PATH
except ImportError:
TOOL_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
self.results_text.append(f"插件根目录: {TOOL_PATH}")
# 清理 __pycache__ 目录
pycache_count = clean_pycache(TOOL_PATH)
self.results_text.append(f"已删除 {pycache_count} 个 __pycache__ 目录和 .pyc 文件")
self.results_text.append("\n缓存清理操作已完成!")
def show_reload_ui():
"""显示模块重载UI"""
try:
# 关闭已有的窗口 - 使用多种方法确保关闭
window_title = f"{TOOL_NAME} - 模块重载工具"
window_closed = False
# 方法1: 使用窗口标题查找
for widget in QtWidgets.QApplication.topLevelWidgets():
if widget.isVisible() and hasattr(widget, 'windowTitle') and widget.windowTitle() == window_title:
print(f"关闭已有的模块重载工具窗口: {widget}")
widget.close()
widget.deleteLater()
window_closed = True
# 方法2: 使用类型名称字符串查找
if not window_closed:
for widget in QtWidgets.QApplication.topLevelWidgets():
if widget.isVisible() and widget.__class__.__name__ == "ModuleReloaderUI":
print(f"关闭已有的模块重载工具窗口: {widget}")
widget.close()
widget.deleteLater()
window_closed = True
# 方法3: 使用Maya命令关闭窗口
try:
import maya.cmds as cmds
if cmds.window("moduleReloaderWindow", exists=True):
cmds.deleteUI("moduleReloaderWindow", window=True)
print("使用Maya命令关闭模块重载工具窗口")
window_closed = True
except Exception as e:
print(f"使用Maya命令关闭窗口失败: {str(e)}")
# 创建新窗口
dialog = ModuleReloaderUI()
dialog.setObjectName("moduleReloaderWindow") # 设置窗口名称便于后续关闭
dialog.show()
return dialog
except Exception as e:
traceback.print_exc()
print(f"显示UI失败: {str(e)}。回退到命令行重载。")
return reload_all()
# Main function to be called from Maya
def main():
"""从Maya调用的主函数"""
try:
# 检查Qt模块是否可用
if HAS_QT:
# 显示重载UI
show_reload_ui()
else:
# 如果Qt不可用回退到命令行重载
reload_all()
print("模块重载操作已完成!")
return True
except Exception as e:
print(f"模块重载过程中出错: {str(e)}")
traceback.print_exc()
return False
# Allow running this script directly
if __name__ == "__main__":
main()