From 4f1e68dcb52b9f6ac55b60d769a570c293423afd Mon Sep 17 00:00:00 2001
From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com>
Date: Mon, 9 Dec 2024 19:46:03 -0800
Subject: [PATCH 01/12] PlugLayout : Use placeholder for errored or invalid
customWidgets
---
Changes.md | 1 +
python/GafferUI/PlugLayout.py | 22 ++++++++++++++++++++--
2 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/Changes.md b/Changes.md
index 4e95dd544f5..1f00830006d 100644
--- a/Changes.md
+++ b/Changes.md
@@ -17,6 +17,7 @@ Improvements
- The shading mode menu icon now updates to indicate when a non-default shading mode is in use.
- Added the ability to toggle between default shading and the last selected shading mode by Ctrl + clicking the shading mode menu button.
- PythonEditor : Added workaround for slow code completion caused by poorly performing Python property getters.
+- PlugLayout : A warning widget is now displayed when an invalid custom widget is registered.
Fixes
-----
diff --git a/python/GafferUI/PlugLayout.py b/python/GafferUI/PlugLayout.py
index ecabda5c7e5..0d2c69ab3a0 100644
--- a/python/GafferUI/PlugLayout.py
+++ b/python/GafferUI/PlugLayout.py
@@ -426,9 +426,14 @@ def __createPlugWidget( self, plug ) :
def __createCustomWidget( self, name ) :
widgetType = self.__itemMetadataValue( name, "widgetType" )
- widgetClass = self.__import( widgetType )
+ try :
+ widgetClass = self.__import( widgetType )
+ result = widgetClass( self.__parent )
+ except Exception as e :
+ message = "Could not create custom widget \"{}\" : {}".format( name, str( e ) )
+ IECore.msg( IECore.Msg.Level.Error, "GafferUI.PlugLayout", message )
- result = widgetClass( self.__parent )
+ result = _MissingCustomWidget( self.__parent, message )
return result
@@ -753,3 +758,16 @@ def update( self, section ) :
def __collapsibleStateChanged( self, collapsible, subsection ) :
subsection.saveState( "collapsed", collapsible.getCollapsed() )
+
+class _MissingCustomWidget( GafferUI.Widget ) :
+
+ def __init__( self, parent, warning, **kw ) :
+
+ self.__image = GafferUI.Image( "warningSmall.png" )
+ self.__warning = warning
+
+ GafferUI.Widget.__init__( self, self.__image, **kw )
+
+ def getToolTip( self ) :
+
+ return self.__warning
From d32925da6350fec56c01e20c321d28691f085da4 Mon Sep 17 00:00:00 2001
From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com>
Date: Mon, 9 Dec 2024 19:47:53 -0800
Subject: [PATCH 02/12] PlugLayout : Support width metadata on customWidgets
---
Changes.md | 4 +++-
python/GafferUI/PlugLayout.py | 25 +++++++++++++++----------
2 files changed, 18 insertions(+), 11 deletions(-)
diff --git a/Changes.md b/Changes.md
index 1f00830006d..b3477c219a3 100644
--- a/Changes.md
+++ b/Changes.md
@@ -17,7 +17,9 @@ Improvements
- The shading mode menu icon now updates to indicate when a non-default shading mode is in use.
- Added the ability to toggle between default shading and the last selected shading mode by Ctrl + clicking the shading mode menu button.
- PythonEditor : Added workaround for slow code completion caused by poorly performing Python property getters.
-- PlugLayout : A warning widget is now displayed when an invalid custom widget is registered.
+- PlugLayout :
+ - A warning widget is now displayed when an invalid custom widget is registered.
+ - `layout:customWidget::width` and `layout:customWidget::minimumWidth` metadata registrations are now supported for custom widgets.
Fixes
-----
diff --git a/python/GafferUI/PlugLayout.py b/python/GafferUI/PlugLayout.py
index 0d2c69ab3a0..a600073e12c 100644
--- a/python/GafferUI/PlugLayout.py
+++ b/python/GafferUI/PlugLayout.py
@@ -390,22 +390,26 @@ def __import( self, path ) :
return result
+ def __setWidthFromMetadata( self, widget, item ) :
+
+ width = self.__itemMetadataValue( item, "width" )
+ if width is not None :
+ widget._qtWidget().setFixedWidth( width )
+
+ minimumWidth = self.__itemMetadataValue( item, "minimumWidth" )
+ if minimumWidth is not None :
+ widget._qtWidget().setMinimumWidth( minimumWidth )
+
+ if widget._qtWidget().layout() is not None and ( width is not None or minimumWidth is not None ) :
+ widget._qtWidget().layout().setSizeConstraint( QtWidgets.QLayout.SetDefaultConstraint )
+
def __createPlugWidget( self, plug ) :
result = GafferUI.PlugValueWidget.create( plug )
if result is None :
return result
- width = self.__itemMetadataValue( plug, "width" )
- if width is not None :
- result._qtWidget().setFixedWidth( width )
-
- minimumWidth = self.__itemMetadataValue( plug, "minimumWidth" )
- if minimumWidth is not None :
- result._qtWidget().setMinimumWidth( minimumWidth )
-
- if result._qtWidget().layout() is not None and ( width is not None or minimumWidth is not None ) :
- result._qtWidget().layout().setSizeConstraint( QtWidgets.QLayout.SetDefaultConstraint )
+ self.__setWidthFromMetadata( result, plug )
if isinstance( result, GafferUI.PlugValueWidget ) and not result.hasLabel() and self.__itemMetadataValue( plug, "label" ) != "" :
result = GafferUI.PlugWidget( result )
@@ -429,6 +433,7 @@ def __createCustomWidget( self, name ) :
try :
widgetClass = self.__import( widgetType )
result = widgetClass( self.__parent )
+ self.__setWidthFromMetadata( result, name )
except Exception as e :
message = "Could not create custom widget \"{}\" : {}".format( name, str( e ) )
IECore.msg( IECore.Msg.Level.Error, "GafferUI.PlugLayout", message )
From c942d4030970efe2edee47a6868e0fe12e8b8639 Mon Sep 17 00:00:00 2001
From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com>
Date: Mon, 9 Dec 2024 19:48:47 -0800
Subject: [PATCH 03/12] ScriptWindow : Improve lifetime handling of acquired
ScriptWindows
Rather than brute-forcing this via `connectFront`, we now ensure that we hold
a strong reference to ScriptWindows created via `acquire()` when we have connections
to an application's `scripts` plug childAdded and removed signals.
---
python/GafferUI/ScriptWindow.py | 24 ++++++++++++++-----
python/GafferUITest/ScriptWindowTest.py | 32 +++++++++++++++++++++++++
2 files changed, 50 insertions(+), 6 deletions(-)
diff --git a/python/GafferUI/ScriptWindow.py b/python/GafferUI/ScriptWindow.py
index 6d715cc47f5..63a2671ef8a 100644
--- a/python/GafferUI/ScriptWindow.py
+++ b/python/GafferUI/ScriptWindow.py
@@ -167,6 +167,7 @@ def __closed( self, widget ) :
if scriptParent is not None :
scriptParent.removeChild( self.__script )
+ __automaticallyCreatedInstances = [] # strong references to instances made by acquire()
__instances = [] # weak references to all instances - used by acquire()
## Returns the ScriptWindow for the specified script, creating one
# if necessary.
@@ -178,7 +179,13 @@ def acquire( script, createIfNecessary=True ) :
if scriptWindow is not None and scriptWindow.scriptNode().isSame( script ) :
return scriptWindow
- return ScriptWindow( script ) if createIfNecessary else None
+ if createIfNecessary :
+ w = ScriptWindow( script )
+ if ScriptWindow.__connected( script.ancestor( Gaffer.ApplicationRoot ) ) :
+ ScriptWindow.__automaticallyCreatedInstances.append( w )
+ return w
+
+ return None
## Returns an IECore.MenuDefinition which is used to define the menu bars for all ScriptWindows
# created as part of the specified application. This can be edited at any time to modify subsequently
@@ -208,17 +215,22 @@ def menuDefinition( applicationOrApplicationRoot ) :
@classmethod
def connect( cls, applicationRoot ) :
- applicationRoot["scripts"].childAddedSignal().connectFront( ScriptWindow.__scriptAdded )
- applicationRoot["scripts"].childRemovedSignal().connect( ScriptWindow.__staticScriptRemoved )
+ applicationRoot._scriptWindowChildAddedConnection = applicationRoot["scripts"].childAddedSignal().connect( ScriptWindow.__scriptAdded )
+ applicationRoot._scriptWindowChildRemovedConnection = applicationRoot["scripts"].childRemovedSignal().connect( ScriptWindow.__staticScriptRemoved )
+
+ @staticmethod
+ def __connected( applicationRoot ) :
+
+ childAddedConnection = getattr( applicationRoot, "_scriptWindowChildAddedConnection", None )
+ childRemovedConnection = getattr( applicationRoot, "_scriptWindowChildRemovedConnection", None )
+ return childAddedConnection is not None and childRemovedConnection is not None and childRemovedConnection.connected() and childRemovedConnection.connected()
- __automaticallyCreatedInstances = [] # strong references to instances made by __scriptAdded()
@staticmethod
def __scriptAdded( scriptContainer, script ) :
- w = ScriptWindow( script )
+ w = ScriptWindow.acquire( script )
w.setVisible( True )
w.getLayout().restoreWindowState()
- ScriptWindow.__automaticallyCreatedInstances.append( w )
@staticmethod
def __staticScriptRemoved( scriptContainer, script ) :
diff --git a/python/GafferUITest/ScriptWindowTest.py b/python/GafferUITest/ScriptWindowTest.py
index eac295f0fc2..0db0c6e2087 100644
--- a/python/GafferUITest/ScriptWindowTest.py
+++ b/python/GafferUITest/ScriptWindowTest.py
@@ -87,6 +87,38 @@ def testAcquire( self ) :
w6 = GafferUI.ScriptWindow.acquire( s3, createIfNecessary = True )
self.assertTrue( w6.scriptNode().isSame( s3 ) )
+ def testLifetimeOfApplicationScriptWindows( self ) :
+
+ class testApp( Gaffer.Application ) :
+
+ def __init__( self ) :
+
+ Gaffer.Application.__init__( self )
+
+ def __scriptAdded( scriptContainer, script ) :
+
+ w = GafferUI.ScriptWindow.acquire( script )
+ w.setTitle( "modified" )
+ self.assertEqual( w.getTitle(), "modified" )
+
+ a = testApp().root()
+ GafferUI.ScriptWindow.connect( a )
+
+ # Acquire and modify the ScriptWindow before it is
+ # shown by the application to ensure that our modified
+ # ScriptWindow survives to be the one shown.
+ a["scripts"].childAddedSignal().connectFront( __scriptAdded )
+
+ s = Gaffer.ScriptNode()
+ a["scripts"]["s"] = s
+
+ self.waitForIdle( 1000 )
+
+ w = GafferUI.ScriptWindow.acquire( s )
+ self.assertEqual( w.getTitle(), "modified" )
+
+ del a["scripts"]["s"]
+
def testTitleChangedSignal( self ) :
self.__title = ""
From a50f86ab8fbf326335f7c5692a27934ae289e541 Mon Sep 17 00:00:00 2001
From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com>
Date: Mon, 9 Dec 2024 19:49:22 -0800
Subject: [PATCH 04/12] ScriptNodeAlgo : Add methods to edit and acquire
`renderPass` plug
---
Changes.md | 1 +
include/GafferSceneUI/ScriptNodeAlgo.h | 11 ++++
python/GafferSceneUI/RenderPassEditor.py | 15 ++---
.../GafferSceneUITest/ScriptNodeAlgoTest.py | 59 ++++++++++++++++++-
src/GafferSceneUI/ScriptNodeAlgo.cpp | 44 ++++++++++++++
.../ScriptNodeAlgoBinding.cpp | 20 +++++++
6 files changed, 138 insertions(+), 12 deletions(-)
diff --git a/Changes.md b/Changes.md
index b3477c219a3..cb423f5bfc3 100644
--- a/Changes.md
+++ b/Changes.md
@@ -45,6 +45,7 @@ API
- Backward compatibility is not trivial to maintain in this case.
- PlugAlgo : Added `contextSensitiveSource()` method.
- ShaderPlug : Added Python binding for `parameterSource()` method.
+- ScriptNodeAlgo : Added `setCurrentRenderPass()`, `getCurrentRenderPass()`, and `acquireRenderPassPlug()` methods.
1.5.2.0 (relative to 1.5.1.0)
=======
diff --git a/include/GafferSceneUI/ScriptNodeAlgo.h b/include/GafferSceneUI/ScriptNodeAlgo.h
index 13d57eab047..7a743ca4ba3 100644
--- a/include/GafferSceneUI/ScriptNodeAlgo.h
+++ b/include/GafferSceneUI/ScriptNodeAlgo.h
@@ -40,6 +40,7 @@
#include "GafferScene/VisibleSet.h"
+#include "Gaffer/NameValuePlug.h"
#include "Gaffer/Signals.h"
#include "IECore/PathMatcher.h"
@@ -112,6 +113,16 @@ GAFFERSCENEUI_API std::vector getLastSelectedPath( const
/// Returns a signal emitted when either the selected paths or last selected path change for `script`.
GAFFERSCENEUI_API ChangedSignal &selectedPathsChangedSignal( Gaffer::ScriptNode *script );
+/// Render Passes
+/// =============
+
+/// Acquires a plug used to specify the current render pass for the script.
+GAFFERSCENEUI_API Gaffer::NameValuePlug *acquireRenderPassPlug( Gaffer::ScriptNode *script, bool createIfMissing = true );
+/// Sets the current render pass for the script.
+GAFFERSCENEUI_API void setCurrentRenderPass( Gaffer::ScriptNode *script, std::string renderPass );
+/// Returns the current render pass for the script.
+GAFFERSCENEUI_API std::string getCurrentRenderPass( const Gaffer::ScriptNode *script );
+
} // namespace ScriptNodeAlgo
} // namespace GafferSceneUI
diff --git a/python/GafferSceneUI/RenderPassEditor.py b/python/GafferSceneUI/RenderPassEditor.py
index 6d2d55c7b74..b931933d703 100644
--- a/python/GafferSceneUI/RenderPassEditor.py
+++ b/python/GafferSceneUI/RenderPassEditor.py
@@ -360,17 +360,10 @@ def __setActiveRenderPass( self, pathListing ) :
self.__popup.popup( parent = self )
return
- ## \todo Perhaps we should add `ScriptNodeAlgo.set/getCurrentRenderPass()`
- # to wrap this up for general consumption?
- if "renderPass" not in script["variables"] :
- renderPassPlug = Gaffer.NameValuePlug( "renderPass", "", "renderPass", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic )
- script["variables"].addChild( renderPassPlug )
- Gaffer.MetadataAlgo.setReadOnly( renderPassPlug["name"], True )
- else :
- renderPassPlug = script["variables"]["renderPass"]
-
- currentRenderPass = renderPassPlug["value"].getValue()
- renderPassPlug["value"].setValue( selectedPassNames[0] if selectedPassNames[0] != currentRenderPass else "" )
+ GafferSceneUI.ScriptNodeAlgo.setCurrentRenderPass(
+ script,
+ selectedPassNames[0] if selectedPassNames[0] != GafferSceneUI.ScriptNodeAlgo.getCurrentRenderPass( script ) else ""
+ )
def __columnContextMenuSignal( self, column, pathListing, menuDefinition ) :
diff --git a/python/GafferSceneUITest/ScriptNodeAlgoTest.py b/python/GafferSceneUITest/ScriptNodeAlgoTest.py
index c2cb0500f9c..9cee235a184 100644
--- a/python/GafferSceneUITest/ScriptNodeAlgoTest.py
+++ b/python/GafferSceneUITest/ScriptNodeAlgoTest.py
@@ -17,7 +17,7 @@
#
# * Neither the name of John Haddon nor the names of
# any other contributors to this software may be used to endorse or
-# promote products derived from this software without specifiscript prior
+# promote products derived from this software without specific prior
# written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
@@ -204,5 +204,62 @@ def testVisibleSetExpansionUtilities( self ) :
self.assertEqual( GafferSceneUI.ScriptNodeAlgo.getVisibleSet( script ).expansions, IECore.PathMatcher( [ "/", "/A", "/A/C" ] ) )
self.assertEqual( newLeafs, IECore.PathMatcher( [ "/A/C/G", "/A/C/F" ] ) )
+ def testAcquireRenderPassPlug( self ) :
+
+ s1 = Gaffer.ScriptNode()
+ s2 = Gaffer.ScriptNode()
+
+ self.assertIsNone( GafferSceneUI.ScriptNodeAlgo.acquireRenderPassPlug( s1, createIfMissing = False ) )
+
+ p1A = GafferSceneUI.ScriptNodeAlgo.acquireRenderPassPlug( s1 )
+ p1B = GafferSceneUI.ScriptNodeAlgo.acquireRenderPassPlug( s1 )
+
+ p2A = GafferSceneUI.ScriptNodeAlgo.acquireRenderPassPlug( s2 )
+ p2B = GafferSceneUI.ScriptNodeAlgo.acquireRenderPassPlug( s2 )
+
+ self.assertIsNotNone( p1A )
+ self.assertIsNotNone( p2A )
+
+ self.assertTrue( p1A.isSame( p1B ) )
+ self.assertTrue( p2A.isSame( p2B ) )
+ self.assertFalse( p1A.isSame( p2A ) )
+
+ def testAcquireManuallyCreatedRenderPassPlug( self ) :
+
+ s = Gaffer.ScriptNode()
+ s["variables"]["renderPass"] = Gaffer.NameValuePlug( "renderPass", "", "renderPass" )
+
+ self.assertTrue( GafferSceneUI.ScriptNodeAlgo.acquireRenderPassPlug( s ).isSame( s["variables"]["renderPass"] ) )
+
+ s1 = Gaffer.ScriptNode()
+ s1["variables"]["renderPass"] = Gaffer.NameValuePlug( "renderPass", IECore.IntData( 0 ), "renderPass" )
+
+ self.assertRaises( IECore.Exception, GafferSceneUI.ScriptNodeAlgo.acquireRenderPassPlug, s1 )
+
+ def testSetCurrentRenderPass( self ) :
+
+ script = Gaffer.ScriptNode()
+ self.assertNotIn( "renderPass", script["variables"] )
+
+ GafferSceneUI.ScriptNodeAlgo.setCurrentRenderPass( script, "testA" )
+ self.assertIn( "renderPass", script["variables"] )
+ self.assertEqual( "testA", script["variables"]["renderPass"]["value"].getValue() )
+
+ script2 = Gaffer.ScriptNode()
+ script2["variables"]["renderPass"] = Gaffer.NameValuePlug( "renderPass", 123.0, "renderPass" )
+ self.assertRaises( IECore.Exception, GafferSceneUI.ScriptNodeAlgo.setCurrentRenderPass, script2, "testB" )
+
+ def testGetCurrentRenderPass( self ) :
+
+ script = Gaffer.ScriptNode()
+ self.assertEqual( "", GafferSceneUI.ScriptNodeAlgo.getCurrentRenderPass( script ) )
+
+ GafferSceneUI.ScriptNodeAlgo.setCurrentRenderPass( script, "testA" )
+ self.assertEqual( "testA", GafferSceneUI.ScriptNodeAlgo.getCurrentRenderPass( script ) )
+
+ script2 = Gaffer.ScriptNode()
+ script2["variables"]["renderPass"] = Gaffer.NameValuePlug( "renderPass", 123.0, "renderPass" )
+ self.assertRaises( IECore.Exception, GafferSceneUI.ScriptNodeAlgo.getCurrentRenderPass, script2 )
+
if __name__ == "__main__":
unittest.main()
diff --git a/src/GafferSceneUI/ScriptNodeAlgo.cpp b/src/GafferSceneUI/ScriptNodeAlgo.cpp
index bf401331704..af6a22a62f6 100644
--- a/src/GafferSceneUI/ScriptNodeAlgo.cpp
+++ b/src/GafferSceneUI/ScriptNodeAlgo.cpp
@@ -40,6 +40,9 @@
#include "Gaffer/Context.h"
#include "Gaffer/ScriptNode.h"
+#include "Gaffer/NameValuePlug.h"
+#include "Gaffer/CompoundDataPlug.h"
+#include "Gaffer/MetadataAlgo.h"
#include "boost/bind/bind.hpp"
@@ -146,5 +149,46 @@ std::vector ScriptNodeAlgo::getLastSelectedPath( const G
ScriptNodeAlgo::ChangedSignal &ScriptNodeAlgo::selectedPathsChangedSignal( Gaffer::ScriptNode *script )
{
return changedSignals( script ).selectedPathsChangedSignal;
+}
+
+NameValuePlug *ScriptNodeAlgo::acquireRenderPassPlug( Gaffer::ScriptNode *script, bool createIfMissing )
+{
+ if( const auto renderPassPlug = script->variablesPlug()->getChild( "renderPass" ) )
+ {
+ if( renderPassPlug->valuePlug() )
+ {
+ return renderPassPlug;
+ }
+ else
+ {
+ throw IECore::Exception( fmt::format( "Plug type of {} is {}, but must be StringPlug", renderPassPlug->valuePlug()->fullName(), renderPassPlug->valuePlug()->typeName() ) );
+ }
+ }
+
+ if( createIfMissing )
+ {
+ auto renderPassPlug = new NameValuePlug( "renderPass", new StringPlug(), "renderPass", Gaffer::Plug::Flags::Default | Gaffer::Plug::Flags::Dynamic );
+ MetadataAlgo::setReadOnly( renderPassPlug->namePlug(), true );
+ script->variablesPlug()->addChild( renderPassPlug );
+
+ return renderPassPlug;
+ }
+
+ return nullptr;
+}
+
+void ScriptNodeAlgo::setCurrentRenderPass( Gaffer::ScriptNode *script, std::string renderPass )
+{
+ auto renderPassPlug = acquireRenderPassPlug( script );
+ renderPassPlug->valuePlug()->setValue( renderPass );
+}
+
+std::string ScriptNodeAlgo::getCurrentRenderPass( const Gaffer::ScriptNode *script )
+{
+ if( const auto renderPassPlug = acquireRenderPassPlug( const_cast( script ), /* createIfMissing = */ false ) )
+ {
+ return renderPassPlug->valuePlug()->getValue();
+ }
+ return "";
}
diff --git a/src/GafferSceneUIModule/ScriptNodeAlgoBinding.cpp b/src/GafferSceneUIModule/ScriptNodeAlgoBinding.cpp
index 6f0f79ddad7..ed3374d1ab1 100644
--- a/src/GafferSceneUIModule/ScriptNodeAlgoBinding.cpp
+++ b/src/GafferSceneUIModule/ScriptNodeAlgoBinding.cpp
@@ -98,6 +98,23 @@ std::string getLastSelectedPathWrapper( const ScriptNode &script )
return result;
}
+NameValuePlugPtr acquireRenderPassPlugWrapper( Gaffer::ScriptNode &script, bool createIfMissing )
+{
+ IECorePython::ScopedGILRelease gilRelease;
+ return acquireRenderPassPlug( &script, createIfMissing );
+}
+
+void setCurrentRenderPassWrapper( ScriptNode &script, const std::string &renderPass )
+{
+ IECorePython::ScopedGILRelease gilRelease;
+ setCurrentRenderPass( &script, renderPass );
+}
+
+std::string getCurrentRenderPassWrapper( ScriptNode &script )
+{
+ return getCurrentRenderPass( &script );
+}
+
} // namespace
void GafferSceneUIModule::bindScriptNodeAlgo()
@@ -117,4 +134,7 @@ void GafferSceneUIModule::bindScriptNodeAlgo()
def( "setLastSelectedPath", &setLastSelectedPathWrapper );
def( "getLastSelectedPath", &getLastSelectedPathWrapper );
def( "selectedPathsChangedSignal", &selectedPathsChangedSignal, return_value_policy() );
+ def( "acquireRenderPassPlug", &acquireRenderPassPlugWrapper, ( arg( "script" ), arg( "createIfMissing" ) = true ) );
+ def( "setCurrentRenderPass", &setCurrentRenderPassWrapper );
+ def( "getCurrentRenderPass", &getCurrentRenderPassWrapper );
}
From 5f48121327c81190cc99123c71ceca8f2a16cc61 Mon Sep 17 00:00:00 2001
From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com>
Date: Mon, 9 Dec 2024 19:50:37 -0800
Subject: [PATCH 05/12] RenderPassEditor : Crop icons to 14 pixels
This will allow them to be reused elsewhere, such as in MenuButtons without causing them to become overly large.
---
python/GafferUI/_StyleSheet.py | 6 +++++
resources/graphics.py | 4 +--
resources/graphics.svg | 46 +++++++++++++++++-----------------
3 files changed, 31 insertions(+), 25 deletions(-)
diff --git a/python/GafferUI/_StyleSheet.py b/python/GafferUI/_StyleSheet.py
index fc8181e41a3..3387257c861 100644
--- a/python/GafferUI/_StyleSheet.py
+++ b/python/GafferUI/_StyleSheet.py
@@ -1307,6 +1307,12 @@ def styleColor( key ) :
padding-bottom: 0px;
}
+ *[gafferClass="GafferSceneUI.RenderPassEditor"] QTreeView::item {
+ min-height: 22px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ }
+
*[gafferClass="GafferSceneUI._HistoryWindow"] QTreeView::item {
height: 18px;
padding-top: 0px;
diff --git a/resources/graphics.py b/resources/graphics.py
index 00b6e58866c..0b7ace2f53c 100644
--- a/resources/graphics.py
+++ b/resources/graphics.py
@@ -463,8 +463,8 @@
"renderPassEditor" : {
"options" : {
- "requiredWidth" : 16,
- "requiredHeight" : 16,
+ "requiredWidth" : 14,
+ "requiredHeight" : 14,
"validatePixelAlignment" : True
},
diff --git a/resources/graphics.svg b/resources/graphics.svg
index 99f53b3c2c5..48d4a282b42 100644
--- a/resources/graphics.svg
+++ b/resources/graphics.svg
@@ -3555,41 +3555,41 @@
@@ -10189,10 +10189,10 @@
transform="translate(0,159.23177)">
Date: Mon, 9 Dec 2024 19:51:22 -0800
Subject: [PATCH 06/12] RenderPassEditor : Add RenderPassChooserWidget
---
Changes.md | 1 +
python/GafferSceneUI/RenderPassEditor.py | 281 +++++++++++++++++++++++
startup/gui/project.py | 5 +
startup/gui/renderPassEditor.py | 12 +
4 files changed, 299 insertions(+)
diff --git a/Changes.md b/Changes.md
index cb423f5bfc3..6457525e7b8 100644
--- a/Changes.md
+++ b/Changes.md
@@ -5,6 +5,7 @@ Features
--------
- PrimitiveVariableTweaks : Added node for tweaking primitive variables. Can affect just part of a primitive based on ids or a mask.
+- Menu Bar : Added a "Render Pass" menu to the Menu Bar that can be used to choose the current render pass from those provided by the focus node.
Improvements
------------
diff --git a/python/GafferSceneUI/RenderPassEditor.py b/python/GafferSceneUI/RenderPassEditor.py
index b931933d703..c2af53d1e2b 100644
--- a/python/GafferSceneUI/RenderPassEditor.py
+++ b/python/GafferSceneUI/RenderPassEditor.py
@@ -35,16 +35,21 @@
##########################################################################
import collections
+import functools
import imath
+import os
import traceback
import IECore
import Gaffer
import GafferUI
+import GafferImage
import GafferScene
import GafferSceneUI
+from GafferUI.PlugValueWidget import sole
+
from . import _GafferSceneUI
from Qt import QtWidgets
@@ -219,6 +224,22 @@ def pathGroupingFunction() :
return _GafferSceneUI._RenderPassEditor.RenderPassPath.pathGroupingFunction()
+ @staticmethod
+ def _createRenderAdaptors() :
+
+ adaptors = GafferScene.SceneProcessor()
+
+ adaptors["__renderAdaptors"] = GafferScene.SceneAlgo.createRenderAdaptors()
+ ## \todo We currently masquerade as the RenderPassWedge in order to include
+ # adaptors that disable render passes. We may want to find a more general
+ # client name for this usage...
+ adaptors["__renderAdaptors"]["client"].setValue( "RenderPassWedge" )
+ adaptors["__renderAdaptors"]["in"].setInput( adaptors["in"] )
+
+ adaptors["out"].setInput( adaptors["__renderAdaptors"]["out"] )
+
+ return adaptors
+
def __repr__( self ) :
return "GafferSceneUI.RenderPassEditor( scriptNode )"
@@ -891,3 +912,263 @@ def __combiner( results ) :
)
# Remove circular references that would keep the widget in limbo.
e.__traceback__ = None
+
+class RenderPassChooserWidget( GafferUI.Widget ) :
+
+ def __init__( self, settingsNode, **kw ) :
+
+ renderPassPlug = GafferSceneUI.ScriptNodeAlgo.acquireRenderPassPlug( settingsNode["__scriptNode"].getInput().node() )
+ self.__renderPassPlugValueWidget = _RenderPassPlugValueWidget(
+ renderPassPlug["value"],
+ showLabel = True
+ )
+ GafferUI.Widget.__init__( self, self.__renderPassPlugValueWidget, **kw )
+
+RenderPassEditor.RenderPassChooserWidget = RenderPassChooserWidget
+
+class _RenderPassPlugValueWidget( GafferUI.PlugValueWidget ) :
+
+ ## \todo We're cheekily reusing the Editor.Settings node here
+ # in order to take advantage of the existing hack allowing
+ # BackgroundTask to find the cancellation subject via the
+ # "__scriptNode" plug. This should be replaced with a cleaner
+ # way for BackgroundTask to recover the ScriptNode.
+ class Settings( GafferUI.Editor.Settings ) :
+
+ def __init__( self ) :
+
+ GafferUI.Editor.Settings.__init__( self )
+
+ self["in"] = GafferScene.ScenePlug()
+ self["__adaptors"] = GafferSceneUI.RenderPassEditor._createRenderAdaptors()
+ self["__adaptors"]["in"].setInput( self["in"] )
+
+ IECore.registerRunTimeTyped( Settings, typeName = "GafferSceneUI::RenderPassPlugValueWidget::Settings" )
+
+ def __init__( self, plug, showLabel = False, **kw ) :
+
+ self.__settings = self.Settings()
+ self.__settings.setName( "RenderPassPlugValueWidgetSettings" )
+ self.__settings["__scriptNode"].setInput( plug.node().scriptNode()["fileName"] )
+
+ self.__listContainer = GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 )
+
+ GafferUI.PlugValueWidget.__init__( self, self.__listContainer, plug, **kw )
+
+ with self.__listContainer :
+ if showLabel :
+ GafferUI.Label( "Render Pass" )
+ self.__busyWidget = GafferUI.BusyWidget( size = 18 )
+ self.__busyWidget.setVisible( False )
+ self.__menuButton = GafferUI.MenuButton(
+ "",
+ menu = GafferUI.Menu( Gaffer.WeakMethod( self.__menuDefinition ) ),
+ highlightOnOver = False
+ )
+ # Ignore the width in X so MenuButton width is limited by the overall width of the widget
+ self.__menuButton._qtWidget().setSizePolicy( QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Fixed )
+
+ self.__currentRenderPass = ""
+ self.__renderPasses = {}
+
+ self.__displayGrouped = False
+ self.__hideDisabled = False
+
+ self.__focusChangedConnection = plug.node().scriptNode().focusChangedSignal().connect(
+ Gaffer.WeakMethod( self.__focusChanged ), scoped = True
+ )
+
+ self.__updateSettingsInput()
+ self.__updateMenuButton()
+
+ def __del__( self ) :
+
+ # Remove connection to ScriptNode now, on the UI thread.
+ # See comment in `GafferUI.Editor.__del__()` for details.
+ self.__settings.plugDirtiedSignal().disconnectAllSlots()
+ self.__settings["__scriptNode"].setInput( None )
+
+ def getToolTip( self ) :
+
+ if self.__currentRenderPass == "" :
+ return "No render pass is active."
+
+ if self.__currentRenderPass not in self.__renderPasses.get( "all", [] ) :
+ return "{} is not provided by the focus node.".format( self.__currentRenderPass )
+ else :
+ return "{} is the current render pass.".format( self.__currentRenderPass )
+
+ def _auxiliaryPlugs( self, plug ) :
+
+ return [ self.__settings["__adaptors"]["out"]["globals"] ]
+
+ @staticmethod
+ def _valuesForUpdate( plugs, auxiliaryPlugs ) :
+
+ result = []
+
+ for plug, ( globalsPlug, ) in zip( plugs, auxiliaryPlugs ) :
+
+ renderPasses = {}
+
+ with Gaffer.Context( Gaffer.Context.current() ) as context :
+ for renderPass in globalsPlug.getValue().get( "option:renderPass:names", IECore.StringVectorData() ) :
+ renderPasses.setdefault( "all", [] ).append( renderPass )
+ context["renderPass"] = renderPass
+ if globalsPlug.getValue().get( "option:renderPass:enabled", IECore.BoolData( True ) ).value :
+ renderPasses.setdefault( "enabled", [] ).append( renderPass )
+
+ result.append( {
+ "value" : plug.getValue(),
+ "renderPasses" : renderPasses
+ } )
+
+ return result
+
+ def _updateFromValues( self, values, exception ) :
+
+ self.__currentRenderPass = sole( v["value"] for v in values )
+ self.__renderPasses = sole( v["renderPasses"] for v in values )
+
+ if self.__currentRenderPass is not None :
+ self.__busyWidget.setVisible( False )
+ self.__updateMenuButton()
+
+ def _updateFromEditable( self ) :
+
+ self.__menuButton.setEnabled( self._editable() )
+
+ def __setDisplayGrouped( self, grouped ) :
+
+ self.__displayGrouped = grouped
+
+ def __setHideDisabled( self, hide ) :
+
+ self.__hideDisabled = hide
+
+ def __menuDefinition( self ) :
+
+ result = IECore.MenuDefinition()
+
+ result.append( "/__RenderPassesDivider__", { "divider" : True, "label" : "Render Passes" } )
+
+ renderPasses = self.__renderPasses.get( "enabled", [] ) if self.__hideDisabled else self.__renderPasses.get( "all", [] )
+
+ if self.__renderPasses is None :
+ result.append( "/Refresh", { "command" : Gaffer.WeakMethod( self.__refreshMenu ) } )
+ elif len( renderPasses ) == 0 :
+ result.append( "/No Render Passes Available", { "active" : False } )
+ else :
+ groupingFn = GafferSceneUI.RenderPassEditor.pathGroupingFunction()
+ prefixes = IECore.PathMatcher()
+ if self.__displayGrouped :
+ for name in renderPasses :
+ prefixes.addPath( groupingFn( name ) )
+
+ for name in sorted( renderPasses ) :
+
+ prefix = "/"
+ if self.__displayGrouped :
+ if prefixes.match( name ) & IECore.PathMatcher.Result.ExactMatch :
+ prefix += name
+ else :
+ prefix = groupingFn( name )
+
+ result.append(
+ os.path.join( prefix, name ),
+ {
+ "command" : functools.partial( Gaffer.WeakMethod( self.__setCurrentRenderPass ), name ),
+ "icon" : self.__renderPassIcon( name, activeIndicator = True ),
+ }
+ )
+
+ result.append( "/__NoneDivider__", { "divider" : True } )
+
+ result.append(
+ "/None",
+ {
+ "command" : functools.partial( Gaffer.WeakMethod( self.__setCurrentRenderPass ), "" ),
+ "icon" : "activeRenderPass.png" if self.__currentRenderPass == "" else None,
+ }
+ )
+
+ result.append( "/__OptionsDivider__", { "divider" : True, "label" : "Options" } )
+
+ result.append(
+ "/Display Grouped",
+ {
+ "checkBox" : self.__displayGrouped,
+ "command" : functools.partial( Gaffer.WeakMethod( self.__setDisplayGrouped ) ),
+ "description" : "Toggle grouped display of render passes."
+ }
+ )
+
+ result.append(
+ "/Hide Disabled",
+ {
+ "checkBox" : self.__hideDisabled,
+ "command" : functools.partial( Gaffer.WeakMethod( self.__setHideDisabled ) ),
+ "description" : "Hide render passes disabled for rendering."
+ }
+ )
+
+ return result
+
+ def __refreshMenu( self ) :
+
+ self.__busyWidget.setVisible( True )
+
+ def __setCurrentRenderPass( self, renderPass, *unused ) :
+
+ for plug in self.getPlugs() :
+ plug.setValue( renderPass )
+
+ def __renderPassIcon( self, renderPass, activeIndicator = False ) :
+
+ if renderPass == "" :
+ return None
+
+ if activeIndicator and renderPass == self.__currentRenderPass :
+ return "activeRenderPass.png"
+ elif renderPass not in self.__renderPasses.get( "all", [] ) :
+ return "warningSmall.png"
+ elif renderPass in self.__renderPasses.get( "enabled", [] ) :
+ return "renderPass.png"
+ else :
+ return "disabledRenderPass.png"
+
+ def __updateMenuButton( self ) :
+
+ self.__menuButton.setText( self.__currentRenderPass or "None" )
+ self.__menuButton.setImage( self.__renderPassIcon( self.__currentRenderPass ) )
+
+ def __focusChanged( self, scriptNode, node ) :
+
+ self.__updateSettingsInput()
+
+ def __updateSettingsInput( self ) :
+
+ self.__settings["in"].setInput( self.__scenePlugFromFocus() )
+
+ def __scenePlugFromFocus( self ) :
+
+ focusNode = self.getPlug().node().scriptNode().getFocus()
+
+ if focusNode is not None :
+ outputScene = next(
+ ( p for p in GafferScene.ScenePlug.RecursiveOutputRange( focusNode ) if not p.getName().startswith( "__" ) ),
+ None
+ )
+ if outputScene is not None :
+ return outputScene
+
+ outputImage = next(
+ ( p for p in GafferImage.ImagePlug.RecursiveOutputRange( focusNode ) if not p.getName().startswith( "__" ) ),
+ None
+ )
+ if outputImage is not None :
+ return GafferScene.SceneAlgo.sourceScene( outputImage )
+
+ return None
+
+RenderPassEditor._RenderPassPlugValueWidget = _RenderPassPlugValueWidget
diff --git a/startup/gui/project.py b/startup/gui/project.py
index da3f6e00987..dac015353e5 100644
--- a/startup/gui/project.py
+++ b/startup/gui/project.py
@@ -45,6 +45,8 @@
import GafferDispatch
import GafferTractor
+import GafferSceneUI
+
##########################################################################
# Note this file is shared with the `dispatch` app. We need to ensure any
# changes here have the desired behaviour in both applications.
@@ -67,6 +69,9 @@ def __scriptAdded( container, script ) :
GafferImage.FormatPlug.acquireDefaultFormatPlug( script )
+ renderPassPlug = GafferSceneUI.ScriptNodeAlgo.acquireRenderPassPlug( script )
+ Gaffer.Metadata.registerValue( renderPassPlug["value"], "plugValueWidget:type", "GafferSceneUI.RenderPassEditor._RenderPassPlugValueWidget" )
+
application.root()["scripts"].childAddedSignal().connect( __scriptAdded )
##########################################################################
diff --git a/startup/gui/renderPassEditor.py b/startup/gui/renderPassEditor.py
index ea2278851bb..97032493e69 100644
--- a/startup/gui/renderPassEditor.py
+++ b/startup/gui/renderPassEditor.py
@@ -38,6 +38,7 @@
import IECore
import Gaffer
+import GafferUI
import GafferSceneUI
GafferSceneUI.RenderPassEditor.registerOption( "*", "renderPass:enabled" )
@@ -130,3 +131,14 @@ def __defaultPathGroupingFunction( renderPassName ) :
return renderPassName.split( "_" )[0] if "_" in renderPassName else ""
GafferSceneUI.RenderPassEditor.registerPathGroupingFunction( __defaultPathGroupingFunction )
+
+def __compoundEditorCreated( editor ) :
+
+ if editor.scriptNode().ancestor( Gaffer.ApplicationRoot ).getName() == "gui" :
+
+ Gaffer.Metadata.registerValue( editor.settings(), "layout:customWidget:renderPassSelector:widgetType", "GafferSceneUI.RenderPassEditor.RenderPassChooserWidget" )
+ Gaffer.Metadata.registerValue( editor.settings(), "layout:customWidget:renderPassSelector:section", "Settings" )
+ Gaffer.Metadata.registerValue( editor.settings(), "layout:customWidget:renderPassSelector:index", 0 )
+ Gaffer.Metadata.registerValue( editor.settings(), "layout:customWidget:renderPassSelector:width", 185 )
+
+GafferUI.CompoundEditor.instanceCreatedSignal().connect( __compoundEditorCreated )
From 45359633a7cad48a76a311b333de93dd671cb7b7 Mon Sep 17 00:00:00 2001
From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com>
Date: Mon, 9 Dec 2024 19:51:52 -0800
Subject: [PATCH 07/12] _StyleSheet : Darken MenuButtons added to main MenuBar
Our default style stands out as a bit bright against the dark MenuBar background.
While only necessary for the RenderPassChooserWidget, this has been applied generally so any future MenuButtons also get the same treatment.
---
python/GafferUI/_StyleSheet.py | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/python/GafferUI/_StyleSheet.py b/python/GafferUI/_StyleSheet.py
index 3387257c861..deab75e131a 100644
--- a/python/GafferUI/_StyleSheet.py
+++ b/python/GafferUI/_StyleSheet.py
@@ -1538,7 +1538,18 @@ def styleColor( key ) :
padding: 2px;
}
- *[gafferClass="GafferUI.EditScopeUI.EditScopePlugValueWidget"] QPushButton[gafferWithFrame="true"][gafferMenuIndicator="true"]
+ #gafferMenuBarWidgetContainer QPushButton[gafferWithFrame="true"][gafferMenuIndicator="true"]
+ {
+ border: 1px solid rgb( 70, 70, 70 );
+ border-top-color: rgb( 108, 108, 108 );
+ border-left-color: rgb( 108, 108, 108 );
+ background-color : qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgb( 108, 108, 108 ), stop: 0.1 rgb( 91, 91, 91 ), stop: 0.90 rgb( 81, 81, 81 ));
+ margin-top: 2px;
+ margin-bottom: 2px;
+ }
+
+ *[gafferClass="GafferUI.EditScopeUI.EditScopePlugValueWidget"] QPushButton[gafferWithFrame="true"][gafferMenuIndicator="true"],
+ #gafferMenuBarWidgetContainer *[gafferClass="GafferUI.EditScopeUI.EditScopePlugValueWidget"] QPushButton[gafferWithFrame="true"][gafferMenuIndicator="true"]
{
border: 1px solid rgb( 46, 75, 107 );
border-top-color: rgb( 75, 113, 155 );
From 6fb82490eb31c97d666631d0d825d1b758e7f3f2 Mon Sep 17 00:00:00 2001
From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com>
Date: Mon, 9 Dec 2024 19:52:19 -0800
Subject: [PATCH 08/12] RenderPassEditor : Display adaptor disabled render
passes
Pipelines can register render adaptors to `client = "RenderPassWedge"` that
disable render passes. We want to display the state of these to the user
and distinguish them from regular user-disabled render passes.
---
Changes.md | 1 +
python/GafferSceneUI/RenderPassEditor.py | 42 ++++++++++++++++++-
.../GafferSceneUITest/RenderPassEditorTest.py | 41 ++++++++++++++++++
resources/graphics.py | 1 +
resources/graphics.svg | 34 +++++++++++++++
.../RenderPassEditorBinding.cpp | 34 +++++++++++++--
6 files changed, 148 insertions(+), 5 deletions(-)
diff --git a/Changes.md b/Changes.md
index 6457525e7b8..65035a91f1b 100644
--- a/Changes.md
+++ b/Changes.md
@@ -21,6 +21,7 @@ Improvements
- PlugLayout :
- A warning widget is now displayed when an invalid custom widget is registered.
- `layout:customWidget::width` and `layout:customWidget::minimumWidth` metadata registrations are now supported for custom widgets.
+- RenderPassEditor : Render passes disabled by render adaptors registered to `client = "RenderPassWedge"` are now shown as disabled. To differentiate these from user disabled render passes, an orange dot is shown in the corner of the disabled icon and the tooltip describes them as automatically disabled.
Fixes
-----
diff --git a/python/GafferSceneUI/RenderPassEditor.py b/python/GafferSceneUI/RenderPassEditor.py
index c2af53d1e2b..bcf8e314a5c 100644
--- a/python/GafferSceneUI/RenderPassEditor.py
+++ b/python/GafferSceneUI/RenderPassEditor.py
@@ -67,6 +67,12 @@ def __init__( self ) :
self["editScope"] = Gaffer.Plug()
self["displayGrouped"] = Gaffer.BoolPlug()
+ self["__adaptors"] = GafferSceneUI.RenderPassEditor._createRenderAdaptors()
+ self["__adaptors"]["in"].setInput( self["in"] )
+
+ self["__adaptedIn"] = GafferScene.ScenePlug()
+ self["__adaptedIn"].setInput( self["__adaptors"]["out"] )
+
IECore.registerRunTimeTyped( Settings, typeName = "GafferSceneUI::RenderPassEditor::Settings" )
def __init__( self, scriptNode, **kw ) :
@@ -236,7 +242,19 @@ def _createRenderAdaptors() :
adaptors["__renderAdaptors"]["client"].setValue( "RenderPassWedge" )
adaptors["__renderAdaptors"]["in"].setInput( adaptors["in"] )
- adaptors["out"].setInput( adaptors["__renderAdaptors"]["out"] )
+ adaptors["__adaptorSwitch"] = Gaffer.Switch()
+ adaptors["__adaptorSwitch"].setup( GafferScene.ScenePlug() )
+ adaptors["__adaptorSwitch"]["in"]["in0"].setInput( adaptors["__renderAdaptors"]["out"] )
+ adaptors["__adaptorSwitch"]["in"]["in1"].setInput( adaptors["in"] )
+
+ adaptors["__contextQuery"] = Gaffer.ContextQuery()
+ adaptors["__contextQuery"].addQuery( Gaffer.BoolPlug( "disableAdaptors", defaultValue = False ) )
+ adaptors["__contextQuery"]["queries"][0]["name"].setValue( "renderPassEditor:disableAdaptors" )
+
+ adaptors["__adaptorSwitch"]["index"].setInput( adaptors["__contextQuery"]["out"][0]["value"] )
+ adaptors["__adaptorSwitch"]["deleteContextVariables"].setValue( "renderPassEditor:disableAdaptors" )
+
+ adaptors["out"].setInput( adaptors["__adaptorSwitch"]["out"] )
return adaptors
@@ -281,7 +299,7 @@ def __setPathListingPath( self ) :
# control of updates ourselves in _updateFromContext(), using LazyMethod to defer the calls to this
# function until we are visible and playback has stopped.
contextCopy = Gaffer.Context( self.context() )
- self.__pathListing.setPath( _GafferSceneUI._RenderPassEditor.RenderPassPath( self.settings()["in"], contextCopy, "/", filter = self.__filter, grouped = self.settings()["displayGrouped"].getValue() ) )
+ self.__pathListing.setPath( _GafferSceneUI._RenderPassEditor.RenderPassPath( self.settings()["__adaptedIn"], contextCopy, "/", filter = self.__filter, grouped = self.settings()["displayGrouped"].getValue() ) )
def __displayGroupedChanged( self ) :
@@ -1015,8 +1033,13 @@ def _valuesForUpdate( plugs, auxiliaryPlugs ) :
for renderPass in globalsPlug.getValue().get( "option:renderPass:names", IECore.StringVectorData() ) :
renderPasses.setdefault( "all", [] ).append( renderPass )
context["renderPass"] = renderPass
+ context["renderPassEditor:disableAdaptors"] = False
if globalsPlug.getValue().get( "option:renderPass:enabled", IECore.BoolData( True ) ).value :
renderPasses.setdefault( "enabled", [] ).append( renderPass )
+ else :
+ context["renderPassEditor:disableAdaptors"] = True
+ if globalsPlug.getValue().get( "option:renderPass:enabled", IECore.BoolData( True ) ).value :
+ renderPasses.setdefault( "adaptorDisabled", [] ).append( renderPass )
result.append( {
"value" : plug.getValue(),
@@ -1079,6 +1102,7 @@ def __menuDefinition( self ) :
{
"command" : functools.partial( Gaffer.WeakMethod( self.__setCurrentRenderPass ), name ),
"icon" : self.__renderPassIcon( name, activeIndicator = True ),
+ "description" : self.__renderPassDescription( name )
}
)
@@ -1123,6 +1147,18 @@ def __setCurrentRenderPass( self, renderPass, *unused ) :
for plug in self.getPlugs() :
plug.setValue( renderPass )
+ def __renderPassDescription( self, renderPass ) :
+
+ if renderPass == "" :
+ return ""
+
+ if renderPass in self.__renderPasses.get( "adaptorDisabled", [] ) :
+ return "{} has been automatically disabled by a render adaptor.".format( renderPass )
+ elif renderPass not in self.__renderPasses.get( "enabled", [] ) :
+ return "{} has been disabled.".format( renderPass )
+
+ return ""
+
def __renderPassIcon( self, renderPass, activeIndicator = False ) :
if renderPass == "" :
@@ -1134,6 +1170,8 @@ def __renderPassIcon( self, renderPass, activeIndicator = False ) :
return "warningSmall.png"
elif renderPass in self.__renderPasses.get( "enabled", [] ) :
return "renderPass.png"
+ elif renderPass in self.__renderPasses.get( "adaptorDisabled", [] ) :
+ return "adaptorDisabledRenderPass.png"
else :
return "disabledRenderPass.png"
diff --git a/python/GafferSceneUITest/RenderPassEditorTest.py b/python/GafferSceneUITest/RenderPassEditorTest.py
index 430b2956bf6..c72f7981bdf 100644
--- a/python/GafferSceneUITest/RenderPassEditorTest.py
+++ b/python/GafferSceneUITest/RenderPassEditorTest.py
@@ -167,6 +167,47 @@ def testFn( name ) :
else :
self.assertIsNone( inspectionContext )
+ def testRenderPassPathAdaptorDisablingPasses( self ) :
+
+ def createAdaptor() :
+
+ node = GafferScene.SceneProcessor()
+ node["options"] = GafferScene.CustomOptions()
+ node["options"]["in"].setInput( node["in"] )
+ node["options"]["options"].addChild( Gaffer.NameValuePlug( "renderPass:enabled", False ) )
+
+ node["switch"] = Gaffer.NameSwitch()
+ node["switch"].setup( node["options"]["out"] )
+ node["switch"]["in"][0]["value"].setInput( node["in"] )
+ node["switch"]["in"][1]["value"].setInput( node["options"]["out"] )
+ node["switch"]["in"][1]["name"].setValue( "B C" )
+ node["switch"]["selector"].setValue( "${renderPass}" )
+
+ node["out"].setInput( node["switch"]["out"]["value"] )
+
+ return node
+
+ GafferScene.SceneAlgo.registerRenderAdaptor( "RenderPassEditorTest", createAdaptor, client = "RenderPassWedge" )
+ self.addCleanup( GafferScene.SceneAlgo.deregisterRenderAdaptor, "RenderPassEditorTest" )
+
+ renderPasses = GafferScene.RenderPasses()
+ renderPasses["names"].setValue( IECore.StringVectorData( [ "A", "B", "C", "D" ] ) )
+
+ adaptors = GafferSceneUI.RenderPassEditor._createRenderAdaptors()
+ adaptors["in"].setInput( renderPasses["out"] )
+
+ context = Gaffer.Context()
+ path = _GafferSceneUI._RenderPassEditor.RenderPassPath( adaptors["out"], context, "/" )
+
+ self.assertEqual( [ str( c ) for c in path.children() ], [ "/A", "/B", "/C", "/D" ] )
+
+ pathCopy = path.copy()
+
+ for p in [ "/A", "/B", "/C", "/D" ] :
+ pathCopy.setFromString( p )
+ self.assertEqual( pathCopy.property( "renderPassPath:enabled" ), p in ( "/A", "/D" ) )
+ self.assertTrue( pathCopy.property( "renderPassPath:enabledWithoutAdaptors" ) )
+
def testSearchFilter( self ) :
renderPasses = GafferScene.RenderPasses()
diff --git a/resources/graphics.py b/resources/graphics.py
index 0b7ace2f53c..451924ee0e0 100644
--- a/resources/graphics.py
+++ b/resources/graphics.py
@@ -471,6 +471,7 @@
"ids" : [
"renderPass",
"disabledRenderPass",
+ "adaptorDisabledRenderPass",
"renderPassFolder",
"activeRenderPass",
"activeRenderPassFadedHighlighted",
diff --git a/resources/graphics.svg b/resources/graphics.svg
index 48d4a282b42..85fddf966e1 100644
--- a/resources/graphics.svg
+++ b/resources/graphics.svg
@@ -3592,6 +3592,14 @@
width="14"
style="display:inline;opacity:0.1;fill:none;fill-opacity:1;stroke:none;stroke-width:0.597614;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.10173;stroke-opacity:1;paint-order:markers stroke fill"
inkscape:label="activeRenderPassFadedHighlightedIcon" />
+
+
+
+
+
+
( g_disableAdaptorsContextName, &disableAdaptors );
+ }
if( canceller )
{
scopedContext.setCanceller( canceller );
@@ -452,9 +460,12 @@ RenderPassPath::Ptr constructor2( ScenePlug &scene, Context &context, const std:
// RenderPassNameColumn
//////////////////////////////////////////////////////////////////////////
+ConstStringDataPtr g_adaptorDisabledRenderPassIcon = new StringData( "adaptorDisabledRenderPass.png" );
ConstStringDataPtr g_disabledRenderPassIcon = new StringData( "disabledRenderPass.png" );
ConstStringDataPtr g_renderPassIcon = new StringData( "renderPass.png" );
ConstStringDataPtr g_renderPassFolderIcon = new StringData( "renderPassFolder.png" );
+ConstStringDataPtr g_disabledToolTip = new StringData( "Disabled." );
+ConstStringDataPtr g_adaptorDisabledToolTip = new StringData( "Automatically disabled by a render adaptor.");
const Color4fDataPtr g_dimmedForegroundColor = new Color4fData( Imath::Color4f( 152, 152, 152, 255 ) / 255.0f );
class RenderPassNameColumn : public StandardPathColumn
@@ -482,8 +493,25 @@ class RenderPassNameColumn : public StandardPathColumn
{
if( const auto renderPassEnabled = runTimeCast( path.property( g_renderPassEnabledPropertyName, canceller ) ) )
{
- result.icon = renderPassEnabled->readable() ? g_renderPassIcon : g_disabledRenderPassIcon;
- result.foreground = renderPassEnabled->readable() ? nullptr : g_dimmedForegroundColor;
+ if( renderPassEnabled->readable() )
+ {
+ result.icon = g_renderPassIcon;
+ }
+ else
+ {
+ result.foreground = g_dimmedForegroundColor;
+ const auto renderPassEnabledWithoutAdaptors = runTimeCast( path.property( g_renderPassEnabledWithoutAdaptorsPropertyName, canceller ) );
+ if( !renderPassEnabledWithoutAdaptors || !renderPassEnabledWithoutAdaptors->readable() )
+ {
+ result.icon = g_disabledRenderPassIcon;
+ result.toolTip = g_disabledToolTip;
+ }
+ else
+ {
+ result.icon = g_adaptorDisabledRenderPassIcon;
+ result.toolTip = g_adaptorDisabledToolTip;
+ }
+ }
}
else
{
From e1a03659c822d577bf084e47c0414631af51395c Mon Sep 17 00:00:00 2001
From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com>
Date: Mon, 9 Dec 2024 19:53:05 -0800
Subject: [PATCH 09/12] RenderPassEditor : Display adaptor deleted render
passes as disabled
As render passes could have been deleted by a render adaptor, we build our
RenderPassPaths from a PathMatcher generated with the render adaptors disabled
and then later test to see whether the render pass still exists with adaptors enabled.
From the end user's perspective, there's no functional difference between render
passes deleted or disabled by a render adaptor, so we present both as automatically
disabled.
---
Changes.md | 2 +-
python/GafferSceneUI/RenderPassEditor.py | 7 ++++-
.../GafferSceneUITest/RenderPassEditorTest.py | 29 +++++++++++++++++++
.../RenderPassEditorBinding.cpp | 17 +++++++++--
4 files changed, 51 insertions(+), 4 deletions(-)
diff --git a/Changes.md b/Changes.md
index 65035a91f1b..e800170ba1d 100644
--- a/Changes.md
+++ b/Changes.md
@@ -21,7 +21,7 @@ Improvements
- PlugLayout :
- A warning widget is now displayed when an invalid custom widget is registered.
- `layout:customWidget::width` and `layout:customWidget::minimumWidth` metadata registrations are now supported for custom widgets.
-- RenderPassEditor : Render passes disabled by render adaptors registered to `client = "RenderPassWedge"` are now shown as disabled. To differentiate these from user disabled render passes, an orange dot is shown in the corner of the disabled icon and the tooltip describes them as automatically disabled.
+- RenderPassEditor : Render passes deleted or disabled by render adaptors registered to `client = "RenderPassWedge"` are now shown as disabled. To differentiate these from user disabled render passes, an orange dot is shown in the corner of the disabled icon and the tooltip describes them as automatically disabled.
Fixes
-----
diff --git a/python/GafferSceneUI/RenderPassEditor.py b/python/GafferSceneUI/RenderPassEditor.py
index bcf8e314a5c..deb73d0990b 100644
--- a/python/GafferSceneUI/RenderPassEditor.py
+++ b/python/GafferSceneUI/RenderPassEditor.py
@@ -1030,11 +1030,16 @@ def _valuesForUpdate( plugs, auxiliaryPlugs ) :
renderPasses = {}
with Gaffer.Context( Gaffer.Context.current() ) as context :
+ adaptedRenderPassNames = globalsPlug.getValue().get( "option:renderPass:names", IECore.StringVectorData() )
+ context["renderPassEditor:disableAdaptors"] = True
for renderPass in globalsPlug.getValue().get( "option:renderPass:names", IECore.StringVectorData() ) :
renderPasses.setdefault( "all", [] ).append( renderPass )
context["renderPass"] = renderPass
context["renderPassEditor:disableAdaptors"] = False
- if globalsPlug.getValue().get( "option:renderPass:enabled", IECore.BoolData( True ) ).value :
+ if renderPass not in adaptedRenderPassNames :
+ # The render pass has been deleted by a render adaptor so present it as disabled
+ renderPasses.setdefault( "adaptorDisabled", [] ).append( renderPass )
+ elif globalsPlug.getValue().get( "option:renderPass:enabled", IECore.BoolData( True ) ).value :
renderPasses.setdefault( "enabled", [] ).append( renderPass )
else :
context["renderPassEditor:disableAdaptors"] = True
diff --git a/python/GafferSceneUITest/RenderPassEditorTest.py b/python/GafferSceneUITest/RenderPassEditorTest.py
index c72f7981bdf..3954c4b4c4d 100644
--- a/python/GafferSceneUITest/RenderPassEditorTest.py
+++ b/python/GafferSceneUITest/RenderPassEditorTest.py
@@ -208,6 +208,35 @@ def createAdaptor() :
self.assertEqual( pathCopy.property( "renderPassPath:enabled" ), p in ( "/A", "/D" ) )
self.assertTrue( pathCopy.property( "renderPassPath:enabledWithoutAdaptors" ) )
+ def testRenderPassPathAdaptorDeletingPasses( self ) :
+
+ def createAdaptor() :
+
+ node = GafferScene.DeleteRenderPasses()
+ node["names"].setValue( "B C" )
+ return node
+
+ GafferScene.SceneAlgo.registerRenderAdaptor( "RenderPassEditorTest", createAdaptor, client = "RenderPassWedge" )
+ self.addCleanup( GafferScene.SceneAlgo.deregisterRenderAdaptor, "RenderPassEditorTest" )
+
+ renderPasses = GafferScene.RenderPasses()
+ renderPasses["names"].setValue( IECore.StringVectorData( [ "A", "B", "C", "D" ] ) )
+
+ adaptors = GafferSceneUI.RenderPassEditor._createRenderAdaptors()
+ adaptors["in"].setInput( renderPasses["out"] )
+
+ context = Gaffer.Context()
+ path = _GafferSceneUI._RenderPassEditor.RenderPassPath( adaptors["out"], context, "/" )
+
+ self.assertEqual( [ str( c ) for c in path.children() ], [ "/A", "/B", "/C", "/D" ] )
+
+ pathCopy = path.copy()
+
+ for p in [ "/A", "/B", "/C", "/D" ] :
+ pathCopy.setFromString( p )
+ self.assertEqual( pathCopy.property( "renderPassPath:enabled" ), p in ( "/A", "/D" ) )
+ self.assertTrue( pathCopy.property( "renderPassPath:enabledWithoutAdaptors" ) )
+
def testSearchFilter( self ) :
renderPasses = GafferScene.RenderPasses()
diff --git a/src/GafferSceneUIModule/RenderPassEditorBinding.cpp b/src/GafferSceneUIModule/RenderPassEditorBinding.cpp
index 859ab499faf..c315b2d8022 100644
--- a/src/GafferSceneUIModule/RenderPassEditorBinding.cpp
+++ b/src/GafferSceneUIModule/RenderPassEditorBinding.cpp
@@ -192,7 +192,7 @@ PathMatcher pathMatcherCacheGetter( const PathMatcherCacheGetterKey &key, size_t
}
using PathMatcherCache = IECorePreview::LRUCache;
-PathMatcherCache g_pathMatcherCache( pathMatcherCacheGetter, 25 );
+PathMatcherCache g_pathMatcherCache( pathMatcherCacheGetter, 50 );
const InternedString g_renderPassContextName( "renderPass" );
const InternedString g_disableAdaptorsContextName( "renderPassEditor:disableAdaptors" );
@@ -320,6 +320,15 @@ class RenderPassPath : public Gaffer::Path
}
else if( name == g_renderPassEnabledPropertyName || name == g_renderPassEnabledWithoutAdaptorsPropertyName )
{
+ if(
+ name == g_renderPassEnabledPropertyName &&
+ !( pathMatcher( canceller, /* disableAdaptors = */ false ).match( names() ) & PathMatcher::ExactMatch )
+ )
+ {
+ // The render pass has been deleted by a render adaptor, so present it to the user as disabled.
+ return new BoolData( false );
+ }
+
const PathMatcher p = pathMatcher( canceller );
if( p.match( names() ) & PathMatcher::ExactMatch )
{
@@ -402,7 +411,7 @@ class RenderPassPath : public Gaffer::Path
// practical as render pass names are used in output file paths where the included '/' characters would be
// interpreted as subdirectories. Validation in the UI will prevent users from inserting invalid characters
// such as '/' into render pass names.
- const IECore::PathMatcher pathMatcher( const IECore::Canceller *canceller ) const
+ const IECore::PathMatcher pathMatcher( const IECore::Canceller *canceller, bool disableAdaptors = true ) const
{
Context::EditableScope scopedContext( m_context.get() );
if( canceller )
@@ -410,6 +419,10 @@ class RenderPassPath : public Gaffer::Path
scopedContext.setCanceller( canceller );
}
+ if( disableAdaptors )
+ {
+ scopedContext.set( g_disableAdaptorsContextName, &disableAdaptors );
+ }
if( ConstStringVectorDataPtr renderPassData = m_scene.get()->globals()->member( g_renderPassNamesOption ) )
{
const PathMatcherCacheGetterKey key( renderPassData, m_grouped );
From a2072a74fcf2d21de6146eddcdbf9593deabc54f Mon Sep 17 00:00:00 2001
From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com>
Date: Wed, 15 Jan 2025 17:59:45 -0800
Subject: [PATCH 10/12] _StyleSheet : Set minimum height of Menu Bar
MenuButtons
We often use 14px high icons in these, but some standard icons such as warningSmall.png are only 12px high. This can result in a difference in height when switching icons on some desktop environments, so we set a min height of 14px to ensure consistency when displaying shorter icons.
We also apply this to the EditScopePlugValueWidget and _RenderPassPlugValueWidget to maintain the same height consistency when they are used in other parts of the UI.
---
python/GafferUI/_StyleSheet.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/python/GafferUI/_StyleSheet.py b/python/GafferUI/_StyleSheet.py
index deab75e131a..377a7f688c4 100644
--- a/python/GafferUI/_StyleSheet.py
+++ b/python/GafferUI/_StyleSheet.py
@@ -1538,6 +1538,11 @@ def styleColor( key ) :
padding: 2px;
}
+ *[gafferClass="GafferSceneUI.RenderPassEditor._RenderPassPlugValueWidget"] QPushButton[gafferWithFrame="true"][gafferMenuIndicator="true"]
+ {
+ min-height: 14px;
+ }
+
#gafferMenuBarWidgetContainer QPushButton[gafferWithFrame="true"][gafferMenuIndicator="true"]
{
border: 1px solid rgb( 70, 70, 70 );
@@ -1546,6 +1551,7 @@ def styleColor( key ) :
background-color : qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgb( 108, 108, 108 ), stop: 0.1 rgb( 91, 91, 91 ), stop: 0.90 rgb( 81, 81, 81 ));
margin-top: 2px;
margin-bottom: 2px;
+ min-height: 14px;
}
*[gafferClass="GafferUI.EditScopeUI.EditScopePlugValueWidget"] QPushButton[gafferWithFrame="true"][gafferMenuIndicator="true"],
@@ -1557,6 +1563,7 @@ def styleColor( key ) :
background-color : qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgb( 69, 113, 161 ), stop: 0.1 rgb( 48, 99, 153 ), stop: 0.90 rgb( 54, 88, 125 ));
margin-top: 2px;
margin-bottom: 2px;
+ min-height: 14px;
}
*[gafferClass="GafferSceneUI.InteractiveRenderUI._ViewRenderControlUI"] QPushButton[gafferWithFrame="true"] {
From 91ffd8b92e8393cae32d8059fbc6f173dade337b Mon Sep 17 00:00:00 2001
From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com>
Date: Wed, 15 Jan 2025 18:00:50 -0800
Subject: [PATCH 11/12] RenderPassEditor : Enable render adaptors rather than
disable
---
python/GafferSceneUI/RenderPassEditor.py | 17 +--
.../GafferSceneUITest/RenderPassEditorTest.py | 66 ++++++++++-
.../RenderPassEditorBinding.cpp | 107 +++++++++---------
3 files changed, 124 insertions(+), 66 deletions(-)
diff --git a/python/GafferSceneUI/RenderPassEditor.py b/python/GafferSceneUI/RenderPassEditor.py
index deb73d0990b..4f102b883ed 100644
--- a/python/GafferSceneUI/RenderPassEditor.py
+++ b/python/GafferSceneUI/RenderPassEditor.py
@@ -244,15 +244,15 @@ def _createRenderAdaptors() :
adaptors["__adaptorSwitch"] = Gaffer.Switch()
adaptors["__adaptorSwitch"].setup( GafferScene.ScenePlug() )
- adaptors["__adaptorSwitch"]["in"]["in0"].setInput( adaptors["__renderAdaptors"]["out"] )
- adaptors["__adaptorSwitch"]["in"]["in1"].setInput( adaptors["in"] )
+ adaptors["__adaptorSwitch"]["in"]["in0"].setInput( adaptors["in"] )
+ adaptors["__adaptorSwitch"]["in"]["in1"].setInput( adaptors["__renderAdaptors"]["out"] )
adaptors["__contextQuery"] = Gaffer.ContextQuery()
- adaptors["__contextQuery"].addQuery( Gaffer.BoolPlug( "disableAdaptors", defaultValue = False ) )
- adaptors["__contextQuery"]["queries"][0]["name"].setValue( "renderPassEditor:disableAdaptors" )
+ adaptors["__contextQuery"].addQuery( Gaffer.BoolPlug( "enableAdaptors", defaultValue = False ) )
+ adaptors["__contextQuery"]["queries"][0]["name"].setValue( "renderPassEditor:enableAdaptors" )
adaptors["__adaptorSwitch"]["index"].setInput( adaptors["__contextQuery"]["out"][0]["value"] )
- adaptors["__adaptorSwitch"]["deleteContextVariables"].setValue( "renderPassEditor:disableAdaptors" )
+ adaptors["__adaptorSwitch"]["deleteContextVariables"].setValue( "renderPassEditor:enableAdaptors" )
adaptors["out"].setInput( adaptors["__adaptorSwitch"]["out"] )
@@ -1030,19 +1030,20 @@ def _valuesForUpdate( plugs, auxiliaryPlugs ) :
renderPasses = {}
with Gaffer.Context( Gaffer.Context.current() ) as context :
+ context["renderPassEditor:enableAdaptors"] = True
adaptedRenderPassNames = globalsPlug.getValue().get( "option:renderPass:names", IECore.StringVectorData() )
- context["renderPassEditor:disableAdaptors"] = True
+ context["renderPassEditor:enableAdaptors"] = False
for renderPass in globalsPlug.getValue().get( "option:renderPass:names", IECore.StringVectorData() ) :
renderPasses.setdefault( "all", [] ).append( renderPass )
context["renderPass"] = renderPass
- context["renderPassEditor:disableAdaptors"] = False
+ context["renderPassEditor:enableAdaptors"] = True
if renderPass not in adaptedRenderPassNames :
# The render pass has been deleted by a render adaptor so present it as disabled
renderPasses.setdefault( "adaptorDisabled", [] ).append( renderPass )
elif globalsPlug.getValue().get( "option:renderPass:enabled", IECore.BoolData( True ) ).value :
renderPasses.setdefault( "enabled", [] ).append( renderPass )
else :
- context["renderPassEditor:disableAdaptors"] = True
+ context["renderPassEditor:enableAdaptors"] = False
if globalsPlug.getValue().get( "option:renderPass:enabled", IECore.BoolData( True ) ).value :
renderPasses.setdefault( "adaptorDisabled", [] ).append( renderPass )
diff --git a/python/GafferSceneUITest/RenderPassEditorTest.py b/python/GafferSceneUITest/RenderPassEditorTest.py
index 3954c4b4c4d..83c7e1d97e8 100644
--- a/python/GafferSceneUITest/RenderPassEditorTest.py
+++ b/python/GafferSceneUITest/RenderPassEditorTest.py
@@ -202,11 +202,17 @@ def createAdaptor() :
self.assertEqual( [ str( c ) for c in path.children() ], [ "/A", "/B", "/C", "/D" ] )
pathCopy = path.copy()
+ c = _GafferSceneUI._RenderPassEditor.RenderPassNameColumn()
for p in [ "/A", "/B", "/C", "/D" ] :
pathCopy.setFromString( p )
- self.assertEqual( pathCopy.property( "renderPassPath:enabled" ), p in ( "/A", "/D" ) )
- self.assertTrue( pathCopy.property( "renderPassPath:enabledWithoutAdaptors" ) )
+ cellData = c.cellData( pathCopy, None )
+ if p in ( "/A", "/D" ) :
+ self.assertEqual( cellData.icon, "renderPass.png" )
+ self.assertEqual( cellData.toolTip, None )
+ else :
+ self.assertEqual( cellData.icon, "adaptorDisabledRenderPass.png" )
+ self.assertEqual( cellData.toolTip, "Automatically disabled by a render adaptor." )
def testRenderPassPathAdaptorDeletingPasses( self ) :
@@ -231,11 +237,17 @@ def createAdaptor() :
self.assertEqual( [ str( c ) for c in path.children() ], [ "/A", "/B", "/C", "/D" ] )
pathCopy = path.copy()
+ c = _GafferSceneUI._RenderPassEditor.RenderPassNameColumn()
for p in [ "/A", "/B", "/C", "/D" ] :
pathCopy.setFromString( p )
- self.assertEqual( pathCopy.property( "renderPassPath:enabled" ), p in ( "/A", "/D" ) )
- self.assertTrue( pathCopy.property( "renderPassPath:enabledWithoutAdaptors" ) )
+ cellData = c.cellData( pathCopy, None )
+ if p in ( "/A", "/D" ) :
+ self.assertEqual( cellData.icon, "renderPass.png" )
+ self.assertEqual( cellData.toolTip, None )
+ else :
+ self.assertEqual( cellData.icon, "adaptorDisabledRenderPass.png" )
+ self.assertEqual( cellData.toolTip, "Automatically disabled by a render adaptor." )
def testSearchFilter( self ) :
@@ -294,6 +306,52 @@ def testDisabledRenderPassFilter( self ) :
self.assertEqual( [ str( c ) for c in path.children() ], [ "/A", "/B", "/C", "/D" ] )
+ def testDisabledRenderPassFilterWithAdaptor( self ) :
+
+ def createAdaptor() :
+
+ node = GafferScene.SceneProcessor()
+ node["options"] = GafferScene.CustomOptions()
+ node["options"]["in"].setInput( node["in"] )
+ node["options"]["options"].addChild( Gaffer.NameValuePlug( "renderPass:enabled", False ) )
+
+ node["switch"] = Gaffer.NameSwitch()
+ node["switch"].setup( node["options"]["out"] )
+ node["switch"]["in"][0]["value"].setInput( node["in"] )
+ node["switch"]["in"][1]["value"].setInput( node["options"]["out"] )
+ node["switch"]["in"][1]["name"].setValue( "B" )
+ node["switch"]["selector"].setValue( "${renderPass}" )
+
+ node["delete"] = GafferScene.DeleteRenderPasses()
+ node["delete"]["names"].setValue( "C" )
+ node["delete"]["in"].setInput( node["switch"]["out"]["value"] )
+
+ node["out"].setInput( node["delete"]["out"] )
+
+ return node
+
+ GafferScene.SceneAlgo.registerRenderAdaptor( "RenderPassEditorTest", createAdaptor, client = "RenderPassWedge" )
+ self.addCleanup( GafferScene.SceneAlgo.deregisterRenderAdaptor, "RenderPassEditorTest" )
+
+ renderPasses = GafferScene.RenderPasses()
+ renderPasses["names"].setValue( IECore.StringVectorData( ["A", "B", "C", "D"] ) )
+
+ adaptors = GafferSceneUI.RenderPassEditor._createRenderAdaptors()
+ adaptors["in"].setInput( renderPasses["out"] )
+
+ context = Gaffer.Context()
+ path = _GafferSceneUI._RenderPassEditor.RenderPassPath( adaptors["out"], context, "/" )
+ self.assertEqual( [ str( c ) for c in path.children() ], [ "/A", "/B", "/C", "/D" ] )
+
+ disabledRenderPassFilter = _GafferSceneUI._RenderPassEditor.DisabledRenderPassFilter()
+ path.setFilter( disabledRenderPassFilter )
+
+ self.assertEqual( [ str( c ) for c in path.children() ], [ "/A", "/D" ] )
+
+ disabledRenderPassFilter.setEnabled( False )
+
+ self.assertEqual( [ str( c ) for c in path.children() ], [ "/A", "/B", "/C", "/D" ] )
+
def testPathGroupingFunction( self ) :
renderPasses = GafferScene.RenderPasses()
diff --git a/src/GafferSceneUIModule/RenderPassEditorBinding.cpp b/src/GafferSceneUIModule/RenderPassEditorBinding.cpp
index c315b2d8022..67975bda4e2 100644
--- a/src/GafferSceneUIModule/RenderPassEditorBinding.cpp
+++ b/src/GafferSceneUIModule/RenderPassEditorBinding.cpp
@@ -195,10 +195,9 @@ using PathMatcherCache = IECorePreview::LRUCache( g_disableAdaptorsContextName, &disableAdaptors );
- }
if( canceller )
{
scopedContext.setCanceller( canceller );
@@ -411,7 +395,7 @@ class RenderPassPath : public Gaffer::Path
// practical as render pass names are used in output file paths where the included '/' characters would be
// interpreted as subdirectories. Validation in the UI will prevent users from inserting invalid characters
// such as '/' into render pass names.
- const IECore::PathMatcher pathMatcher( const IECore::Canceller *canceller, bool disableAdaptors = true ) const
+ const IECore::PathMatcher pathMatcher( const IECore::Canceller *canceller ) const
{
Context::EditableScope scopedContext( m_context.get() );
if( canceller )
@@ -419,10 +403,6 @@ class RenderPassPath : public Gaffer::Path
scopedContext.setCanceller( canceller );
}
- if( disableAdaptors )
- {
- scopedContext.set( g_disableAdaptorsContextName, &disableAdaptors );
- }
if( ConstStringVectorDataPtr renderPassData = m_scene.get()->globals()->member( g_renderPassNamesOption ) )
{
const PathMatcherCacheGetterKey key( renderPassData, m_grouped );
@@ -497,41 +477,49 @@ class RenderPassNameColumn : public StandardPathColumn
{
CellData result = StandardPathColumn::cellData( path, canceller );
- const auto renderPassName = runTimeCast( path.property( g_renderPassNamePropertyName, canceller ) );
- if( !renderPassName )
+ if( !runTimeCast( path.property( g_renderPassNamePropertyName, canceller ) ) )
{
result.icon = g_renderPassFolderIcon;
+ return result;
}
- else
+
+ // Enable render adaptors as they may have disabled or deleted render passes.
+ auto pathCopy = runTimeCast( path.copy() );
+ if( !pathCopy )
{
- if( const auto renderPassEnabled = runTimeCast( path.property( g_renderPassEnabledPropertyName, canceller ) ) )
- {
- if( renderPassEnabled->readable() )
- {
- result.icon = g_renderPassIcon;
- }
- else
- {
- result.foreground = g_dimmedForegroundColor;
- const auto renderPassEnabledWithoutAdaptors = runTimeCast( path.property( g_renderPassEnabledWithoutAdaptorsPropertyName, canceller ) );
- if( !renderPassEnabledWithoutAdaptors || !renderPassEnabledWithoutAdaptors->readable() )
- {
- result.icon = g_disabledRenderPassIcon;
- result.toolTip = g_disabledToolTip;
- }
- else
- {
- result.icon = g_adaptorDisabledRenderPassIcon;
- result.toolTip = g_adaptorDisabledToolTip;
- }
- }
- }
- else
- {
- result.icon = g_renderPassIcon;
- }
+ return result;
+ }
+ ContextPtr adaptorEnabledContext = new Context( *pathCopy->getContext() );
+ adaptorEnabledContext->set( g_enableAdaptorsContextName, true );
+ pathCopy->setContext( adaptorEnabledContext );
+
+ bool enabled = true;
+ if( !runTimeCast( pathCopy->property( g_renderPassNamePropertyName, canceller ) ) )
+ {
+ // The render pass has been deleted by a render adaptor, so present it to the user as disabled.
+ enabled = false;
+ }
+ else if( const auto enabledData = runTimeCast( pathCopy->property( g_renderPassEnabledPropertyName, canceller ) ) )
+ {
+ enabled = enabledData->readable();
+ }
+
+ if( enabled )
+ {
+ result.icon = g_renderPassIcon;
+ return result;
}
+ // Check `renderPass:enabled` without render adaptors enabled
+ // to determine whether the render pass was disabled upstream
+ // or by a render adaptor.
+ const auto enabledData = runTimeCast( path.property( g_renderPassEnabledPropertyName, canceller ) );
+ enabled = !enabledData || enabledData->readable();
+
+ result.icon = enabled ? g_adaptorDisabledRenderPassIcon : g_disabledRenderPassIcon;
+ result.toolTip = enabled ? g_adaptorDisabledToolTip : g_disabledToolTip;
+ result.foreground = g_dimmedForegroundColor;
+
return result;
}
@@ -733,10 +721,21 @@ class DisabledRenderPassFilter : public Gaffer::PathFilter
leaf = std::all_of( c.begin(), c.end(), [this, canceller] ( const auto &p ) { return remove( p, canceller ); } );
}
+ // Enable render adaptors so we remove any render passes
+ // that have been disabled or deleted by them.
+ auto pathCopy = runTimeCast( path->copy() );
+ if( !pathCopy )
+ {
+ return true;
+ }
+ ContextPtr adaptorEnabledContext = new Context( *pathCopy->getContext() );
+ adaptorEnabledContext->set( g_enableAdaptorsContextName, true );
+ pathCopy->setContext( adaptorEnabledContext );
+
bool enabled = false;
- if( runTimeCast( path->property( g_renderPassNamePropertyName, canceller ) ) )
+ if( runTimeCast( pathCopy->property( g_renderPassNamePropertyName, canceller ) ) )
{
- if( const auto enabledData = IECore::runTimeCast( path->property( g_renderPassEnabledPropertyName, canceller ) ) )
+ if( const auto enabledData = IECore::runTimeCast( pathCopy->property( g_renderPassEnabledPropertyName, canceller ) ) )
{
enabled = enabledData->readable();
}
From 742eb0d43e8d0ed0f0e7d31cfc253ab3d170bdc5 Mon Sep 17 00:00:00 2001
From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com>
Date: Wed, 15 Jan 2025 18:00:29 -0800
Subject: [PATCH 12/12] RenderPassEditor : Make changing render pass undoable
---
Changes.md | 4 +++-
python/GafferSceneUI/RenderPassEditor.py | 14 ++++++++------
2 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/Changes.md b/Changes.md
index e800170ba1d..017dad90d3a 100644
--- a/Changes.md
+++ b/Changes.md
@@ -21,7 +21,9 @@ Improvements
- PlugLayout :
- A warning widget is now displayed when an invalid custom widget is registered.
- `layout:customWidget::width` and `layout:customWidget::minimumWidth` metadata registrations are now supported for custom widgets.
-- RenderPassEditor : Render passes deleted or disabled by render adaptors registered to `client = "RenderPassWedge"` are now shown as disabled. To differentiate these from user disabled render passes, an orange dot is shown in the corner of the disabled icon and the tooltip describes them as automatically disabled.
+- RenderPassEditor :
+ - Render passes deleted or disabled by render adaptors registered to `client = "RenderPassWedge"` are now shown as disabled. To differentiate these from user disabled render passes, an orange dot is shown in the corner of the disabled icon and the tooltip describes them as automatically disabled.
+ - Changing the current render pass is now undoable.
Fixes
-----
diff --git a/python/GafferSceneUI/RenderPassEditor.py b/python/GafferSceneUI/RenderPassEditor.py
index 4f102b883ed..d96335acc71 100644
--- a/python/GafferSceneUI/RenderPassEditor.py
+++ b/python/GafferSceneUI/RenderPassEditor.py
@@ -399,10 +399,11 @@ def __setActiveRenderPass( self, pathListing ) :
self.__popup.popup( parent = self )
return
- GafferSceneUI.ScriptNodeAlgo.setCurrentRenderPass(
- script,
- selectedPassNames[0] if selectedPassNames[0] != GafferSceneUI.ScriptNodeAlgo.getCurrentRenderPass( script ) else ""
- )
+ with Gaffer.UndoScope( script ) :
+ GafferSceneUI.ScriptNodeAlgo.setCurrentRenderPass(
+ script,
+ selectedPassNames[0] if selectedPassNames[0] != GafferSceneUI.ScriptNodeAlgo.getCurrentRenderPass( script ) else ""
+ )
def __columnContextMenuSignal( self, column, pathListing, menuDefinition ) :
@@ -1150,8 +1151,9 @@ def __refreshMenu( self ) :
def __setCurrentRenderPass( self, renderPass, *unused ) :
- for plug in self.getPlugs() :
- plug.setValue( renderPass )
+ with Gaffer.UndoScope( self.scriptNode() ) :
+ for plug in self.getPlugs() :
+ plug.setValue( renderPass )
def __renderPassDescription( self, renderPass ) :