From 26b821d5bc46bf9ad6685811e7e691733df4e37e Mon Sep 17 00:00:00 2001 From: Pierre Tremblay Date: Wed, 20 Nov 2019 13:23:52 -0800 Subject: [PATCH 1/7] Attribute changed notifications. --- lib/ufe/StagesSubject.cpp | 15 ++++++++++++--- .../ballset/StandaloneScene/top_layer.ma | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/ufe/StagesSubject.cpp b/lib/ufe/StagesSubject.cpp index 48f8db0961..eb0cbea9e1 100644 --- a/lib/ufe/StagesSubject.cpp +++ b/lib/ufe/StagesSubject.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -147,12 +148,12 @@ void StagesSubject::afterOpen() me, &StagesSubject::stageChanged, stage); } - // Set up our stage to AL_usdmaya_ProxyShape UFE path (and reverse) + // Set up our stage to proxy shape UFE path (and reverse) // mapping. We do this with the following steps: // - get all proxyShape nodes in the scene. - // - get their AL Python wrapper - // - get their Dag paths + // - get their Dag paths. // - convert the Dag paths to UFE paths. + // - get their stage. g_StageMap.clear(); auto proxyShapeNames = ProxyShapeHandler::getAllNames(); for (const auto& psn : proxyShapeNames) @@ -207,6 +208,14 @@ void StagesSubject::stageChanged(UsdNotice::ObjectsChanged const& notice, UsdSta { auto usdPrimPathStr = changedPath.GetPrimPath().GetString(); auto ufePath = stagePath(sender) + Ufe::PathSegment(usdPrimPathStr, g_USDRtid, '/'); + + // isPrimPropertyPath() does not consider relational attributes + // isPropertyPath() does consider relational attributes + // isRelationalAttributePath() considers only relational attributes + if (changedPath.IsPrimPropertyPath()) { + Ufe::Attributes::notify(ufePath, changedPath.GetName()); + } + // We need to determine if the change is a Transform3d change. // We must at least pick up xformOp:translate, xformOp:rotateXYZ, // and xformOp:scale. diff --git a/test/lib/ufe/test-samples/ballset/StandaloneScene/top_layer.ma b/test/lib/ufe/test-samples/ballset/StandaloneScene/top_layer.ma index 2b042039dd..fa66252b63 100644 --- a/test/lib/ufe/test-samples/ballset/StandaloneScene/top_layer.ma +++ b/test/lib/ufe/test-samples/ballset/StandaloneScene/top_layer.ma @@ -113,7 +113,7 @@ createNode script -n "uiConfigurationScriptNode"; + " -width 1\n -height 1\n -sceneRenderFilter 0\n $editorName;\n modelEditor -e -viewSelected 0 $editorName;\n\t\tif (!$useSceneConfig) {\n\t\t\tpanel -e -l $label $panelName;\n\t\t}\n\t}\n\n\n\t$panelName = `sceneUIReplacement -getNextPanel \"modelPanel\" (localizedPanelLabel(\"Persp View\")) `;\n\tif (\"\" != $panelName) {\n\t\t$label = `panel -q -label $panelName`;\n\t\tmodelPanel -edit -l (localizedPanelLabel(\"Persp View\")) -mbv $menusOkayInPanels $panelName;\n\t\t$editorName = $panelName;\n modelEditor -e \n -camera \"persp\" \n -useInteractiveMode 0\n -displayLights \"default\" \n -displayAppearance \"smoothShaded\" \n -activeOnly 0\n -ignorePanZoom 0\n -wireframeOnShaded 0\n -headsUpDisplay 1\n -holdOuts 1\n -selectionHiliteDisplay 1\n -useDefaultMaterial 0\n -bufferMode \"double\" \n -twoSidedLighting 0\n -backfaceCulling 0\n -xray 0\n -jointXray 0\n" + " -activeComponentsXray 0\n -displayTextures 0\n -smoothWireframe 0\n -lineWidth 1\n -textureAnisotropic 0\n -textureHilight 1\n -textureSampling 2\n -textureDisplay \"modulate\" \n -textureMaxSize 16384\n -fogging 0\n -fogSource \"fragment\" \n -fogMode \"linear\" \n -fogStart 0\n -fogEnd 100\n -fogDensity 0.1\n -fogColor 0.5 0.5 0.5 1 \n -depthOfFieldPreview 1\n -maxConstantTransparency 1\n -rendererName \"vp2Renderer\" \n -objectFilterShowInHUD 1\n -isFiltered 0\n -colorResolution 256 256 \n -bumpResolution 512 512 \n -textureCompression 0\n -transparencyAlgorithm \"frontAndBackCull\" \n -transpInShadows 0\n -cullingOverride \"none\" \n -lowQualityLighting 0\n -maximumNumHardwareLights 1\n -occlusionCulling 0\n -shadingModel 0\n" + " -useBaseRenderer 0\n -useReducedRenderer 0\n -smallObjectCulling 0\n -smallObjectThreshold -1 \n -interactiveDisableShadows 0\n -interactiveBackFaceCull 0\n -sortTransparent 1\n -controllers 1\n -nurbsCurves 1\n -nurbsSurfaces 1\n -polymeshes 1\n -subdivSurfaces 1\n -planes 1\n -lights 1\n -cameras 1\n -controlVertices 1\n -hulls 1\n -grid 1\n -imagePlane 1\n -joints 1\n -ikHandles 1\n -deformers 1\n -dynamics 1\n -particleInstancers 1\n -fluids 1\n -hairSystems 1\n -follicles 1\n -nCloths 1\n -nParticles 1\n -nRigids 1\n -dynamicConstraints 1\n -locators 1\n -manipulators 1\n -pluginShapes 1\n -dimensions 1\n -handles 1\n -pivots 1\n -textures 1\n" - + " -strokes 1\n -motionTrails 1\n -clipGhosts 1\n -greasePencils 1\n -shadows 0\n -captureSequenceNumber -1\n -width 1125\n -height 825\n -sceneRenderFilter 0\n $editorName;\n modelEditor -e -viewSelected 0 $editorName;\n\t\tif (!$useSceneConfig) {\n\t\t\tpanel -e -l $label $panelName;\n\t\t}\n\t}\n\n\n\t$panelName = `sceneUIReplacement -getNextPanel \"outlinerPanel\" (localizedPanelLabel(\"ToggledOutliner\")) `;\n\tif (\"\" != $panelName) {\n\t\t$label = `panel -q -label $panelName`;\n\t\toutlinerPanel -edit -l (localizedPanelLabel(\"ToggledOutliner\")) -mbv $menusOkayInPanels $panelName;\n\t\t$editorName = $panelName;\n outlinerEditor -e \n -docTag \"isolOutln_fromSeln\" \n -showShapes 0\n -showAssignedMaterials 0\n -showTimeEditor 1\n -showReferenceNodes 0\n -showReferenceMembers 0\n -showAttributes 0\n -showConnected 0\n -showAnimCurvesOnly 0\n -showMuteInfo 0\n" + + " -strokes 1\n -motionTrails 1\n -clipGhosts 1\n -greasePencils 1\n -shadows 0\n -captureSequenceNumber -1\n -width 1125\n -height 825\n -sceneRenderFilter 0\n $editorName;\n modelEditor -e -viewSelected 0 $editorName;\n\t\tif (!$useSceneConfig) {\n\t\t\tpanel -e -l $label $panelName;\n\t\t}\n\t}\n\n\n\t$panelName = `sceneUIReplacement -getNextPanel \"outlinerPanel\" (localizedPanelLabel(\"ToggledOutliner\")) `;\n\tif (\"\" != $panelName) {\n\t\t$label = `panel -q -label $panelName`;\n\t\toutlinerPanel -edit -l (localizedPanelLabel(\"ToggledOutliner\")) -mbv $menusOkayInPanels $panelName;\n\t\t$editorName = $panelName;\n outlinerEditor -e \n -docTag \"isolOutln_fromSeln\" \n -showShapes 1\n -showAssignedMaterials 0\n -showTimeEditor 1\n -showReferenceNodes 0\n -showReferenceMembers 0\n -showAttributes 0\n -showConnected 0\n -showAnimCurvesOnly 0\n -showMuteInfo 0\n" + " -organizeByLayer 1\n -organizeByClip 1\n -showAnimLayerWeight 1\n -autoExpandLayers 1\n -autoExpand 0\n -showDagOnly 1\n -showAssets 1\n -showContainedOnly 1\n -showPublishedAsConnected 0\n -showParentContainers 0\n -showContainerContents 1\n -ignoreDagHierarchy 0\n -expandConnections 0\n -showUpstreamCurves 1\n -showUnitlessCurves 1\n -showCompounds 1\n -showLeafs 1\n -showNumericAttrsOnly 0\n -highlightActive 1\n -autoSelectNewObjects 0\n -doNotSelectNewObjects 0\n -dropIsParent 1\n -transmitFilters 0\n -setFilter \"defaultSetFilter\" \n -showSetMembers 1\n -allowMultiSelection 1\n -alwaysToggleSelect 0\n -directSelect 0\n -isSet 0\n -isSetMember 0\n -displayMode \"DAG\" \n -expandObjects 0\n -setsIgnoreFilters 1\n" + " -containersIgnoreFilters 0\n -editAttrName 0\n -showAttrValues 0\n -highlightSecondary 0\n -showUVAttrsOnly 0\n -showTextureNodesOnly 0\n -attrAlphaOrder \"default\" \n -animLayerFilterOptions \"allAffecting\" \n -sortOrder \"none\" \n -longNames 0\n -niceNames 1\n -showNamespace 1\n -showPinIcons 0\n -mapMotionTrails 0\n -ignoreHiddenAttribute 0\n -ignoreOutlinerColor 0\n -renderFilterVisible 0\n -renderFilterIndex 0\n -selectionOrder \"chronological\" \n -expandAttribute 0\n $editorName;\n\t\tif (!$useSceneConfig) {\n\t\t\tpanel -e -l $label $panelName;\n\t\t}\n\t}\n\n\n\t$panelName = `sceneUIReplacement -getNextPanel \"outlinerPanel\" (localizedPanelLabel(\"Outliner\")) `;\n\tif (\"\" != $panelName) {\n\t\t$label = `panel -q -label $panelName`;\n\t\toutlinerPanel -edit -l (localizedPanelLabel(\"Outliner\")) -mbv $menusOkayInPanels $panelName;\n" + "\t\t$editorName = $panelName;\n outlinerEditor -e \n -showShapes 0\n -showAssignedMaterials 0\n -showTimeEditor 1\n -showReferenceNodes 0\n -showReferenceMembers 0\n -showAttributes 0\n -showConnected 0\n -showAnimCurvesOnly 0\n -showMuteInfo 0\n -organizeByLayer 1\n -organizeByClip 1\n -showAnimLayerWeight 1\n -autoExpandLayers 1\n -autoExpand 0\n -showDagOnly 1\n -showAssets 1\n -showContainedOnly 1\n -showPublishedAsConnected 0\n -showParentContainers 0\n -showContainerContents 1\n -ignoreDagHierarchy 0\n -expandConnections 0\n -showUpstreamCurves 1\n -showUnitlessCurves 1\n -showCompounds 1\n -showLeafs 1\n -showNumericAttrsOnly 0\n -highlightActive 1\n -autoSelectNewObjects 0\n -doNotSelectNewObjects 0\n -dropIsParent 1\n" From b836e907b03fd45dd632d10ad225797164501607 Mon Sep 17 00:00:00 2001 From: Pierre Tremblay Date: Sun, 24 Nov 2019 14:44:11 -0800 Subject: [PATCH 2/7] Added attribute changed notification test, use ufeSelectCmd from Maya. --- lib/ufe/StagesSubject.cpp | 41 ++- lib/ufe/StagesSubject.h | 22 ++ lib/ufe/UsdAttribute.cpp | 45 ++- test/lib/ufe/CMakeLists.txt | 5 - test/lib/ufe/testAttribute.py | 210 +++++++++++- test/lib/ufe/ufeScripts/__init__.py | 0 test/lib/ufe/ufeScripts/ufeSelectCmd.py | 302 ------------------ .../ufe/ufeTestPlugins/ufeTestCmdsPlugin.py | 53 --- test/lib/ufe/ufeTestUtils/mayaUtils.py | 2 +- test/lib/ufe/ufeTestUtils/ufeUtils.py | 4 +- 10 files changed, 312 insertions(+), 372 deletions(-) delete mode 100644 test/lib/ufe/ufeScripts/__init__.py delete mode 100644 test/lib/ufe/ufeScripts/ufeSelectCmd.py delete mode 100644 test/lib/ufe/ufeTestPlugins/ufeTestCmdsPlugin.py diff --git a/lib/ufe/StagesSubject.cpp b/lib/ufe/StagesSubject.cpp index eb0cbea9e1..c5598f35c9 100644 --- a/lib/ufe/StagesSubject.cpp +++ b/lib/ufe/StagesSubject.cpp @@ -31,6 +31,14 @@ #include #include +#include + +namespace { +bool inAttributeChangedNotificationGuard = false; + +std::unordered_map pendingAttributeChangedNotifications; + +} MAYAUSD_NS_DEF { namespace ufe { @@ -213,7 +221,13 @@ void StagesSubject::stageChanged(UsdNotice::ObjectsChanged const& notice, UsdSta // isPropertyPath() does consider relational attributes // isRelationalAttributePath() considers only relational attributes if (changedPath.IsPrimPropertyPath()) { - Ufe::Attributes::notify(ufePath, changedPath.GetName()); + if (inAttributeChangedNotificationGuard) { + pendingAttributeChangedNotifications[ufePath] = + changedPath.GetName(); + } + else { + Ufe::Attributes::notify(ufePath, changedPath.GetName()); + } } // We need to determine if the change is a Transform3d change. @@ -232,5 +246,30 @@ void StagesSubject::onStageSet(const UsdMayaProxyStageSetNotice& notice) afterOpen(); } +AttributeChangedNotificationGuard::AttributeChangedNotificationGuard() +{ + if (inAttributeChangedNotificationGuard) { + TF_CODING_ERROR("Attribute changed notification guard cannot be nested."); + } + + if (!pendingAttributeChangedNotifications.empty()) { + TF_CODING_ERROR("Stale pending attribute changed notifications."); + } + + inAttributeChangedNotificationGuard = true; + +} + +AttributeChangedNotificationGuard::~AttributeChangedNotificationGuard() +{ + inAttributeChangedNotificationGuard = false; + + for (const auto& notificationInfo : pendingAttributeChangedNotifications) { + Ufe::Attributes::notify(notificationInfo.first, notificationInfo.second); + } + + pendingAttributeChangedNotifications.clear(); +} + } // namespace ufe } // namespace MayaUsd diff --git a/lib/ufe/StagesSubject.h b/lib/ufe/StagesSubject.h index 3a3587ed4d..efb399f2e6 100644 --- a/lib/ufe/StagesSubject.h +++ b/lib/ufe/StagesSubject.h @@ -86,5 +86,27 @@ class MAYAUSD_CORE_PUBLIC StagesSubject : public TfWeakBase }; // StagesSubject +//! \brief Guard to delay attribute changed notifications. +/*! + Instantiating an object of this class allows the attribute changed + notifications to be delayed until the guard expires. + + The guard collapses down notifications for a given UFE path, which is + desirable to avoid duplicate notifications. However, it is an error to + have notifications for more than one attribute within a single guard. + */ +class MAYAUSD_CORE_PUBLIC AttributeChangedNotificationGuard { +public: + + AttributeChangedNotificationGuard(); + ~AttributeChangedNotificationGuard(); + + //@{ + //! Cannot be copied or assigned. + AttributeChangedNotificationGuard(const AttributeChangedNotificationGuard&) = delete; + const AttributeChangedNotificationGuard& operator&(const AttributeChangedNotificationGuard&) = delete; + //@} +}; + } // namespace ufe } // namespace MayaUsd diff --git a/lib/ufe/UsdAttribute.cpp b/lib/ufe/UsdAttribute.cpp index 991307281f..11285f4f44 100644 --- a/lib/ufe/UsdAttribute.cpp +++ b/lib/ufe/UsdAttribute.cpp @@ -15,6 +15,7 @@ // #include "UsdAttribute.h" +#include "StagesSubject.h" #include #include @@ -47,6 +48,36 @@ static constexpr char kErrorMsgEnumNoValue[] = "Enum string attribute has no val namespace { +template +bool setUsdAttr(const PXR_NS::UsdAttribute& attr, const T& value) +{ + // As of 24-Nov-2019, calling Set() on a UsdAttribute causes two "info only" + // change notifications to be sent (see StagesSubject::stageChanged). With + // the current USD implementation (USD 19.11), UsdAttribute::Set() ends up + // in UsdStage::_SetValueImpl(). This function calls in sequence: + // - UsdStage::_CreateAttributeSpecForEditing(), which has an SdfChangeBlock + // whose expiry causes a notification to be sent. + // - SdfLayer::SetField(), which also has an SdfChangeBlock whose + // expiry causes a notification to be sent. + // These two calls appear to be made on all calls to UsdAttribute::Set(), + // not just on the first call. + // + // Trying to wrap the call to UsdAttribute::Set() inside an additional + // SdfChangeBlock fails: no notifications are sent at all. This is most + // likely because of the warning given in the SdfChangeBlock documentation: + // + // https://graphics.pixar.com/usd/docs/api/class_sdf_change_block.html + // + // which stages that "it is not safe to use [...] [a] downstream API [such + // as Usd] while a changeblock is open [...]". + // + // Therefore, we have implemented an attribute change block notification of + // our own in the StagesSubject, which we invoke here, so that only a + // single UFE attribute changed notification is generated. + MayaUsd::ufe::AttributeChangedNotificationGuard guard; (void) guard; + return attr.Set(value); +} + std::string getUsdAttributeValueAsString(const PXR_NS::UsdAttribute& attr) { if (!attr.HasValue()) return std::string(); @@ -92,7 +123,7 @@ void setUsdAttributeVectorFromUfe(PXR_NS::UsdAttribute& attr, const U& value) T vec; UFE_ASSERT_MSG(attr.Get(&vec), kErrorMsgInvalidType); vec.Set(value.x(), value.y(), value.z()); - bool b = attr.Set(vec); + bool b = setUsdAttr(attr, vec); UFE_ASSERT_MSG(b, kErrorMsgFailedSet); } @@ -202,7 +233,7 @@ void UsdAttributeEnumString::set(const std::string& value) PXR_NS::TfToken dummy; UFE_ASSERT_MSG(fUsdAttr.Get(&dummy), kErrorMsgInvalidType); PXR_NS::TfToken tok(value); - bool b = fUsdAttr.Set(tok); + bool b = setUsdAttr(fUsdAttr, tok); UFE_ASSERT_MSG(b, kErrorMsgFailedSet); } @@ -269,7 +300,7 @@ void TypedUsdAttribute::set(const std::string& value) { std::string dummy; UFE_ASSERT_MSG(fUsdAttr.Get(&dummy), kErrorMsgInvalidType); - bool b = fUsdAttr.Set(value); + bool b = setUsdAttr(fUsdAttr, value); UFE_ASSERT_MSG(b, kErrorMsgFailedSet); return; } @@ -278,7 +309,7 @@ void TypedUsdAttribute::set(const std::string& value) PXR_NS::TfToken dummy; UFE_ASSERT_MSG(fUsdAttr.Get(&dummy), kErrorMsgInvalidType); PXR_NS::TfToken tok(value); - bool b = fUsdAttr.Set(tok); + bool b = setUsdAttr(fUsdAttr, tok); UFE_ASSERT_MSG(b, kErrorMsgFailedSet); return; } @@ -300,7 +331,7 @@ void TypedUsdAttribute::set(const Ufe::Color3f& value) GfVec3f vec; UFE_ASSERT_MSG(fUsdAttr.Get(&vec), kErrorMsgInvalidType); vec.Set(value.r(), value.g(), value.b()); - bool b = fUsdAttr.Set(vec); + bool b = setUsdAttr(fUsdAttr, vec); UFE_ASSERT_MSG(b, kErrorMsgFailedSet); } @@ -360,7 +391,7 @@ void TypedUsdAttribute::set(const T& value) { T dummy; UFE_ASSERT_MSG(fUsdAttr.Get(&dummy), kErrorMsgInvalidType); - bool b = fUsdAttr.Set(value); + bool b = setUsdAttr(fUsdAttr, value); UFE_ASSERT_MSG(b, kErrorMsgFailedSet); } @@ -481,7 +512,7 @@ bool UsdAttribute::setValue(const std::string& value) if (!cast.IsEmpty()) cast.Swap(val); - return fUsdAttr.Set(val); + return setUsdAttr(fUsdAttr, val); } #endif diff --git a/test/lib/ufe/CMakeLists.txt b/test/lib/ufe/CMakeLists.txt index dfab9e3880..7f421a33dd 100644 --- a/test/lib/ufe/CMakeLists.txt +++ b/test/lib/ufe/CMakeLists.txt @@ -56,12 +56,8 @@ add_custom_target(${TARGET_NAME} ALL) mayaUsd_copyDirectory(${TARGET_NAME} DESTINATION ${CMAKE_CURRENT_BINARY_DIR} DIRECTORY test-samples) -mayaUsd_copyDirectory(${TARGET_NAME} DESTINATION ${CMAKE_CURRENT_BINARY_DIR} - DIRECTORY ufeScripts) mayaUsd_copyDirectory(${TARGET_NAME} DESTINATION ${CMAKE_CURRENT_BINARY_DIR} DIRECTORY ufeTestUtils) -mayaUsd_copyDirectory(${TARGET_NAME} DESTINATION ${CMAKE_CURRENT_BINARY_DIR} - DIRECTORY ufeTestPlugins) mayaUsd_copyFiles(${TARGET_NAME} DESTINATION ${CMAKE_CURRENT_BINARY_DIR} FILES ${test_script_files}) mayaUsd_copyFiles(${TARGET_NAME} DESTINATION ${CMAKE_CURRENT_BINARY_DIR} @@ -75,7 +71,6 @@ set(pythonPath set(mayaPluginPath "${CMAKE_INSTALL_PREFIX}/plugin/adsk/plugin" - "${CMAKE_CURRENT_BINARY_DIR}/ufeTestPlugins" ) set(path diff --git a/test/lib/ufe/testAttribute.py b/test/lib/ufe/testAttribute.py index 8d3b5381cc..ae8a6c50af 100644 --- a/test/lib/ufe/testAttribute.py +++ b/test/lib/ufe/testAttribute.py @@ -21,8 +21,23 @@ from pxr import UsdGeom import random +import maya.cmds as cmds +import maya.internal.ufeSupport.ufeCmdWrapper as ufeCmd + import unittest +class TestObserver(ufe.Observer): + def __init__(self): + super(TestObserver, self).__init__() + self.changed = 0 + + def __call__(self, notification): + if isinstance(notification, ufe.AttributeChanged): + self.changed += 1 + + def notifications(self): + return self.changed + class AttributeTestCase(unittest.TestCase): '''Verify the Attribute UFE interface, for multiple runtimes. ''' @@ -39,6 +54,11 @@ def setUpClass(cls): random.seed() + @classmethod + def tearDownClass(cls): + # See comments in MayaUFEPickWalkTesting.tearDownClass + cmds.file(new=True, force=True) + def setUp(self): '''Called initially to set up the maya test environment''' self.assertTrue(self.pluginsLoaded) @@ -51,6 +71,23 @@ def assertColorAlmostEqual(self, ufeColor, usdColor): for va, vb in zip(ufeColor.color, usdColor): self.assertAlmostEqual(va, vb, places=6) + def runUndoRedo(self, attr, newVal, decimalPlaces=None): + oldVal = attr.get() + assert oldVal != newVal, "Undo / redo testing requires setting a value different from the current value" + + ufeCmd.execute(attr.setCmd(newVal)) + + if decimalPlaces is not None: + self.assertAlmostEqual(attr.get(), newVal, decimalPlaces) + newVal = attr.get() + else: + self.assertEqual(attr.get(), newVal) + + cmds.undo() + self.assertEqual(attr.get(), oldVal) + cmds.redo() + self.assertEqual(attr.get(), newVal) + def runTestAttribute(self, path, attrName, ufeAttrClass, ufeAttrType): '''Engine method to run attribute test.''' @@ -142,6 +179,9 @@ def testAttributeEnumString(self): # Verify that the new UFE value matches what is directly in USD. self.assertEqual(ufeAttr.get(), usdAttr.Get()) + # Change back to 'inherited' using a command. + self.runUndoRedo(ufeAttr, UsdGeom.Tokens.inherited) + def testAttributeBool(self): '''Test the Bool attribute type.''' @@ -165,6 +205,8 @@ def testAttributeBool(self): # Then make sure that new UFE value matches what it in USD. self.assertEqual(ufeAttr.get(), usdAttr.Get()) + self.runUndoRedo(ufeAttr, not ufeAttr.get()) + def testAttributeInt(self): '''Test the Int attribute type.''' @@ -188,6 +230,8 @@ def testAttributeInt(self): # Then make sure that new UFE value matches what it in USD. self.assertEqual(ufeAttr.get(), usdAttr.Get()) + self.runUndoRedo(ufeAttr, ufeAttr.get()+1) + def testAttributeFloat(self): '''Test the Float attribute type.''' @@ -211,6 +255,11 @@ def testAttributeFloat(self): # Then make sure that new UFE value matches what it in USD. self.assertEqual(ufeAttr.get(), usdAttr.Get()) + # Python floating-point numbers are doubles. If stored in a float + # attribute, the resulting precision will be less than the original + # Python value. + self.runUndoRedo(ufeAttr, ufeAttr.get() + 1.0, decimalPlaces=6) + def _testAttributeDouble(self): '''Test the Double attribute type.''' @@ -241,6 +290,8 @@ def testAttributeStringString(self): # Then make sure that new UFE value matches what it in USD. self.assertEqual(ufeAttr.get(), usdAttr.Get()) + self.runUndoRedo(ufeAttr, 'potato') + def testAttributeStringToken(self): '''Test the String (Token) attribute type.''' @@ -265,6 +316,8 @@ def testAttributeStringToken(self): # Then make sure that new UFE value matches what it in USD. self.assertEqual(ufeAttr.get(), usdAttr.Get()) + self.runUndoRedo(ufeAttr, 'Box') + def testAttributeColorFloat3(self): '''Test the ColorFloat3 attribute type.''' @@ -289,6 +342,13 @@ def testAttributeColorFloat3(self): # Then make sure that new UFE value matches what it in USD. self.assertColorAlmostEqual(ufeAttr.get(), usdAttr.Get()) + # The following causes a segmentation fault on CentOS 7. + # self.runUndoRedo(ufeAttr, + # ufe.Color3f(vec.r()+1.0, vec.g()+2.0, vec.b()+3.0)) + # Entered as MAYA-102168. + newVec = ufe.Color3f(vec.color[0]+1.0, vec.color[1]+2.0, vec.color[2]+3.0) + self.runUndoRedo(ufeAttr, newVec) + def _testAttributeInt3(self): '''Test the Int3 attribute type.''' @@ -312,13 +372,16 @@ def testAttributeFloat3(self): # Compare the initial UFE value to that directly from USD. self.assertVectorAlmostEqual(ufeAttr.get(), usdAttr.Get()) - # Set the attribute in UFE with some random color values. + # Set the attribute in UFE with some random values. vec = ufe.Vector3f(random.random(), random.random(), random.random()) ufeAttr.set(vec) # Then make sure that new UFE value matches what it in USD. self.assertVectorAlmostEqual(ufeAttr.get(), usdAttr.Get()) + self.runUndoRedo(ufeAttr, + ufe.Vector3f(vec.x()+1.0, vec.y()+2.0, vec.z()+3.0)) + def testAttributeDouble3(self): '''Test the Double3 attribute type.''' @@ -342,3 +405,148 @@ def testAttributeDouble3(self): # Then make sure that new UFE value matches what it in USD. self.assertVectorAlmostEqual(ufeAttr.get(), usdAttr.Get()) + + self.runUndoRedo(ufeAttr, + ufe.Vector3d(vec.x()-1.0, vec.y()-2.0, vec.z()-3.0)) + + def testObservation(self): + '''Test Attributes observation interface. + + Test both global attribute observation and per-node attribute + observation. + ''' + + # Create three observers, one for global attribute observation, and two + # on different UFE items. + proxyShapePathSegment = mayaUtils.createUfePathSegment( + "|world|transform1|proxyShape1") + path = ufe.Path([ + proxyShapePathSegment, + usdUtils.createUfePathSegment('/Room_set/Props/Ball_34')]) + ball34 = ufe.Hierarchy.createItem(path) + path = ufe.Path([ + proxyShapePathSegment, + usdUtils.createUfePathSegment('/Room_set/Props/Ball_35')]) + ball35 = ufe.Hierarchy.createItem(path) + + (ball34Obs, ball35Obs, globalObs) = [TestObserver() for i in range(3)] + + # Maya registers a single global observer on startup. + self.assertEqual(ufe.Attributes.nbObservers(), 1) + + # No item-specific observers. + self.assertFalse(ufe.Attributes.hasObservers(ball34.path())) + self.assertFalse(ufe.Attributes.hasObservers(ball35.path())) + self.assertEqual(ufe.Attributes.nbObservers(ball34), 0) + self.assertEqual(ufe.Attributes.nbObservers(ball35), 0) + self.assertFalse(ufe.Attributes.hasObserver(ball34, ball34Obs)) + self.assertFalse(ufe.Attributes.hasObserver(ball35, ball35Obs)) + + # No notifications yet. + self.assertEqual(ball34Obs.notifications(), 0) + self.assertEqual(ball35Obs.notifications(), 0) + self.assertEqual(globalObs.notifications(), 0) + + # Add a global observer. + ufe.Attributes.addObserver(globalObs) + + self.assertEqual(ufe.Attributes.nbObservers(), 2) + self.assertFalse(ufe.Attributes.hasObservers(ball34.path())) + self.assertFalse(ufe.Attributes.hasObservers(ball35.path())) + self.assertEqual(ufe.Attributes.nbObservers(ball34), 0) + self.assertEqual(ufe.Attributes.nbObservers(ball35), 0) + self.assertFalse(ufe.Attributes.hasObserver(ball34, ball34Obs)) + self.assertFalse(ufe.Attributes.hasObserver(ball35, ball35Obs)) + + # Add item-specific observers. + ufe.Attributes.addObserver(ball34, ball34Obs) + + self.assertEqual(ufe.Attributes.nbObservers(), 2) + self.assertTrue(ufe.Attributes.hasObservers(ball34.path())) + self.assertFalse(ufe.Attributes.hasObservers(ball35.path())) + self.assertEqual(ufe.Attributes.nbObservers(ball34), 1) + self.assertEqual(ufe.Attributes.nbObservers(ball35), 0) + self.assertTrue(ufe.Attributes.hasObserver(ball34, ball34Obs)) + self.assertFalse(ufe.Attributes.hasObserver(ball34, ball35Obs)) + self.assertFalse(ufe.Attributes.hasObserver(ball35, ball35Obs)) + + ufe.Attributes.addObserver(ball35, ball35Obs) + + self.assertTrue(ufe.Attributes.hasObservers(ball35.path())) + self.assertEqual(ufe.Attributes.nbObservers(ball34), 1) + self.assertEqual(ufe.Attributes.nbObservers(ball35), 1) + self.assertTrue(ufe.Attributes.hasObserver(ball35, ball35Obs)) + self.assertFalse(ufe.Attributes.hasObserver(ball35, ball34Obs)) + + # Make a change to ball34, global and ball34 observers change. + ball34Attrs = ufe.Attributes.attributes(ball34) + ball34XlateAttr = ball34Attrs.attribute('xformOp:translate') + + self.assertEqual(ball34Obs.notifications(), 0) + + ufeCmd.execute(ball34XlateAttr.setCmd(ufe.Vector3d(1, 2, 3))) + + self.assertEqual(ball34Obs.notifications(), 1) + self.assertEqual(ball35Obs.notifications(), 0) + self.assertEqual(globalObs.notifications(), 1) + + # Undo, redo + cmds.undo() + + self.assertEqual(ball34Obs.notifications(), 2) + self.assertEqual(ball35Obs.notifications(), 0) + self.assertEqual(globalObs.notifications(), 2) + + cmds.redo() + + self.assertEqual(ball34Obs.notifications(), 3) + self.assertEqual(ball35Obs.notifications(), 0) + self.assertEqual(globalObs.notifications(), 3) + + # Make a change to ball35, global and ball35 observers change. + ball35Attrs = ufe.Attributes.attributes(ball35) + ball35XlateAttr = ball35Attrs.attribute('xformOp:translate') + + ufeCmd.execute(ball35XlateAttr.setCmd(ufe.Vector3d(1, 2, 3))) + + self.assertEqual(ball34Obs.notifications(), 3) + self.assertEqual(ball35Obs.notifications(), 1) + self.assertEqual(globalObs.notifications(), 4) + + # Undo, redo + cmds.undo() + + self.assertEqual(ball34Obs.notifications(), 3) + self.assertEqual(ball35Obs.notifications(), 2) + self.assertEqual(globalObs.notifications(), 5) + + cmds.redo() + + self.assertEqual(ball34Obs.notifications(), 3) + self.assertEqual(ball35Obs.notifications(), 3) + self.assertEqual(globalObs.notifications(), 6) + + # Test removeObserver. + ufe.Attributes.removeObserver(ball34, ball34Obs) + + self.assertFalse(ufe.Attributes.hasObservers(ball34.path())) + self.assertTrue(ufe.Attributes.hasObservers(ball35.path())) + self.assertEqual(ufe.Attributes.nbObservers(ball34), 0) + self.assertEqual(ufe.Attributes.nbObservers(ball35), 1) + self.assertFalse(ufe.Attributes.hasObserver(ball34, ball34Obs)) + + ufeCmd.execute(ball34XlateAttr.setCmd(ufe.Vector3d(4, 5, 6))) + + self.assertEqual(ball34Obs.notifications(), 3) + self.assertEqual(ball35Obs.notifications(), 3) + self.assertEqual(globalObs.notifications(), 7) + + ufe.Attributes.removeObserver(globalObs) + + self.assertEqual(ufe.Attributes.nbObservers(), 1) + + ufeCmd.execute(ball34XlateAttr.setCmd(ufe.Vector3d(7, 8, 9))) + + self.assertEqual(ball34Obs.notifications(), 3) + self.assertEqual(ball35Obs.notifications(), 3) + self.assertEqual(globalObs.notifications(), 7) diff --git a/test/lib/ufe/ufeScripts/__init__.py b/test/lib/ufe/ufeScripts/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/lib/ufe/ufeScripts/ufeSelectCmd.py b/test/lib/ufe/ufeScripts/ufeSelectCmd.py deleted file mode 100644 index cc697f5104..0000000000 --- a/test/lib/ufe/ufeScripts/ufeSelectCmd.py +++ /dev/null @@ -1,302 +0,0 @@ -#!/usr/bin/env python - -# -# Copyright 2019 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. -# - -#- -# =========================================================================== -# WARNING: PROTOTYPE CODE -# -# The code in this file is intended as an engineering prototype, to demonstrate -# UFE integration in Maya. Its performance and stability may make it -# unsuitable for production use. -# -# Autodesk believes production quality would be better achieved with a C++ -# version of this code. -# -# =========================================================================== -#+ - -"""Maya UFE selection command. - -Operations in Maya are performed on the selection. This imposes the -requirement that selection in Maya must be an undoable operation, so that -operations after an undo have the proper selection as input. - -Maya selections done through non-UFE UIs already go through the undoable select -command. Selections done through UFE UIs must use the undoable command in this -module. -""" - -# Code in this module was initially in the mayaUfe module, which defines -# the initialization and finalization functions for the mayaUfe plugin. -# This confusingly caused two instances of each command class to exist (as -# demonstrated by different values of id() on the class), one used by the -# command creator, and the other by the rest of the Python code. -# Separating out the command code into this module fixed the issue. PPT, -# 26-Jan-2018. - -import maya.api.OpenMaya as OpenMaya - -import maya.cmds as cmds - -import ufe.PyUfe as PyUfe - -# Messages should be localized. -kUfeSelectCmdPrivate = 'Private UFE select command %s arguments not set up.' - -def append(item): - return SelectAppendCmd.execute(item) - -def remove(item): - return SelectRemoveCmd.execute(item) - -def clear(): - SelectClearCmd.execute() - -def replaceWith(selection): - SelectReplaceWithCmd.execute(selection) - -#============================================================================== -# CLASS SelectCmdBase -#============================================================================== - -class SelectCmdBase(OpenMaya.MPxCommand): - """Base class for UFE selection commands. - - This command is intended as a base class for concrete UFE select commands. - """ - - def __init__(self): - super(SelectCmdBase, self).__init__() - self.globalSelection = PyUfe.GlobalSelection.get() - - def isUndoable(self): - return True - - def validateArgs(self): - return True - - def doIt(self, args): - # Completely ignore the MArgList argument, as it's unnecessary: - # arguments to the commands are passed in Python object form - # directly to the command's constructor. - - if self.validateArgs() is False: - self.displayWarning(kUfeSelectCmdPrivate % self.kCmdName) - else: - self.redoIt() - -#============================================================================== -# CLASS SelectAppendCmd -#============================================================================== - -class SelectAppendCmd(SelectCmdBase): - """Append an item to the UFE selection. - - This command is a private implementation detail of this module and should - not be called otherwise.""" - - kCmdName = 'ufeSelectAppend' - - # Command data. Must be set before creating an instance of the command - # and executing it. - item = None - - # Command return data. Set by doIt(). - result = None - - @staticmethod - def execute(item): - """Append the item to the UFE selection, and add an entry to the - undo queue.""" - - # Would be nice to have a guard context to restore the class data - # to its previous value (which is None). Not obvious how to write - # a Python context manager for this, as Python simply binds names - # to objects in a scope. - SelectAppendCmd.item = item - cmds.ufeSelectAppend() - result = SelectAppendCmd.result - SelectAppendCmd.item = None - SelectAppendCmd.result = None - return result - - @staticmethod - def creator(): - return SelectAppendCmd(SelectAppendCmd.item) - - def __init__(self, item): - super(SelectAppendCmd, self).__init__() - self.item = item - self.result = None - - def validateArgs(self): - return self.item is not None - - def doIt(self, args): - super(SelectAppendCmd, self).doIt(args) - # Save the result out as a class member. - SelectAppendCmd.result = self.result - - def redoIt(self): - self.result = self.globalSelection.append(self.item) - - def undoIt(self): - if self.result: - self.globalSelection.remove(self.item) - -#============================================================================== -# CLASS SelectRemoveCmd -#============================================================================== - -class SelectRemoveCmd(SelectCmdBase): - """Append an item to the UFE selection. - - This command is a private implementation detail of this module and should - not be called otherwise.""" - - kCmdName = 'ufeSelectRemove' - - # Command data. Must be set before creating an instance of the command - # and executing it. - item = None - - # Command return data. Set by doIt(). - result = None - - @staticmethod - def execute(item): - """Remove the item from the UFE selection, and add an entry to the - undo queue.""" - - # See SelectAppendCmd.execute comments. - SelectRemoveCmd.item = item - cmds.ufeSelectRemove() - result = SelectRemoveCmd.result - SelectRemoveCmd.item = None - SelectRemoveCmd.result = None - return result - - @staticmethod - def creator(): - return SelectRemoveCmd(SelectRemoveCmd.item) - - def __init__(self, item): - super(SelectRemoveCmd, self).__init__() - self.item = item - self.result = None - - def validateArgs(self): - return self.item is not None - - def doIt(self, args): - super(SelectRemoveCmd, self).doIt(args) - # Save the result out as a class member. - SelectRemoveCmd.result = self.result - - def redoIt(self): - self.result = self.globalSelection.remove(self.item) - - def undoIt(self): - # This is not a true undo! Selection.remove removes an item regardless - # of its position in the list, but append places the item in the last - # position. Saving and restoring the complete list is O(n) for n - # elements in the list; we want an O(1) solution. Selection.remove - # should return the list position of the removed element, for undo O(1) - # re-insertion using a future Selection.insert(). In C++, this list - # position would be an iterator; a matching Python binding would most - # likely require custom pybind code. PPT, 26-Jan-2018. - if self.result: - self.globalSelection.append(self.item) - -#============================================================================== -# CLASS SelectClearCmd -#============================================================================== - -class SelectClearCmd(SelectCmdBase): - """Clear the UFE selection. - - This command is a private implementation detail of this module and should - not be called otherwise.""" - - kCmdName = 'ufeSelectClear' - - @staticmethod - def execute(): - """Clear the UFE selection, and add an entry to the undo queue.""" - cmds.ufeSelectClear() - - @staticmethod - def creator(): - return SelectClearCmd() - - def __init__(self): - super(SelectClearCmd, self).__init__() - self.savedSelection = None - - def redoIt(self): - self.savedSelection = self.globalSelection - self.globalSelection.clear() - - def undoIt(self): - self.globalSelection.replaceWith(self.savedSelection) - -#============================================================================== -# CLASS SelectReplaceWithCmd -#============================================================================== - -class SelectReplaceWithCmd(SelectCmdBase): - """Replace the UFE selection with a new selection. - - This command is a private implementation detail of this module and should - not be called otherwise.""" - - kCmdName = 'ufeSelectReplaceWith' - - # Command data. Must be set before creating an instance of the command - # and executing it. - selection = None - - @staticmethod - def execute(selection): - """Replace the UFE selection with a new selection, and add an entry to - the undo queue.""" - - # See SelectAppendCmd.execute comments. - SelectReplaceWithCmd.selection = selection - cmds.ufeSelectReplaceWith() - SelectReplaceWithCmd.selection = None - - @staticmethod - def creator(): - return SelectReplaceWithCmd(SelectReplaceWithCmd.selection) - - def __init__(self, selection): - super(SelectReplaceWithCmd, self).__init__() - self.selection = selection - self.savedSelection = None - - def redoIt(self): - # No easy way to copy a Selection, so create a new one and call - # replaceWith(). selection[:], copy.copy(selection), and - # copy.deepcopy(selection) all raise Python exceptions. - self.savedSelection = PyUfe.Selection() - self.savedSelection.replaceWith(self.globalSelection) - self.globalSelection.replaceWith(self.selection) - - def undoIt(self): - self.globalSelection.replaceWith(self.savedSelection) diff --git a/test/lib/ufe/ufeTestPlugins/ufeTestCmdsPlugin.py b/test/lib/ufe/ufeTestPlugins/ufeTestCmdsPlugin.py deleted file mode 100644 index 8eb495c262..0000000000 --- a/test/lib/ufe/ufeTestPlugins/ufeTestCmdsPlugin.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python - -# -# Copyright 2019 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. -# - -""" -Plugin to register commands for UFE tests. -""" - -import maya.api.OpenMaya as OpenMaya - -from ufeScripts import ufeSelectCmd - -ufeTestCmdsVersion = '0.1' - -# Using the Maya Python API 2.0. -def maya_useNewAPI(): - pass - -commands = [ufeSelectCmd.SelectAppendCmd, ufeSelectCmd.SelectRemoveCmd, - ufeSelectCmd.SelectClearCmd, ufeSelectCmd.SelectReplaceWithCmd] - -def initializePlugin(mobject): - mplugin = OpenMaya.MFnPlugin(mobject, "Autodesk", ufeTestCmdsVersion, "Any") - - for cmd in commands: - try: - mplugin.registerCommand(cmd.kCmdName, cmd.creator) - except: - OpenMaya.MGlobal.displayError('Register failed for %s' % cmd.kCmdName) - -def uninitializePlugin(mobject): - """ Uninitialize all the nodes """ - mplugin = OpenMaya.MFnPlugin(mobject, "Autodesk", ufeTestCmdsVersion, "Any") - - for cmd in commands: - try: - mplugin.deregisterCommand(cmd.kCmdName) - except: - OpenMaya.MGlobal.displayError('Unregister failed for %s' % cmd.kCmdName) diff --git a/test/lib/ufe/ufeTestUtils/mayaUtils.py b/test/lib/ufe/ufeTestUtils/mayaUtils.py index 04de70c4a0..cad1922d65 100644 --- a/test/lib/ufe/ufeTestUtils/mayaUtils.py +++ b/test/lib/ufe/ufeTestUtils/mayaUtils.py @@ -26,7 +26,7 @@ import ufe -ALL_PLUGINS = ["mayaUsdPlugin", "ufeTestCmdsPlugin"] +ALL_PLUGINS = ["mayaUsdPlugin", "ufeSupport"] mayaRuntimeID = 1 mayaSeparator = "|" diff --git a/test/lib/ufe/ufeTestUtils/ufeUtils.py b/test/lib/ufe/ufeTestUtils/ufeUtils.py index 383d3648cc..dd28168218 100644 --- a/test/lib/ufe/ufeTestUtils/ufeUtils.py +++ b/test/lib/ufe/ufeTestUtils/ufeUtils.py @@ -22,7 +22,7 @@ import ufe from maya import cmds -from ufeScripts import ufeSelectCmd +from maya.internal.ufeSupport import ufeSelectCmd def getUfeGlobalSelectionList(): """ @@ -50,4 +50,4 @@ def selectPath(path, replace=False): selection.append(sceneItem) ufeSelectCmd.replaceWith(selection) else: - ufeSelectCmd.append(sceneItem) \ No newline at end of file + ufeSelectCmd.append(sceneItem) From cfe29528f13b6865660ff680c6952a39c1cc41e4 Mon Sep 17 00:00:00 2001 From: Pierre Tremblay Date: Wed, 27 Nov 2019 12:12:30 -0800 Subject: [PATCH 3/7] Addressed review comments. --- lib/ufe/StagesSubject.cpp | 29 +- lib/ufe/UsdAttribute.cpp | 2 +- test/lib/ufe/CMakeLists.txt | 5 + test/lib/ufe/ufeScripts/__init__.py | 0 test/lib/ufe/ufeScripts/ufeSelectCmd.py | 302 ++++++++++++++++++ .../ufe/ufeTestPlugins/ufeTestCmdsPlugin.py | 53 +++ test/lib/ufe/ufeTestUtils/mayaUtils.py | 14 +- 7 files changed, 392 insertions(+), 13 deletions(-) create mode 100644 test/lib/ufe/ufeScripts/__init__.py create mode 100644 test/lib/ufe/ufeScripts/ufeSelectCmd.py create mode 100644 test/lib/ufe/ufeTestPlugins/ufeTestCmdsPlugin.py diff --git a/lib/ufe/StagesSubject.cpp b/lib/ufe/StagesSubject.cpp index c5598f35c9..efc9141fca 100644 --- a/lib/ufe/StagesSubject.cpp +++ b/lib/ufe/StagesSubject.cpp @@ -34,7 +34,15 @@ #include namespace { -bool inAttributeChangedNotificationGuard = false; + +// The attribute change notification guard is not meant to be nested, but +// use a counter nonetheless to provide consistent behavior in such cases. +int attributeChangedNotificationGuardCount = 0; + +bool inAttributeChangedNotificationGuard() +{ + return attributeChangedNotificationGuardCount > 0; +} std::unordered_map pendingAttributeChangedNotifications; @@ -221,7 +229,7 @@ void StagesSubject::stageChanged(UsdNotice::ObjectsChanged const& notice, UsdSta // isPropertyPath() does consider relational attributes // isRelationalAttributePath() considers only relational attributes if (changedPath.IsPrimPropertyPath()) { - if (inAttributeChangedNotificationGuard) { + if (inAttributeChangedNotificationGuard()) { pendingAttributeChangedNotifications[ufePath] = changedPath.GetName(); } @@ -248,21 +256,30 @@ void StagesSubject::onStageSet(const UsdMayaProxyStageSetNotice& notice) AttributeChangedNotificationGuard::AttributeChangedNotificationGuard() { - if (inAttributeChangedNotificationGuard) { + if (inAttributeChangedNotificationGuard()) { TF_CODING_ERROR("Attribute changed notification guard cannot be nested."); } - if (!pendingAttributeChangedNotifications.empty()) { + if (attributeChangedNotificationGuardCount == 0 && + !pendingAttributeChangedNotifications.empty()) { TF_CODING_ERROR("Stale pending attribute changed notifications."); } - inAttributeChangedNotificationGuard = true; + ++attributeChangedNotificationGuardCount; } AttributeChangedNotificationGuard::~AttributeChangedNotificationGuard() { - inAttributeChangedNotificationGuard = false; + --attributeChangedNotificationGuardCount; + + if (attributeChangedNotificationGuardCount < 0) { + TF_CODING_ERROR("Corrupt attribute changed notification guard."); + } + + if (attributeChangedNotificationGuardCount > 0 ) { + return; + } for (const auto& notificationInfo : pendingAttributeChangedNotifications) { Ufe::Attributes::notify(notificationInfo.first, notificationInfo.second); diff --git a/lib/ufe/UsdAttribute.cpp b/lib/ufe/UsdAttribute.cpp index 11285f4f44..50456c5f7f 100644 --- a/lib/ufe/UsdAttribute.cpp +++ b/lib/ufe/UsdAttribute.cpp @@ -74,7 +74,7 @@ bool setUsdAttr(const PXR_NS::UsdAttribute& attr, const T& value) // Therefore, we have implemented an attribute change block notification of // our own in the StagesSubject, which we invoke here, so that only a // single UFE attribute changed notification is generated. - MayaUsd::ufe::AttributeChangedNotificationGuard guard; (void) guard; + MayaUsd::ufe::AttributeChangedNotificationGuard guard; return attr.Set(value); } diff --git a/test/lib/ufe/CMakeLists.txt b/test/lib/ufe/CMakeLists.txt index 7f421a33dd..dfab9e3880 100644 --- a/test/lib/ufe/CMakeLists.txt +++ b/test/lib/ufe/CMakeLists.txt @@ -56,8 +56,12 @@ add_custom_target(${TARGET_NAME} ALL) mayaUsd_copyDirectory(${TARGET_NAME} DESTINATION ${CMAKE_CURRENT_BINARY_DIR} DIRECTORY test-samples) +mayaUsd_copyDirectory(${TARGET_NAME} DESTINATION ${CMAKE_CURRENT_BINARY_DIR} + DIRECTORY ufeScripts) mayaUsd_copyDirectory(${TARGET_NAME} DESTINATION ${CMAKE_CURRENT_BINARY_DIR} DIRECTORY ufeTestUtils) +mayaUsd_copyDirectory(${TARGET_NAME} DESTINATION ${CMAKE_CURRENT_BINARY_DIR} + DIRECTORY ufeTestPlugins) mayaUsd_copyFiles(${TARGET_NAME} DESTINATION ${CMAKE_CURRENT_BINARY_DIR} FILES ${test_script_files}) mayaUsd_copyFiles(${TARGET_NAME} DESTINATION ${CMAKE_CURRENT_BINARY_DIR} @@ -71,6 +75,7 @@ set(pythonPath set(mayaPluginPath "${CMAKE_INSTALL_PREFIX}/plugin/adsk/plugin" + "${CMAKE_CURRENT_BINARY_DIR}/ufeTestPlugins" ) set(path diff --git a/test/lib/ufe/ufeScripts/__init__.py b/test/lib/ufe/ufeScripts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/lib/ufe/ufeScripts/ufeSelectCmd.py b/test/lib/ufe/ufeScripts/ufeSelectCmd.py new file mode 100644 index 0000000000..cc697f5104 --- /dev/null +++ b/test/lib/ufe/ufeScripts/ufeSelectCmd.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python + +# +# Copyright 2019 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. +# + +#- +# =========================================================================== +# WARNING: PROTOTYPE CODE +# +# The code in this file is intended as an engineering prototype, to demonstrate +# UFE integration in Maya. Its performance and stability may make it +# unsuitable for production use. +# +# Autodesk believes production quality would be better achieved with a C++ +# version of this code. +# +# =========================================================================== +#+ + +"""Maya UFE selection command. + +Operations in Maya are performed on the selection. This imposes the +requirement that selection in Maya must be an undoable operation, so that +operations after an undo have the proper selection as input. + +Maya selections done through non-UFE UIs already go through the undoable select +command. Selections done through UFE UIs must use the undoable command in this +module. +""" + +# Code in this module was initially in the mayaUfe module, which defines +# the initialization and finalization functions for the mayaUfe plugin. +# This confusingly caused two instances of each command class to exist (as +# demonstrated by different values of id() on the class), one used by the +# command creator, and the other by the rest of the Python code. +# Separating out the command code into this module fixed the issue. PPT, +# 26-Jan-2018. + +import maya.api.OpenMaya as OpenMaya + +import maya.cmds as cmds + +import ufe.PyUfe as PyUfe + +# Messages should be localized. +kUfeSelectCmdPrivate = 'Private UFE select command %s arguments not set up.' + +def append(item): + return SelectAppendCmd.execute(item) + +def remove(item): + return SelectRemoveCmd.execute(item) + +def clear(): + SelectClearCmd.execute() + +def replaceWith(selection): + SelectReplaceWithCmd.execute(selection) + +#============================================================================== +# CLASS SelectCmdBase +#============================================================================== + +class SelectCmdBase(OpenMaya.MPxCommand): + """Base class for UFE selection commands. + + This command is intended as a base class for concrete UFE select commands. + """ + + def __init__(self): + super(SelectCmdBase, self).__init__() + self.globalSelection = PyUfe.GlobalSelection.get() + + def isUndoable(self): + return True + + def validateArgs(self): + return True + + def doIt(self, args): + # Completely ignore the MArgList argument, as it's unnecessary: + # arguments to the commands are passed in Python object form + # directly to the command's constructor. + + if self.validateArgs() is False: + self.displayWarning(kUfeSelectCmdPrivate % self.kCmdName) + else: + self.redoIt() + +#============================================================================== +# CLASS SelectAppendCmd +#============================================================================== + +class SelectAppendCmd(SelectCmdBase): + """Append an item to the UFE selection. + + This command is a private implementation detail of this module and should + not be called otherwise.""" + + kCmdName = 'ufeSelectAppend' + + # Command data. Must be set before creating an instance of the command + # and executing it. + item = None + + # Command return data. Set by doIt(). + result = None + + @staticmethod + def execute(item): + """Append the item to the UFE selection, and add an entry to the + undo queue.""" + + # Would be nice to have a guard context to restore the class data + # to its previous value (which is None). Not obvious how to write + # a Python context manager for this, as Python simply binds names + # to objects in a scope. + SelectAppendCmd.item = item + cmds.ufeSelectAppend() + result = SelectAppendCmd.result + SelectAppendCmd.item = None + SelectAppendCmd.result = None + return result + + @staticmethod + def creator(): + return SelectAppendCmd(SelectAppendCmd.item) + + def __init__(self, item): + super(SelectAppendCmd, self).__init__() + self.item = item + self.result = None + + def validateArgs(self): + return self.item is not None + + def doIt(self, args): + super(SelectAppendCmd, self).doIt(args) + # Save the result out as a class member. + SelectAppendCmd.result = self.result + + def redoIt(self): + self.result = self.globalSelection.append(self.item) + + def undoIt(self): + if self.result: + self.globalSelection.remove(self.item) + +#============================================================================== +# CLASS SelectRemoveCmd +#============================================================================== + +class SelectRemoveCmd(SelectCmdBase): + """Append an item to the UFE selection. + + This command is a private implementation detail of this module and should + not be called otherwise.""" + + kCmdName = 'ufeSelectRemove' + + # Command data. Must be set before creating an instance of the command + # and executing it. + item = None + + # Command return data. Set by doIt(). + result = None + + @staticmethod + def execute(item): + """Remove the item from the UFE selection, and add an entry to the + undo queue.""" + + # See SelectAppendCmd.execute comments. + SelectRemoveCmd.item = item + cmds.ufeSelectRemove() + result = SelectRemoveCmd.result + SelectRemoveCmd.item = None + SelectRemoveCmd.result = None + return result + + @staticmethod + def creator(): + return SelectRemoveCmd(SelectRemoveCmd.item) + + def __init__(self, item): + super(SelectRemoveCmd, self).__init__() + self.item = item + self.result = None + + def validateArgs(self): + return self.item is not None + + def doIt(self, args): + super(SelectRemoveCmd, self).doIt(args) + # Save the result out as a class member. + SelectRemoveCmd.result = self.result + + def redoIt(self): + self.result = self.globalSelection.remove(self.item) + + def undoIt(self): + # This is not a true undo! Selection.remove removes an item regardless + # of its position in the list, but append places the item in the last + # position. Saving and restoring the complete list is O(n) for n + # elements in the list; we want an O(1) solution. Selection.remove + # should return the list position of the removed element, for undo O(1) + # re-insertion using a future Selection.insert(). In C++, this list + # position would be an iterator; a matching Python binding would most + # likely require custom pybind code. PPT, 26-Jan-2018. + if self.result: + self.globalSelection.append(self.item) + +#============================================================================== +# CLASS SelectClearCmd +#============================================================================== + +class SelectClearCmd(SelectCmdBase): + """Clear the UFE selection. + + This command is a private implementation detail of this module and should + not be called otherwise.""" + + kCmdName = 'ufeSelectClear' + + @staticmethod + def execute(): + """Clear the UFE selection, and add an entry to the undo queue.""" + cmds.ufeSelectClear() + + @staticmethod + def creator(): + return SelectClearCmd() + + def __init__(self): + super(SelectClearCmd, self).__init__() + self.savedSelection = None + + def redoIt(self): + self.savedSelection = self.globalSelection + self.globalSelection.clear() + + def undoIt(self): + self.globalSelection.replaceWith(self.savedSelection) + +#============================================================================== +# CLASS SelectReplaceWithCmd +#============================================================================== + +class SelectReplaceWithCmd(SelectCmdBase): + """Replace the UFE selection with a new selection. + + This command is a private implementation detail of this module and should + not be called otherwise.""" + + kCmdName = 'ufeSelectReplaceWith' + + # Command data. Must be set before creating an instance of the command + # and executing it. + selection = None + + @staticmethod + def execute(selection): + """Replace the UFE selection with a new selection, and add an entry to + the undo queue.""" + + # See SelectAppendCmd.execute comments. + SelectReplaceWithCmd.selection = selection + cmds.ufeSelectReplaceWith() + SelectReplaceWithCmd.selection = None + + @staticmethod + def creator(): + return SelectReplaceWithCmd(SelectReplaceWithCmd.selection) + + def __init__(self, selection): + super(SelectReplaceWithCmd, self).__init__() + self.selection = selection + self.savedSelection = None + + def redoIt(self): + # No easy way to copy a Selection, so create a new one and call + # replaceWith(). selection[:], copy.copy(selection), and + # copy.deepcopy(selection) all raise Python exceptions. + self.savedSelection = PyUfe.Selection() + self.savedSelection.replaceWith(self.globalSelection) + self.globalSelection.replaceWith(self.selection) + + def undoIt(self): + self.globalSelection.replaceWith(self.savedSelection) diff --git a/test/lib/ufe/ufeTestPlugins/ufeTestCmdsPlugin.py b/test/lib/ufe/ufeTestPlugins/ufeTestCmdsPlugin.py new file mode 100644 index 0000000000..8eb495c262 --- /dev/null +++ b/test/lib/ufe/ufeTestPlugins/ufeTestCmdsPlugin.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python + +# +# Copyright 2019 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. +# + +""" +Plugin to register commands for UFE tests. +""" + +import maya.api.OpenMaya as OpenMaya + +from ufeScripts import ufeSelectCmd + +ufeTestCmdsVersion = '0.1' + +# Using the Maya Python API 2.0. +def maya_useNewAPI(): + pass + +commands = [ufeSelectCmd.SelectAppendCmd, ufeSelectCmd.SelectRemoveCmd, + ufeSelectCmd.SelectClearCmd, ufeSelectCmd.SelectReplaceWithCmd] + +def initializePlugin(mobject): + mplugin = OpenMaya.MFnPlugin(mobject, "Autodesk", ufeTestCmdsVersion, "Any") + + for cmd in commands: + try: + mplugin.registerCommand(cmd.kCmdName, cmd.creator) + except: + OpenMaya.MGlobal.displayError('Register failed for %s' % cmd.kCmdName) + +def uninitializePlugin(mobject): + """ Uninitialize all the nodes """ + mplugin = OpenMaya.MFnPlugin(mobject, "Autodesk", ufeTestCmdsVersion, "Any") + + for cmd in commands: + try: + mplugin.deregisterCommand(cmd.kCmdName) + except: + OpenMaya.MGlobal.displayError('Unregister failed for %s' % cmd.kCmdName) diff --git a/test/lib/ufe/ufeTestUtils/mayaUtils.py b/test/lib/ufe/ufeTestUtils/mayaUtils.py index cad1922d65..8c9837e7e6 100644 --- a/test/lib/ufe/ufeTestUtils/mayaUtils.py +++ b/test/lib/ufe/ufeTestUtils/mayaUtils.py @@ -26,8 +26,6 @@ import ufe -ALL_PLUGINS = ["mayaUsdPlugin", "ufeSupport"] - mayaRuntimeID = 1 mayaSeparator = "|" @@ -64,10 +62,14 @@ def isMayaUsdPluginLoaded(): Returns: True if plugins loaded successfully. False if a plugin failed to load """ - successLoad = True - for plugin in ALL_PLUGINS: - successLoad = successLoad and loadPlugin(plugin) - return successLoad + # Load the mayaUsdPlugin first. + if not loadPlugin("mayaUsdPlugin"): + return False + + # Load the UFE support plugin, for ufeSelectCmd support. If this plugin + # isn't included in the distribution of Maya (e.g. Maya 2019 or 2020), use + # fallback test plugin. + return loadPlugin("ufeSupport") or loadPlugin("ufeTestCmdsPlugin") def createUfePathSegment(mayaPath): """ From 55cd5ad048bd3a0d350d392bcb3d949d5be8bc08 Mon Sep 17 00:00:00 2001 From: Pierre Tremblay Date: Wed, 27 Nov 2019 12:19:22 -0800 Subject: [PATCH 4/7] Added missing file to review comment commit. --- test/lib/ufe/ufeTestUtils/ufeUtils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/lib/ufe/ufeTestUtils/ufeUtils.py b/test/lib/ufe/ufeTestUtils/ufeUtils.py index dd28168218..de0d36ce3f 100644 --- a/test/lib/ufe/ufeTestUtils/ufeUtils.py +++ b/test/lib/ufe/ufeTestUtils/ufeUtils.py @@ -22,7 +22,11 @@ import ufe from maya import cmds -from maya.internal.ufeSupport import ufeSelectCmd +try: + from maya.internal.ufeSupport import ufeSelectCmd +except ImportError: + # Maya 2019 and 2020 don't have ufeSupport plugin, so use fallback. + from ufeScripts import ufeSelectCmd def getUfeGlobalSelectionList(): """ From f6828ee019ac39f1c5e460186e2f6925bd01113b Mon Sep 17 00:00:00 2001 From: Pierre Tremblay Date: Wed, 29 Jan 2020 07:57:08 -0800 Subject: [PATCH 5/7] Fixed UFE v1 compilation. --- lib/ufe/StagesSubject.cpp | 69 +++++++++++++++++++++------------------ lib/ufe/StagesSubject.h | 4 +++ 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/lib/ufe/StagesSubject.cpp b/lib/ufe/StagesSubject.cpp index efc9141fca..7bf2ef0355 100644 --- a/lib/ufe/StagesSubject.cpp +++ b/lib/ufe/StagesSubject.cpp @@ -31,6 +31,8 @@ #include #include + +#ifdef UFE_V2_FEATURES_AVAILABLE #include namespace { @@ -47,6 +49,7 @@ bool inAttributeChangedNotificationGuard() std::unordered_map pendingAttributeChangedNotifications; } +#endif MAYAUSD_NS_DEF { namespace ufe { @@ -225,18 +228,20 @@ void StagesSubject::stageChanged(UsdNotice::ObjectsChanged const& notice, UsdSta auto usdPrimPathStr = changedPath.GetPrimPath().GetString(); auto ufePath = stagePath(sender) + Ufe::PathSegment(usdPrimPathStr, g_USDRtid, '/'); - // isPrimPropertyPath() does not consider relational attributes - // isPropertyPath() does consider relational attributes - // isRelationalAttributePath() considers only relational attributes - if (changedPath.IsPrimPropertyPath()) { - if (inAttributeChangedNotificationGuard()) { - pendingAttributeChangedNotifications[ufePath] = - changedPath.GetName(); - } - else { - Ufe::Attributes::notify(ufePath, changedPath.GetName()); - } - } +#ifdef UFE_V2_FEATURES_AVAILABLE + // isPrimPropertyPath() does not consider relational attributes + // isPropertyPath() does consider relational attributes + // isRelationalAttributePath() considers only relational attributes + if (changedPath.IsPrimPropertyPath()) { + if (inAttributeChangedNotificationGuard()) { + pendingAttributeChangedNotifications[ufePath] = + changedPath.GetName(); + } + else { + Ufe::Attributes::notify(ufePath, changedPath.GetName()); + } + } +#endif // We need to determine if the change is a Transform3d change. // We must at least pick up xformOp:translate, xformOp:rotateXYZ, @@ -254,39 +259,39 @@ void StagesSubject::onStageSet(const UsdMayaProxyStageSetNotice& notice) afterOpen(); } +#ifdef UFE_V2_FEATURES_AVAILABLE AttributeChangedNotificationGuard::AttributeChangedNotificationGuard() { - if (inAttributeChangedNotificationGuard()) { - TF_CODING_ERROR("Attribute changed notification guard cannot be nested."); - } + if (inAttributeChangedNotificationGuard()) { + TF_CODING_ERROR("Attribute changed notification guard cannot be nested."); + } - if (attributeChangedNotificationGuardCount == 0 && - !pendingAttributeChangedNotifications.empty()) { - TF_CODING_ERROR("Stale pending attribute changed notifications."); - } + if (attributeChangedNotificationGuardCount == 0 && + !pendingAttributeChangedNotifications.empty()) { + TF_CODING_ERROR("Stale pending attribute changed notifications."); + } - ++attributeChangedNotificationGuardCount; + ++attributeChangedNotificationGuardCount; } AttributeChangedNotificationGuard::~AttributeChangedNotificationGuard() { - --attributeChangedNotificationGuardCount; - - if (attributeChangedNotificationGuardCount < 0) { - TF_CODING_ERROR("Corrupt attribute changed notification guard."); - } + if (--attributeChangedNotificationGuardCount < 0) { + TF_CODING_ERROR("Corrupt attribute changed notification guard."); + } - if (attributeChangedNotificationGuardCount > 0 ) { - return; - } + if (attributeChangedNotificationGuardCount > 0 ) { + return; + } - for (const auto& notificationInfo : pendingAttributeChangedNotifications) { - Ufe::Attributes::notify(notificationInfo.first, notificationInfo.second); - } + for (const auto& notificationInfo : pendingAttributeChangedNotifications) { + Ufe::Attributes::notify(notificationInfo.first, notificationInfo.second); + } - pendingAttributeChangedNotifications.clear(); + pendingAttributeChangedNotifications.clear(); } +#endif } // namespace ufe } // namespace MayaUsd diff --git a/lib/ufe/StagesSubject.h b/lib/ufe/StagesSubject.h index efb399f2e6..840658398f 100644 --- a/lib/ufe/StagesSubject.h +++ b/lib/ufe/StagesSubject.h @@ -26,6 +26,8 @@ #include +#include // For UFE_V2_FEATURES_AVAILABLE + PXR_NAMESPACE_USING_DIRECTIVE MAYAUSD_NS_DEF { @@ -86,6 +88,7 @@ class MAYAUSD_CORE_PUBLIC StagesSubject : public TfWeakBase }; // StagesSubject +#ifdef UFE_V2_FEATURES_AVAILABLE //! \brief Guard to delay attribute changed notifications. /*! Instantiating an object of this class allows the attribute changed @@ -107,6 +110,7 @@ class MAYAUSD_CORE_PUBLIC AttributeChangedNotificationGuard { const AttributeChangedNotificationGuard& operator&(const AttributeChangedNotificationGuard&) = delete; //@} }; +#endif } // namespace ufe } // namespace MayaUsd From aab0ed38c3c6900fb83feda5dd655bf8bec62fad Mon Sep 17 00:00:00 2001 From: Pierre Tremblay Date: Fri, 31 Jan 2020 11:03:44 -0800 Subject: [PATCH 6/7] Addressed code review comments. --- lib/ufe/StagesSubject.cpp | 4 +-- test/lib/ufe/testAttribute.py | 63 ++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/lib/ufe/StagesSubject.cpp b/lib/ufe/StagesSubject.cpp index 7bf2ef0355..2a57962999 100644 --- a/lib/ufe/StagesSubject.cpp +++ b/lib/ufe/StagesSubject.cpp @@ -20,12 +20,12 @@ #include "ProxyShapeHandler.h" #include "private/InPathChange.h" -#include +#include #include +#include #include #include #include -#include #include #include diff --git a/test/lib/ufe/testAttribute.py b/test/lib/ufe/testAttribute.py index ae8a6c50af..ac067c3fa9 100644 --- a/test/lib/ufe/testAttribute.py +++ b/test/lib/ufe/testAttribute.py @@ -29,14 +29,15 @@ class TestObserver(ufe.Observer): def __init__(self): super(TestObserver, self).__init__() - self.changed = 0 + self._notifications = 0 def __call__(self, notification): if isinstance(notification, ufe.AttributeChanged): - self.changed += 1 + self._notifications += 1 + @property def notifications(self): - return self.changed + return self._notifications class AttributeTestCase(unittest.TestCase): '''Verify the Attribute UFE interface, for multiple runtimes. @@ -443,9 +444,9 @@ def testObservation(self): self.assertFalse(ufe.Attributes.hasObserver(ball35, ball35Obs)) # No notifications yet. - self.assertEqual(ball34Obs.notifications(), 0) - self.assertEqual(ball35Obs.notifications(), 0) - self.assertEqual(globalObs.notifications(), 0) + self.assertEqual(ball34Obs.notifications, 0) + self.assertEqual(ball35Obs.notifications, 0) + self.assertEqual(globalObs.notifications, 0) # Add a global observer. ufe.Attributes.addObserver(globalObs) @@ -482,26 +483,26 @@ def testObservation(self): ball34Attrs = ufe.Attributes.attributes(ball34) ball34XlateAttr = ball34Attrs.attribute('xformOp:translate') - self.assertEqual(ball34Obs.notifications(), 0) + self.assertEqual(ball34Obs.notifications, 0) ufeCmd.execute(ball34XlateAttr.setCmd(ufe.Vector3d(1, 2, 3))) - self.assertEqual(ball34Obs.notifications(), 1) - self.assertEqual(ball35Obs.notifications(), 0) - self.assertEqual(globalObs.notifications(), 1) + self.assertEqual(ball34Obs.notifications, 1) + self.assertEqual(ball35Obs.notifications, 0) + self.assertEqual(globalObs.notifications, 1) # Undo, redo cmds.undo() - self.assertEqual(ball34Obs.notifications(), 2) - self.assertEqual(ball35Obs.notifications(), 0) - self.assertEqual(globalObs.notifications(), 2) + self.assertEqual(ball34Obs.notifications, 2) + self.assertEqual(ball35Obs.notifications, 0) + self.assertEqual(globalObs.notifications, 2) cmds.redo() - self.assertEqual(ball34Obs.notifications(), 3) - self.assertEqual(ball35Obs.notifications(), 0) - self.assertEqual(globalObs.notifications(), 3) + self.assertEqual(ball34Obs.notifications, 3) + self.assertEqual(ball35Obs.notifications, 0) + self.assertEqual(globalObs.notifications, 3) # Make a change to ball35, global and ball35 observers change. ball35Attrs = ufe.Attributes.attributes(ball35) @@ -509,22 +510,22 @@ def testObservation(self): ufeCmd.execute(ball35XlateAttr.setCmd(ufe.Vector3d(1, 2, 3))) - self.assertEqual(ball34Obs.notifications(), 3) - self.assertEqual(ball35Obs.notifications(), 1) - self.assertEqual(globalObs.notifications(), 4) + self.assertEqual(ball34Obs.notifications, 3) + self.assertEqual(ball35Obs.notifications, 1) + self.assertEqual(globalObs.notifications, 4) # Undo, redo cmds.undo() - self.assertEqual(ball34Obs.notifications(), 3) - self.assertEqual(ball35Obs.notifications(), 2) - self.assertEqual(globalObs.notifications(), 5) + self.assertEqual(ball34Obs.notifications, 3) + self.assertEqual(ball35Obs.notifications, 2) + self.assertEqual(globalObs.notifications, 5) cmds.redo() - self.assertEqual(ball34Obs.notifications(), 3) - self.assertEqual(ball35Obs.notifications(), 3) - self.assertEqual(globalObs.notifications(), 6) + self.assertEqual(ball34Obs.notifications, 3) + self.assertEqual(ball35Obs.notifications, 3) + self.assertEqual(globalObs.notifications, 6) # Test removeObserver. ufe.Attributes.removeObserver(ball34, ball34Obs) @@ -537,9 +538,9 @@ def testObservation(self): ufeCmd.execute(ball34XlateAttr.setCmd(ufe.Vector3d(4, 5, 6))) - self.assertEqual(ball34Obs.notifications(), 3) - self.assertEqual(ball35Obs.notifications(), 3) - self.assertEqual(globalObs.notifications(), 7) + self.assertEqual(ball34Obs.notifications, 3) + self.assertEqual(ball35Obs.notifications, 3) + self.assertEqual(globalObs.notifications, 7) ufe.Attributes.removeObserver(globalObs) @@ -547,6 +548,6 @@ def testObservation(self): ufeCmd.execute(ball34XlateAttr.setCmd(ufe.Vector3d(7, 8, 9))) - self.assertEqual(ball34Obs.notifications(), 3) - self.assertEqual(ball35Obs.notifications(), 3) - self.assertEqual(globalObs.notifications(), 7) + self.assertEqual(ball34Obs.notifications, 3) + self.assertEqual(ball35Obs.notifications, 3) + self.assertEqual(globalObs.notifications, 7) From e4da124de7bb488a19027a51a7e54b7a07aeafbc Mon Sep 17 00:00:00 2001 From: Pierre Tremblay Date: Mon, 3 Feb 2020 10:25:57 -0800 Subject: [PATCH 7/7] Fixed UFE v1 compilation. --- lib/ufe/StagesSubject.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/ufe/StagesSubject.cpp b/lib/ufe/StagesSubject.cpp index 2a57962999..033f843fd9 100644 --- a/lib/ufe/StagesSubject.cpp +++ b/lib/ufe/StagesSubject.cpp @@ -20,7 +20,9 @@ #include "ProxyShapeHandler.h" #include "private/InPathChange.h" +#ifdef UFE_V2_FEATURES_AVAILABLE #include +#endif #include #include #include