From c512522d90df16225a47078bee99385666a8d2e7 Mon Sep 17 00:00:00 2001 From: Pierre Baillargeon Date: Tue, 28 Nov 2023 12:29:45 -0500 Subject: [PATCH] EMSUSD-215 support custom display name for USD attributes Add support to modify the names of USD attributes to nicer names for UI display purpose. We already had an algorithm to automatically make the names prettier, this adds support for user-defined attribute names. The user-defined attribute names are defined in a JSON file named "attribute_mappings.json". There are two such files in two locations: - one in the `lib` folder under the installation folder of the plugin. - one in the `prefs` folder of the user Maya folder. Each such file contains a section for prefixes to be removed and a section to map attribute names to nicer names. Note that the mappings are applied after the prefixes are removed. The format of these JSON files is shown in the following example: { "version": 1.0, "removed_prefixes": [ "abc", "def" ], "attribute_mappings": { "example-attribute-name": "example-display-name", "foo": "bar", }, } The built-in attribute mappings do the following: - Remove the "xformOp:" prefix. - Map "xfor - Map "xformOpOrder" to "Transform Order". Modify the USD attribute-related classes: - Add a displayName funtion to the UsdAttrbute class. - Add a displayName funtion to the UsdAttributeHolder class. - Add a displayName funtion to the UsdShaderAttributeHolder class. Add support for display name in the Attribute Editor template: - Refactor custom-control for various types into their own files. - Add a AttributeCustomControl base class to handle display names. - Make the default control creation use the display names. Add new support functions: - Add new JSON helper function to convert floating-point values. - Add the getMayaPrefDir function to get the preference folder. - Add the joinPaths function to join together multiple file paths. - Add loadAttributeNameMappings function to load the user-defined attribute name mapping from the JSON file. - Add getAttributeDisplayName function to convert an attribute name into its display name. - Use the plugin location to derive the built-in file location. Added a simple unit test. --- lib/mayaUsd/resources/ae/CMakeLists.txt | 13 +- .../resources/ae/attribute_mappings.json | 7 + .../resources/ae/usdschemabase/__init__.py | 2 +- .../ae/usdschemabase/ae_auto_template.py | 30 ++ .../resources/ae/usdschemabase/ae_template.py | 352 +----------------- .../ae/usdschemabase/array_custom_control.py | 142 +++++++ .../usdschemabase/attribute_custom_control.py | 58 +++ .../connections_custom_control.py | 86 +++++ ...enum_control.py => enum_custom_control.py} | 15 +- ...age_control.py => image_custom_control.py} | 13 +- .../usdschemabase/metadata_custom_control.py | 156 ++++++++ lib/mayaUsd/ufe/UsdAttribute.cpp | 9 + lib/mayaUsd/ufe/UsdAttribute.h | 10 + lib/mayaUsd/ufe/UsdAttributeHolder.cpp | 14 + lib/mayaUsd/ufe/UsdAttributeHolder.h | 1 + lib/mayaUsd/ufe/UsdShaderAttributeHolder.cpp | 11 + lib/mayaUsd/ufe/UsdShaderAttributeHolder.h | 1 + lib/mayaUsd/utils/CMakeLists.txt | 2 + lib/mayaUsd/utils/displayName.cpp | 146 ++++++++ lib/mayaUsd/utils/displayName.h | 46 +++ lib/mayaUsd/utils/json.cpp | 14 + lib/mayaUsd/utils/json.h | 4 + lib/mayaUsd/utils/utilFileSystem.cpp | 19 + lib/mayaUsd/utils/utilFileSystem.h | 15 + plugin/adsk/plugin/plugin.cpp | 3 + test/lib/testAttributeEditorTemplate.py | 10 +- test/lib/ufe/testAttribute.py | 3 + 27 files changed, 823 insertions(+), 359 deletions(-) create mode 100644 lib/mayaUsd/resources/ae/attribute_mappings.json create mode 100644 lib/mayaUsd/resources/ae/usdschemabase/ae_auto_template.py create mode 100644 lib/mayaUsd/resources/ae/usdschemabase/array_custom_control.py create mode 100644 lib/mayaUsd/resources/ae/usdschemabase/attribute_custom_control.py create mode 100644 lib/mayaUsd/resources/ae/usdschemabase/connections_custom_control.py rename lib/mayaUsd/resources/ae/usdschemabase/{custom_enum_control.py => enum_custom_control.py} (89%) rename lib/mayaUsd/resources/ae/usdschemabase/{custom_image_control.py => image_custom_control.py} (95%) create mode 100644 lib/mayaUsd/resources/ae/usdschemabase/metadata_custom_control.py create mode 100644 lib/mayaUsd/utils/displayName.cpp create mode 100644 lib/mayaUsd/utils/displayName.h diff --git a/lib/mayaUsd/resources/ae/CMakeLists.txt b/lib/mayaUsd/resources/ae/CMakeLists.txt index 57c00aa2ef..35fa0dc771 100644 --- a/lib/mayaUsd/resources/ae/CMakeLists.txt +++ b/lib/mayaUsd/resources/ae/CMakeLists.txt @@ -12,9 +12,20 @@ install(FILES __init__.py DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/python/ufe_ae/ set(MAYAUSD_AE_TEMPLATES usdschemabase) foreach(_SUBDIR ${MAYAUSD_AE_TEMPLATES}) - install(FILES ${_SUBDIR}/__init__.py ${_SUBDIR}/custom_image_control.py ${_SUBDIR}/custom_enum_control.py ${_SUBDIR}/ae_template.py + install(FILES + ${_SUBDIR}/__init__.py + ${_SUBDIR}/ae_template.py + ${_SUBDIR}/ae_auto_template.py + ${_SUBDIR}/array_custom_control.py + ${_SUBDIR}/attribute_custom_control.py + ${_SUBDIR}/connections_custom_control.py + ${_SUBDIR}/enum_custom_control.py + ${_SUBDIR}/image_custom_control.py + ${_SUBDIR}/metadata_custom_control.py DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/python/ufe_ae/usd/nodes/${_SUBDIR} ) endforeach() install(FILES __init__.py DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/python/${PROJECT_NAME}) + +install(FILES "attribute_mappings.json" DESTINATION "${CMAKE_INSTALL_PREFIX}/lib") diff --git a/lib/mayaUsd/resources/ae/attribute_mappings.json b/lib/mayaUsd/resources/ae/attribute_mappings.json new file mode 100644 index 0000000000..2db5028623 --- /dev/null +++ b/lib/mayaUsd/resources/ae/attribute_mappings.json @@ -0,0 +1,7 @@ +{ + "version": 1.0, + "removed_prefixes": [ "xformOp:" ], + "attribute_mappings": { + "xformOpOrder": "Transform Order" + } +} diff --git a/lib/mayaUsd/resources/ae/usdschemabase/__init__.py b/lib/mayaUsd/resources/ae/usdschemabase/__init__.py index 0d6a8a8564..a004849188 100644 --- a/lib/mayaUsd/resources/ae/usdschemabase/__init__.py +++ b/lib/mayaUsd/resources/ae/usdschemabase/__init__.py @@ -1,5 +1,5 @@ from .ae_template import AETemplate -from .custom_enum_control import customEnumControlCreator import ufe if hasattr(ufe.Attributes, "getEnums"): + from .enum_custom_control import customEnumControlCreator AETemplate.prependControlCreator(customEnumControlCreator) diff --git a/lib/mayaUsd/resources/ae/usdschemabase/ae_auto_template.py b/lib/mayaUsd/resources/ae/usdschemabase/ae_auto_template.py new file mode 100644 index 0000000000..f7d6116d1c --- /dev/null +++ b/lib/mayaUsd/resources/ae/usdschemabase/ae_auto_template.py @@ -0,0 +1,30 @@ +# Copyright 2023 Autodesk +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import maya.cmds as cmds + +class AEUITemplate: + ''' + Helper class to push/pop the Attribute Editor Template. This makes + sure that controls are aligned properly. + ''' + + def __enter__(self): + cmds.setUITemplate('attributeEditorTemplate', pst=True) + return self + + def __exit__(self, mytype, value, tb): + cmds.setUITemplate(ppt=True) + diff --git a/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py b/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py index 0afbc1089b..9fafc35db2 100644 --- a/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py +++ b/lib/mayaUsd/resources/ae/usdschemabase/ae_template.py @@ -13,14 +13,17 @@ # limitations under the License. # -from .custom_image_control import customImageControlCreator +from .image_custom_control import customImageControlCreator +from .metadata_custom_control import MetaDataCustomControl +from .connections_custom_control import connectionsCustomControlCreator +from .array_custom_control import arrayCustomControlCreator +from .attribute_custom_control import getNiceAttributeName import collections import fnmatch from functools import partial import re import ufe -import usdUfe import maya.mel as mel import maya.cmds as cmds import mayaUsd.ufe as mayaUsdUfe @@ -28,35 +31,10 @@ import maya.internal.common.ufe_ae.template as ufeAeTemplate from mayaUsdLibRegisterStrings import getMayaUsdLibString -try: - # This helper class was only added recently to Maya. - import maya.internal.ufeSupport.attributes as attributes - hasAEPopupMenu = 'AEPopupMenu' in dir(attributes) -except: - hasAEPopupMenu = False - -from maya.common.ui import LayoutManager, ParentManager -from maya.common.ui import setClipboardData -from maya.OpenMaya import MGlobal - # We manually import all the classes which have a 'GetSchemaAttributeNames' # method so we have access to it and the 'pythonClass' method. from pxr import Usd, UsdGeom, UsdLux, UsdRender, UsdRi, UsdShade, UsdSkel, UsdUI, UsdVol, Kind, Tf, Sdr, Sdf -nameTxt = 'nameTxt' -attrValueFld = 'attrValueFld' -attrTypeFld = 'attrTypeFld' - -# Helper class to push/pop the Attribute Editor Template. This makes -# sure that controls are aligned properly. -class AEUITemplate: - def __enter__(self): - cmds.setUITemplate('attributeEditorTemplate', pst=True) - return self - - def __exit__(self, mytype, value, tb): - cmds.setUITemplate(ppt=True) - # Custom control, but does not have any UI. Instead we use # this control to be notified from UFE when any attribute has changed # so we can update the AE. This is to fix refresh issue @@ -108,293 +86,6 @@ def onReplace(self, *args): # Nothing needed here since we don't create any UI. pass -class MetaDataCustomControl(object): - # Custom control for all prim metadata 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. - 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 - - # There are four metadata that we always show: primPath, kind, active, instanceable - # We use a dictionary to store the various other metadata that this prim contains. - self.extraMetadata = dict() - - def onCreate(self, *args): - # Metadata: PrimPath - # The prim path is for display purposes only - it is not editable, but we - # allow keyboard focus so you copy the value. - self.primPath = cmds.textFieldGrp(label='Prim Path', editable=False, enableKeyboardFocus=True) - - # Metadata: Kind - # We add the known Kind types, in a certain order ("model hierarchy") and then any - # extra ones that were added by extending the kind registry. - # Note: we remove the "model" kind because in the USD docs it states, - # "No prim should have the exact kind "model". - allKinds = Kind.Registry.GetAllKinds() - allKinds.remove(Kind.Tokens.model) - knownKinds = [Kind.Tokens.group, Kind.Tokens.assembly, Kind.Tokens.component, Kind.Tokens.subcomponent] - temp1 = [ele for ele in allKinds if ele not in knownKinds] - knownKinds.extend(temp1) - - # If this prim's kind is not registered, we need to manually - # add it to the list. - model = Usd.ModelAPI(self.prim) - primKind = model.GetKind() - if primKind not in knownKinds: - knownKinds.insert(0, primKind) - if '' not in knownKinds: - knownKinds.insert(0, '') # Set metadata value to "" (or empty). - - self.kind = cmds.optionMenuGrp(label='Kind', - cc=self._onKindChanged, - ann=getMayaUsdLibString('kKindMetadataAnn')) - - for ele in knownKinds: - cmds.menuItem(label=ele) - - # Metadata: Active - self.active = cmds.checkBoxGrp(label='Active', - ncb=1, - cc1=self._onActiveChanged, - ann=getMayaUsdLibString('kActiveMetadataAnn')) - - # Metadata: Instanceable - self.instan = cmds.checkBoxGrp(label='Instanceable', - ncb=1, - cc1=self._onInstanceableChanged, - ann=getMayaUsdLibString('kInstanceableMetadataAnn')) - - # Get all the other Metadata and remove the ones above, as well as a few - # we don't ever want to show. - allMetadata = self.prim.GetAllMetadata() - keysToDelete = ['kind', 'active', 'instanceable', 'typeName', 'documentation'] - for key in keysToDelete: - allMetadata.pop(key, None) - if allMetadata: - cmds.separator(h=10, style='single', hr=True) - - for k in allMetadata: - # All extra metadata is for display purposes only - it is not editable, but we - # allow keyboard focus so you copy the value. - mdLabel = mayaUsdLib.Util.prettifyName(k) if self.useNiceName else k - self.extraMetadata[k] = cmds.textFieldGrp(label=mdLabel, editable=False, enableKeyboardFocus=True) - - # Update all metadata values. - 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): - # PrimPath - cmds.textFieldGrp(self.primPath, edit=True, text=str(self.prim.GetPath())) - - # Kind - model = Usd.ModelAPI(self.prim) - primKind = model.GetKind() - if not primKind: - # Special case to handle the empty string (for meta data value empty). - cmds.optionMenuGrp(self.kind, edit=True, select=1) - else: - cmds.optionMenuGrp(self.kind, edit=True, value=primKind) - - # Active - cmds.checkBoxGrp(self.active, edit=True, value1=self.prim.IsActive()) - - # Instanceable - cmds.checkBoxGrp(self.instan, edit=True, value1=self.prim.IsInstanceable()) - - # All other metadata types - for k in self.extraMetadata: - v = self.prim.GetMetadata(k) if k != 'customData' else self.prim.GetCustomData() - cmds.textFieldGrp(self.extraMetadata[k], edit=True, text=str(v)) - - def _onKindChanged(self, value): - with mayaUsdLib.UsdUndoBlock(): - model = Usd.ModelAPI(self.prim) - model.SetKind(value) - - def _onActiveChanged(self, value): - with mayaUsdLib.UsdUndoBlock(): - try: - usdUfe.ToggleActiveCommand(self.prim).execute() - except Exception as ex: - # Note: the command might not work because there is a stronger - # opinion, so update the checkbox. - cmds.checkBoxGrp(self.active, edit=True, value1=self.prim.IsActive()) - cmds.error(str(ex)) - - def _onInstanceableChanged(self, value): - with mayaUsdLib.UsdUndoBlock(): - try: - usdUfe.ToggleInstanceableCommand(self.prim).execute() - except Exception as ex: - # Note: the command might not work because there is a stronger - # opinion, so update the checkbox. - cmds.checkBoxGrp(self.instan, edit=True, value1=self.prim.IsInstanceable()) - cmds.error(str(ex)) - -# Custom control for all array attribute. -class ArrayCustomControl(object): - - if hasAEPopupMenu: - class ArrayAEPopup(attributes.AEPopupMenu): - '''Override the attribute AEPopupMenu so we can add extra menu items. - ''' - def __init__(self, uiControl, ufeAttr, hasValue, values): - self.hasValue = hasValue - self.values = values - super(ArrayCustomControl.ArrayAEPopup, self).__init__(uiControl, ufeAttr) - - def _copyAttributeValue(self): - setClipboardData(str(self.values)) - - def _printToScriptEditor(self): - MGlobal.displayInfo(str(self.values)) - - COPY_ACTION = (getMayaUsdLibString('kMenuCopyValue'), _copyAttributeValue, []) - PRINT_ACTION = (getMayaUsdLibString('kMenuPrintValue'), _printToScriptEditor, []) - - HAS_VALUE_MENU = [COPY_ACTION, PRINT_ACTION] - - def _buildMenu(self, addItemCmd): - super(ArrayCustomControl.ArrayAEPopup, self)._buildMenu(addItemCmd) - if self.hasValue: - cmds.menuItem(divider=True, parent=self.popupMenu) - self._buildFromActions(self.HAS_VALUE_MENU, addItemCmd) - - def __init__(self, ufeAttr, prim, attrName, useNiceName): - self.ufeAttr = ufeAttr - self.prim = prim - self.attrName = attrName - self.useNiceName = useNiceName - super(ArrayCustomControl, self).__init__() - - def onCreate(self, *args): - attr = self.prim.GetAttribute(self.attrName) - typeName = attr.GetTypeName() - if typeName.isArray: - values = attr.Get() - hasValue = True if values and len(values) > 0 else False - - # build the array type string - # We want something like int[size] or int[] if empty - typeNameStr = str(typeName.scalarType) - typeNameStr += ("[" + str(len(values)) + "]") if hasValue else "[]" - - attrLabel = mayaUsdLib.Util.prettifyName(self.attrName) if self.useNiceName else self.attrName - singleWidgetWidth = mel.eval('global int $gAttributeEditorTemplateSingleWidgetWidth; $gAttributeEditorTemplateSingleWidgetWidth += 0') - with AEUITemplate(): - # See comment in ConnectionsCustomControl below for why nc=5. - rl = cmds.rowLayout(nc=5, adj=3) - with LayoutManager(rl): - cmds.text(nameTxt, al='right', label=attrLabel, annotation=attr.GetDocumentation()) - cmds.textField(attrTypeFld, editable=False, text=typeNameStr, font='obliqueLabelFont', width=singleWidgetWidth*1.5) - - if hasAEPopupMenu: - pMenu = self.ArrayAEPopup(rl, self.ufeAttr, hasValue, values) - self.updateUi(self.ufeAttr, rl) - self.attachCallbacks(self.ufeAttr, rl, None) - else: - if hasValue: - cmds.popupMenu() - cmds.menuItem( label=getMayaUsdLibString('kMenuCopyValue'), command=lambda *args: setClipboardData(str(values)) ) - cmds.menuItem( label=getMayaUsdLibString('kMenuPrintValue'), command=lambda *args: MGlobal.displayInfo(str(values)) ) - - else: - errorMsgFormat = getMayaUsdLibString('kErrorAttributeMustBeArray') - errorMsg = cmds.format(errorMsgFormat, stringArg=(self.attrName)) - cmds.error(errorMsg) - - def onReplace(self, *args): - pass - - # Only used when hasAEPopupMenu is True. - def updateUi(self, attr, uiControlName): - if not hasAEPopupMenu: - return - - with ParentManager(uiControlName): - bgClr = attributes.getAttributeColorRGB(self.ufeAttr) - if bgClr: - isLocked = attributes.isAttributeLocked(self.ufeAttr) - cmds.textField(attrTypeFld, edit=True, backgroundColor=bgClr) - - # Only used when hasAEPopupMenu is True. - def attachCallbacks(self, ufeAttr, uiControl, changedCommand): - if not hasAEPopupMenu: - return - - # Create change callback for UFE locked/unlock synchronization. - cb = attributes.createChangeCb(self.updateUi, ufeAttr, uiControl) - cmds.textField(attrTypeFld, edit=True, parent=uiControl, changeCommand=cb) - - -def showEditorForUSDPrim(usdPrimPathStr): - # Simple helper to open the AE on input prim. - mel.eval('evalDeferred "showEditor(\\\"%s\\\")"' % usdPrimPathStr) - -# Custom control for all attributes that have connections. -class ConnectionsCustomControl(object): - def __init__(self, ufeItem, prim, attrName, useNiceName): - self.path = ufeItem.path() - self.prim = prim - self.attrName = attrName - self.useNiceName = useNiceName - super(ConnectionsCustomControl, self).__init__() - - def onCreate(self, *args): - frontPath = self.path.popSegment() - attr = self.prim.GetAttribute(self.attrName) - attrLabel = self.attrName - if self.useNiceName: - attrLabel = mayaUsdLib.Util.prettifyName(self.attrName) - ufeItem = ufe.SceneItem(self.path) - if ufeItem: - try: - ufeAttrS = ufe.Attributes.attributes(ufeItem) - ufeAttr = ufeAttrS.attribute(self.attrName) - if ufeAttr.hasMetadata("uiname"): - attrLabel = str(ufeAttr.getMetadata("uiname")) - except: - pass - - attrType = attr.GetMetadata('typeName') - - singleWidgetWidth = mel.eval('global int $gAttributeEditorTemplateSingleWidgetWidth; $gAttributeEditorTemplateSingleWidgetWidth += 0') - with AEUITemplate(): - # Because of the way the Maya AE template is defined we use a 5 column setup, even - # though we only have two fields. We resize the main field and purposely set the - # adjustable column to 3 (one we don't have a field in). We want the textField to - # remain at a given width. - rl = cmds.rowLayout(nc=5, adj=3) - with LayoutManager(rl): - cmds.text(nameTxt, al='right', label=attrLabel, annotation=attr.GetDocumentation()) - cmds.textField(attrTypeFld, editable=False, text=attrType, backgroundColor=[0.945, 0.945, 0.647], font='obliqueLabelFont', width=singleWidgetWidth*1.5) - - # Add a menu item for each connection. - cmds.popupMenu() - for c in attr.GetConnections(): - parentPath = c.GetParentPath() - primName = parentPath.MakeRelativePath(parentPath.GetParentPath()) - mLabel = '%s%s...' % (primName, c.elementString) - - usdSeg = ufe.PathSegment(str(c.GetPrimPath()), mayaUsdUfe.getUsdRunTimeId(), '/') - newPath = (frontPath + usdSeg) - newPathStr = ufe.PathString.string(newPath) - cmds.menuItem(label=mLabel, command=lambda *args: showEditorForUSDPrim(newPathStr)) - - def onReplace(self, *args): - # We only display the attribute name and type. Neither of these are time - # varying, so we don't need to implement the replace. - pass - class NoticeListener(object): # Inserted as a custom control, but does not have any UI. Instead we use @@ -427,25 +118,10 @@ def __OnPrimsChanged(self, notice, sender): if hasattr(ctrl, 'refresh'): ctrl.refresh() -def connectionsCustomControlCreator(aeTemplate, c): - if aeTemplate.attributeHasConnections(c): - return ConnectionsCustomControl(aeTemplate.item, aeTemplate.prim, c, aeTemplate.useNiceName) - else: - return None - -def arrayCustomControlCreator(aeTemplate, c): - # Note: UsdGeom.Tokens.xformOpOrder is a exception we want it to display normally. - if c == UsdGeom.Tokens.xformOpOrder: - return None - - if aeTemplate.isArrayAttribute(c): - ufeAttr = aeTemplate.attrS.attribute(c) - return ArrayCustomControl(ufeAttr, aeTemplate.prim, c, aeTemplate.useNiceName) - else: - return None - def defaultControlCreator(aeTemplate, c): - cmds.editorTemplate(addControl=[c]) + ufeAttr = aeTemplate.attrS.attribute(c) + uiLabel = getNiceAttributeName(ufeAttr, c) if aeTemplate.useNiceName else c + cmds.editorTemplate(addControl=[c], label=uiLabel) return None class AEShaderLayout(object): @@ -658,10 +334,14 @@ def addControls(self, controls): for c in controls: if c not in self.suppressedAttrs: for controlCreator in AETemplate._controlCreators: - createdControl = controlCreator(self, c) - if createdControl: - self.defineCustom(createdControl, c) - break + try: + createdControl = controlCreator(self, c) + if createdControl: + self.defineCustom(createdControl, c) + break + except Exception: + # Do not let one custom control failure affect others. + pass self.addedAttrs.append(c) def suppress(self, control): diff --git a/lib/mayaUsd/resources/ae/usdschemabase/array_custom_control.py b/lib/mayaUsd/resources/ae/usdschemabase/array_custom_control.py new file mode 100644 index 0000000000..6e4ec54cb4 --- /dev/null +++ b/lib/mayaUsd/resources/ae/usdschemabase/array_custom_control.py @@ -0,0 +1,142 @@ +# Copyright 2023 Autodesk +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from .attribute_custom_control import AttributeCustomControl +from .ae_auto_template import AEUITemplate + +import mayaUsd.lib as mayaUsdLib +from mayaUsdLibRegisterStrings import getMayaUsdLibString + +import maya.cmds as cmds +import maya.mel as mel +import maya.internal.ufeSupport.attributes as attributes +from maya.common.ui import setClipboardData, LayoutManager, ParentManager +from maya.OpenMaya import MGlobal + +from pxr import UsdGeom + +try: + # This helper class was only added recently to Maya. + hasAEPopupMenu = 'AEPopupMenu' in dir(attributes) +except: + hasAEPopupMenu = False + +nameTxt = 'nameTxt' +attrTypeFld = 'attrTypeFld' +attrValueFld = 'attrValueFld' + +class ArrayCustomControl(AttributeCustomControl): + + if hasAEPopupMenu: + class ArrayAEPopup(attributes.AEPopupMenu): + '''Override the attribute AEPopupMenu so we can add extra menu items. + ''' + def __init__(self, uiControl, ufeAttr, hasValue, values): + self.hasValue = hasValue + self.values = values + super(ArrayCustomControl.ArrayAEPopup, self).__init__(uiControl, ufeAttr) + + def _copyAttributeValue(self): + setClipboardData(str(self.values)) + + def _printToScriptEditor(self): + MGlobal.displayInfo(str(self.values)) + + COPY_ACTION = (getMayaUsdLibString('kMenuCopyValue'), _copyAttributeValue, []) + PRINT_ACTION = (getMayaUsdLibString('kMenuPrintValue'), _printToScriptEditor, []) + + HAS_VALUE_MENU = [COPY_ACTION, PRINT_ACTION] + + def _buildMenu(self, addItemCmd): + super(ArrayCustomControl.ArrayAEPopup, self)._buildMenu(addItemCmd) + if self.hasValue: + cmds.menuItem(divider=True, parent=self.popupMenu) + self._buildFromActions(self.HAS_VALUE_MENU, addItemCmd) + + def __init__(self, ufeAttr, prim, attrName, useNiceName): + super(ArrayCustomControl, self).__init__(ufeAttr, attrName, useNiceName) + self.prim = prim + + def onCreate(self, *args): + attr = self.prim.GetAttribute(self.attrName) + typeName = attr.GetTypeName() + if typeName.isArray: + values = attr.Get() + hasValue = True if values and len(values) > 0 else False + + # build the array type string + # We want something like int[size] or int[] if empty + typeNameStr = str(typeName.scalarType) + typeNameStr += ("[" + str(len(values)) + "]") if hasValue else "[]" + + attrLabel = self.getUILabel() + singleWidgetWidth = mel.eval('global int $gAttributeEditorTemplateSingleWidgetWidth; $gAttributeEditorTemplateSingleWidgetWidth += 0') + with AEUITemplate(): + # See comment in ConnectionsCustomControl below for why nc=5. + rl = cmds.rowLayout(nc=5, adj=3) + with LayoutManager(rl): + cmds.text(nameTxt, al='right', label=attrLabel, annotation=attr.GetDocumentation()) + cmds.textField(attrTypeFld, editable=False, text=typeNameStr, font='obliqueLabelFont', width=singleWidgetWidth*1.5) + + if hasAEPopupMenu: + pMenu = self.ArrayAEPopup(rl, self.ufeAttr, hasValue, values) + self.updateUi(self.ufeAttr, rl) + self.attachCallbacks(self.ufeAttr, rl, None) + else: + if hasValue: + cmds.popupMenu() + cmds.menuItem( label=getMayaUsdLibString('kMenuCopyValue'), command=lambda *args: setClipboardData(str(values)) ) + cmds.menuItem( label=getMayaUsdLibString('kMenuPrintValue'), command=lambda *args: MGlobal.displayInfo(str(values)) ) + + else: + errorMsgFormat = getMayaUsdLibString('kErrorAttributeMustBeArray') + errorMsg = cmds.format(errorMsgFormat, stringArg=(self.attrName)) + cmds.error(errorMsg) + + def onReplace(self, *args): + pass + + # Only used when hasAEPopupMenu is True. + def updateUi(self, attr, uiControlName): + if not hasAEPopupMenu: + return + + with ParentManager(uiControlName): + bgClr = attributes.getAttributeColorRGB(self.ufeAttr) + if bgClr: + isLocked = attributes.isAttributeLocked(self.ufeAttr) + cmds.textField(attrTypeFld, edit=True, backgroundColor=bgClr) + + # Only used when hasAEPopupMenu is True. + def attachCallbacks(self, ufeAttr, uiControl, changedCommand): + if not hasAEPopupMenu: + return + + # Create change callback for UFE locked/unlock synchronization. + cb = attributes.createChangeCb(self.updateUi, ufeAttr, uiControl) + cmds.textField(attrTypeFld, edit=True, parent=uiControl, changeCommand=cb) + + +def arrayCustomControlCreator(aeTemplate, c): + # Note: UsdGeom.Tokens.xformOpOrder is a exception we want it to display normally. + if c == UsdGeom.Tokens.xformOpOrder: + return None + + if aeTemplate.isArrayAttribute(c): + ufeAttr = aeTemplate.attrS.attribute(c) + return ArrayCustomControl(ufeAttr, aeTemplate.prim, c, aeTemplate.useNiceName) + else: + return None + diff --git a/lib/mayaUsd/resources/ae/usdschemabase/attribute_custom_control.py b/lib/mayaUsd/resources/ae/usdschemabase/attribute_custom_control.py new file mode 100644 index 0000000000..79429bcdb7 --- /dev/null +++ b/lib/mayaUsd/resources/ae/usdschemabase/attribute_custom_control.py @@ -0,0 +1,58 @@ +# Copyright 2023 Autodesk +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import mayaUsd.lib + +from maya.OpenMaya import MGlobal + +import os.path +import json + + +def getNiceAttributeName(ufeAttr, attrName): + ''' + Convert the attribute name into nice name. + ''' + # Note: the uiname metadata comes from LookdevX and was used for connections. + if hasattr(ufeAttr, 'displayName'): + attrName = ufeAttr.displayName + elif ufeAttr.hasMetadata("uiname"): + attrName = str(ufeAttr.getMetadata("uiname")) + return mayaUsd.lib.Util.prettifyName(attrName) + + +class AttributeCustomControl(object): + ''' + Base class for attribute custom controls. + Takes care of managing the attribute label. + ''' + def __init__(self, ufeAttr, attrName, useNiceName): + super(AttributeCustomControl, self).__init__() + self.ufeAttr = ufeAttr + self.attrName = attrName + self.useNiceName = useNiceName + + def getAttributeUILabel(self, ufeAttr, attrName): + ''' + Return the label to be used in the UI for the given attribute. + ''' + return getNiceAttributeName(ufeAttr, attrName) if self.useNiceName else attrName + + def getUILabel(self): + ''' + Return the label to be used in the UI for the attribute set on this object. + ''' + return self.getAttributeUILabel(self.ufeAttr, self.attrName) + diff --git a/lib/mayaUsd/resources/ae/usdschemabase/connections_custom_control.py b/lib/mayaUsd/resources/ae/usdschemabase/connections_custom_control.py new file mode 100644 index 0000000000..27353dc8bb --- /dev/null +++ b/lib/mayaUsd/resources/ae/usdschemabase/connections_custom_control.py @@ -0,0 +1,86 @@ +# Copyright 2023 Autodesk +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from .attribute_custom_control import AttributeCustomControl +from .ae_auto_template import AEUITemplate + +import mayaUsd.lib as mayaUsdLib +import mayaUsd.ufe as mayaUsdUfe + +import maya.cmds as cmds +import maya.mel as mel +from maya.common.ui import LayoutManager + +import ufe + +nameTxt = 'nameTxt' +attrTypeFld = 'attrTypeFld' +attrValueFld = 'attrValueFld' + +class ConnectionsCustomControl(AttributeCustomControl): + ''' + Custom control for all attributes that have connections. + ''' + + def __init__(self, ufeItem, ufeAttr, prim, attrName, useNiceName): + super(ConnectionsCustomControl, self).__init__(ufeAttr, attrName, useNiceName) + self.path = ufeItem.path() + self.prim = prim + + def onCreate(self, *args): + frontPath = self.path.popSegment() + attr = self.prim.GetAttribute(self.attrName) + attrLabel = self.getUILabel() + attrType = attr.GetMetadata('typeName') + + singleWidgetWidth = mel.eval('global int $gAttributeEditorTemplateSingleWidgetWidth; $gAttributeEditorTemplateSingleWidgetWidth += 0') + with AEUITemplate(): + # Because of the way the Maya AE template is defined we use a 5 column setup, even + # though we only have two fields. We resize the main field and purposely set the + # adjustable column to 3 (one we don't have a field in). We want the textField to + # remain at a given width. + rl = cmds.rowLayout(nc=5, adj=3) + with LayoutManager(rl): + cmds.text(nameTxt, al='right', label=attrLabel, annotation=attr.GetDocumentation()) + cmds.textField(attrTypeFld, editable=False, text=attrType, backgroundColor=[0.945, 0.945, 0.647], font='obliqueLabelFont', width=singleWidgetWidth*1.5) + + # Add a menu item for each connection. + cmds.popupMenu() + for c in attr.GetConnections(): + parentPath = c.GetParentPath() + primName = parentPath.MakeRelativePath(parentPath.GetParentPath()) + mLabel = '%s%s...' % (primName, c.elementString) + + usdSeg = ufe.PathSegment(str(c.GetPrimPath()), mayaUsdUfe.getUsdRunTimeId(), '/') + newPath = (frontPath + usdSeg) + newPathStr = ufe.PathString.string(newPath) + cmds.menuItem(label=mLabel, command=lambda *args: _showEditorForUSDPrim(newPathStr)) + + def onReplace(self, *args): + # We only display the attribute name and type. Neither of these are time + # varying, so we don't need to implement the replace. + pass + +def _showEditorForUSDPrim(usdPrimPathStr): + # Simple helper to open the AE on input prim. + mel.eval('evalDeferred "showEditor(\\\"%s\\\")"' % usdPrimPathStr) + + +def connectionsCustomControlCreator(aeTemplate, c): + if aeTemplate.attributeHasConnections(c): + ufeAttr = aeTemplate.attrS.attribute(c) + return ConnectionsCustomControl(aeTemplate.item, ufeAttr, aeTemplate.prim, c, aeTemplate.useNiceName) + else: + return None diff --git a/lib/mayaUsd/resources/ae/usdschemabase/custom_enum_control.py b/lib/mayaUsd/resources/ae/usdschemabase/enum_custom_control.py similarity index 89% rename from lib/mayaUsd/resources/ae/usdschemabase/custom_enum_control.py rename to lib/mayaUsd/resources/ae/usdschemabase/enum_custom_control.py index 9092eecf07..bc86e2ec70 100644 --- a/lib/mayaUsd/resources/ae/usdschemabase/custom_enum_control.py +++ b/lib/mayaUsd/resources/ae/usdschemabase/enum_custom_control.py @@ -13,26 +13,25 @@ # limitations under the License. # +from .attribute_custom_control import AttributeCustomControl + import ufe import mayaUsd.ufe as mayaUsdUfe import mayaUsd.lib as mayaUsdLib import maya.cmds as cmds import maya.internal.ufeSupport.attributes as attributes -class CustomEnumControl(object): +class EnumCustomControl(AttributeCustomControl): def __init__(self, ufeAttr, ufeAttrType, prim, attrName, useNiceName): - self.ufeAttr = ufeAttr + super(EnumCustomControl, self).__init__(ufeAttr, attrName, useNiceName) self.prim = prim self.ufeAttrType = ufeAttrType - self.attrName = attrName - self.useNiceName = useNiceName ufeAttrs = ufe.Attributes.attributes(ufeAttr.sceneItem()) self.enums = ufeAttrs.getEnums(ufeAttr.name) - super(CustomEnumControl, self).__init__() def onCreate(self, *args): # Create the control. - attrLabel = mayaUsdLib.Util.prettifyName(self.attrName) if self.useNiceName else self.attrName + attrLabel = self.getUILabel() self.uiControl = cmds.optionMenuGrp(label=attrLabel) attributes.AEPopupMenu(self.uiControl, self.ufeAttr) @@ -48,7 +47,7 @@ def onCreate(self, *args): def onReplace(self, *args): if len(self.enums) > 0: self.attachMenuItems() - attrLabel = mayaUsdLib.Util.prettifyName(self.attrName) if self.useNiceName else self.attrName + attrLabel = self.getUILabel() cmds.optionMenuGrp(self.uiControl, e=True, label=attrLabel) self.updateUi() self.attachCallbacks(self.updateEnumDataReader) @@ -95,7 +94,7 @@ def customEnumControlCreator(aeTemplate, c): enums = ufeAttrs.getEnums(ufeAttr.name) # For now only integer enums are supported. if ufeAttrType == ufe.Attribute.kInt and len(enums) > 0: - return CustomEnumControl(ufeAttr, ufeAttrType, aeTemplate.prim, c, aeTemplate.useNiceName) + return EnumCustomControl(ufeAttr, ufeAttrType, aeTemplate.prim, c, aeTemplate.useNiceName) else: return None diff --git a/lib/mayaUsd/resources/ae/usdschemabase/custom_image_control.py b/lib/mayaUsd/resources/ae/usdschemabase/image_custom_control.py similarity index 95% rename from lib/mayaUsd/resources/ae/usdschemabase/custom_image_control.py rename to lib/mayaUsd/resources/ae/usdschemabase/image_custom_control.py index a1db71e28d..b36cf224c2 100644 --- a/lib/mayaUsd/resources/ae/usdschemabase/custom_image_control.py +++ b/lib/mayaUsd/resources/ae/usdschemabase/image_custom_control.py @@ -13,6 +13,8 @@ # limitations under the License. # +from .attribute_custom_control import AttributeCustomControl + import mayaUsd.lib as mayaUsdLib from mayaUsdLibRegisterStrings import getMayaUsdLibString import mayaUsd_USDRootFileRelative as murel @@ -26,29 +28,24 @@ import ufe -import os.path - try: from PySide2 import QtCore except: from PySide6 import QtCore -class ImageCustomControl(object): +class ImageCustomControl(AttributeCustomControl): filenameField = "UIFilenameField" def __init__(self, ufeAttr, prim, attrName, useNiceName): - self.ufeAttr = ufeAttr + super(ImageCustomControl, self).__init__(ufeAttr, attrName, useNiceName) self.prim = prim - self.attrName = attrName - self.useNiceName = useNiceName self.controlName = None - super(ImageCustomControl, self).__init__() def onCreate(self, *args): cmds.setUITemplate("attributeEditorTemplate", pst=True) - attrUIName = mayaUsdLib.Util.prettifyName(self.attrName) if self.useNiceName else self.attrName + attrUIName = self.getUILabel() ufeAttr = self.ufeAttr createdControl = cmds.rowLayout(nc=3) diff --git a/lib/mayaUsd/resources/ae/usdschemabase/metadata_custom_control.py b/lib/mayaUsd/resources/ae/usdschemabase/metadata_custom_control.py new file mode 100644 index 0000000000..69bd53edc7 --- /dev/null +++ b/lib/mayaUsd/resources/ae/usdschemabase/metadata_custom_control.py @@ -0,0 +1,156 @@ +# Copyright 2023 Autodesk +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from maya import cmds + +from mayaUsdLibRegisterStrings import getMayaUsdLibString +import mayaUsd.lib as mayaUsdLib + +import usdUfe + +from pxr import Usd, Kind + +class MetaDataCustomControl(object): + # Custom control for all prim metadata 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(MetaDataCustomControl, 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 + + # There are four metadata that we always show: primPath, kind, active, instanceable + # We use a dictionary to store the various other metadata that this prim contains. + self.extraMetadata = dict() + + def onCreate(self, *args): + # Metadata: PrimPath + # The prim path is for display purposes only - it is not editable, but we + # allow keyboard focus so you copy the value. + self.primPath = cmds.textFieldGrp(label='Prim Path', editable=False, enableKeyboardFocus=True) + + # Metadata: Kind + # We add the known Kind types, in a certain order ("model hierarchy") and then any + # extra ones that were added by extending the kind registry. + # Note: we remove the "model" kind because in the USD docs it states, + # "No prim should have the exact kind "model". + allKinds = Kind.Registry.GetAllKinds() + allKinds.remove(Kind.Tokens.model) + knownKinds = [Kind.Tokens.group, Kind.Tokens.assembly, Kind.Tokens.component, Kind.Tokens.subcomponent] + temp1 = [ele for ele in allKinds if ele not in knownKinds] + knownKinds.extend(temp1) + + # If this prim's kind is not registered, we need to manually + # add it to the list. + model = Usd.ModelAPI(self.prim) + primKind = model.GetKind() + if primKind not in knownKinds: + knownKinds.insert(0, primKind) + if '' not in knownKinds: + knownKinds.insert(0, '') # Set metadata value to "" (or empty). + + self.kind = cmds.optionMenuGrp(label='Kind', + cc=self._onKindChanged, + ann=getMayaUsdLibString('kKindMetadataAnn')) + + for ele in knownKinds: + cmds.menuItem(label=ele) + + # Metadata: Active + self.active = cmds.checkBoxGrp(label='Active', + ncb=1, + cc1=self._onActiveChanged, + ann=getMayaUsdLibString('kActiveMetadataAnn')) + + # Metadata: Instanceable + self.instan = cmds.checkBoxGrp(label='Instanceable', + ncb=1, + cc1=self._onInstanceableChanged, + ann=getMayaUsdLibString('kInstanceableMetadataAnn')) + + # Get all the other Metadata and remove the ones above, as well as a few + # we don't ever want to show. + allMetadata = self.prim.GetAllMetadata() + keysToDelete = ['kind', 'active', 'instanceable', 'typeName', 'documentation'] + for key in keysToDelete: + allMetadata.pop(key, None) + if allMetadata: + cmds.separator(h=10, style='single', hr=True) + + for k in allMetadata: + # All extra metadata is for display purposes only - it is not editable, but we + # allow keyboard focus so you copy the value. + mdLabel = mayaUsdLib.Util.prettifyName(k) if self.useNiceName else k + self.extraMetadata[k] = cmds.textFieldGrp(label=mdLabel, editable=False, enableKeyboardFocus=True) + + # Update all metadata values. + 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): + # PrimPath + cmds.textFieldGrp(self.primPath, edit=True, text=str(self.prim.GetPath())) + + # Kind + model = Usd.ModelAPI(self.prim) + primKind = model.GetKind() + if not primKind: + # Special case to handle the empty string (for meta data value empty). + cmds.optionMenuGrp(self.kind, edit=True, select=1) + else: + cmds.optionMenuGrp(self.kind, edit=True, value=primKind) + + # Active + cmds.checkBoxGrp(self.active, edit=True, value1=self.prim.IsActive()) + + # Instanceable + cmds.checkBoxGrp(self.instan, edit=True, value1=self.prim.IsInstanceable()) + + # All other metadata types + for k in self.extraMetadata: + v = self.prim.GetMetadata(k) if k != 'customData' else self.prim.GetCustomData() + cmds.textFieldGrp(self.extraMetadata[k], edit=True, text=str(v)) + + def _onKindChanged(self, value): + with mayaUsdLib.UsdUndoBlock(): + model = Usd.ModelAPI(self.prim) + model.SetKind(value) + + def _onActiveChanged(self, value): + with mayaUsdLib.UsdUndoBlock(): + try: + usdUfe.ToggleActiveCommand(self.prim).execute() + except Exception as ex: + # Note: the command might not work because there is a stronger + # opinion, so update the checkbox. + cmds.checkBoxGrp(self.active, edit=True, value1=self.prim.IsActive()) + cmds.error(str(ex)) + + def _onInstanceableChanged(self, value): + with mayaUsdLib.UsdUndoBlock(): + try: + usdUfe.ToggleInstanceableCommand(self.prim).execute() + except Exception as ex: + # Note: the command might not work because there is a stronger + # opinion, so update the checkbox. + cmds.checkBoxGrp(self.instan, edit=True, value1=self.prim.IsInstanceable()) + cmds.error(str(ex)) diff --git a/lib/mayaUsd/ufe/UsdAttribute.cpp b/lib/mayaUsd/ufe/UsdAttribute.cpp index bc9a783896..c5531fefb1 100644 --- a/lib/mayaUsd/ufe/UsdAttribute.cpp +++ b/lib/mayaUsd/ufe/UsdAttribute.cpp @@ -371,6 +371,15 @@ std::string UsdAttribute::name() const return _attrHolder->name(); } +#ifdef UFE_V4_FEATURES_AVAILABLE +std::string UsdAttribute::_displayName() const +#else +std::string UsdAttribute::displayName() const +#endif +{ + return _attrHolder->displayName(); +} + #ifdef UFE_V4_FEATURES_AVAILABLE std::string UsdAttribute::_documentation() const #else diff --git a/lib/mayaUsd/ufe/UsdAttribute.h b/lib/mayaUsd/ufe/UsdAttribute.h index 598ac7e763..6cda54ca6b 100644 --- a/lib/mayaUsd/ufe/UsdAttribute.h +++ b/lib/mayaUsd/ufe/UsdAttribute.h @@ -29,10 +29,18 @@ #endif // Ufe::Attribute overrides (minus the type method) +#ifdef UFE_V5_FEATURES_AVAILABLE +#define UFE_V5_ATTRIBUTE_OVERRIDES \ + std::string displayName() const override { return UsdAttribute::_displayName(); } +#else +#define UFE_V5_ATTRIBUTE_OVERRIDES +#endif + #ifdef UFE_V4_FEATURES_AVAILABLE #define UFE_ATTRIBUTE_OVERRIDES \ bool hasValue() const override { return UsdAttribute::_hasValue(); } \ std::string name() const override { return UsdAttribute::_name(); } \ + UFE_V5_ATTRIBUTE_OVERRIDES \ std::string documentation() const override { return UsdAttribute::_documentation(); } \ std::string string() const override \ { \ @@ -124,6 +132,7 @@ class MAYAUSD_CORE_PUBLIC UsdAttribute #ifdef UFE_V4_FEATURES_AVAILABLE bool _hasValue() const; std::string _name() const; + std::string _displayName() const; std::string _documentation() const; std::string _string(const Ufe::SceneItem::Ptr& item) const; @@ -136,6 +145,7 @@ class MAYAUSD_CORE_PUBLIC UsdAttribute // Ufe::Attribute override methods that we've mimic'd here. bool hasValue() const; std::string name() const; + std::string displayName() const; std::string documentation() const; std::string string(const Ufe::SceneItem::Ptr& item) const; #ifdef UFE_V3_FEATURES_AVAILABLE diff --git a/lib/mayaUsd/ufe/UsdAttributeHolder.cpp b/lib/mayaUsd/ufe/UsdAttributeHolder.cpp index 68ee60eecc..082135209d 100644 --- a/lib/mayaUsd/ufe/UsdAttributeHolder.cpp +++ b/lib/mayaUsd/ufe/UsdAttributeHolder.cpp @@ -18,6 +18,7 @@ #include "Utils.h" #include "private/UfeNotifGuard.h" +#include #include #include @@ -182,6 +183,19 @@ std::string UsdAttributeHolder::name() const } } +std::string UsdAttributeHolder::displayName() const +{ + if (isValid()) { + std::string dn = _usdAttr.GetDisplayName(); + if (dn.empty()) { + dn = getAttributeDisplayName(_usdAttr.GetName().GetString()); + } + return dn; + } else { + return std::string(); + } +} + std::string UsdAttributeHolder::documentation() const { if (isValid()) { diff --git a/lib/mayaUsd/ufe/UsdAttributeHolder.h b/lib/mayaUsd/ufe/UsdAttributeHolder.h index d73b40d744..74f090ffdf 100644 --- a/lib/mayaUsd/ufe/UsdAttributeHolder.h +++ b/lib/mayaUsd/ufe/UsdAttributeHolder.h @@ -48,6 +48,7 @@ class UsdAttributeHolder virtual bool hasValue() const; virtual std::string name() const; + virtual std::string displayName() const; virtual std::string documentation() const; #ifdef UFE_V3_FEATURES_AVAILABLE diff --git a/lib/mayaUsd/ufe/UsdShaderAttributeHolder.cpp b/lib/mayaUsd/ufe/UsdShaderAttributeHolder.cpp index 57e8bab8a4..67d90a2dd1 100644 --- a/lib/mayaUsd/ufe/UsdShaderAttributeHolder.cpp +++ b/lib/mayaUsd/ufe/UsdShaderAttributeHolder.cpp @@ -158,6 +158,17 @@ std::string UsdShaderAttributeHolder::name() const return PXR_NS::UsdShadeUtils::GetFullName(_sdrProp->GetName(), _sdrType); } +std::string UsdShaderAttributeHolder::displayName() const +{ + Ufe::Value retVal + = UsdShaderAttributeDef(_sdrProp).getMetadata(PXR_NS::MayaUsdMetadata->UIName); + std::string name = retVal.safeGet({}); + if (!name.empty()) { + return name; + } + return _Base::displayName(); +} + std::string UsdShaderAttributeHolder::documentation() const { return _sdrProp->GetHelp(); } #ifdef UFE_V3_FEATURES_AVAILABLE diff --git a/lib/mayaUsd/ufe/UsdShaderAttributeHolder.h b/lib/mayaUsd/ufe/UsdShaderAttributeHolder.h index 0827dd5cde..ca1e1b2eaf 100644 --- a/lib/mayaUsd/ufe/UsdShaderAttributeHolder.h +++ b/lib/mayaUsd/ufe/UsdShaderAttributeHolder.h @@ -54,6 +54,7 @@ class UsdShaderAttributeHolder : public UsdAttributeHolder virtual bool hasValue() const; virtual std::string name() const; + virtual std::string displayName() const; virtual std::string documentation() const; #ifdef UFE_V3_FEATURES_AVAILABLE diff --git a/lib/mayaUsd/utils/CMakeLists.txt b/lib/mayaUsd/utils/CMakeLists.txt index 3efc476430..8aeead120c 100644 --- a/lib/mayaUsd/utils/CMakeLists.txt +++ b/lib/mayaUsd/utils/CMakeLists.txt @@ -8,6 +8,7 @@ target_sources(${PROJECT_NAME} converter.cpp customLayerData.cpp diagnosticDelegate.cpp + displayName.cpp dynamicAttribute.cpp editability.cpp json.cpp @@ -37,6 +38,7 @@ set(HEADERS customLayerData.h converter.h diagnosticDelegate.h + displayName.h dynamicAttribute.h editability.h hash.h diff --git a/lib/mayaUsd/utils/displayName.cpp b/lib/mayaUsd/utils/displayName.cpp new file mode 100644 index 0000000000..cdc60db020 --- /dev/null +++ b/lib/mayaUsd/utils/displayName.cpp @@ -0,0 +1,146 @@ +// +// Copyright 2022 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "displayName.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include + +namespace { + +using RemovedPrefixes = std::set; +using AttributeMappings = std::map; + +RemovedPrefixes removedPrefixes; +AttributeMappings attributeMappings; + +const std::string versionKey = "version"; +const std::string removedPrefixesKey = "removed_prefixes"; +const std::string attributeMappingsKey = "attribute_mappings"; +const std::string mappingFileName = "attribute_mappings.json"; + +using MayaUsd::convertJsonKeyToValue; +using MayaUsd::convertToArray; +using MayaUsd::convertToDouble; +using MayaUsd::convertToObject; +using MayaUsd::convertToString; +using MayaUsd::convertToValue; + +// Extract the version entry from the given JSON. +double getAttributeMappingsVersion(const PXR_NS::JsObject& mappingJSON) +{ + return convertToDouble(convertJsonKeyToValue(mappingJSON, versionKey)); +} + +// Extract the valid removed prefix entries from the given JSON and add them to the given set. +void loadRemovedPrefixes(const PXR_NS::JsObject& mappingJSON, RemovedPrefixes& removed) +{ + const PXR_NS::JsArray array + = convertToArray(convertJsonKeyToValue(mappingJSON, removedPrefixesKey)); + + for (auto& value : array) + removed.insert(PXR_NS::TfStringToLower(convertToString(value))); +} + +// Extract the attribute mappings entries from the given JSON and add +// them into the given map, overwriting previous entries if necessary. +void loadAttributeMappings(const PXR_NS::JsObject& mappingJSON, AttributeMappings& mappings) +{ + const PXR_NS::JsObject obj + = convertToObject(convertJsonKeyToValue(mappingJSON, attributeMappingsKey)); + + for (auto& item : obj) { + const std::string key = PXR_NS::TfStringToLower(item.first); + const std::string value = convertToString(item.second); + // Note: don't use std::map::insert() as that fails to overwrite existing entries. + mappings[key] = value; + } +} + +void loadFolderAttributeNameMappings(const std::string& folder) +{ + if (folder.empty()) + return; + + // Verify if the file exists to avoid reporting errors about non-existant + // attribute mappings. + std::string filename = PXR_NS::UsdMayaUtilFileSystem::appendPaths(folder, mappingFileName); + if (!PXR_NS::TfPathExists(filename)) + return; + + try { + std::ifstream mappingFile(filename); + const PXR_NS::JsObject mappingJSON = convertToObject(PXR_NS::JsParseStream(mappingFile)); + + if (getAttributeMappingsVersion(mappingJSON) < 1.0) + return; + + loadRemovedPrefixes(mappingJSON, removedPrefixes); + loadAttributeMappings(mappingJSON, attributeMappings); + } catch (std::exception& ex) { + const std::string msg = PXR_NS::TfStringPrintf( + "Could not load the attribute mappings JSON file [%s].\n%s", + filename.c_str(), + ex.what()); + MGlobal::displayInfo(msg.c_str()); + } +} + +} // namespace + +namespace MAYAUSD_NS_DEF { + +// Load the attribute mappings. +void loadAttributeNameMappings(const std::string& pluginFilePath) +{ + loadFolderAttributeNameMappings( + PXR_NS::UsdMayaUtilFileSystem::joinPaths({ pluginFilePath, "..", "..", "..", "lib" })); + + // Note: order is important as the following user-defined mappings take precedence + // and must be loaded last, possibly over-writing existing mappings. + loadFolderAttributeNameMappings(PXR_NS::UsdMayaUtilFileSystem::getMayaPrefDir()); +} + +// Convert the attribute name into a nice name. +std::string getAttributeDisplayName(const std::string& attrName) +{ + std::string niceName = attrName; + std::string lowerName = PXR_NS::TfStringToLower(attrName); + for (const std::string& prefix : removedPrefixes) { + if (PXR_NS::TfStringStartsWith(lowerName, prefix)) { + niceName = niceName.substr(prefix.size()); + lowerName = lowerName.substr(prefix.size()); + } + } + + if (attributeMappings.count(lowerName)) { + niceName = attributeMappings[lowerName]; + } + + return niceName; +} + +} // namespace MAYAUSD_NS_DEF diff --git a/lib/mayaUsd/utils/displayName.h b/lib/mayaUsd/utils/displayName.h new file mode 100644 index 0000000000..5da8ba5bd2 --- /dev/null +++ b/lib/mayaUsd/utils/displayName.h @@ -0,0 +1,46 @@ +// +// Copyright 2023 Autodesk +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#ifndef PXRUSDMAYA_UTIL_DISPLAY_NAME_H +#define PXRUSDMAYA_UTIL_DISPLAY_NAME_H + +#include + +#include + +namespace MAYAUSD_NS_DEF { + +// Load the attribute mappings. +// +// The attribute mappings are kept in a JSON file named 'attribute_mappings.json'. +// The JSON format is: +// { +// "version": 1.0, +// "removed_prefixes": [ "abc", "def" ], +// "attribute_mappings": { +// "example-attribute-name": "example-display-name", +// "foo": "bar", +// }, +// } +MAYAUSD_CORE_PUBLIC +void loadAttributeNameMappings(const std::string& pluginFilePath); + +// Convert the attribute name into a nice display name. +std::string getAttributeDisplayName(const std::string& attrName); + +} // namespace MAYAUSD_NS_DEF + +#endif diff --git a/lib/mayaUsd/utils/json.cpp b/lib/mayaUsd/utils/json.cpp index 669bc1aedd..3f9050574c 100644 --- a/lib/mayaUsd/utils/json.cpp +++ b/lib/mayaUsd/utils/json.cpp @@ -51,6 +51,20 @@ MString convertToMString(const PXR_NS::JsValue& value) return MString(convertToString(value).c_str()); } +PXR_NS::JsValue convertToValue(double value) +{ + // Provided for call consistency with other data types. + return PXR_NS::JsValue(value); +} + +double convertToDouble(const PXR_NS::JsValue& value) +{ + if (!value.IsReal()) + throw std::runtime_error(invalidJson); + + return value.GetReal(); +} + PXR_NS::JsValue convertToValue(const Ufe::Path& path) { return convertToValue(Ufe::PathString::string(path)); diff --git a/lib/mayaUsd/utils/json.h b/lib/mayaUsd/utils/json.h index 6ee8d0a568..85ddc2574e 100644 --- a/lib/mayaUsd/utils/json.h +++ b/lib/mayaUsd/utils/json.h @@ -38,12 +38,16 @@ MAYAUSD_CORE_PUBLIC PXR_NS::JsValue convertToValue(const Ufe::Path& path); MAYAUSD_CORE_PUBLIC PXR_NS::JsValue convertToValue(const MDagPath& path); +MAYAUSD_CORE_PUBLIC +PXR_NS::JsValue convertToValue(double value); MAYAUSD_CORE_PUBLIC std::string convertToString(const PXR_NS::JsValue& value); MAYAUSD_CORE_PUBLIC MString convertToMString(const PXR_NS::JsValue& value); MAYAUSD_CORE_PUBLIC +double convertToDouble(const PXR_NS::JsValue& value); +MAYAUSD_CORE_PUBLIC Ufe::Path convertToUfePath(const PXR_NS::JsValue& value); MAYAUSD_CORE_PUBLIC MDagPath convertToDagPath(const PXR_NS::JsValue& value); diff --git a/lib/mayaUsd/utils/utilFileSystem.cpp b/lib/mayaUsd/utils/utilFileSystem.cpp index a8d0932c55..6e0259be27 100644 --- a/lib/mayaUsd/utils/utilFileSystem.cpp +++ b/lib/mayaUsd/utils/utilFileSystem.cpp @@ -578,6 +578,12 @@ std::string UsdMayaUtilFileSystem::getMayaWorkspaceScenesDir() return UsdMayaUtil::convert(scenesFolder); } +std::string UsdMayaUtilFileSystem::getMayaPrefDir() +{ + MString prefFolder = MGlobal::executeCommandStringResult("internalVar -userPrefDir"); + return UsdMayaUtil::convert(prefFolder); +} + std::string UsdMayaUtilFileSystem::resolveRelativePathWithinMayaContext( const MObject& proxyShape, const std::string& relativeFilePath) @@ -697,6 +703,19 @@ std::string UsdMayaUtilFileSystem::appendPaths(const std::string& a, const std:: return aPath.string(); } +std::string UsdMayaUtilFileSystem::joinPaths(const std::vector& paths) +{ + if (paths.size() == 0) + return {}; + + ghc::filesystem::path fullPath(paths.front()); + + for (size_t i = 1; i < paths.size(); ++i) + fullPath /= ghc::filesystem::path(paths[i]); + + return fullPath.string(); +} + size_t UsdMayaUtilFileSystem::writeToFilePath(const char* filePath, const void* buffer, const size_t size) { diff --git a/lib/mayaUsd/utils/utilFileSystem.h b/lib/mayaUsd/utils/utilFileSystem.h index 4a638a6973..9193d419ec 100644 --- a/lib/mayaUsd/utils/utilFileSystem.h +++ b/lib/mayaUsd/utils/utilFileSystem.h @@ -105,6 +105,11 @@ std::string getMayaReferencedFileDir(const MObject& proxyShapeNode); MAYAUSD_CORE_PUBLIC std::string getMayaSceneFileDir(); +/*! \brief returns Maya preferences directory. + */ +MAYAUSD_CORE_PUBLIC +std::string getMayaPrefDir(); + /*! \brief returns parent directory of the given layer. */ MAYAUSD_CORE_PUBLIC @@ -276,6 +281,16 @@ bool pathAppendPath(std::string& a, const std::string& b); MAYAUSD_CORE_PUBLIC std::string appendPaths(const std::string& a, const std::string& b); +/** + * Appends all given paths and returns the resulting path. + * + * @param paths a vector of strings representing paths + * + * @return all paths joined by a seperator + */ +MAYAUSD_CORE_PUBLIC +std::string joinPaths(const std::vector& paths); + /** * Writes data to a file path on disk. * diff --git a/plugin/adsk/plugin/plugin.cpp b/plugin/adsk/plugin/plugin.cpp index 37fa5c1d7c..6d8985b40c 100644 --- a/plugin/adsk/plugin/plugin.cpp +++ b/plugin/adsk/plugin/plugin.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include @@ -277,6 +278,8 @@ MStatus initializePlugin(MObject obj) UsdMayaExitNotice::InstallListener(); UsdMayaDiagnosticDelegate::InstallDelegate(); + MayaUsd::loadAttributeNameMappings(plugin.loadPath().asChar()); + #ifdef UFE_V3_FEATURES_AVAILABLE // Install notifications PrimUpdaterManager::getInstance(); diff --git a/test/lib/testAttributeEditorTemplate.py b/test/lib/testAttributeEditorTemplate.py index f8aadd4a83..1551d0cfe5 100644 --- a/test/lib/testAttributeEditorTemplate.py +++ b/test/lib/testAttributeEditorTemplate.py @@ -209,14 +209,14 @@ def testAECustomImageControl(self): self.assertIsNotNone(frameLayout, 'Could not find "Shader: Dot" frameLayout') # We should also have custom image control for 'Inputs In'. - InputsInControl = self.searchForMayaControl(frameLayout, cmds.text, 'Inputs In') - self.assertIsNotNone(InputsInControl, 'Could not find D_filename "Inputs In" control') + InputsInControl = self.searchForMayaControl(frameLayout, cmds.text, 'In') + self.assertIsNotNone(InputsInControl, 'Could not find D_filename "In" control') def testAECustomEnumControl(self): '''Simple test for the customEnumControlCreator in AE template.''' from ufe_ae.usd.nodes.usdschemabase.ae_template import AETemplate - from ufe_ae.usd.nodes.usdschemabase.custom_enum_control import customEnumControlCreator + from ufe_ae.usd.nodes.usdschemabase.enum_custom_control import customEnumControlCreator if customEnumControlCreator not in AETemplate._controlCreators: self.skipTest('Test only available if AE template has customEnumControlCreator.') @@ -244,8 +244,8 @@ def testAECustomEnumControl(self): self.assertIsNotNone(frameLayout, 'Could not find "Alpha" frameLayout') # We should also have custom enum control for 'Inputs Alpha Mode'. - InputsAlphaModeControl = self.searchForMayaControl(frameLayout, cmds.text, 'Inputs Alpha Mode') - self.assertIsNotNone(InputsAlphaModeControl, 'Could not find gltf_pbr1 "Inputs Alpha Mode" control') + InputsAlphaModeControl = self.searchForMayaControl(frameLayout, cmds.text, 'Alpha Mode') + self.assertIsNotNone(InputsAlphaModeControl, 'Could not find gltf_pbr1 "Alpha Mode" control') def testAEConnectionsCustomControl(self): '''Simple test for the connectionsCustomControlCreator in AE template.''' diff --git a/test/lib/ufe/testAttribute.py b/test/lib/ufe/testAttribute.py index 4ef2582e69..9d59feee38 100644 --- a/test/lib/ufe/testAttribute.py +++ b/test/lib/ufe/testAttribute.py @@ -349,6 +349,9 @@ def testAttributeGeneric(self): # Now we test the Generic specific methods. self.assertEqual(ufeAttr.nativeType(), usdAttr.GetTypeName().type.typeName) + if ufeUtils.ufeFeatureSetVersion() >= 5: + self.assertEqual(ufeAttr.displayName, "Transform Order") + # Run test using Maya's getAttr command. self.runMayaGetAttrTest(ufeAttr)