400 lines
14 KiB
Python
400 lines
14 KiB
Python
#!/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) |