Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EMSUSD-1630: Support custom callback for primitives AE template #3972

Merged
merged 3 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions lib/mayaUsd/commands/layerEditorCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1078,8 +1078,7 @@ class RefreshSystemLockLayer : public BaseCmd

void _notifySystemLockIsRefreshed()
{
auto dstCallbacks = UsdUfe::getUICallbacks(TfToken("onRefreshSystemLock"));
if (dstCallbacks.size() == 0)
if (!UsdUfe::isUICallbackRegistered(TfToken("onRefreshSystemLock")))
return;

PXR_NS::VtDictionary callbackContext;
Expand All @@ -1095,8 +1094,7 @@ class RefreshSystemLockLayer : public BaseCmd
VtStringArray lockedArray(affectedLayers.begin(), affectedLayers.end());
callbackData["affectedLayerIds"] = lockedArray;

for (UsdUfe::UICallback::Ptr& dstCallback : dstCallbacks)
(*dstCallback)(callbackContext, callbackData);
UsdUfe::triggerUICallback(TfToken("onRefreshSystemLock"), callbackContext, callbackData);
}

UsdStageWeakPtr getStage()
Expand Down
1 change: 1 addition & 0 deletions lib/mayaUsd/python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@
from usdUfe import clearAllEditRouters
from usdUfe import OperationEditRouterContext
from usdUfe import AttributeEditRouterContext
from usdUfe import triggerUICallback
from usdUfe import registerUICallback
from usdUfe import unregisterUICallback
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ supresses the given attribute. `suppressArrayAttribute` which supresses all
array attribute if the option to display is turned off.

The constructor of the AE template calls `buildUI` to fit each attribute in a
custom control. Afterward it calls the functions `createAppliedSchemasSection`,
`createCustomExtraAttrs` and `createMetadataSection` to create some UI sections
that are always present.
custom control. Afterward it calls the functions (in order) to create some UI sections that are always present:
* `createAppliedSchemasSection`
* `createCustomCallbackSection` - see below for more info
* `createCustomExtraAttrs`
* `createMetadataSection`

The `buildUI` function goes through each USD schema of the USD prim. If the
schema is one of the "well-known" ones that have a specialized UI section,
Expand Down Expand Up @@ -105,3 +107,71 @@ There are various helper functions and classes.

- `AEShaderLayout` class: sort and order attributes. Used by LookdevX.

## Callbacks

There is a special custom callback section `createCustomCallbackSection()` called during `buildUI()` in the AE template that gives users the opportunity to add layout section(s) to the AE template. For example the Arnold plugin which has its own USD prims. Using this callback section a plugin can organize plugin specific attributes of a prim into AE layout section(s).

To use this callback section a plugin needs to create a callback function and then register for the AE template UI callback:

```python
# AE template UI callback function.
import ufe
import maya.internal.common.ufe_ae.template as ufeAeTemplate
from ufe_ae.usd.nodes.usdschemabase.ae_template import AETemplate as mayaUsd_AETemplate

# Callback function which receives two params as input: callback context and
# callback data (empty).
def onBuildAETemplateCallback(context, data):
# In the callback context you can retrieve the ufe path string.
ufe_path_string = context.get('ufe_path_string')
ufePath = ufe.PathString.path(ufe_path_string)
ufeItem = ufe.Hierarchy.createItem(ufePath)

# The callback data is empty.

# Then you can call the special static method above to get the MayaUsd
# AE template class object. Using that object you can then create
# layout sections and add controls.
#
# Any controls added should be done using mayaUsdAETemplate.addControls()
# which takes care of checking the AE state (show array attributes, nice
# name, etc). It also adds the added attributes to the addedAttrs list.
# This will keep the attributes from also appearing in the "Extra
# Attributes" section.
#
# If you don't want attributes shown in the AE, you should call
# mayaUsdAETemplate.suppress().
#
# You can also inject a function into the attribute nice naming.
# See [Attribute Nice Naming callback] section below for more info.
#
mayaUsdAETemplate = mayaUsd_AETemplate.getAETemplateForCustomCallback()
if mayaUsdAETemplate:
# Create a new section and add attributes to it.
with ufeAeTemplate.Layout(self.mayaUsdAETemplate, 'My Section', collapse=True):
self.mayaUsdAETemplate.addControls(['param1', 'param2'])

# Register your callback so it will be called during the MayaUsd AE
# template UI building code. This would probably done during plugin loading.
# The first param string 'onBuildAETemplate' is the callback operation and the
# second is the name of your callback function.
import mayaUsd.lib as mayaUsdLib
from usdUfe import registerUICallback
mayaUsdLib.registerUICallback('onBuildAETemplate', onBuildAETemplateCallback)

# During your plugin unload you should unregister the callback (and any attribute
# nice naming function you also added).
mayaUsdLib.unregisterUICallback('onBuildAETemplate', onBuildAETemplateCallback)
```

## Attribute Nice Naming callback

