diff --git a/lib/mayaUsd/resources/ae/CMakeLists.txt b/lib/mayaUsd/resources/ae/CMakeLists.txt index 9687299914..472eac4234 100644 --- a/lib/mayaUsd/resources/ae/CMakeLists.txt +++ b/lib/mayaUsd/resources/ae/CMakeLists.txt @@ -37,3 +37,30 @@ foreach(_SUBDIR ${MAYAUSD_AE_TEMPLATES}) endforeach() install(FILES __init__.py DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/python/${PROJECT_NAME}) + +# Install shared components + +if(MAYA_APP_VERSION VERSION_GREATER_EQUAL 2023) + foreach(_SUBDIR ${MAYAUSD_AE_TEMPLATES}) + install(FILES + ${_SUBDIR}/lightCustomControl.py + DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/python/ufe_ae/usd/nodes/${_SUBDIR} + ) + endforeach() + + set(MAYAUSD_SHARED_COMPONENTS usd-shared-components/src/python/usdSharedComponents) + install(FILES + ${MAYAUSD_SHARED_COMPONENTS}/collection/__init__.py + ${MAYAUSD_SHARED_COMPONENTS}/collection/widget.py + DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/python/usd_shared_components/collection/ + ) + + install(FILES + ${MAYAUSD_SHARED_COMPONENTS}/common/__init__.py + ${MAYAUSD_SHARED_COMPONENTS}/common/list.py + ${MAYAUSD_SHARED_COMPONENTS}/common/persistentStorage.py + ${MAYAUSD_SHARED_COMPONENTS}/common/resizable.py + ${MAYAUSD_SHARED_COMPONENTS}/common/theme.py + DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/python/usd_shared_components/common/ + ) +endif() diff --git a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/collection/__init__.py b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/collection/__init__.py new file mode 100644 index 0000000000..bba3421a13 --- /dev/null +++ b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/collection/__init__.py @@ -0,0 +1 @@ +# Shared common components for USD plugins for 3dsMax and Maya diff --git a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/collection/widget.py b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/collection/widget.py new file mode 100644 index 0000000000..03a0428760 --- /dev/null +++ b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/collection/widget.py @@ -0,0 +1,48 @@ +from ..common.list import StringList +from ..common.resizable import Resizable + +try: + from PySide6.QtWidgets import QWidget, QVBoxLayout +except ImportError: + from PySide2.QtWidgets import QWidget, QVBoxLayout # type: ignore + +from pxr import Usd + +class CollectionWidget(QWidget): + def __init__(self, collection: Usd.CollectionAPI = None, parent: QWidget = None): + super(CollectionWidget, self).__init__(parent) + + self._collection: Usd.CollectionAPI = collection + + includes = [] + excludes = [] + shouldIncludeAll = False + + if self._collection is not None: + includeRootAttribute = self._collection.GetIncludeRootAttr() + if includeRootAttribute.IsAuthored(): + shouldIncludeAll = self._collection.GetIncludeRootAttr().Get() + + for p in self._collection.GetIncludesRel().GetTargets(): + includes.append(p.pathString) + for p in self._collection.GetExcludesRel().GetTargets(): + excludes.append(p.pathString) + + self.mainLayout = QVBoxLayout(self) + self.mainLayout.setContentsMargins(0,0,0,0) + + self._include = StringList( includes, "Include", "Include all", self) + self._include.cbIncludeAll.setChecked(shouldIncludeAll) + self._include.cbIncludeAll.stateChanged.connect(self.onIncludeAllToggle) + self._resizableInclude = Resizable(self._include, "USD_Light_Linking", "IncludeListHeight", self) + + self.mainLayout.addWidget(self._resizableInclude) + + self._exclude = StringList( excludes, "Exclude", "", self) + self._resizableExclude = Resizable(self._exclude, "USD_Light_Linking", "ExcludeListHeight", self) + + self.mainLayout.addWidget(self._resizableExclude) + self.mainLayout.addStretch(1) + + def onIncludeAllToggle(self): + self._collection.GetIncludeRootAttr().Set(self._include.cbIncludeAll.isChecked()) \ No newline at end of file diff --git a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/__init__.py b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/__init__.py new file mode 100644 index 0000000000..bba3421a13 --- /dev/null +++ b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/__init__.py @@ -0,0 +1 @@ +# Shared common components for USD plugins for 3dsMax and Maya diff --git a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/list.py b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/list.py new file mode 100644 index 0000000000..477430fb58 --- /dev/null +++ b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/list.py @@ -0,0 +1,102 @@ +from typing import Sequence, Union +from .theme import Theme + +try: + from PySide6.QtCore import ( + QModelIndex, + QPersistentModelIndex, + QSize, + QStringListModel, + Qt, + Signal, + ) + from PySide6.QtGui import QPainter + from PySide6.QtWidgets import QStyleOptionViewItem, QStyledItemDelegate, QListView, QLabel, QVBoxLayout, QHBoxLayout, QWidget, QCheckBox +except: + from PySide2.QtCore import ( + QModelIndex, + QPersistentModelIndex, + QSize, + QStringListModel, + Qt, + Signal, + ) + from PySide2.QtGui import QPainter # type: ignore + from PySide2.QtWidgets import QStyleOptionViewItem, QStyledItemDelegate, QListView, QLabel, QVBoxLayout, QHBoxLayout, QWidget, QCheckBox # type: ignore + +class _StringList(QListView): + selectedItemsChanged = Signal() + + class Delegate(QStyledItemDelegate): + def __init__(self, model: QStringListModel, parent=None): + super(_StringList.Delegate, self).__init__(parent) + self._model = model + + def sizeHint(self, option: QStyleOptionViewItem, index: Union[QModelIndex, QPersistentModelIndex]): + s: int = Theme.instance().uiScaled(24) + return QSize(s, s) + + def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: Union[QModelIndex, QPersistentModelIndex]): + s: str = self._model.data(index, Qt.DisplayRole) + Theme.instance().paintStringListEntry(painter, option.rect, s) + + def __init__(self, items: Sequence[str] = [], parent=None): + super(_StringList, self).__init__(parent) + self._model = QStringListModel(items, self) + self.setModel(self._model) + + self.setUniformItemSizes(True) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setTextElideMode(Qt.TextElideMode.ElideMiddle) + self.setSelectionBehavior(QListView.SelectRows) + self.setSelectionMode(QListView.MultiSelection) + self.setContentsMargins(0,0,0,0) + + self.selectionModel().selectionChanged.connect(lambda: self.selectedItemsChanged.emit()) + + def drawFrame(self, painter: QPainter): + pass + + @property + def items(self) -> Sequence[str]: + return self._model.stringList + + @items.setter + def items(self, items: Sequence[str]): + self._model.setStringList(items) + + @property + def selectedItems(self) -> Sequence[str]: + return [index.data(Qt.DisplayRole) for index in self.selectedIndexes()] + + +class StringList(QWidget): + + def __init__(self, items: Sequence[str] = [], headerTitle: str = "", toggleTitle: str = "", parent=None): + super().__init__() + self.list = _StringList(items, self) + + layout = QVBoxLayout(self) + LEFT_RIGHT_MARGINS = 2 + layout.setContentsMargins(LEFT_RIGHT_MARGINS, 0, LEFT_RIGHT_MARGINS, 0) + + headerWidget = QWidget(self) + headerLayout = QHBoxLayout(headerWidget) + + titleLabel = QLabel(headerTitle, self) + headerLayout.addWidget(titleLabel) + headerLayout.addStretch(1) + headerLayout.setContentsMargins(0,0,0,0) + + # only add the check box on the header if there's a label + if toggleTitle != None and toggleTitle != "": + self.cbIncludeAll = QCheckBox(toggleTitle, self) + self.cbIncludeAll.setCheckable(True) + headerLayout.addWidget(self.cbIncludeAll) + + headerWidget.setLayout(headerLayout) + + layout.addWidget(headerWidget) + layout.addWidget(self.list) + + self.setLayout(layout) \ No newline at end of file diff --git a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/persistentStorage.py b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/persistentStorage.py new file mode 100644 index 0000000000..821247783d --- /dev/null +++ b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/persistentStorage.py @@ -0,0 +1,36 @@ +try: + from PySide6.QtCore import QSettings +except: + from PySide2.QtCore import QSettings # type: ignore + + +class PersistentStorage(object): + _instance = None + + def __init__(self): + raise RuntimeError("Call instance() instead") + + @classmethod + def instance(cls): + if cls._instance is None: + cls._instance = cls.__new__(cls) + return cls._instance + + @classmethod + def injectInstance(cls, persistentStorage): + cls._instance = persistentStorage + + def _settings(self) -> QSettings: + return QSettings("settings.ini", QSettings.IniFormat) + + def set(self, group: str, key: str, value: object): + settings: QSettings = self._settings() + settings.beginGroup(group) + settings.setValue(key, value) + settings.endGroup() + settings.sync() + + def get(self, group: str, key: str, default: object = None) -> object: + settings: QSettings = self._settings() + settings.beginGroup(group) + return settings.value(key, default, type=type(default)) diff --git a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/resizable.py b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/resizable.py new file mode 100644 index 0000000000..a904dc9ce4 --- /dev/null +++ b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/resizable.py @@ -0,0 +1,233 @@ +from .theme import Theme +from .persistentStorage import PersistentStorage +from typing import Union +try: + from PySide6.QtCore import Qt, Signal, QRect + from PySide6.QtWidgets import QWidget, QStackedLayout, QVBoxLayout, QSizePolicy +except ImportError: + from PySide2.QtCore import Qt, Signal, QRect # type: ignore + from PySide2.QtWidgets import QWidget, QStackedLayout, QVBoxLayout, QSizePolicy # type: ignore + + +class Resizable(QWidget): + + __slots__ = [ + "_contentLayout", + "_minContentSize", + "_maxContentSize", + "_persistentStorageGroup", + "_persistentStorageKey", + "_contentSize", + "_dragStartContentSize", + "_widget", + "_overlay", + ] + + class _Overlay(QWidget): + + dragged = Signal(int) + dragging = Signal(bool) + + __slots__ = ["_active", "_mousePressGlobalPosY", "_maskRect"] + + def __init__(self, parent=None): + super(Resizable._Overlay, self).__init__(parent) + + self._active:bool = False + self._mousePressGlobalPosY: Union[int, None] = None + self._maskRect: QRect = None + + self.setWindowFlags(Qt.FramelessWindowHint) + self.setAttribute(Qt.WA_TranslucentBackground) + self.setAttribute(Qt.WA_NoSystemBackground) + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + s: int = Theme.instance().resizableActiveAreaSize() + self.setMinimumSize(s, s) + self.setMouseTracking(True) + self.setFocusPolicy(Qt.NoFocus) + + def paintEvent(self, _): + if self._active: + Theme.instance().paintResizableOverlay(self) + + def resizeEvent(self, event): + super().resizeEvent(event) + s: int = Theme.instance().resizableActiveAreaSize() + self._maskRect = QRect(0, event.size().height() - s, event.size().width(), s) + if not self._active: + self.setMask(self._maskRect) + + def mouseMoveEvent(self, event): + if self._mousePressGlobalPosY is not None: + diff = event.globalPos().y() - self._mousePressGlobalPosY + # self._mousePressGlobalPosY = event.globalPos().y() + self.dragged.emit(diff) + event.accept() + else: + _overActiveArea: bool = event.pos().y() >= self.height() - Theme.instance().resizableActiveAreaSize() + if _overActiveArea != self._active: + self._active = _overActiveArea + if self._active: + self.clearMask() + else: + self.setMask(self._maskRect) + self.update() + self.setCursor(Qt.SizeVerCursor if self._active else Qt.ArrowCursor) + event.ignore() + + def mousePressEvent(self, event): + if self._active: + self.clearMask() + self._mousePressGlobalPosY = event.globalPos().y() + self.dragging.emit(True) + event.accept() + else: + event.ignore() + + def mouseReleaseEvent(self, event): + if self._active: + self._mousePressGlobalPosY = None + self.dragging.emit(False) + event.accept() + else: + event.ignore() + + def enterEvent(self, event): + self._active = event.pos().y() >= self.height() - Theme.instance().resizableActiveAreaSize() + if self._active: + event.accept() + self._active = False + self.update() + self.setCursor(Qt.SizeVerCursor) + else: + event.ignore() + + def leaveEvent(self, event): + if self._active: + self._active = False + self.update() + self.setCursor(Qt.ArrowCursor) + event.accept() + else: + event.ignore() + + def __init__( + self, + w: QWidget = None, + persistentStorageGroup: str = None, + persistentStorageKey: str = None, + parent: QWidget = None, + ): + super(Resizable, self).__init__(parent) + + stackedLayout = QStackedLayout() + stackedLayout.setContentsMargins(0, 0, 0, 0) + stackedLayout.setSpacing(0) + stackedLayout.setStackingMode(QStackedLayout.StackAll) + + contentWidget = QWidget(self) + self._contentLayout = QVBoxLayout(contentWidget) + self._contentLayout.setContentsMargins(0, 0, 0, Theme.instance().resizableContentMargin()) + self._contentLayout.setSpacing(0) + + self._minContentSize: int = 0 + self._maxContentSize: int = 500 + + self._persistentStorageGroup: str = persistentStorageGroup + self._persistentStorageKey: str = persistentStorageKey + + self._contentSize: int = -1 + self._dragStartContentSize: int = -1 + self._widget: QWidget = None + + self.loadPersistentStorage() + + if w is not None: + self.widget = w + + self._overlay = Resizable._Overlay(self) + stackedLayout.addWidget(contentWidget) + stackedLayout.addWidget(self._overlay) + stackedLayout.setCurrentIndex(1) + + self._overlay.dragged.connect(self.onResizeHandleDragged) + self._overlay.dragging.connect(self.onResizeHandleDragging) + + mainLayout = QVBoxLayout(self) + mainLayout.setContentsMargins(0, 0, 0, 0) + mainLayout.setSpacing(0) + mainLayout.addLayout(stackedLayout, 1) + + def onResizeHandleDragging(self, dragging: bool): + if dragging: + self._dragStartContentSize = self._contentSize + else: + self.savePersistentStorage() + + def onResizeHandleDragged(self, dy): + height = self._dragStartContentSize + dy + self.contentSize = height + + @property + def widget(self): + return self._widget + + @widget.setter + def widget(self, w: QWidget): + if self._widget != w: + if self._widget is not None: + self._contentLayout.removeWidget(self._widget) + self._widget.hide() + self._widget = w + if self._widget is not None: + self._contentLayout.insertWidget(0, self._widget, 1) + self._widget.show() + if self._contentSize == -1: + self.contentSize = self._widget.height() + else: + self.contentSize = self._contentSize + + @property + def contentSize(self): + return self._contentSize + + @contentSize.setter + def contentSize(self, s: int): + self._contentSize = max(self._minContentSize, min(self._maxContentSize, s)) + if self._widget is not None: + self._widget.setFixedHeight(self._contentSize) + + @property + def minContentSize(self): + return self._minContentSize + + @minContentSize.setter + def minContentSize(self, s: int): + self._minContentSize = max(0, s) + if self._contentSize != -1 and self._minContentSize > self._contentSize: + self.contentSize = self._minContentSize + + @property + def maxContentSize(self) -> int: + return self._maxContentSize + + @maxContentSize.setter + def maxContentSize(self, s: int): + self._maxContentSize = max(0, s) + if self._contentSize != -1 and self._maxContentSize < self._contentSize: + self.contentSize = self._maxContentSize + + def setPersistentStorage(self, group: str, key: str): + self._persistentStorageGroup = group + self._persistentStorageKey = key + + def savePersistentStorage(self): + if self._contentSize != -1 and self._persistentStorageGroup is not None and self._persistentStorageKey is not None: + s: float = Theme.instance().uiUnScaled(float(self._contentSize)) + PersistentStorage.instance().set(self._persistentStorageGroup, self._persistentStorageKey, s) + + def loadPersistentStorage(self): + if self._persistentStorageGroup is not None and self._persistentStorageKey is not None: + s: float = float(PersistentStorage.instance().get(self._persistentStorageGroup, self._persistentStorageKey, -1.0)) + if s != -1.0: + self.contentSize = round(Theme.instance().uiScaled(s)) diff --git a/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/theme.py b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/theme.py new file mode 100644 index 0000000000..fe66f23f11 --- /dev/null +++ b/lib/mayaUsd/resources/ae/usd-shared-components/src/python/usdSharedComponents/common/theme.py @@ -0,0 +1,108 @@ +try: + from PySide6.QtCore import QRect, Qt + from PySide6.QtGui import QPainter, QColor, QPen, QIcon + from PySide6.QtWidgets import QWidget +except: + from PySide2.QtCore import QRect # type: ignore + from PySide2.QtGui import QPainter, QColor, QPen, QIcon # type: ignore + from PySide2.QtWidgets import QWidget # type: ignore + +from typing import Union +from enum import Flag, auto + + +class Theme(object): + _instance = None + + __slots__ = ["_palette"] + + def __init__(self): + raise RuntimeError("Call instance() instead") + + @classmethod + def instance(cls): + if cls._instance is None: + cls._instance = cls.__new__(cls) + cls._instance._palette = None + return cls._instance + + @classmethod + def injectInstance(cls, theme): + cls._instance = theme + + @property + def uiScaleFactor(self) -> float: + ### Returns the UI scale factor. + return 1.0 + + def uiScaled(self, value: Union[float, int]): + ### Returns the scaled value. + if isinstance(value, float): + return value * self.uiScaleFactor + if isinstance(value, int): + return int(round(float(value) * self.uiScaleFactor)) + raise ValueError("Value must be a float or an int") + + def uiUnScaled(self, value: Union[float, int]): + ### Returns the scaled value. + return float(value) / self.uiScaleFactor + + class Palette(object): + colorResizeBorderActive: QColor = QColor(0x5285a6) + + @property + def palette(self) -> Palette: + if self._palette is None: + self._palette = self.Palette() + return self._palette + + class State(Flag): + Normal = auto() + Hover = auto() + Pressed = auto() + Disabled = auto() + Empty = auto() + + def paintResizableOverlay(self, widget: QWidget): + """ Paints the overlay of a Resizable. + + Args: + widget (QWidget): the widget to paint the overlay on. + """ + + painter = QPainter(widget) + painter.setRenderHint(QPainter.Antialiasing, False) + rect = widget.rect() + rect.adjust(0, 0, 0, - self.resizableContentMargin()) + lineWidth: int = self.uiScaled(2) + if lineWidth == 1: + rect.adjust(1, 1, -1, -1) + elif lineWidth > 1: + halfLineWidth = round(lineWidth / 2.0) + rect.adjust(halfLineWidth, halfLineWidth, -halfLineWidth, -halfLineWidth) + + painter.setPen(QPen(self.palette.colorResizeBorderActive, lineWidth, Qt.SolidLine, Qt.SquareCap, Qt.MiterJoin)) + painter.setBrush(Qt.NoBrush) + painter.drawRect(rect) + + def resizableActiveAreaSize(self) -> int: + """Returns the size of the active area at the bottom of a Resizable.""" + + return self.uiScaled(6) + + def resizableContentMargin(self) -> int: + """Returns the bottom margin of the content of a Resizable. + + The content margin is the the part of the active area extending the + content widget area to make the active area of the Resizable easier to + grab without overlapping too much of the content. + """ + + return self.uiScaled(2) + + + def paintList(self, widget: QWidget, updateRect: QRect, state: State): + raise RuntimeError("Needs to be implemented in derived class") + + def paintStringListEntry(self, painter: QPainter, rect: QRect, string: str): + raise RuntimeError("Needs to be implemented in derived class") \ No newline at end of file diff --git a/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py b/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py index f6e012d6d2..bc2d28bd94 100644 --- a/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py +++ b/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py @@ -22,6 +22,11 @@ from .materialCustomControl import MaterialCustomControl from .metadataCustomControl import MetadataCustomControl from .observers import UfeAttributesObserver, UfeConnectionChangedObserver, UsdNoticeListener +try: + from .lightCustomControl import LightLinkingCustomControl + lightLinkingSupported = True +except: + lightLinkingSupported = False import collections import fnmatch @@ -204,7 +209,6 @@ def get(self): folderIndex[groups].items.append(attributeInfo.name) return self._attributeLayout - # SchemaBase template class for categorization of the attributes. # We no longer use the base class ufeAeTemplate.Template as we want to control # the placement of the metadata at the bottom (after extra attributes). @@ -471,6 +475,12 @@ def createMetadataSection(self, sectionName, attrs, collapse): usdNoticeControl = UsdNoticeListener(self.prim, [metaDataControl]) self.defineCustom(metaDataControl) self.defineCustom(usdNoticeControl) + + def createLightLinkingSection(self, sectionName, attrs, collapse): + # We don't use createSection() because these are metadata (not attributes). + with ufeAeTemplate.Layout(self, 'Light Linking', collapse): + lightLinkingControl = LightLinkingCustomControl(self.item, self.prim, self.useNiceName) + self.defineCustom(lightLinkingControl) def createCustomExtraAttrs(self, sectionName, attrs, collapse): # We are not using the maya default "Extra Attributes" section @@ -642,16 +652,19 @@ def isSectionOpen(sectionName): # By default, calls the generic createSection, which will search # in the list of known custom control creators for the one to be # used. + customAttributes = { + 'shader': self.createShaderAttributesSection, + 'transforms': self.createTransformAttributesSection, + 'display': self.createDisplaySection, + 'extraAttributes': self.createCustomExtraAttrs, + 'metadata': self.createMetadataSection, + 'customCallbacks': self.createCustomCallbackSection, + } + # only support lightLinking custom attribute for Maya 2023+ + if lightLinkingSupported: + customAttributes['lightLinkCollectionAPI'] = self.createLightLinkingSection sectionCreators = collections.defaultdict( - lambda : self.createSection, - { - 'shader': self.createShaderAttributesSection, - 'transforms': self.createTransformAttributesSection, - 'display': self.createDisplaySection, - 'extraAttributes': self.createCustomExtraAttrs, - 'metadata': self.createMetadataSection, - 'customCallbacks': self.createCustomCallbackSection, - }) + lambda : self.createSection, customAttributes) # Create the section in the specified order. for typeName in schemasOrder: diff --git a/lib/mayaUsd/resources/ae/usdschemabase/lightCustomControl.py b/lib/mayaUsd/resources/ae/usdschemabase/lightCustomControl.py new file mode 100644 index 0000000000..9d1d1c5a98 --- /dev/null +++ b/lib/mayaUsd/resources/ae/usdschemabase/lightCustomControl.py @@ -0,0 +1,48 @@ +import maya.cmds as cmds +from pxr import Usd + +class LightLinkingCustomControl(object): + '''Custom control for the light linking data we want to display.''' + def __init__(self, item, prim, useNiceName): + # In Maya 2022.1 we need to hold onto the Ufe SceneItem to make + # sure it doesn't go stale. This is not needed in latest Maya. + super(LightLinkingCustomControl, self).__init__() + mayaVer = '%s.%s' % (cmds.about(majorVersion=True), cmds.about(minorVersion=True)) + self.item = item if mayaVer == '2022.1' else None + self.prim = prim + self.useNiceName = useNiceName + + def onCreate(self, *args): + if self.prim.IsValid() == True and self.prim.HasAPI(Usd.CollectionAPI, 'lightLink'): + try: + try: + from shiboken6 import wrapInstance + from PySide6.QtWidgets import QWidget + except: + from shiboken2 import wrapInstance # type: ignore + from PySide2.QtWidgets import QWidget # type: ignore + + from maya.OpenMayaUI import MQtUtil + + self.parent = cmds.setParent(q=True) + ptr = MQtUtil.findControl(self.parent) + parentWidget = wrapInstance(int(ptr), QWidget) + + from usd_shared_components.collection.widget import CollectionWidget # type: ignore + + self.widget = CollectionWidget(Usd.CollectionAPI.Get(self.prim, 'lightLink')) + parentWidget.layout().addWidget(self.widget) + + except Exception as ex: + print('Failed to create Light custom control: %s' % (ex)) + + self.refresh() + + def onReplace(self, *args): + # Nothing needed here since USD data is not time varying. Normally this template + # is force rebuilt all the time, except in response to time change from Maya. In + # that case we don't need to update our controls since none will change. + pass + + def refresh(self): + pass \ No newline at end of file diff --git a/test/lib/testAttributeEditorTemplate.py b/test/lib/testAttributeEditorTemplate.py index 12080e79d8..4500ed0d61 100644 --- a/test/lib/testAttributeEditorTemplate.py +++ b/test/lib/testAttributeEditorTemplate.py @@ -501,11 +501,19 @@ def testAEForLight(self): # Note: different version of USD can have different schemas, # so we only compare the ones we are interested in verifying. - expectedInitialSectionLabels = [ - 'Light ', - 'Cylinder Light', - 'Light Link Collection ', - 'Shadow Link Collection '] + expectedInitialSectionLabels = [] + if mayaUtils.mayaMajorMinorVersions() >= (2023, 0): + expectedInitialSectionLabels = [ + 'Light ', + 'Cylinder Light', + 'Light Linking', + 'Shadow Link Collection '] + else: + expectedInitialSectionLabels = [ + 'Light ', + 'Cylinder Light', + 'Light Link Collection ', + 'Shadow Link Collection '] self.assertListEqual( actualSectionLabels[0:len(expectedInitialSectionLabels)], expectedInitialSectionLabels)