diff --git a/Changes.md b/Changes.md index 42dae03d5f..719dc828e2 100644 --- a/Changes.md +++ b/Changes.md @@ -28,6 +28,7 @@ Improvements - Spreadsheet : Added yellow underlining to the currently active row. - PlugLayout : Summaries and activators are now evaluated in a context determined relative to the focus node. - Editor : The node graph is now evaluated in a context determined relative to the focus node. +- LightEditor, RenderPassEditor : The "Disable Edit" right-click menu item and D shortcut now act as a toggle, where edits disabled in the current session via these actions can be reenabled with D or by selecting "Reenable Edit" from the right-click menu. Fixes ----- @@ -55,6 +56,7 @@ Fixes - FreezeTransform : Constant primitive variables with point/vector interpretations are now also transformed. - usdview : Added Windows support (#5599). - ContextTracker : Removed unnecessary reference increment/decrement from `isTracked()`, `context()` and `isEnabled()`. +- Menu : Fixed bug causing a menu item's tooltip to not hide when moving the cursor to another menu item without a tooltip. API --- diff --git a/include/GafferSceneUI/Private/AttributeInspector.h b/include/GafferSceneUI/Private/AttributeInspector.h index 394982de0d..f1749a78c4 100644 --- a/include/GafferSceneUI/Private/AttributeInspector.h +++ b/include/GafferSceneUI/Private/AttributeInspector.h @@ -65,9 +65,9 @@ class GAFFERSCENEUI_API AttributeInspector : public Inspector GafferScene::SceneAlgo::History::ConstPtr history() const override; IECore::ConstObjectPtr value( const GafferScene::SceneAlgo::History *history) const override; + IECore::ConstObjectPtr fallbackValue( const GafferScene::SceneAlgo::History *history, std::string &description ) const override; Gaffer::ValuePlugPtr source( const GafferScene::SceneAlgo::History *history, std::string &editWarning ) const override; EditFunctionOrFailure editFunction( Gaffer::EditScope *scope, const GafferScene::SceneAlgo::History *history) const override; - IECore::ConstObjectPtr fallbackValue( const GafferScene::SceneAlgo::History *history, std::string &description ) const override; bool attributeExists() const; diff --git a/include/GafferSceneUI/Private/Inspector.h b/include/GafferSceneUI/Private/Inspector.h index 678e1c2e0b..04f4e9c224 100644 --- a/include/GafferSceneUI/Private/Inspector.h +++ b/include/GafferSceneUI/Private/Inspector.h @@ -170,7 +170,7 @@ class GAFFERSCENEUI_API Inspector : public IECore::RefCounted, public Gaffer::Si /// history class? virtual Gaffer::ValuePlugPtr source( const GafferScene::SceneAlgo::History *history, std::string &editWarning ) const; - using EditFunction = std::function; + using EditFunction = std::function; using EditFunctionOrFailure = boost::variant; /// Should be implemented to return a function that will acquire /// an edit from the EditScope at the specified point in the history. @@ -183,6 +183,14 @@ class GAFFERSCENEUI_API Inspector : public IECore::RefCounted, public Gaffer::Si /// > that edits the processor itself. virtual EditFunctionOrFailure editFunction( Gaffer::EditScope *editScope, const GafferScene::SceneAlgo::History *history ) const; + using DisableEditFunction = std::function; + using DisableEditFunctionOrFailure = boost::variant; + /// Can be implemented to return a function that will disable an edit + /// at the specified plug. If this is not possible, should return an + /// error explaining why (this is typically due to `readOnly` metadata). + /// Called with `history->context` as the current context. + virtual DisableEditFunctionOrFailure disableEditFunction( Gaffer::ValuePlug *plug, const GafferScene::SceneAlgo::History *history ) const; + protected : Gaffer::EditScope *targetEditScope() const; @@ -338,11 +346,21 @@ class GAFFERSCENEUI_API Inspector::Result : public IECore::RefCounted /// Returns a plug that can be used to edit the property /// represented by this inspector, creating it if necessary. /// Throws if `!editable()`. - Gaffer::ValuePlugPtr acquireEdit() const; + Gaffer::ValuePlugPtr acquireEdit( bool createIfNecessary = true ) const; /// Returns a warning associated with the plug returned /// by `acquireEdit()`. This should be displayed to the user. std::string editWarning() const; + /// Returns `true` if `disableEdit()` will disable the edit + /// at `source()`, and `false` otherwise. + bool canDisableEdit() const; + /// If `canDisableEdit()` returns false, returns the reason why. + /// This should be displayed to the user. + std::string nonDisableableReason() const; + /// Disables the edit at `source()`. Throws if + /// `!canDisableEdit()` + void disableEdit() const; + private : Result( const IECore::ConstObjectPtr &value, const Gaffer::EditScopePtr &editScope ); @@ -359,6 +377,8 @@ class GAFFERSCENEUI_API Inspector::Result : public IECore::RefCounted EditFunctionOrFailure m_editFunction; std::string m_editWarning; + DisableEditFunctionOrFailure m_disableEditFunction; + }; } // namespace Private diff --git a/include/GafferSceneUI/Private/OptionInspector.h b/include/GafferSceneUI/Private/OptionInspector.h index 4fd1dd7957..73a42b6b52 100644 --- a/include/GafferSceneUI/Private/OptionInspector.h +++ b/include/GafferSceneUI/Private/OptionInspector.h @@ -63,9 +63,9 @@ class GAFFERSCENEUI_API OptionInspector : public Inspector GafferScene::SceneAlgo::History::ConstPtr history() const override; IECore::ConstObjectPtr value( const GafferScene::SceneAlgo::History *history ) const override; + IECore::ConstObjectPtr fallbackValue( const GafferScene::SceneAlgo::History *history, std::string &description ) const override; Gaffer::ValuePlugPtr source( const GafferScene::SceneAlgo::History *history, std::string &editWarning ) const override; EditFunctionOrFailure editFunction( Gaffer::EditScope *scope, const GafferScene::SceneAlgo::History *history ) const override; - IECore::ConstObjectPtr fallbackValue( const GafferScene::SceneAlgo::History *history, std::string &description ) const override; private : diff --git a/include/GafferSceneUI/Private/ParameterInspector.h b/include/GafferSceneUI/Private/ParameterInspector.h index 0ba4f86762..ad9680e8b2 100644 --- a/include/GafferSceneUI/Private/ParameterInspector.h +++ b/include/GafferSceneUI/Private/ParameterInspector.h @@ -67,9 +67,9 @@ class GAFFERSCENEUI_API ParameterInspector : public AttributeInspector private : IECore::ConstObjectPtr value( const GafferScene::SceneAlgo::History *history ) const override; + IECore::ConstObjectPtr fallbackValue( const GafferScene::SceneAlgo::History *history, std::string &description ) const override; Gaffer::ValuePlugPtr source( const GafferScene::SceneAlgo::History *history, std::string &editWarning ) const override; EditFunctionOrFailure editFunction( Gaffer::EditScope *editScope, const GafferScene::SceneAlgo::History *history ) const override; - IECore::ConstObjectPtr fallbackValue( const GafferScene::SceneAlgo::History *history, std::string &description ) const override; const IECoreScene::ShaderNetwork::Parameter m_parameter; diff --git a/include/GafferSceneUI/Private/SetMembershipInspector.h b/include/GafferSceneUI/Private/SetMembershipInspector.h index 51622c6db9..b06507e064 100644 --- a/include/GafferSceneUI/Private/SetMembershipInspector.h +++ b/include/GafferSceneUI/Private/SetMembershipInspector.h @@ -75,13 +75,14 @@ class GAFFERSCENEUI_API SetMembershipInspector : public Inspector GafferScene::SceneAlgo::History::ConstPtr history() const override; IECore::ConstObjectPtr value( const GafferScene::SceneAlgo::History *history) const override; + IECore::ConstObjectPtr fallbackValue( const GafferScene::SceneAlgo::History *history, std::string &description ) const override; /// For the given `history`, returns either the "sets" `StringPlug` of an `ObjectSource` /// node, the "name" `StringPlug` of a `Set` node, the `Spreadsheet::RowPlug` for the /// appropriate row of a set membership processor spreadsheet or `nullptr` if none of /// those are found. Gaffer::ValuePlugPtr source( const GafferScene::SceneAlgo::History *history, std::string &editWarning ) const override; EditFunctionOrFailure editFunction( Gaffer::EditScope *scope, const GafferScene::SceneAlgo::History *history ) const override; - IECore::ConstObjectPtr fallbackValue( const GafferScene::SceneAlgo::History *history, std::string &description ) const override; + DisableEditFunctionOrFailure disableEditFunction( Gaffer::ValuePlug *plug, const GafferScene::SceneAlgo::History *history ) const override; private : diff --git a/python/GafferSceneUI/_InspectorColumn.py b/python/GafferSceneUI/_InspectorColumn.py index 7001498931..490b954f9f 100644 --- a/python/GafferSceneUI/_InspectorColumn.py +++ b/python/GafferSceneUI/_InspectorColumn.py @@ -162,9 +162,11 @@ def __editSelectedCells( pathListing, quickBoolean = True ) : __inspectorColumnPopup.popup( parent = pathListing ) -def __disablableInspectionTweaks( pathListing ) : +def __toggleableInspections( pathListing ) : - tweaks = [] + inspections = [] + nonEditableReason = "" + toggleShouldDisable = True path = pathListing.getPath().copy() for columnSelection, column in zip( pathListing.getSelection(), pathListing.getColumns() ) : @@ -176,43 +178,55 @@ def __disablableInspectionTweaks( pathListing ) : with inspectionContext : inspection = column.inspector().inspect() - if inspection is not None and inspection.editable() : - source = inspection.source() - editScope = inspection.editScope() - if ( - ( - ( - isinstance( source, ( Gaffer.TweakPlug, Gaffer.NameValuePlug ) ) and - source["enabled"].getValue() - ) or - isinstance( column.inspector(), GafferSceneUI.Private.SetMembershipInspector ) - ) and - ( editScope is None or editScope.isAncestorOf( source ) ) - ) : - tweaks.append( ( pathString, column.inspector() ) ) - else : - return [] - else : - return [] + if inspection is None : + continue - return tweaks + canReenableEdit = False + if not inspection.canDisableEdit() and inspection.editable() : + edit = inspection.acquireEdit( createIfNecessary = False ) + canReenableEdit = isinstance( edit, ( Gaffer.NameValuePlug, Gaffer.OptionalValuePlug, Gaffer.TweakPlug ) ) and Gaffer.Metadata.value( edit, "inspector:disabledEdit" ) + if canReenableEdit : + toggleShouldDisable = False -def __disableEdits( pathListing ) : + if canReenableEdit or inspection.canDisableEdit() : + inspections.append( inspection ) + elif nonEditableReason == "" : + # Prefix reason with the column header to disambiguate when more than one column has selection + nonEditableReason = "{} : ".format( column.headerData().value ) if len( [ x for x in pathListing.getSelection() if not x.isEmpty() ] ) > 1 else "" + nonEditableReason += inspection.nonDisableableReason() if toggleShouldDisable else inspection.nonEditableReason() - edits = __disablableInspectionTweaks( pathListing ) - path = pathListing.getPath().copy() - with Gaffer.UndoScope( pathListing.ancestor( GafferUI.Editor ).scriptNode() ) : - for pathString, inspector in edits : - path.setFromString( pathString ) - with path.inspectionContext() : - inspection = inspector.inspect() - if inspection is not None and inspection.editable() : - source = inspection.source() + return inspections, nonEditableReason, toggleShouldDisable - if isinstance( source, ( Gaffer.TweakPlug, Gaffer.NameValuePlug ) ) : - source["enabled"].setValue( False ) - elif isinstance( inspector, GafferSceneUI.Private.SetMembershipInspector ) : - inspector.editSetMembership( inspection, pathString, GafferScene.EditScopeAlgo.SetMembership.Unchanged ) +def __toggleEditEnabled( pathListing ) : + + global __inspectorColumnPopup + + inspections, nonEditableReason, shouldDisable = __toggleableInspections( pathListing ) + + if nonEditableReason != "" : + with GafferUI.PopupWindow() as __inspectorColumnPopup : + with GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) : + GafferUI.Image( "warningSmall.png" ) + GafferUI.Label( "

{}

".format( nonEditableReason ) ) + + __inspectorColumnPopup.popup( parent = pathListing ) + + return + + with Gaffer.UndoScope( pathListing.ancestor( GafferUI.Editor ).scriptNode() ) : + for inspection in inspections : + if shouldDisable : + inspection.disableEdit() + # We register non-persistent metadata on disabled edits to later determine + # whether the disabled edit is a suitable candidate for enabling. This allows + # investigative toggling of edits in the current session while avoiding enabling + # edits the user may not expect to exist, such as previously unedited spreadsheet + # cells in EditScope processors. + Gaffer.Metadata.registerValue( inspection.source(), "inspector:disabledEdit", True, persistent = False ) + else : + edit = inspection.acquireEdit( createIfNecessary = False ) + edit["enabled"].setValue( True ) + Gaffer.Metadata.deregisterValue( edit, "inspector:disabledEdit" ) def __removableAttributeInspections( pathListing ) : @@ -385,12 +399,14 @@ def __contextMenu( column, pathListing, menuDefinition ) : "command" : functools.partial( __editSelectedCells, pathListing, toggleOnly ), } ) + inspections, nonEditableReason, disable = __toggleableInspections( pathListing ) menuDefinition.append( - "Disable Edit", + "{} Edit{}".format( "Disable" if disable else "Reenable", "s" if len( inspections ) > 1 else "" ), { - "command" : functools.partial( __disableEdits, pathListing ), - "active" : len( __disablableInspectionTweaks( pathListing ) ) > 0, + "command" : functools.partial( __toggleEditEnabled, pathListing ), + "active" : len( inspections ) > 0 and nonEditableReason == "", "shortCut" : "D", + "description" : nonEditableReason, } ) @@ -427,8 +443,11 @@ def __keyPress( column, pathListing, event ) : return True if event.key == "D" : - if len( __disablableInspectionTweaks( pathListing ) ) > 0 : - __disableEdits( pathListing ) + inspections, nonEditableReason, _ = __toggleableInspections( pathListing ) + # We allow toggling when there is a nonEditableReason to let __toggleEditEnabled + # present the reason to the user via a popup. + if len( inspections ) > 0 or nonEditableReason != "" : + __toggleEditEnabled( pathListing ) return True if event.key in ( "Backspace", "Delete" ) : diff --git a/python/GafferSceneUITest/AttributeInspectorTest.py b/python/GafferSceneUITest/AttributeInspectorTest.py index 63846f4dbd..2c2ab6b8e7 100644 --- a/python/GafferSceneUITest/AttributeInspectorTest.py +++ b/python/GafferSceneUITest/AttributeInspectorTest.py @@ -219,7 +219,7 @@ def testSourceAndEdits( self ) : source = s["light"]["visualiserAttributes"]["scale"], sourceType = SourceType.Other, editable=False, - nonEditableReason = "The target EditScope (editScope1) is not in the scene history." + nonEditableReason = "The target edit scope editScope1 is not in the scene history." ) # If it is in the history though, and we're told to use it, then we will. @@ -369,7 +369,7 @@ def testSourceAndEdits( self ) : source = independentAttributeTweakPlug, sourceType = SourceType.Downstream, editable = False, - nonEditableReason = "The target EditScope (editScope2) is disabled." + nonEditableReason = "The target edit scope editScope2 is disabled." ) s["editScope2"]["enabled"].setValue( True ) @@ -455,7 +455,7 @@ def testEditScopeNotInHistory( self ) : source = light["visualiserAttributes"]["scale"], sourceType = SourceType.Other, editable = False, - nonEditableReason = "The target EditScope (EditScope) is not in the scene history." + nonEditableReason = "The target edit scope EditScope is not in the scene history." ) self.__assertExpectedResult( @@ -471,7 +471,7 @@ def testEditScopeNotInHistory( self ) : source = attributeTweaks["tweaks"][0], sourceType = SourceType.Other, editable = False, - nonEditableReason = "The target EditScope (EditScope) is not in the scene history." + nonEditableReason = "The target edit scope EditScope is not in the scene history." ) def testDisabledTweaks( self ) : @@ -821,6 +821,105 @@ def testLightFilter( self ) : edit = edit ) + def testAcquireEditCreateIfNecessary( self ) : + + s = Gaffer.ScriptNode() + + s["light"] = GafferSceneTest.TestLight() + s["light"]["visualiserAttributes"]["scale"]["enabled"].setValue( True ) + + s["group"] = GafferScene.Group() + s["editScope"] = Gaffer.EditScope() + + s["group"]["in"][0].setInput( s["light"]["out"] ) + + s["editScope"].setup( s["group"]["out"] ) + s["editScope"]["in"].setInput( s["group"]["out"] ) + + inspection = self.__inspect( s["group"]["out"], "/group/light", "gl:visualiser:scale", None ) + self.assertEqual( inspection.acquireEdit( createIfNecessary = False ), s["light"]["visualiserAttributes"]["scale"] ) + + inspection = self.__inspect( s["editScope"]["out"], "/group/light", "gl:visualiser:scale", s["editScope"] ) + self.assertIsNone( inspection.acquireEdit( createIfNecessary = False ) ) + + edit = inspection.acquireEdit( createIfNecessary = True ) + self.assertIsNotNone( edit ) + self.assertEqual( inspection.acquireEdit( createIfNecessary = False ), edit ) + + def testDisableEdit( self ) : + + s = Gaffer.ScriptNode() + + s["light"] = GafferSceneTest.TestLight() + s["light"]["visualiserAttributes"]["scale"]["enabled"].setValue( True ) + + s["group"] = GafferScene.Group() + s["editScope1"] = Gaffer.EditScope() + s["editScope2"] = Gaffer.EditScope() + + s["group"]["in"][0].setInput( s["light"]["out"] ) + + s["editScope1"].setup( s["group"]["out"] ) + s["editScope1"]["in"].setInput( s["group"]["out"] ) + + s["editScope2"].setup( s["editScope1"]["out"] ) + s["editScope2"]["in"].setInput( s["editScope1"]["out"] ) + + Gaffer.MetadataAlgo.setReadOnly( s["light"]["visualiserAttributes"]["scale"]["enabled"], True ) + inspection = self.__inspect( s["group"]["out"], "/group/light", "gl:visualiser:scale", None ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "light.visualiserAttributes.scale.enabled is locked." ) + self.assertRaisesRegex( IECore.Exception, "Cannot disable edit : light.visualiserAttributes.scale.enabled is locked.", inspection.disableEdit ) + + Gaffer.MetadataAlgo.setReadOnly( s["light"]["visualiserAttributes"]["scale"]["enabled"], False ) + Gaffer.MetadataAlgo.setReadOnly( s["light"]["visualiserAttributes"]["scale"], True ) + inspection = self.__inspect( s["group"]["out"], "/group/light", "gl:visualiser:scale", None ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "light.visualiserAttributes.scale is locked." ) + self.assertRaisesRegex( IECore.Exception, "Cannot disable edit : light.visualiserAttributes.scale is locked.", inspection.disableEdit ) + + Gaffer.MetadataAlgo.setReadOnly( s["light"]["visualiserAttributes"]["scale"], False ) + inspection = self.__inspect( s["group"]["out"], "/group/light", "gl:visualiser:scale", None ) + self.assertTrue( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "" ) + inspection.disableEdit() + self.assertFalse( s["light"]["visualiserAttributes"]["scale"]["enabled"].getValue() ) + + lightEdit = GafferScene.EditScopeAlgo.acquireAttributeEdit( + s["editScope1"], "/group/light", "gl:visualiser:scale", createIfNecessary = True + ) + lightEdit["enabled"].setValue( True ) + lightEdit["value"].setValue( 2.0 ) + + inspection = self.__inspect( s["editScope1"]["out"], "/group/light", "gl:visualiser:scale", s["editScope2"] ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "The target edit scope editScope2 is not in the scene history." ) + + inspection = self.__inspect( s["editScope2"]["out"], "/group/light", "gl:visualiser:scale", s["editScope2"] ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "Edit is not in the current edit scope. Change scope to editScope1 to disable." ) + self.assertRaisesRegex( IECore.Exception, "Cannot disable edit : Edit is not in the current edit scope. Change scope to editScope1 to disable.", inspection.disableEdit ) + + Gaffer.MetadataAlgo.setReadOnly( s["editScope1"], True ) + inspection = self.__inspect( s["editScope1"]["out"], "/group/light", "gl:visualiser:scale", s["editScope1"] ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "editScope1 is locked." ) + self.assertRaisesRegex( IECore.Exception, "Cannot disable edit : editScope1 is locked.", inspection.disableEdit ) + + Gaffer.MetadataAlgo.setReadOnly( s["editScope1"], False ) + inspection = self.__inspect( s["editScope1"]["out"], "/group/light", "gl:visualiser:scale", s["editScope1"] ) + self.assertTrue( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "" ) + inspection.disableEdit() + self.assertFalse( lightEdit["enabled"].getValue() ) + + inspection = self.__inspect( s["editScope1"]["out"], "/group/light", "gl:visualiser:scale", s["editScope1"] ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "Edit is not in the current edit scope. Change scope to None to disable." ) + + inspection = self.__inspect( s["editScope1"]["out"], "/group/light", "gl:visualiser:scale", None ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "light.visualiserAttributes.scale.enabled is not enabled." ) if __name__ == "__main__" : unittest.main() diff --git a/python/GafferSceneUITest/OptionInspectorTest.py b/python/GafferSceneUITest/OptionInspectorTest.py index 3735046cfc..016a61967e 100644 --- a/python/GafferSceneUITest/OptionInspectorTest.py +++ b/python/GafferSceneUITest/OptionInspectorTest.py @@ -162,7 +162,7 @@ def testSourceAndEdits( self ) : source = s["standardOptions"]["options"]["renderCamera"], sourceType = SourceType.Other, editable = False, - nonEditableReason = "The target EditScope (editScope1) is not in the scene history." + nonEditableReason = "The target edit scope editScope1 is not in the scene history." ) # If it is in the history though, and we're told to use it, then we will. @@ -285,7 +285,7 @@ def testSourceAndEdits( self ) : source = s["independentOptions"]["options"]["renderCamera"], sourceType = SourceType.Downstream, editable = False, - nonEditableReason = "The target EditScope (editScope2) is disabled." + nonEditableReason = "The target edit scope editScope2 is disabled." ) s["editScope2"]["enabled"].setValue( True ) @@ -561,7 +561,7 @@ def testRenderPassSourceAndEdits( self ) : source = s["standardOptions"]["options"]["renderCamera"], sourceType = SourceType.Other, editable = False, - nonEditableReason = "The target EditScope (editScope1) is not in the scene history." + nonEditableReason = "The target edit scope editScope1 is not in the scene history." ) # If it is in the history though, and we're told to use it, then we will. @@ -684,7 +684,7 @@ def testRenderPassSourceAndEdits( self ) : source = s["independentOptions"]["options"]["renderCamera"], sourceType = SourceType.Downstream, editable = False, - nonEditableReason = "The target EditScope (editScope2) is disabled." + nonEditableReason = "The target edit scope editScope2 is disabled." ) s["editScope2"]["enabled"].setValue( True ) @@ -892,5 +892,100 @@ def testDefaultValueMetadata( self ) : edit = edit ) + def testAcquireEditCreateIfNecessary( self ) : + + s = Gaffer.ScriptNode() + + s["standardOptions"] = GafferScene.StandardOptions() + s["standardOptions"]["options"]["renderCamera"]["enabled"].setValue( True ) + s["standardOptions"]["options"]["renderCamera"]["value"].setValue( "/defaultCamera" ) + + s["group"] = GafferScene.Group() + s["editScope"] = Gaffer.EditScope() + + s["group"]["in"][0].setInput( s["standardOptions"]["out"] ) + + s["editScope"].setup( s["group"]["out"] ) + s["editScope"]["in"].setInput( s["group"]["out"] ) + + inspection = self.__inspect( s["group"]["out"], "render:camera", None ) + self.assertEqual( inspection.acquireEdit( createIfNecessary = False ), s["standardOptions"]["options"]["renderCamera"] ) + + inspection = self.__inspect( s["editScope"]["out"], "render:camera", s["editScope"] ) + self.assertIsNone( inspection.acquireEdit( createIfNecessary = False ) ) + + edit = inspection.acquireEdit( createIfNecessary = True ) + self.assertIsNotNone( edit ) + self.assertEqual( inspection.acquireEdit( createIfNecessary = False ), edit ) + + def testDisableEdit( self ) : + + s = Gaffer.ScriptNode() + + s["standardOptions"] = GafferScene.StandardOptions() + s["standardOptions"]["options"]["renderCamera"]["enabled"].setValue( True ) + s["standardOptions"]["options"]["renderCamera"]["value"].setValue( "/defaultCamera" ) + + s["group"] = GafferScene.Group() + s["editScope1"] = Gaffer.EditScope() + s["editScope2"] = Gaffer.EditScope() + + s["group"]["in"][0].setInput( s["standardOptions"]["out"] ) + + s["editScope1"].setup( s["group"]["out"] ) + s["editScope1"]["in"].setInput( s["group"]["out"] ) + + s["editScope2"].setup( s["editScope1"]["out"] ) + s["editScope2"]["in"].setInput( s["editScope1"]["out"] ) + + inspection = self.__inspect( s["editScope2"]["out"], "render:camera", s["editScope2"] ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "Edit is not in the current edit scope. Change scope to None to disable." ) + + Gaffer.MetadataAlgo.setReadOnly( s["standardOptions"]["options"], True ) + inspection = self.__inspect( s["group"]["out"], "render:camera", None ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "standardOptions.options is locked." ) + self.assertRaisesRegex( IECore.Exception, "Cannot disable edit : standardOptions.options is locked.", inspection.disableEdit ) + + Gaffer.MetadataAlgo.setReadOnly( s["standardOptions"]["options"], False ) + inspection = self.__inspect( s["group"]["out"], "render:camera", None ) + self.assertTrue( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "" ) + inspection.disableEdit() + self.assertFalse( s["standardOptions"]["options"]["renderCamera"]["enabled"].getValue() ) + + inspection = self.__inspect( s["editScope2"]["out"], "render:camera", None ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "No editable source found in history." ) + + cameraEdit = GafferScene.EditScopeAlgo.acquireOptionEdit( + s["editScope1"], "render:camera", createIfNecessary = True + ) + cameraEdit["enabled"].setValue( True ) + cameraEdit["value"].setValue( "/bar" ) + + inspection = self.__inspect( s["editScope1"]["out"], "render:camera", s["editScope2"] ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "The target edit scope editScope2 is not in the scene history." ) + + inspection = self.__inspect( s["editScope2"]["out"], "render:camera", s["editScope2"] ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "Edit is not in the current edit scope. Change scope to editScope1 to disable." ) + self.assertRaisesRegex( IECore.Exception, "Cannot disable edit : Edit is not in the current edit scope. Change scope to editScope1 to disable.", inspection.disableEdit ) + + Gaffer.MetadataAlgo.setReadOnly( s["editScope1"], True ) + inspection = self.__inspect( s["editScope1"]["out"], "render:camera", s["editScope1"] ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "editScope1 is locked." ) + self.assertRaisesRegex( IECore.Exception, "Cannot disable edit : editScope1 is locked.", inspection.disableEdit ) + + Gaffer.MetadataAlgo.setReadOnly( s["editScope1"], False ) + inspection = self.__inspect( s["editScope1"]["out"], "render:camera", s["editScope1"] ) + self.assertTrue( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "" ) + inspection.disableEdit() + self.assertFalse( cameraEdit["enabled"].getValue() ) + if __name__ == "__main__" : unittest.main() diff --git a/python/GafferSceneUITest/ParameterInspectorTest.py b/python/GafferSceneUITest/ParameterInspectorTest.py index 51fce112d4..f37651b572 100644 --- a/python/GafferSceneUITest/ParameterInspectorTest.py +++ b/python/GafferSceneUITest/ParameterInspectorTest.py @@ -156,7 +156,7 @@ def testSourceAndEdits( self ) : self.__assertExpectedResult( self.__inspect( s["group"]["out"], "/group/light", "intensity", s["editScope1"] ), source = s["light"]["parameters"]["intensity"], sourceType = SourceType.Other, - editable = False, nonEditableReason = "The target EditScope (editScope1) is not in the scene history." + editable = False, nonEditableReason = "The target edit scope editScope1 is not in the scene history." ) # If it is in the history though, and we're told to use it, then we will. @@ -302,7 +302,7 @@ def testSourceAndEdits( self ) : self.__assertExpectedResult( self.__inspect( s["independentLightTweak"]["out"], "/group/light", "intensity", s["editScope2"] ), source = independentLightTweakPlug, sourceType = SourceType.Downstream, - editable = False, nonEditableReason = "The target EditScope (editScope2) is disabled." + editable = False, nonEditableReason = "The target edit scope editScope2 is disabled." ) s["editScope2"]["enabled"].setValue( True ) @@ -378,7 +378,7 @@ def testEditScopeNotInHistory( self ) : self.__assertExpectedResult( self.__inspect( light["out"], "/light", "exposure", editScope ), source = light["parameters"]["exposure"], sourceType = SourceType.Other, - editable = False, nonEditableReason = "The target EditScope (EditScope) is not in the scene history." + editable = False, nonEditableReason = "The target edit scope EditScope is not in the scene history." ) self.__assertExpectedResult( @@ -390,9 +390,112 @@ def testEditScopeNotInHistory( self ) : self.__assertExpectedResult( self.__inspect( shaderTweaks["out"], "/light", "exposure", editScope ), source = shaderTweaks["tweaks"][0], sourceType = SourceType.Other, - editable = False, nonEditableReason = "The target EditScope (EditScope) is not in the scene history." + editable = False, nonEditableReason = "The target edit scope EditScope is not in the scene history." ) + def testAcquireEditCreateIfNecessary( self ) : + + s = Gaffer.ScriptNode() + + s["light"] = GafferSceneTest.TestLight() + s["group"] = GafferScene.Group() + s["editScope"] = Gaffer.EditScope() + + s["group"]["in"][0].setInput( s["light"]["out"] ) + + s["editScope"].setup( s["group"]["out"] ) + s["editScope"]["in"].setInput( s["group"]["out"] ) + + inspection = self.__inspect( s["group"]["out"], "/group/light", "exposure", None ) + self.assertEqual( inspection.acquireEdit( createIfNecessary = False ), s["light"]["parameters"]["exposure"] ) + + inspection = self.__inspect( s["editScope"]["out"], "/group/light", "exposure", s["editScope"] ) + self.assertIsNone( inspection.acquireEdit( createIfNecessary = False ) ) + + edit = inspection.acquireEdit( createIfNecessary = True ) + self.assertIsNotNone( edit ) + self.assertEqual( inspection.acquireEdit( createIfNecessary = False ), edit ) + + def testDisableEdit( self ) : + + s = Gaffer.ScriptNode() + + s["light"] = GafferSceneTest.TestLight() + + s["lightFilter"] = GafferScene.PathFilter() + s["lightFilter"]["paths"].setValue( IECore.StringVectorData( [ "/light" ] ) ) + + s["shaderTweaks"] = GafferScene.ShaderTweaks() + s["shaderTweaks"]["in"].setInput( s["light"]["out"] ) + s["shaderTweaks"]["filter"].setInput( s["lightFilter"]["out"] ) + exposureTweak = Gaffer.TweakPlug( "exposure", 10 ) + s["shaderTweaks"]["tweaks"].addChild( exposureTweak ) + + s["editScope"] = Gaffer.EditScope() + s["editScope"].setup( s["shaderTweaks"]["out"] ) + s["editScope"]["in"].setInput( s["shaderTweaks"]["out"] ) + + s["editScope2"] = Gaffer.EditScope() + s["editScope2"].setup( s["editScope"]["out"] ) + s["editScope2"]["in"].setInput( s["editScope"]["out"] ) + + Gaffer.MetadataAlgo.setReadOnly( exposureTweak["enabled"], True ) + inspection = self.__inspect( s["shaderTweaks"]["out"], "/light", "exposure", None ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "shaderTweaks.tweaks.tweak.enabled is locked." ) + self.assertRaisesRegex( IECore.Exception, "Cannot disable edit : shaderTweaks.tweaks.tweak.enabled is locked.", inspection.disableEdit ) + + Gaffer.MetadataAlgo.setReadOnly( exposureTweak["enabled"], False ) + Gaffer.MetadataAlgo.setReadOnly( exposureTweak, True ) + inspection = self.__inspect( s["shaderTweaks"]["out"], "/light", "exposure", None ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "shaderTweaks.tweaks.tweak is locked." ) + self.assertRaisesRegex( IECore.Exception, "Cannot disable edit : shaderTweaks.tweaks.tweak is locked.", inspection.disableEdit ) + + Gaffer.MetadataAlgo.setReadOnly( exposureTweak, False ) + inspection = self.__inspect( s["shaderTweaks"]["out"], "/light", "exposure", None ) + self.assertTrue( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "" ) + inspection.disableEdit() + self.assertFalse( exposureTweak["enabled"].getValue() ) + + lightEdit = GafferScene.EditScopeAlgo.acquireParameterEdit( + s["editScope"], "/light", "light", ( "", "exposure" ), createIfNecessary = True + ) + lightEdit["enabled"].setValue( True ) + lightEdit["value"].setValue( 2.0 ) + + inspection = self.__inspect( s["editScope"]["out"], "/light", "exposure", s["editScope2"] ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "The target edit scope editScope2 is not in the scene history." ) + + inspection = self.__inspect( s["editScope2"]["out"], "/light", "exposure", s["editScope2"] ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "Edit is not in the current edit scope. Change scope to editScope to disable." ) + self.assertRaisesRegex( IECore.Exception, "Cannot disable edit : Edit is not in the current edit scope. Change scope to editScope to disable.", inspection.disableEdit ) + + Gaffer.MetadataAlgo.setReadOnly( s["editScope"], True ) + inspection = self.__inspect( s["editScope"]["out"], "/light", "exposure", s["editScope"] ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "editScope is locked." ) + self.assertRaisesRegex( IECore.Exception, "Cannot disable edit : editScope is locked.", inspection.disableEdit ) + + Gaffer.MetadataAlgo.setReadOnly( s["editScope"], False ) + inspection = self.__inspect( s["editScope"]["out"], "/light", "exposure", s["editScope"] ) + self.assertTrue( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "" ) + inspection.disableEdit() + self.assertFalse( lightEdit["enabled"].getValue() ) + + inspection = self.__inspect( s["editScope"]["out"], "/light", "exposure", s["editScope"] ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "Edit is not in the current edit scope. Change scope to None to disable." ) + + inspection = self.__inspect( s["editScope"]["out"], "/light", "exposure", None ) + self.assertEqual( inspection.source(), s["light"]["parameters"]["exposure"] ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "Disabling edits not supported for this plug." ) + def testDisabledTweaks( self ) : light = GafferSceneTest.TestLight() diff --git a/python/GafferSceneUITest/SetMembershipInspectorTest.py b/python/GafferSceneUITest/SetMembershipInspectorTest.py index 9f4f3070af..d16f991cf2 100644 --- a/python/GafferSceneUITest/SetMembershipInspectorTest.py +++ b/python/GafferSceneUITest/SetMembershipInspectorTest.py @@ -192,7 +192,7 @@ def testSourceAndEdits( self ) : source = s["plane"]["sets"], sourceType = SourceType.Other, editable = False, - nonEditableReason = "The target EditScope (editScope1) is not in the scene history." + nonEditableReason = "The target edit scope editScope1 is not in the scene history." ) # If it is in the history though, and we're told to use it, then we will. @@ -329,7 +329,7 @@ def testSourceAndEdits( self ) : source = s["independentSet"]["name"], sourceType = SourceType.Downstream, editable = False, - nonEditableReason = "The target EditScope (editScope2) is disabled." + nonEditableReason = "The target edit scope editScope2 is disabled." ) s["editScope2"]["enabled"].setValue( True ) @@ -594,6 +594,100 @@ def testSetNodeEditSetMembership( self ) : self.assertFalse( inspector.editSetMembership( inspection, "/plane", GafferScene.EditScopeAlgo.SetMembership.Removed ) ) + def testAcquireEditCreateIfNecessary( self ) : + + s = Gaffer.ScriptNode() + + s["plane"] = GafferScene.Plane() + s["plane"]["sets"].setValue( "planeSetA planeSetB" ) + + s["group"] = GafferScene.Group() + s["editScope"] = Gaffer.EditScope() + + s["group"]["in"][0].setInput( s["plane"]["out"] ) + s["editScope"].setup( s["group"]["out"] ) + s["editScope"]["in"].setInput( s["group"]["out"] ) + + inspection = self.__inspect( s["group"]["out"], "/group/plane", "planeSetA", None ) + self.assertEqual( inspection.acquireEdit( createIfNecessary = False ), s["plane"]["sets"] ) + + inspection = self.__inspect( s["editScope"]["out"], "/group/plane", "planeSetA", s["editScope"] ) + self.assertIsNone( inspection.acquireEdit( createIfNecessary = False ) ) + + edit = inspection.acquireEdit( createIfNecessary = True ) + self.assertIsNotNone( edit ) + self.assertEqual( inspection.acquireEdit( createIfNecessary = False ), edit ) + + def testDisableEdit( self ) : + + s = Gaffer.ScriptNode() + + s["plane"] = GafferScene.Plane() + s["plane"]["sets"].setValue( "planeSetA planeSetB" ) + + s["group"] = GafferScene.Group() + + s["editScope1"] = Gaffer.EditScope() + + s["group"]["in"][0].setInput( s["plane"]["out"] ) + + Gaffer.MetadataAlgo.setReadOnly( s["plane"]["sets"], True ) + inspection = self.__inspect( s["group"]["out"], "/group/plane", "planeSetA", None ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "plane.sets is locked." ) + + Gaffer.MetadataAlgo.setReadOnly( s["plane"]["sets"], False ) + inspection = self.__inspect( s["group"]["out"], "/group/plane", "planeSetA", None ) + self.assertTrue( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "" ) + + inspection.disableEdit() + self.assertEqual( s["plane"]["sets"].getValue(), "planeSetB" ) + + inspection = self.__inspect( s["group"]["out"], "/group/plane", "planeSetB", None ) + self.assertTrue( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "" ) + + inspection.disableEdit() + self.assertEqual( s["plane"]["sets"].getValue(), "" ) + + inspection = self.__inspect( s["group"]["out"], "/group/plane", "planeSetB", None ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "plane.sets has no edit to disable." ) + + s["editScope1"].setup( s["group"]["out"] ) + s["editScope1"]["in"].setInput( s["group"]["out"] ) + + for membership in ( GafferScene.EditScopeAlgo.SetMembership.Added, GafferScene.EditScopeAlgo.SetMembership.Removed ) : + + GafferScene.EditScopeAlgo.setSetMembership( + s["editScope1"], + IECore.PathMatcher( [ "group/plane" ] ), + "planeSetEditScope", + membership + ) + + self.assertEqual( + GafferScene.EditScopeAlgo.getSetMembership( s["editScope1"], "/group/plane", "planeSetEditScope"), + membership + ) + + Gaffer.MetadataAlgo.setReadOnly( s["editScope1"], True ) + inspection = self.__inspect( s["editScope1"]["out"], "/group/plane", "planeSetEditScope", None ) + self.assertFalse( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "editScope1 is locked." ) + self.assertRaisesRegex( IECore.Exception, "Cannot disable edit : editScope1 is locked.", inspection.disableEdit ) + + Gaffer.MetadataAlgo.setReadOnly( s["editScope1"], False ) + inspection = self.__inspect( s["editScope1"]["out"], "/group/plane", "planeSetEditScope", None ) + self.assertTrue( inspection.canDisableEdit() ) + self.assertEqual( inspection.nonDisableableReason(), "" ) + + inspection.disableEdit() + self.assertEqual( + GafferScene.EditScopeAlgo.getSetMembership( s["editScope1"], "/group/plane", "planeSetEditScope"), + GafferScene.EditScopeAlgo.SetMembership.Unchanged + ) if __name__ == "__main__" : unittest.main() \ No newline at end of file diff --git a/python/GafferUI/Menu.py b/python/GafferUI/Menu.py index d34d8df71c..adf001fa36 100644 --- a/python/GafferUI/Menu.py +++ b/python/GafferUI/Menu.py @@ -728,6 +728,9 @@ def event( self, qEvent ) : if action and action.statusTip() : QtWidgets.QToolTip.showText( qEvent.globalPos(), action.statusTip(), self ) return True + elif QtWidgets.QToolTip.isVisible() : + QtWidgets.QToolTip.hideText() + return True return QtWidgets.QMenu.event( self, qEvent ) diff --git a/src/GafferSceneUI/AttributeInspector.cpp b/src/GafferSceneUI/AttributeInspector.cpp index eb745ebfd5..12e9b6873c 100644 --- a/src/GafferSceneUI/AttributeInspector.cpp +++ b/src/GafferSceneUI/AttributeInspector.cpp @@ -394,12 +394,13 @@ Inspector::EditFunctionOrFailure AttributeInspector::editFunction( Gaffer::EditS editScope = EditScopePtr( editScope ), attributeName, context = history->context - ] () { + ] ( bool createIfNecessary ) { Context::Scope scope( context.get() ); return EditScopeAlgo::acquireAttributeEdit( editScope.get(), context->get( ScenePlug::scenePathContextName ), - attributeName + attributeName, + createIfNecessary ); }; } diff --git a/src/GafferSceneUI/Inspector.cpp b/src/GafferSceneUI/Inspector.cpp index 6cec443f42..8ce8c28f61 100644 --- a/src/GafferSceneUI/Inspector.cpp +++ b/src/GafferSceneUI/Inspector.cpp @@ -38,6 +38,7 @@ #include "Gaffer/Animation.h" #include "Gaffer/MetadataAlgo.h" +#include "Gaffer/OptionalValuePlug.h" #include "Gaffer/PathFilter.h" #include "Gaffer/ScriptNode.h" #include "Gaffer/Spreadsheet.h" @@ -211,18 +212,23 @@ Inspector::ResultPtr Inspector::inspect() const if( result->editScope() && !result->m_editScopeInHistory ) { - result->m_editFunction = fmt::format( - "The target EditScope ({}) is not in the scene history.", + const std::string nonEditableReason = fmt::format( + "The target edit scope {} is not in the scene history.", result->editScope()->relativeName( result->editScope()->scriptNode() ) ); + result->m_editFunction = nonEditableReason; + result->m_disableEditFunction = nonEditableReason; result->m_sourceType = Result::SourceType::Other; } - - else if( !result->m_source && !result->editable() ) + else if( !result->m_source ) { - // There's no source plug and no way of making - // the property. - result->m_editFunction = "No editable source found in history."; + if( !result->editable() ) + { + // There's no source plug and no way of making + // the property. + result->m_editFunction = "No editable source found in history."; + } + result->m_disableEditFunction = "No editable source found in history."; } if( fallbackValue ) @@ -289,14 +295,27 @@ void Inspector::inspectHistoryWalk( const GafferScene::SceneAlgo::History *histo if( nonEditableReason.empty() ) { - result->m_editFunction = [source = result->m_source] () { return source; }; + result->m_editFunction = [source = result->m_source] ( bool unused ) { return source; }; + result->m_disableEditFunction = disableEditFunction( result->m_source.get(), history ); result->m_editWarning = editWarning; } else { result->m_editFunction = nonEditableReason; + result->m_disableEditFunction = nonEditableReason; } } + else if( node->ancestor() && node->ancestor() != result->m_editScope ) + { + result->m_disableEditFunction = fmt::format( + "Edit is not in the current edit scope. Change scope to {} to disable.", + node->ancestor()->relativeName( node->ancestor()->scriptNode() ) + ); + } + else if( !node->ancestor() && result->m_editScope ) + { + result->m_disableEditFunction = "Edit is not in the current edit scope. Change scope to None to disable."; + } } } } @@ -338,7 +357,7 @@ void Inspector::inspectHistoryWalk( const GafferScene::SceneAlgo::History *histo else { result->m_editFunction = fmt::format( - "The target EditScope ({}) is disabled.", + "The target edit scope {} is disabled.", editScope->relativeName( editScope->scriptNode() ) ); } @@ -368,6 +387,47 @@ Inspector::EditFunctionOrFailure Inspector::editFunction( Gaffer::EditScope *edi return "Editing not supported"; } +Inspector::DisableEditFunctionOrFailure Inspector::disableEditFunction( Gaffer::ValuePlug *plug, const GafferScene::SceneAlgo::History *history ) const +{ + Gaffer::BoolPlugPtr enabledPlug; + if( auto tweakPlug = runTimeCast( plug ) ) + { + enabledPlug = tweakPlug->enabledPlug(); + } + else if( auto nameValuePlug = runTimeCast( plug ) ) + { + enabledPlug = nameValuePlug->enabledPlug(); + } + else if( auto optionalValuePlug = runTimeCast( plug ) ) + { + enabledPlug = optionalValuePlug->enabledPlug(); + } + + if( enabledPlug ) + { + if( const GraphComponent *readOnlyReason = MetadataAlgo::readOnlyReason( enabledPlug.get() ) ) + { + return fmt::format( "{} is locked.", readOnlyReason->relativeName( readOnlyReason->ancestor() ) ); + } + else if( !enabledPlug->settable() ) + { + return fmt::format( "{} is not settable.", enabledPlug->relativeName( enabledPlug->ancestor() ) ); + } + else if( !enabledPlug->getValue() ) + { + return fmt::format( "{} is not enabled.", enabledPlug->relativeName( enabledPlug->ancestor() ) ); + } + else + { + return [ enabledPlug ] () { enabledPlug->setValue( false ); }; + } + } + else + { + return "Disabling edits not supported for this plug."; + } +} + IECore::ConstObjectPtr Inspector::fallbackValue( const GafferScene::SceneAlgo::History *history, std::string &description ) const { return nullptr; @@ -666,16 +726,41 @@ std::string Inspector::Result::nonEditableReason() const return ""; } -Gaffer::ValuePlugPtr Inspector::Result::acquireEdit() const +Gaffer::ValuePlugPtr Inspector::Result::acquireEdit( bool createIfNecessary ) const { if( m_editFunction.which() == 0 ) { - return boost::get( m_editFunction )(); + return boost::get( m_editFunction )( createIfNecessary ); } throw IECore::Exception( "Not editable : " + boost::get( m_editFunction ) ); } +bool Inspector::Result::canDisableEdit() const +{ + return m_disableEditFunction.which() == 0 && boost::get( m_disableEditFunction ) != nullptr; +} + +std::string Inspector::Result::nonDisableableReason() const +{ + if( m_disableEditFunction.which() == 1 ) + { + return boost::get( m_disableEditFunction ); + } + + return ""; +} + +void Inspector::Result::disableEdit() const +{ + if( m_disableEditFunction.which() == 0 ) + { + return boost::get( m_disableEditFunction )(); + } + + throw IECore::Exception( "Cannot disable edit : " + boost::get( m_disableEditFunction ) ); +} + std::string Inspector::Result::editWarning() const { return m_editWarning; diff --git a/src/GafferSceneUI/OptionInspector.cpp b/src/GafferSceneUI/OptionInspector.cpp index 71e152f362..6ed624fa19 100644 --- a/src/GafferSceneUI/OptionInspector.cpp +++ b/src/GafferSceneUI/OptionInspector.cpp @@ -293,12 +293,13 @@ Inspector::EditFunctionOrFailure OptionInspector::editFunction( Gaffer::EditScop renderPass, option = m_option, context = history->context - ] () { + ] ( bool createIfNecessary ) { Context::Scope scope( context.get() ); return EditScopeAlgo::acquireRenderPassOptionEdit( editScope.get(), renderPass, - option + option, + createIfNecessary ); }; } @@ -326,11 +327,12 @@ Inspector::EditFunctionOrFailure OptionInspector::editFunction( Gaffer::EditScop editScope = EditScopePtr( editScope ), option = m_option, context = history->context - ] () { + ] ( bool createIfNecessary ) { Context::Scope scope( context.get() ); return EditScopeAlgo::acquireOptionEdit( editScope.get(), - option + option, + createIfNecessary ); }; } diff --git a/src/GafferSceneUI/ParameterInspector.cpp b/src/GafferSceneUI/ParameterInspector.cpp index 305e5b49ac..ab54e4da18 100644 --- a/src/GafferSceneUI/ParameterInspector.cpp +++ b/src/GafferSceneUI/ParameterInspector.cpp @@ -215,13 +215,14 @@ Inspector::EditFunctionOrFailure ParameterInspector::editFunction( Gaffer::EditS attributeName = attributeHistory->attributeName, context = attributeHistory->context, parameter = m_parameter - ] () { + ] ( bool createIfNecessary ) { Context::Scope scope( context.get() ); return EditScopeAlgo::acquireParameterEdit( editScope.get(), context->get( ScenePlug::scenePathContextName ), attributeName, - parameter + parameter, + createIfNecessary ); }; } diff --git a/src/GafferSceneUI/SetMembershipInspector.cpp b/src/GafferSceneUI/SetMembershipInspector.cpp index bbf5be0616..5041930888 100644 --- a/src/GafferSceneUI/SetMembershipInspector.cpp +++ b/src/GafferSceneUI/SetMembershipInspector.cpp @@ -127,29 +127,8 @@ HistoryCache g_historyCache( ); -} // namespace - -SetMembershipInspector::SetMembershipInspector( - const GafferScene::ScenePlugPtr &scene, - const Gaffer::PlugPtr &editScope, - IECore::InternedString setName -) : -Inspector( "setMembership", setName.string(), editScope ), -m_scene( scene ), -m_setName( setName ) +bool editSetMembership( Gaffer::Plug *plug, const std::string &setName, const ScenePlug::ScenePath &path, EditScopeAlgo::SetMembership setMembership ) { - m_scene->node()->plugDirtiedSignal().connect( - boost::bind( &SetMembershipInspector::plugDirtied, this, ::_1 ) - ); - - Metadata::plugValueChangedSignal().connect( boost::bind( &SetMembershipInspector::plugMetadataChanged, this, ::_3, ::_4 ) ); - Metadata::nodeValueChangedSignal().connect( boost::bind( &SetMembershipInspector::nodeMetadataChanged, this, ::_2, ::_3 ) ); -} - -bool SetMembershipInspector::editSetMembership( const Result *inspection, const ScenePlug::ScenePath &path, EditScopeAlgo::SetMembership setMembership ) const -{ - PlugPtr plug = inspection->acquireEdit(); - if( auto objectNode = runTimeCast( plug->node() ) ) { std::vector sets; @@ -157,14 +136,14 @@ bool SetMembershipInspector::editSetMembership( const Result *inspection, const if( setMembership == EditScopeAlgo::SetMembership::Added ) { - if( std::find( sets.begin(), sets.end(), m_setName.string() ) == sets.end() ) + if( std::find( sets.begin(), sets.end(), setName ) == sets.end() ) { - sets.push_back( m_setName.string() ); + sets.push_back( setName ); } } else { - sets.erase( std::remove( sets.begin(), sets.end(), m_setName.string() ), sets.end() ); + sets.erase( std::remove( sets.begin(), sets.end(), setName ), sets.end() ); } objectNode->setsPlug()->setValue( boost::algorithm::join( sets, " " ) ); @@ -183,7 +162,7 @@ bool SetMembershipInspector::editSetMembership( const Result *inspection, const EditScopeAlgo::setSetMembership( editScope, m, - m_setName.string(), + setName, setMembership ); @@ -194,6 +173,49 @@ bool SetMembershipInspector::editSetMembership( const Result *inspection, const return false; } +std::string nonDisableableReason( const Gaffer::Plug *plug, const std::string &setName ) +{ + if( const GraphComponent *readOnlyReason = MetadataAlgo::readOnlyReason( plug ) ) + { + return fmt::format( "{} is locked.", readOnlyReason->relativeName( readOnlyReason->ancestor() ) ); + } + else if( auto objectNode = runTimeCast( plug->node() ) ) + { + std::vector sets; + IECore::StringAlgo::tokenize( objectNode->setsPlug()->getValue(), ' ', sets ); + if( std::find( sets.begin(), sets.end(), setName ) == sets.end() ) + { + return fmt::format( "{} has no edit to disable.", plug->relativeName( plug->ancestor() ) ); + } + } + + return ""; +} + +} // namespace + +SetMembershipInspector::SetMembershipInspector( + const GafferScene::ScenePlugPtr &scene, + const Gaffer::PlugPtr &editScope, + IECore::InternedString setName +) : +Inspector( "setMembership", setName.string(), editScope ), +m_scene( scene ), +m_setName( setName ) +{ + m_scene->node()->plugDirtiedSignal().connect( + boost::bind( &SetMembershipInspector::plugDirtied, this, ::_1 ) + ); + + Metadata::plugValueChangedSignal().connect( boost::bind( &SetMembershipInspector::plugMetadataChanged, this, ::_3, ::_4 ) ); + Metadata::nodeValueChangedSignal().connect( boost::bind( &SetMembershipInspector::nodeMetadataChanged, this, ::_2, ::_3 ) ); +} + +bool SetMembershipInspector::editSetMembership( const Result *inspection, const ScenePlug::ScenePath &path, EditScopeAlgo::SetMembership setMembership ) const +{ + return ::editSetMembership( inspection->acquireEdit().get(), m_setName.string(), path, setMembership ); +} + GafferScene::SceneAlgo::History::ConstPtr SetMembershipInspector::history() const { if( !m_scene->existsPlug()->getValue() ) @@ -312,9 +334,29 @@ Inspector::EditFunctionOrFailure SetMembershipInspector::editFunction( Gaffer::E editScope = editScope, setName, context = history->context - ] () { + ] ( bool createIfNecessary ) { Context::Scope scope( context.get() ); - return EditScopeAlgo::acquireSetEdits( editScope, setName ); + return EditScopeAlgo::acquireSetEdits( editScope, setName, createIfNecessary ); + }; + } +} + +Inspector::DisableEditFunctionOrFailure SetMembershipInspector::disableEditFunction( Gaffer::ValuePlug *plug, const GafferScene::SceneAlgo::History *history ) const +{ + const std::string nonDisableableReason = ::nonDisableableReason( plug, m_setName ); + + if( !nonDisableableReason.empty() ) + { + return nonDisableableReason; + } + else + { + return [ + plug = PlugPtr( plug ), + setName = m_setName, + path = history->context->get( ScenePlug::scenePathContextName ) + ] () { + return ::editSetMembership( plug.get(), setName.string(), path, EditScopeAlgo::SetMembership::Unchanged ); }; } } diff --git a/src/GafferSceneUIModule/InspectorBinding.cpp b/src/GafferSceneUIModule/InspectorBinding.cpp index 5bba05dd2e..23d4ff2078 100644 --- a/src/GafferSceneUIModule/InspectorBinding.cpp +++ b/src/GafferSceneUIModule/InspectorBinding.cpp @@ -72,10 +72,16 @@ IECore::ObjectPtr valueWrapper( const GafferSceneUI::Private::Inspector::Result return result.value() ? result.value()->copy() : nullptr; } -Gaffer::ValuePlugPtr acquireEditWrapper( GafferSceneUI::Private::Inspector::Result &result ) +Gaffer::ValuePlugPtr acquireEditWrapper( GafferSceneUI::Private::Inspector::Result &result, bool createIfNecessary ) { ScopedGILRelease gilRelease; - return result.acquireEdit(); + return result.acquireEdit( createIfNecessary ); +} + +void disableEditWrapper( GafferSceneUI::Private::Inspector::Result &result ) +{ + ScopedGILRelease gilRelease; + return result.disableEdit(); } bool editSetMembershipWrapper( @@ -133,8 +139,11 @@ void GafferSceneUIModule::bindInspector() .def( "fallbackDescription", &Inspector::Result::fallbackDescription, return_value_policy() ) .def( "editable", &Inspector::Result::editable ) .def( "nonEditableReason", &Inspector::Result::nonEditableReason ) - .def( "acquireEdit", &acquireEditWrapper ) + .def( "acquireEdit", &acquireEditWrapper, ( arg( "createIfNecessary" ) = true ) ) .def( "editWarning", &Inspector::Result::editWarning ) + .def( "canDisableEdit", &Inspector::Result::canDisableEdit ) + .def( "nonDisableableReason", &Inspector::Result::nonDisableableReason ) + .def( "disableEdit", &disableEditWrapper ) ; enum_( "SourceType" )