A client can add a function to be called when asked for an attribute nice name. That function will be called from `getNiceAttributeName()` giving the client the ability to provide a nice name for the attribute.

The callback function will receive as input two params:
- ufe attribute
- attribute name

And should return the adjusted name or `None` if no adjustment was made.

See `attributeCustomControl.getNiceAttributeName()` for more info.
33 changes: 32 additions & 1 deletion lib/mayaUsd/resources/ae/usdschemabase/ae_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@
import ufe
import maya.mel as mel
import maya.cmds as cmds
import mayaUsd.lib as mayaUsdLib
import mayaUsd.ufe as mayaUsdUfe
import maya.internal.common.ufe_ae.template as ufeAeTemplate
from mayaUsdLibRegisterStrings import getMayaUsdLibString

# 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, UsdShade, Tf, Sdr
from pxr import Usd, UsdGeom, UsdShade, Tf, Sdr, Vt


def defaultControlCreator(aeTemplate, attrName):
Expand Down Expand Up @@ -235,6 +236,7 @@ def __init__(self, ufeSceneItem):
cmds.editorTemplate(beginScrollLayout=True)
self.buildUI()
self.createAppliedSchemasSection()
self.createCustomCallbackSection()
self.createCustomExtraAttrs()
self.createMetadataSection()
cmds.editorTemplate(endScrollLayout=True)
Expand Down Expand Up @@ -492,6 +494,35 @@ def createAppliedSchemasSection(self):
typeName = self.sectionNameFromSchema(typeName)
self.createSection(typeName, attrs, False)

@staticmethod
def getAETemplateForCustomCallback():
global _aeTemplate
return _aeTemplate

def createCustomCallbackSection(self):
'''Special custom callback section that gives users the opportunity to add
layout section(s) to the AE template.
See https://github.com/Autodesk/maya-usd/blob/dev/lib/mayaUsd/resources/ae/usdschemabase/Attribute-Editor-Template-Doc.md
for more info.
'''

# Create the callback context/data (empty).
cbContext = {
'ufe_path_string' : ufe.PathString.string(self.item.path())
}
cbContextDict = Vt._ReturnDictionary(cbContext)
cbDataDict = Vt._ReturnDictionary({})

# Trigger the callback which will give other plugins the opportunity
# to add controls to our AE template.
global _aeTemplate
try:
_aeTemplate = self
cbDataDict = mayaUsdLib.triggerUICallback('onBuildAETemplate', cbContextDict, cbDataDict)
except Exception as ex:
# Do not let any of the callback failures affect our template.
print('Failed triggerUICallback: %s' % ex)
_aeTemplate = None

def buildUI(self):
usdSch = Usd.SchemaRegistry()
Expand Down
37 changes: 36 additions & 1 deletion lib/mayaUsd/resources/ae/usdschemabase/attributeCustomControl.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,51 @@
import os.path
import json

# List of extra functions used when asked to provide an attribute nice name.
_niceAttributeNameFuncs = []

# A client can add a function to be called when asked for an attribute nice name.
# That function will be called from "getNiceAttributeName" below giving the
# client the ability to provide a nice name for the attribute.
#
# Your function will receive as input two params:
# - ufe attribute
# - attribute name
# And should return the adjusted name or None if no adjustment was made.
def prependNiceAttributeNameFunc(niceNameFunc):
global _niceAttributeNameFuncs
_niceAttributeNameFuncs.insert(0, niceNameFunc)

# Used to remove any function added with the "prependNiceAttributeNameFunc()" helper
# above. For example when unloading your plugin.
def removeAttributeNameFunc(niceNameFunc):
global _niceAttributeNameFuncs
if niceNameFunc in _niceAttributeNameFuncs:
_niceAttributeNameFuncs.remove(niceNameFunc)

def getNiceAttributeName(ufeAttr, attrName):
'''
Convert the attribute name into nice name.
Convert the attribute name into a nice name.
'''
# Note: the uiname metadata comes from LookdevX and was used for connections.
if hasattr(ufeAttr, 'displayName'):
attrName = ufeAttr.displayName
elif hasattr(ufeAttr, "hasMetadata") and ufeAttr.hasMetadata("uiname"):
attrName = str(ufeAttr.getMetadata("uiname"))

try:
# If there are any nice attribute name functions registered call them now.
# this gives clients the ability to inject some nice naming.
global _niceAttributeNameFuncs
for fn in _niceAttributeNameFuncs:
tempName = fn(ufeAttr, attrName)
if tempName:
attrName = tempName
except Exception as ex:
# Do not let any of the callback failures affect our template.
print('Failed to call AE nice naming callback for %s: %s' % (attrName, ex))

# Finally use our internal MayaUsd nice naming function.
return mayaUsdUfe.prettifyName(attrName)

def cleanAndFormatTooltip(s):
Expand Down
39 changes: 35 additions & 4 deletions lib/usdUfe/python/wrapUICallback.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,34 @@ getRegisteredPythonCallbacks()
return callbacks;
}

