This commit is contained in:
2025-05-07 01:31:21 +08:00
parent d27ef50341
commit 27240530b6
18 changed files with 2033 additions and 1160 deletions

View File

@@ -19,6 +19,8 @@ import traceback
import locale
import sys
import os
import weakref
#========================================== CONFIG ========================================
import config
TOOL_NAME = config.TOOL_NAME
@@ -40,81 +42,21 @@ 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
TOOL_WIDTH = config.TOOL_WIDTH
TOOL_HEIGHT = config.TOOL_HEIGHT
#========================================= LOCATION =======================================
from scripts.ui import localization
LANG = localization.LANG
#============================================ INIT ==========================================
# 定义窗口大小变化事件处理类
# 用于监听窗口大小变化并调整分割器宽度
class SplitterResizeHandler(QtCore.QObject):
def __init__(self, parent_tab, main_splitter, is_horizontal=True, panel_count=2, panel_ratio=None):
super(SplitterResizeHandler, self).__init__(parent_tab)
self.parent_tab = parent_tab
self.main_splitter = main_splitter
self.is_horizontal = is_horizontal # 是否为水平分割器
self.panel_count = panel_count # 面板数量默认为2左右面板
# 面板比例默认为None则使用默认比例
# 如果是2面板默认比例为[1, 1](左右均等)
# 如果是3面板默认比例为[4, 1, 2]Presets面板较大Assets面板较小Descriptor面板中等
self.panel_ratio = panel_ratio if panel_ratio else ([1, 1] if panel_count == 2 else [4, 1, 2])
# 安装事件过滤器到父标签页
self.parent_tab.installEventFilter(self)
def eventFilter(self, obj, event):
# 监听大小变化事件
if obj == self.parent_tab and event.type() == QtCore.QEvent.Resize:
# 当窗口大小变化时,调整分割器宽度
self.adjustSplitterSizes()
return super(SplitterResizeHandler, self).eventFilter(obj, event)
def adjustSplitterSizes(self):
# 确保分割器存在且父标签页可见
if self.main_splitter and self.parent_tab.isVisible():
# 计算比例总和
ratio_sum = sum(self.panel_ratio)
if self.is_horizontal:
# 获取父标签页当前宽度
width = self.parent_tab.width()
# 根据面板数量设置分割器大小
if self.panel_count == 2:
# 设置分割器左右宽度按比例分配
sizes = [int(width * ratio / ratio_sum) for ratio in self.panel_ratio]
self.main_splitter.setSizes(sizes)
print(f"分割器 - 窗口大小变化,调整水平分割器宽度为: {sizes}")
elif self.panel_count == 3:
# 设置三面板分割器宽度按比例分配
sizes = [int(width * ratio / ratio_sum) for ratio in self.panel_ratio]
self.main_splitter.setSizes(sizes)
print(f"分割器 - 窗口大小变化,调整三面板水平分割器宽度为: {sizes}")
else:
# 获取父标签页当前高度
height = self.parent_tab.height()
# 根据面板数量设置分割器大小
if self.panel_count == 2:
# 设置分割器上下高度按比例分配
sizes = [int(height * ratio / ratio_sum) for ratio in self.panel_ratio]
self.main_splitter.setSizes(sizes)
print(f"分割器 - 窗口大小变化,调整垂直分割器高度为: {sizes}")
elif self.panel_count == 3:
# 设置三面板分割器高度按比例分配
sizes = [int(height * ratio / ratio_sum) for ratio in self.panel_ratio]
self.main_splitter.setSizes(sizes)
print(f"分割器 - 窗口大小变化,调整三面板垂直分割器高度为: {sizes}")
# 使用类来管理UI控件避免全局变量
#============================================ UI BASE ==========================================
class BaseUI(object):
"""
UI基类
所有UI面板的基类提供通用的UI功能
"""
def __init__(self):
"""
初始化UI基类
"""
"""初始化UI基类"""
# 初始化字典
self.controls = {}
self.layouts = {}
@@ -126,214 +68,333 @@ class BaseUI(object):
# 创建主控件
self.main_widget = None
def on_show_event(self, event):
"""
显示事件处理
当面板显示时重置分割器大小
Args:
event: 显示事件对象
"""
# 重置分割器大小
self.reset_splitter_sizes()
# 调用父类的showEvent方法
super(BaseUI, self).showEvent(event)
def reset_splitter_sizes(self):
"""
重置分割器大小,设置三个面板的初始大小
Presets面板较大显示预设图片Assets面板较小Descriptor面板中等
"""
if hasattr(self, 'splitters') and 'main_splitter' in self.splitters and self.splitters["main_splitter"]:
# 尝试从设置中读取分割器大小
preset_size = cmds.optionVar(query="MetaFusionRiggingPresetsPanelSize") if cmds.optionVar(exists="MetaFusionRiggingPresetsPanelSize") else 400
assets_size = cmds.optionVar(query="MetaFusionRiggingAssetsPanelSize") if cmds.optionVar(exists="MetaFusionRiggingAssetsPanelSize") else 100
descriptor_size = cmds.optionVar(query="MetaFusionRiggingDescriptorPanelSize") if cmds.optionVar(exists="MetaFusionRiggingDescriptorPanelSize") else 200
# 设置分割器大小
self.splitters["main_splitter"].setSizes([preset_size, assets_size, descriptor_size])
def create_widgets(self):
"""
创建UI控件
创建所有UI控件并存储到字典中
"""
# 子类实现
"""创建UI控件"""
pass
def create_layouts(self):
"""
创建UI布局
创建所有UI布局并存储到字典中
"""
# 子类实现
"""创建UI布局"""
pass
def connect_ui_signals(self):
"""
连接UI信号和槽
设置UI控件的事件处理函数
"""
# 子类实现
def create_connections(self):
"""连接UI信号和槽"""
pass
def showEvent(self, event):
"""
显示事件
当面板显示时调用on_show_event方法
"""显示事件处理函数"""
# 调用父类的showEvent方法
super(BaseUI, self).showEvent(event)
Args:
event: 显示事件对象
"""
# 调用自定义的显示事件处理方法
self.on_show_event(event)
# 分割器相关的工具函数
def update_splitter_position(splitter, sizes=None):
"""
更新分割器位置
Args:
splitter: 分割器对象
sizes: 分割器大小列表如果为None则使用当前分割器大小
"""
if not splitter:
return
# 记录当前分割器位置
if sizes is None:
sizes = splitter.sizes()
# 将分割器位置保存到设置中
# 如果是三面板分割器,则保存三个大小值
if len(sizes) == 3:
# 保存三面板分割器位置Presets、Assets、Descriptor
cmds.optionVar(intValue=["MetaFusionRiggingPresetsPanelSize", sizes[0]])
cmds.optionVar(intValue=["MetaFusionRiggingAssetsPanelSize", sizes[1]])
cmds.optionVar(intValue=["MetaFusionRiggingDescriptorPanelSize", sizes[2]])
else:
# 保存左右分割器位置
cmds.optionVar(intValue=["MetaFusionRiggingLeftPanelSize", sizes[0]])
cmds.optionVar(intValue=["MetaFusionRiggingRightPanelSize", sizes[1]])
print(f"Splitter sizes updated: {sizes}")
# 通用UI事件处理函数
def on_splitter_moved(ui_instance, pos, index):
"""
分割器移动事件处理
记录当前分割器位置
Args:
ui_instance: UI实例包含splitters字典
pos: 分割器位置
index: 分割器索引
"""
if hasattr(ui_instance, 'splitters') and 'main_splitter' in ui_instance.splitters:
update_splitter_position(ui_instance.splitters["main_splitter"])
# 强制设置分割器均等大小
if hasattr(self, 'splitters') and 'main_splitter' in self.splitters:
setup_splitter(self, 'main_splitter', equal_sizes=True)
#============================================ UI HELPERS ==========================================
def connect_ui_signals(ui_instance, signal_mapping):
"""
连接UI信号和槽
设置UI控件的事件处理函数
Args:
ui_instance: UI实例包含控件和布局
signal_mapping: 信号映射字典,格式为:
{
'widget_type': { # 'buttons', 'inputs', 'splitters'
'widget_name': { # 控件名称
'signal': 'signal_name', # 信号名称,如'clicked', 'valueChanged'
'handler': handler_function, # 处理函数
'args': [arg1, arg2, ...] # 可选,处理函数的参数
}
}
}
"""
"""连接UI信号和槽"""
# 遍历信号映射字典
for widget_type, widgets in signal_mapping.items():
# 获取控件字典
widget_dict = getattr(ui_instance, widget_type, {})
# 遍历控件
# 遍历控件字典
for widget_name, signal_info in widgets.items():
# 检查控件是否存在
if widget_name in widget_dict:
widget = widget_dict[widget_name]
signal_name = signal_info.get('signal')
handler = signal_info.get('handler')
args = signal_info.get('args', [])
# 获取控件
widget = widget_dict.get(widget_name)
if not widget:
# 静默处理未找到的控件,不显示警告
# print(f"警告: 未找到控件 {widget_name}")
continue
# 获取信号对象
signal = getattr(widget, signal_name, None)
# 获取信号名称和处理函数
signal_name = signal_info.get('signal')
handler = signal_info.get('handler')
if not signal_name or not handler:
print(f"警告: 信号名称或处理函数未指定 {widget_name}")
continue
# 连接信号和槽
if signal and handler:
if args:
signal.connect(lambda *extra, h=handler, a=args: h(*a))
else:
signal.connect(handler)
# 获取信号对象
signal = getattr(widget, signal_name, None)
if not signal:
print(f"警告: 未找到信号 {signal_name} 在控件 {widget_name}")
continue
# 获取可选参数
args = signal_info.get('args', [])
# 连接信号和槽
if args:
signal.connect(lambda *_, handler=handler, args=args: handler(*args))
else:
signal.connect(handler)
def connect_maya_selection_changed(ui_instance, handler, parent_widget=None):
"""
连接Maya选择变化事件
"""连接Maya选择变化事件"""
# 如果已经有scriptJob先删除
if hasattr(ui_instance, 'selection_job') and ui_instance.selection_job > 0:
try:
cmds.scriptJob(kill=ui_instance.selection_job, force=True)
except:
pass
Args:
ui_instance: UI实例
handler: 选择变化事件处理函数
parent_widget: 父控件用于设置scriptJob的parent参数
"""
# 如果没有指定父控件则使用UI实例的main_widget
if parent_widget is None and hasattr(ui_instance, 'main_widget'):
parent_widget = ui_instance.main_widget
# 创建新的scriptJob
parent_arg = {}
if parent_widget and parent_widget.objectName():
parent_arg = {"parent": parent_widget.objectName()}
# 创建scriptJob
if parent_widget:
job_id = cmds.scriptJob(event=["SelectionChanged", handler], parent=parent_widget.objectName())
return job_id
return None
ui_instance.selection_job = cmds.scriptJob(
event=["SelectionChanged", handler],
protected=True,
**parent_arg
)
print(f"已连接选择变化事件, scriptJob ID: {ui_instance.selection_job}")
# 获取Maya主窗口
#============================================ MAYA HELPERS ==========================================
def get_maya_main_window():
"""
获取Maya主窗口
Returns:
QWidget: Maya主窗口控件
"""
main_window_ptr = omui.MQtUtil.mainWindow()
return wrapInstance(int(main_window_ptr), QtWidgets.QWidget)
"""获取Maya主窗口"""
ptr = omui.MQtUtil.mainWindow()
if ptr is not None:
return wrapInstance(int(ptr), QtWidgets.QWidget)
def get_parent_widget(widget_name):
"""
根据控件名称查找父容器控件
Args:
widget_name (str): 控件名称
Returns:
QWidget: 找到的父容器控件如果未找到则返回None
"""
# 查找主窗口中的所有控件
main_window = None
for widget in QtWidgets.QApplication.topLevelWidgets():
if widget.objectName() == f"{TOOL_NAME}MainWindow" or widget.objectName().endswith("MainWindow"):
main_window = widget
break
"""根据控件名称查找父容器控件"""
# 获取Maya主窗口
main_window = get_maya_main_window()
if not main_window:
print(f"无法找到主窗口,无法获取父容器: {widget_name}")
return None
# 查找所有子控件
def find_widget(parent, name):
# 检查当前控件
if parent.objectName() == name:
return parent
# 递归查找子控件
for child in parent.children():
if isinstance(child, QtWidgets.QWidget):
# 检查子控件
if child.objectName() == name:
return child
# 递归查找
result = find_widget(child, name)
if result:
return result
return None
# 主窗口中查找指定名称的控件
found_widget = main_window.findChild(QtWidgets.QWidget, widget_name)
if found_widget:
return found_widget
# 主窗口开始查找
return find_widget(main_window, widget_name)
def load_icon(icon_name):
"""
加载图标,支持多种来源
# 如果未找到精确匹配,尝试模糊匹配
for child in main_window.findChildren(QtWidgets.QWidget):
if widget_name.lower() in child.objectName().lower():
return child
Args:
icon_name (str): 图标名称
Returns:
QIcon: 加载的图标对象
"""
if not icon_name:
return QtGui.QIcon()
print(f"无法找到控件: {widget_name}")
return None
# 尝试从插件图标路径加载
if ICONS_PATH and os.path.exists(ICONS_PATH):
# 检查不同的文件扩展名
extensions = ['', '.png', '.jpg', '.svg', '.ico']
for ext in extensions:
path = os.path.join(ICONS_PATH, icon_name + ext)
if os.path.exists(path):
return QtGui.QIcon(path)
# 尝试从Maya内置图标加载
for prefix in [':', ':/']:
try:
icon = QtGui.QIcon(QtGui.QPixmap(f"{prefix}{icon_name}"))
if not icon.isNull():
return icon
except:
continue
# 如果都失败,返回一个空图标
return QtGui.QIcon()
#============================================ SPLITTER ==========================================
def setup_splitter(ui_instance, splitter_name="main_splitter", equal_sizes=True):
"""
设置分割器的属性和大小,确保完全自由调整
Args:
ui_instance: UI实例对象
splitter_name (str): 分割器名称
equal_sizes (bool): 是否设置均等大小
Returns:
bool: 是否成功设置
"""
# 检查分割器是否存在
if not hasattr(ui_instance, 'splitters') or splitter_name not in ui_instance.splitters:
return False
# 获取分割器
splitter = ui_instance.splitters[splitter_name]
# 设置分割器属性
splitter.setOpaqueResize(True)
splitter.setChildrenCollapsible(False)
# 设置伸缩因子和子部件属性
for i in range(splitter.count()):
# 设置伸缩因子
splitter.setStretchFactor(i, 1)
# 设置子部件属性
widget = splitter.widget(i)
if widget:
widget.setMinimumWidth(0)
widget.setMinimumHeight(0)
widget.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
# 设置均等大小
if equal_sizes and splitter.count() > 0:
sizes = [1] * splitter.count()
splitter.setSizes(sizes)
splitter.update()
# 延迟设置确保生效
QtCore.QTimer.singleShot(500, lambda: splitter.setSizes(sizes))
return True
def force_equal_splitter_sizes(ui_instance, splitter_name="main_splitter"):
"""
强制设置分割器大小为均等,并确保完全自由调整
Args:
ui_instance: UI实例对象
splitter_name (str): 分割器名称
"""
# 调用通用的分割器设置函数,设置均等大小
return setup_splitter(ui_instance, splitter_name, equal_sizes=True)
def set_splitter_children_minimum_size(ui_instance, splitter_name="main_splitter", recursive=True):
"""
设置分割器所有子元素的最小宽度和高度为0允许完全自由调整
Args:
ui_instance: UI实例对象
splitter_name (str): 分割器名称
recursive (bool): 是否递归设置所有子元素
"""
# 检查分割器是否存在
if not hasattr(ui_instance, 'splitters') or splitter_name not in ui_instance.splitters:
return
# 获取分割器
splitter = ui_instance.splitters[splitter_name]
# 设置分割器属性
splitter.setOpaqueResize(True)
splitter.setChildrenCollapsible(False)
# 设置所有子部件的最小尺寸为0
for i in range(splitter.count()):
widget = splitter.widget(i)
if widget and recursive:
# 设置最小尺寸为0
widget.setMinimumWidth(0)
widget.setMinimumHeight(0)
widget.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
# 递归设置子控件
_set_widget_children_minimum_size(widget)
def _set_widget_children_minimum_size(widget):
"""
递归设置控件及其所有子控件的最小尺寸为0
Args:
widget: 要设置的控件
"""
# 递归设置所有子部件
for child in widget.findChildren(QtWidgets.QWidget):
# 设置每个子控件的最小宽度为0
child.setMinimumWidth(0)
# 对于按钮、标签等控件不应该设置最小高度为0否则会导致界面异常
if isinstance(child, (QtWidgets.QPushButton, QtWidgets.QToolButton,
QtWidgets.QLabel, QtWidgets.QLineEdit,
QtWidgets.QComboBox, QtWidgets.QCheckBox,
QtWidgets.QRadioButton)):
# 这些控件应该保持其默认高度只设置水平方向的策略为Expanding
policy = child.sizePolicy()
policy.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding)
child.setSizePolicy(policy)
else:
# 其他控件可以设置最小高度为0
child.setMinimumHeight(0)
child.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
# 特别处理容器控件
if isinstance(child, (QtWidgets.QSplitter, QtWidgets.QScrollArea,
QtWidgets.QGroupBox, QtWidgets.QFrame,
QtWidgets.QTabWidget, QtWidgets.QStackedWidget)) and hasattr(child, 'layout') and child.layout():
child.layout().setContentsMargins(0, 0, 0, 0)
child.layout().setSpacing(0)
# 特别处理列表、树和表格控件
elif isinstance(child, (QtWidgets.QListWidget, QtWidgets.QTreeWidget,
QtWidgets.QTableWidget, QtWidgets.QListView,
QtWidgets.QTreeView, QtWidgets.QTableView)):
child.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
child.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
def set_all_controls_minimum_size(ui_instance):
"""
设置UI实例中所有控件的最小尺寸为0确保分割器可以自由移动
Args:
ui_instance: UI实例对象必须包含controls和buttons字典
"""
# 设置所有按钮的最小尺寸
if hasattr(ui_instance, 'buttons'):
for button in ui_instance.buttons.values():
_set_control_minimum_size(button)
# 设置所有控件的最小尺寸
if hasattr(ui_instance, 'controls'):
for control in ui_instance.controls.values():
_set_control_minimum_size(control)
def _set_control_minimum_size(control):
"""
设置单个控件的最小尺寸为0
Args:
control: 要设置的控件
"""
control.setMinimumWidth(0)
# 对于按钮、标签等控件不应该设置最小高度为0否则会导致界面异常
if isinstance(control, (QtWidgets.QPushButton, QtWidgets.QToolButton,
QtWidgets.QLabel, QtWidgets.QLineEdit,
QtWidgets.QComboBox, QtWidgets.QCheckBox,
QtWidgets.QRadioButton)):
# 这些控件应该保持其默认高度只设置水平方向的策略为Expanding
policy = control.sizePolicy()
policy.setHorizontalPolicy(QtWidgets.QSizePolicy.Expanding)
control.setSizePolicy(policy)
else:
# 其他控件可以设置最小高度为0
control.setMinimumHeight(0)
control.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
# 特别处理列表、树和表格控件
if isinstance(control, (QtWidgets.QListWidget, QtWidgets.QTreeWidget,
QtWidgets.QTableWidget, QtWidgets.QListView,
QtWidgets.QTreeView, QtWidgets.QTableView)):
# 确保这些控件可以自由调整大小
control.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)