Skip to content

Commit

Permalink
EMSUSD-1114 - Adds a callback for RefreshSystemLock when a layer's lo…
Browse files Browse the repository at this point in the history
…ck status is changed

- Adds a callback system that allow registering for changes coming from either commands or UI interactions. Each callback can receive a context (PXR_NS::VtDictionary) as well as callback data (PXR_NS::VtDictionary).
- Added onRefreshSystemLock callback to know when a layer's lock status is changed due to disk write permission checeks.
- Added a test case for the callback
- Added a new LayerLocking.md document encompassing Layer Locking how-tos to be used through C++ or scripting with the included examples.
  • Loading branch information
AramAzhari-adsk committed Mar 20, 2024
1 parent b9d1570 commit a2b565b
Show file tree
Hide file tree
Showing 17 changed files with 427 additions and 2 deletions.
1 change: 1 addition & 0 deletions README_DOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
+ [Hydra For Maya](https://github.com/Autodesk/maya-usd/blob/release/maya-hydra/lib/mayaHydra/README.md)
+ [Layer Saving](lib/mayaUsd/nodes/Layer_Saving_Docs.md)
+ [Maya Reference Edit Router](lib/usd/translators/mayaReferenceEditRouter.md)
+ [Layer Locking](doc/LayerLocking.md)
156 changes: 156 additions & 0 deletions doc/LayerLocking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Layer Locking

## What is layer locking?

Layer locking is a collection of commands and UI actions that allows changing
editing and saving permissions on layers of a stage.

It was designed to provide various levels of controls based on the user's interaction:
- Through the Maya USD Layer Editor
- Through commands (C++, Python and MEL)

## What are the types of locks?

There are three types of locks on a layer:
- `Unlocked`: When a layer is allowed to both be edited and saved.
- `Locked` (Edit is Locked): When a layer is allowed to be saved but not allowed to be edited.
- `System-Locked` (Edit and Save are locked): When a layer is not allowed to be saved or edited.

Changing the lock states will perform the following on a layer:
- Calls OpenUSD's [SetPermissionToSave / SetPermissionToEdit](https://openusd.org/release/api/class_sdf_layer.html#a32ad22bde9522ec46ef46ce2b88dfd14)
- Stores the current state of `Locked` and `System-Locked` in LayerLocking.h
- In case of `Locked`, the list of locked layers are also stored in an attribute on the associated proxy shape. This allows retaining the lock state between Maya sessions if the Maya scene is saved.


### USD Layer Editor
- Through action icons on each layer item. Note that the lock button can only be interacted
if the layer isn't already locked as a System-Lock.

![LayerEditor_LockLayerActionButton](images/layerlocking/LayerEditor_LockLayerActionButton.png)

A locked layer appears with a blue lock icon:

![LayerEditor_LockLayerActionButton_Locked](images/layerlocking/LayerEditor_LockLayerActionButton_Locked.png)

A system-locked layer appears similar to a locked layer but it cannot be unlocked through the user interface:

![LayerEditor_LockLayerActionButton_SystemLocked](images/layerlocking/LayerEditor_LockLayerActionButton_SystemLocked.png)

However, if there are already changes on the layer that are unsaved, they are indicated with an asterisk (*)
but still are not counted towards saving:

![LayerEditor_LockLayerActionButton_SystemLocked_modified](images/layerlocking/LayerEditor_LockLayerActionButton_SystemLocked_modified.png)

The lock state can also be changed through context menu items:

![LayerEditor_LockLayerMenuItems](images/layerlocking/LayerEditor_LockLayerMenuItems.png)

Similar to the lock button, some of the operations aren't allowed for a system locked layer:

![LayerEditor_LockLayerMenuItems_SystemLock_Disabled](images/layerlocking/LayerEditor_LockLayerMenuItems_SystemLock_Disabled.png)

Note that there are other factors that may prevent a layer from getting saved or edited:

- If a layer is locked, it cannot be edited
- If a layer is anonymous and its parent layer is locked, the anonymous layer can be edited but cannot be saved.
This is because by saving an anonymous layer, the layer identifier changes which has to be updated on the parent layer.
- If a layer belongs to an Un-shared stage.

Similarly, layers that are locked cannot have new sublayers as that will require changing the locked parent.

### Disk Write Permissions

Each layer is automatically checked for disk write permissions which may apply or remove a System-lock on a layer(s). These are the conditions a disk write check occurs (aka Refresh Sytem Lock):

- When the user initiates a layer reload (through the context menu in the USD Layer Editor).
- When the user initiates a stage reload (through Attribute Editor).
- When the user loads a stage from a file.
- Anything that may cause a session stage change on the LayerTreeModel (including the case of stage reload).
- Using script with the command `refreshSystemLock`.


When evaluating the disk write permissions, files for which the user has no write access will win against user-initiated locks.
For example:

| Condition | Result |
|:--------- |:------- |
| if a layer has a user-initiated lock and the file on disk is locked | the layer becomes system-locked |
| if a layer is locked and the file on disk is unlocked | The layer will be unlocked |
| if a layer is system-locked and the file on disk is unlocked | The layer will be unlocked |

Sometimes there may be a need to re-lock the layers after a refreshSystemLock. There is a callback system that will notify through scripting when a system lock refresh occurs and modifies layer permissions.

## API for Layer Locking

A layer's lock state can be changed through C++ or Python.

### C++

#### Directly using MayaUsd::lockLayer `(Not Un-doable)`:

MayaUsd::lockLayer(proxyShapePath, layer, locktype, updateProxyShapeAttr);

### Using MayaCommandHook::lockLayer `(Un-doable)`:
This uses the underlying `mayaUsdLayerEditor` MEL command.

MayaCommandHook::lockLayer(usdLayer, lockState, includeSubLayers);

### Using MEL or Python scripts:

#### In MEL Script layer editor command `(Un-doable)`:
These commands are un-doable

// Lock Type: 0 = Unlocked, 1 = Locked and 2 = System-Locked.
// Include Sublayers : 0 = Top Layer Only, 1 : Top and Sublayers
mayaUsdLayerEditor -edit -lockLayer 0 0 "proxyShapePath" "layerIdentifier"

// example: locks an anonymousLayer1 without changing the lock state of its sublayers
mayaUsdLayerEditor -edit -lockLayer 1 0 "|PathTo|proxyShape" "anon:00000143164533E0:anonymousLayer1"

// example: locks an exampleLayer.usda as well as its sub-layers
mayaUsdLayerEditor -edit -lockLayer 1 1 "|stage|stageShape1" "d:/Assets/exampleLayer.usda"

// example: System-locks an exampleLayer.usda
mayaUsdLayerEditor -edit -lockLayer 2 0 "|stage|stageShape1" "d:/Assets/exampleLayer.usda"

For more info on the syntax please refer to [Layer Editor Command Flags](../lib/mayaUsd/commands/Readme.md#layereditorcommand)

#### In Python script `(Un-doable)`:

# example: System-locks an exampleLayer
cmds.mayaUsdLayerEditor(exampleLayer.identifier, edit=True, lockLayer=(2, 0, proxyShapePath))

### Disk Write Permission Check (RefreshSystemLock)

#### In C++ `(Un-doable)`:
This can be done by calling the following function which uses a MEL script to call `mayaUsdLayerEditor refreshSystemLock`.

MayaCommandHook::refreshLayerSystemLock(usdLayer, refreshSubLayers);

#### In MEL script `(Un-doable)`:

// 0 = Only top layer, 1 = Include the sublayers
// example: This will perform a write permission check on a layer:
mayaUsdLayerEditor -edit -refreshSystemLock "|stage|stageShape1" 0 "d:/Assets/exampleLayer.usda"

// example: This will perform a write permission check on a layer and its sub-layers
mayaUsdLayerEditor -edit -refreshSystemLock "|stage|stageShape1" 1 "d:/Assets/exampleLayer.usda"

#### In Python script `(Un-doable)`:

// example: This will perform a write permission check on a layer and its sub-layers
cmds.mayaUsdLayerEditor(topLayer.identifier, edit=True, refreshSystemLock=(proxyShapePath, 1))

Note that if there isn't a change in the write permissions, no actions are taken. In order to track the changes due to system-lock refresh, you can use `mayaUsd.lib.registerUICallback` to get notified about the system lock changes due to refreshSystemLock:

def refreshSystemLockCallback(context, callbackData):
# Get the proxy shape path
proxyShapePath = context.get('proxyShapePath')
# Get the list of affected layers
layerIds = callbackData.get('affectedLayerIds')
print("The layers with a change in lock status are:")
for layerId in layerIds:
print(layerIds)

mayaUsd.lib.registerUICallback('onRefreshSystemLock', exampleCallback)

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions lib/mayaUsd/commands/layerEditorCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include <mayaUsd/utils/utilFileSystem.h>

#include <usdUfe/ufe/Utils.h>
#include <usdUfe/utils/uiCallback.h>

#include <pxr/base/tf/diagnostic.h>

Expand Down Expand Up @@ -967,6 +968,10 @@ class RefreshSystemLockLayer : public BaseCmd

updateEditTarget(stage);

if (_layers.size() > 0) {
_notifySystemLockIsRefreshed();
}

return true;
}

Expand All @@ -987,6 +992,10 @@ class RefreshSystemLockLayer : public BaseCmd

updateEditTarget(stage);

if (_layers.size() > 0) {
_notifySystemLockIsRefreshed();
}

return true;
}