void addPythonCallback(const PXR_NS::TfToken& operation, PyObject* uiCallback)
PXR_NS::VtDictionary _triggerUICallback(
const PXR_NS::TfToken& operation,
const PXR_NS::VtDictionary& cbContext,
const PXR_NS::VtDictionary& cbData)
{
if (!UsdUfe::isUICallbackRegistered(operation))
return cbData;

//
// Need to copy the input data into non-const reference to call C++ version.
// This allows the python callback function to modify the data which we then
// return to the triggering function.
//
// Trying to use "PXR_NS::VtDictionary& cbData" results in python error:
//
// Python argument types in usdUfe._usdUfe.triggerUICallback(str, dict, dict)
// did not match C++ signature:
// triggerUICallback(
// class pxrInternal_v0_24__pxrReserved__::TfToken,
// class pxrInternal_v0_24__pxrReserved__::VtDictionary,
// class pxrInternal_v0_24__pxrReserved__::VtDictionary{lvalue})
//
PXR_NS::VtDictionary cbDataReturned(cbData);
UsdUfe::triggerUICallback(operation, cbContext, cbDataReturned);
return cbDataReturned;
}

void _addPythonCallback(const PXR_NS::TfToken& operation, PyObject* uiCallback)
{
if (!uiCallback)
return;
Expand All @@ -117,7 +144,7 @@ void addPythonCallback(const PXR_NS::TfToken& operation, PyObject* uiCallback)
UsdUfe::registerUICallback(operation, cb);
}

void removePythonCallback(const PXR_NS::TfToken& operation, PyObject* uiCallback)
void _removePythonCallback(const PXR_NS::TfToken& operation, PyObject* uiCallback)
{
if (!uiCallback)
return;
Expand All @@ -142,6 +169,10 @@ void removePythonCallback(const PXR_NS::TfToken& operation, PyObject* uiCallback
void wrapUICallback()
{
// Making the callbacks accessible from Python
def("registerUICallback", addPythonCallback);
def("unregisterUICallback", removePythonCallback);
def("registerUICallback", _addPythonCallback);
def("unregisterUICallback", _removePythonCallback);

// Helper function to trigger a callback for a given operation.
// Caller should supply the callback context and data.
def("triggerUICallback", _triggerUICallback);
}
1 change: 1 addition & 0 deletions lib/usdUfe/ufe/UsdContextOps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,7 @@ UsdContextOps::SchemaNameMap UsdContextOps::getSchemaPluginNiceNames() const
{ "usdSkel", "Skeleton" },
{ "usdUI", "UI" },
{ "usdVol", "Volumes" },
{ "usdArnold", "Arnold" }
};
// clang-format on
return schemaPluginNiceNames;
Expand Down
38 changes: 25 additions & 13 deletions lib/usdUfe/utils/uiCallback.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,20 @@

#include <usdUfe/base/tokens.h>

#include <pxr/base/tf/token.h>
#include <pxr/base/tf/hashmap.h>

#include <functional>
#include <vector>

namespace {

UsdUfe::UICallbacks& getRegisteredUICallbacks()
{
static UsdUfe::UICallbacks registeredUICallbacks;
return registeredUICallbacks;
}
using UICallbacks = PXR_NS::
TfHashMap<PXR_NS::TfToken, std::vector<UsdUfe::UICallback::Ptr>, PXR_NS::TfToken::HashFunctor>;

const std::vector<UsdUfe::UICallback::Ptr>& getEmptyCallbacks()
UICallbacks& getRegisteredUICallbacks()
{
static std::vector<UsdUfe::UICallback::Ptr> empty;
return empty;
static UICallbacks registeredUICallbacks;
return registeredUICallbacks;
}

} // namespace
Expand Down Expand Up @@ -59,12 +59,24 @@ void unregisterUICallback(const PXR_NS::TfToken& operation, const UICallback::Pt
getRegisteredUICallbacks().erase(operation);
}

const std::vector<UICallback::Ptr>& getUICallbacks(const PXR_NS::TfToken& operation)
bool isUICallbackRegistered(const PXR_NS::TfToken& operation)
{
UsdUfe::UICallbacks& uiCallbacks = getRegisteredUICallbacks();
const auto& uiCallbacks = getRegisteredUICallbacks();
return (uiCallbacks.count(operation) >= 1);
}

auto foundCallback = uiCallbacks.find(operation);
return (foundCallback == uiCallbacks.end()) ? getEmptyCallbacks() : foundCallback->second;
void triggerUICallback(
const PXR_NS::TfToken& operation,
const PXR_NS::VtDictionary& context,
PXR_NS::VtDictionary& data)
{
UICallbacks& uiCallbacks = getRegisteredUICallbacks();
auto foundCallback = uiCallbacks.find(operation);
if (foundCallback != uiCallbacks.end()) {
for (auto& cb : foundCallback->second) {
(*cb)(context, data);
}
}
}

} // namespace USDUFE_NS_DEF
Loading