Skip to content
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
52 changes: 52 additions & 0 deletions lib/usd/ui/debugTools/CompositionEditorCmd.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
#include "CompositionEditorCmd.h"

#include <mayaUsd/ufe/Utils.h>
#include <mayaUsd/undo/MayaUsdUndoBlock.h>

#include <usdUfe/undo/UsdUndoManager.h>

#include <pxr/usd/sdf/layer.h>
#include <pxr/usd/usd/prim.h>

#include <maya/MArgParser.h>
Expand All @@ -40,6 +44,9 @@
#include <UsdDebugUI/ApplicationHost.h>
#include <UsdDebugUI/CompositionEditorWidget.h>

#include <algorithm>
#include <string>

namespace MAYAUSD_NS_DEF {

const MString CompositionEditorCmd::name("mayaUsdCompositionEditor");
Expand All @@ -53,6 +60,31 @@ constexpr auto kReloadFlagLong = "-reload";

const MString WORKSPACE_CONTROL_NAME = "mayaUsdCompositionEditor";

// RAII guard that opens a named Maya undo chunk
class UndoChunkContext
{
private:
// Maya's undo chunk names cannot contain spaces (the name is split at the
// first space), so replace them with underscores before quoting.
MString cleanChunkName(const std::string& label)
{
std::string name = label.empty() ? "USD Composition Edit" : label;
std::replace(name.begin(), name.end(), ' ', '_');
return MString("\"") + name.c_str() + "\"";
}

public:
explicit UndoChunkContext(const std::string& label)
{
MGlobal::executeCommand(
MString("undoInfo -openChunk -chunkName ") + cleanChunkName(label), false, false);
}
~UndoChunkContext() { MGlobal::executeCommand("undoInfo -closeChunk", false, false); }

UndoChunkContext(const UndoChunkContext&) = delete;
UndoChunkContext& operator=(const UndoChunkContext&) = delete;
};

QPointer<Adsk::UsdDebug::CompositionEditorWidget> g_compositionEditorWidget;
Ufe::Observer::Ptr g_selectionObserver;

Expand Down Expand Up @@ -118,6 +150,26 @@ class MayaCompositionEditorHost : public Adsk::UsdDebug::ApplicationHost
return QColor();
}

bool executeInCmd(
const std::string& editLabel,
const std::string& layerId,
const std::function<bool()>& edit) override
{
if (!edit) {
return false;
}

// Ensure the layer being edited has a UsdUndoStateDelegate so the inverse
// of the edit is recorded
if (PXR_NS::SdfLayerHandle layer = PXR_NS::SdfLayer::Find(layerId)) {
UsdUfe::UsdUndoManager::instance().trackLayerStates(layer);
}

UndoChunkContext undoChunk(editLabel);
MayaUsdUndoBlock undoBlock;
return edit();
}

protected:
MayaCompositionEditorHost() { injectInstance(this); }
};
Expand Down
97 changes: 97 additions & 0 deletions test/lib/testAdskUsdDebugTools.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@

import unittest

import maya.cmds as cmds

from pxr import Sdf, Usd

import mayaUsd.lib as mayaUsdLib


class testAdskUsdDebugTools(unittest.TestCase):
"""
Expand All @@ -25,10 +31,101 @@ class testAdskUsdDebugTools(unittest.TestCase):

def testDebugToolsLoaded(self):
try:
# Import the USD Python bindings first so the boost.python to/from-Python
# converters for USD types (e.g. SdfPath) are registered.
from pxr import Usd, Sdf
import AdskUsdDebug
except Exception as e:
self.fail(f"Autodesk USD Debug Tools module not available or failed to load. {e}")


class testAdskUsdDebugToolsRemoveOpinionUndo(unittest.TestCase):
"""
Verify that removing an opinion through the Debug Tools composition editor
can be undone.
"""

@classmethod
def setUpClass(cls):
cmds.loadPlugin('mayaUsdPlugin')

def setUp(self):
# Skip if the Debug Tools component was not shipped in this build; the
# smoke test above already reports a hard failure when it is expected.
try:
import AdskUsdDebug
except Exception as e:
self.skipTest(f"Autodesk USD Debug Tools module not available: {e}")

cmds.file(force=True, new=True)

self.stage = Usd.Stage.CreateInMemory()

# Mirror executeInCmd step 1: track the layer being edited so its inverse
# edits are recorded by the undo manager.
mayaUsdLib.UsdUndoManager.trackLayerStates(self.stage.GetRootLayer())

cmds.select(clear=True)

def testRemoveOpinionUndoRedo(self):
'''Removing a single property opinion is undoable and redoable.'''
from AdskUsdDebug import RemoveOpinion

prim = self.stage.DefinePrim('/Foo', 'Xform')
prim.CreateAttribute('radius', Sdf.ValueTypeNames.Double).Set(2.0)
layerId = self.stage.GetRootLayer().identifier

self.assertTrue(prim.GetAttribute('radius').HasAuthoredValue())

nbCmds = cmds.undoInfo(q=True)

with mayaUsdLib.UsdUndoBlock():
self.assertTrue(RemoveOpinion(self.stage, layerId, '/Foo.radius'))

# The opinion is gone and exactly one command was added to the queue.
self.assertFalse(prim.GetAttribute('radius').HasAuthoredValue())
self.assertEqual(cmds.undoInfo(q=True), nbCmds + 1)

# Undo restores the removed opinion (value included).
cmds.undo()
self.assertTrue(prim.GetAttribute('radius').HasAuthoredValue())
self.assertEqual(prim.GetAttribute('radius').Get(), 2.0)

# Redo removes it again.
cmds.redo()
self.assertFalse(prim.GetAttribute('radius').HasAuthoredValue())

def testRemoveOpinionsUndoRedo(self):
'''Removing several opinions in one edit is undoable as a single step.'''
from AdskUsdDebug import OpinionRef, RemoveOpinions

prim = self.stage.DefinePrim('/Foo', 'Xform')
prim.CreateAttribute('radius', Sdf.ValueTypeNames.Double).Set(2.0)
prim.CreateAttribute('count', Sdf.ValueTypeNames.Int).Set(7)
layerId = self.stage.GetRootLayer().identifier

nbCmds = cmds.undoInfo(q=True)

with mayaUsdLib.UsdUndoBlock():
self.assertTrue(RemoveOpinions(
self.stage,
[
OpinionRef(layerId, '/Foo.radius'),
OpinionRef(layerId, '/Foo.count'),
]))

# Both opinions removed, batched into a single undoable command.
self.assertFalse(prim.GetAttribute('radius').HasAuthoredValue())
self.assertFalse(prim.GetAttribute('count').HasAuthoredValue())
self.assertEqual(cmds.undoInfo(q=True), nbCmds + 1)

# A single undo restores both opinions.
cmds.undo()
self.assertTrue(prim.GetAttribute('radius').HasAuthoredValue())
self.assertTrue(prim.GetAttribute('count').HasAuthoredValue())
self.assertEqual(prim.GetAttribute('radius').Get(), 2.0)
self.assertEqual(prim.GetAttribute('count').Get(), 7)


if __name__ == '__main__':
unittest.main(verbosity=2)