Expand Down Expand Up @@ -1051,6 +1060,28 @@ class RefreshSystemLockLayer : public BaseCmd
}
}

void _notifySystemLockIsRefreshed()
{
PXR_NS::VtDictionary callbackContext;
callbackContext["proxyShapePath"] = PXR_NS::VtValue(_proxyShapePath.c_str());
PXR_NS::VtDictionary callbackData;

UsdUfe::UICallback::Ptr dstCallback = UsdUfe::getUICallback(TfToken("onRefreshSystemLock"));
if (!dstCallback)
return;

std::vector<std::string> affectedLayers;
affectedLayers.reserve(_layers.size());
for (size_t layerIndex = 0; layerIndex < _layers.size(); layerIndex++) {
affectedLayers.push_back(_layers[layerIndex]->GetIdentifier());
}

VtStringArray lockedArray(affectedLayers.begin(), affectedLayers.end());
callbackData["affectedLayerIds"] = lockedArray;

(*dstCallback)(callbackContext, callbackData);
}

UsdStageWeakPtr getStage()
{
auto prim = UsdMayaQuery::GetPrim(_proxyShapePath.c_str());
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 @@ -14,3 +14,4 @@
from usdUfe import restoreAllDefaultEditRouters
from usdUfe import OperationEditRouterContext
from usdUfe import AttributeEditRouterContext
from usdUfe import registerUICallback
1 change: 1 addition & 0 deletions lib/usdUfe/python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ target_sources(${PYTHON_TARGET_NAME}
wrapEditRouter.cpp
wrapGlobal.cpp
wrapTokens.cpp
wrapUICallback.cpp
wrapUtils.cpp
wrapCommands.cpp
)
Expand Down
1 change: 1 addition & 0 deletions lib/usdUfe/python/module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ PXR_NAMESPACE_USING_DIRECTIVE
TF_WRAP_MODULE
{
TF_WRAP(EditRouter);
TF_WRAP(UICallback);
TF_WRAP(Global);
TF_WRAP(Tokens);
TF_WRAP(Utils);
Expand Down
100 changes: 100 additions & 0 deletions lib/usdUfe/python/wrapUICallback.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//
// Copyright 2024 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 <usdUfe/utils/uiCallback.h>

#include <pxr/base/tf/pyError.h>
#include <pxr/base/tf/pyLock.h>

#include <boost/python.hpp>
#include <boost/python/def.hpp>

#include <iostream>

using namespace boost::python;

namespace {

std::string handlePythonException()
{
PyObject* exc = nullptr;
PyObject* val = nullptr;
PyObject* tb = nullptr;
PyErr_Fetch(&exc, &val, &tb);
handle<> hexc(exc);
handle<> hval(allow_null(val));
handle<> htb(allow_null(tb));
object traceback(import("traceback"));
object format_exception_only(traceback.attr("format_exception_only"));
object formatted_list = format_exception_only(hexc, hval);
object formatted = str("\n").join(formatted_list);
return extract<std::string>(formatted);
}

class PyUICallback : public UsdUfe::UICallback
{
public:
PyUICallback(PyObject* pyCallable)
: _pyCb(pyCallable)
{
}

~PyUICallback() override { }

void
operator()(const PXR_NS::VtDictionary& context, PXR_NS::VtDictionary& callbackData) override
{
// Note: necessary to compile the TF_WARN macro as it refers to USD types without using
// the namespace prefix.
PXR_NAMESPACE_USING_DIRECTIVE;

PXR_NS::TfPyLock pyLock;
if (!PyCallable_Check(_pyCb)) {
return;
}
boost::python::object dictObject(callbackData);
try {
call<void>(_pyCb, context, dictObject);
} catch (const boost::python::error_already_set&) {
const std::string errorMessage = handlePythonException();
boost::python::handle_exception();
PyErr_Clear();
TF_WARN("%s", errorMessage.c_str());
throw std::runtime_error(errorMessage);
} catch (const std::exception& ex) {
TF_WARN("%s", ex.what());
throw;
}
boost::python::extract<PXR_NS::VtDictionary> extractedDict(dictObject);
if (extractedDict.check()) {
callbackData = extractedDict;
}
}

private:
PyObject* _pyCb;
};

} // namespace

void wrapUICallback()
{
// Making the callbacks accessible from Python
def(
"registerUICallback", +[](const PXR_NS::TfToken& operation, PyObject* uiCallback) {
return UsdUfe::registerUICallback(
operation, std::make_shared<PyUICallback>(uiCallback));
});
}
2 changes: 2 additions & 0 deletions lib/usdUfe/utils/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ target_sources(${PROJECT_NAME}
layers.cpp
loadRules.cpp
loadRulesText.cpp
uiCallback.cpp
usdUtils.cpp
Utils.cpp
)
Expand All @@ -20,6 +21,7 @@ set(HEADERS
editRouterContext.h
layers.h
loadRules.h
uiCallback.h
usdUtils.h
Utils.h
)
Expand Down
Loading

0 comments on commit a2b565b

Please sign in to comment.