Files
MetaFusion/scripts/ui/ui_utils.py
2025-05-07 01:31:21 +08:00

400 lines
14 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 -*-
"""
UI Utilities Module for Plugin
UI工具模块 - 提供UI相关的通用函数
"""
#========================================= IMPORT =========================================
from Qt import QtWidgets, QtCore, QtGui
from Qt.QtCompat import wrapInstance
from maya import OpenMayaUI as omui
import maya.cmds as cmds
import maya.mel as mel
import maya.utils as utils
import webbrowser
import subprocess
import importlib
import traceback
import locale
import sys
import os
import weakref
#========================================== CONFIG ========================================
import 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
TOOL_WIDTH = config.TOOL_WIDTH
TOOL_HEIGHT = config.TOOL_HEIGHT
#========================================= LOCATION =======================================
from scripts.ui import localization
LANG = localization.LANG
#============================================ UI BASE ==========================================
class BaseUI(object):
"""
UI基类
所有UI面板的基类提供通用的UI功能
"""
def __init__(self):
"""初始化UI基类"""
# 初始化字典
self.controls = {}
self.layouts = {}
self.buttons = {}
self.splitters = {}
self.inputs = {}
self.labels = {}
# 创建主控件
self.main_widget = None
def create_widgets(self):
"""创建UI控件"""
pass
def create_layouts(self):
"""创建UI布局"""
pass
def create_connections(self):
"""连接UI信号和槽"""
pass
def showEvent(self, event):
"""显示事件处理函数"""
# 调用父类的showEvent方法
super(BaseUI, self).showEvent(event)
# 强制设置分割器均等大小
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信号和槽"""
# 遍历信号映射字典
for widget_type, widgets in signal_mapping.items():
# 获取控件字典
widget_dict = getattr(ui_instance, widget_type, {})
# 遍历控件字典
for widget_name, signal_info in widgets.items():
# 获取控件
widget = widget_dict.get(widget_name)
if not widget:
# 静默处理未找到的控件,不显示警告
# print(f"警告: 未找到控件 {widget_name}")
continue
# 获取信号名称和处理函数
signal_name = signal_info.get('signal')
handler = signal_info.get('handler')
if not signal_name or not handler:
print(f"警告: 信号名称或处理函数未指定 {widget_name}")
continue
# 获取信号对象
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选择变化事件"""
# 如果已经有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
# 创建新的scriptJob
parent_arg = {}
if parent_widget and parent_widget.objectName():
parent_arg = {"parent": parent_widget.objectName()}
ui_instance.selection_job = cmds.scriptJob(
event=["SelectionChanged", handler],
protected=True,
**parent_arg
)
print(f"已连接选择变化事件, scriptJob ID: {ui_instance.selection_job}")
#============================================ MAYA HELPERS ==========================================
def get_maya_main_window():
"""获取Maya主窗口"""
ptr = omui.MQtUtil.mainWindow()
if ptr is not None:
return wrapInstance(int(ptr), QtWidgets.QWidget)
def get_parent_widget(widget_name):
"""根据控件名称查找父容器控件"""
# 获取Maya主窗口
main_window = get_maya_main_window()
if not main_window:
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
# 从主窗口开始查找
return find_widget(main_window, widget_name)
def load_icon(icon_name):
"""
加载图标,支持多种来源
Args:
icon_name (str): 图标名称
Returns:
QIcon: 加载的图标对象
"""
if not icon_name:
return QtGui.QIcon()
# 尝试从插件图标路径加载
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)