From e4e5b4e5aea65d176b9770988f82362ee73a0ba0 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/10] 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 4c123db8dca..00a2c7da19e 100644 --- a/Changes.md +++ b/Changes.md @@ -16,6 +16,7 @@ Improvements ------------ - MergeScenes : Removed unnecessary temporary contexts. +- 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 b9e8e63f3a0a46d33ee6f01ec0e6e6ee48b5e6f5 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/10] 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 00a2c7da19e..759cfd17089 100644 --- a/Changes.md +++ b/Changes.md @@ -16,7 +16,9 @@ Improvements ------------ - MergeScenes : Removed unnecessary temporary contexts. -- 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 27a348b1d0bb8b60585ae9a1d83edd45460146de 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/10] 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 | 26 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/python/GafferUI/ScriptWindow.py b/python/GafferUI/ScriptWindow.py index 6d715cc47f5..803e99da66a 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() : + 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 @@ -201,6 +208,8 @@ def menuDefinition( applicationOrApplicationRoot ) : return menuDefinition + __childAddedConnection = None + __childRemovedConnection = None ## This function provides the top level functionality for instantiating # the UI. Once called, new ScriptWindows will be instantiated for each # script added to the application, and EventLoop.mainEventLoop().stop() will @@ -208,17 +217,20 @@ def menuDefinition( applicationOrApplicationRoot ) : @classmethod def connect( cls, applicationRoot ) : - applicationRoot["scripts"].childAddedSignal().connectFront( ScriptWindow.__scriptAdded ) - applicationRoot["scripts"].childRemovedSignal().connect( ScriptWindow.__staticScriptRemoved ) + ScriptWindow.__childAddedConnection = applicationRoot["scripts"].childAddedSignal().connect( ScriptWindow.__scriptAdded ) + ScriptWindow.__childRemovedConnection = applicationRoot["scripts"].childRemovedSignal().connect( ScriptWindow.__staticScriptRemoved ) + + @staticmethod + def __connected() : + + return ScriptWindow.__childAddedConnection is not None and ScriptWindow.__childRemovedConnection is not None and ScriptWindow.__childAddedConnection.connected() and ScriptWindow.__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..16faad29c74 100644 --- a/python/GafferUITest/ScriptWindowTest.py +++ b/python/GafferUITest/ScriptWindowTest.py @@ -87,6 +87,32 @@ def testAcquire( self ) : w6 = GafferUI.ScriptWindow.acquire( s3, createIfNecessary = True ) self.assertTrue( w6.scriptNode().isSame( s3 ) ) + def testLifetimeOfApplicationScriptWindows( self ) : + + def __scriptAdded( scriptContainer, script ) : + + w = GafferUI.ScriptWindow.acquire( script ) + w.setTitle( "modified" ) + self.assertEqual( w.getTitle(), "modified" ) + + a = Gaffer.ApplicationRoot() + 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 83ff385729a8127adb03aec3fe7b772b0cae1ca8 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/10] 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 | 49 +++++++++++++++ .../ScriptNodeAlgoBinding.cpp | 20 +++++++ 6 files changed, 143 insertions(+), 12 deletions(-) diff --git a/Changes.md b/Changes.md index 759cfd17089..d26244f9b54 100644 --- a/Changes.md +++ b/Changes.md @@ -29,6 +29,7 @@ API --- - PlugLayout : Activations may now depend on the presence of certain plugs, as they are now reevaluated when child plugs are added and removed. +- ScriptNodeAlgo : Added `setCurrentRenderPass()`, `getCurrentRenderPass()`, and `acquireRenderPassPlug()` methods. 1.5.1.0 (relative to 1.5.0.1) ======= 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 e09423df49a..8735a31a711 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..4a04c70e2a5 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,51 @@ 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 ) +{ + for( NameValuePlug::Iterator it( script->variablesPlug() ); !it.done(); ++it ) + { + if( (*it)->getName() == "renderPass" ) + { + if( (*it)->valuePlug() ) + { + return it->get(); + } + else + { + throw IECore::Exception( fmt::format( "Plug type of {} is {}, but must be StringPlug", (*it)->valuePlug()->fullName(), (*it)->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 ) +{ + if( 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 89a42816f123b1e024a8168afe2851b9f9c58152 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/10] 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 b3345204c5b..2a8845a5043 100644 --- a/resources/graphics.py +++ b/resources/graphics.py @@ -461,8 +461,8 @@ "renderPassEditor" : { "options" : { - "requiredWidth" : 16, - "requiredHeight" : 16, + "requiredWidth" : 14, + "requiredHeight" : 14, "validatePixelAlignment" : True }, diff --git a/resources/graphics.svg b/resources/graphics.svg index f94c73565e8..3116b21d337 100644 --- a/resources/graphics.svg +++ b/resources/graphics.svg @@ -3495,41 +3495,41 @@ @@ -10075,10 +10075,10 @@ transform="translate(0,159.23177)"> Date: Mon, 9 Dec 2024 19:51:22 -0800 Subject: [PATCH 06/10] RenderPassEditor : Add RenderPassChooserWidget --- Changes.md | 1 + python/GafferSceneUI/RenderPassEditor.py | 247 +++++++++++++++++++++++ startup/gui/project.py | 5 + startup/gui/renderPassEditor.py | 23 +++ 4 files changed, 276 insertions(+) diff --git a/Changes.md b/Changes.md index d26244f9b54..17428c6c7e9 100644 --- a/Changes.md +++ b/Changes.md @@ -11,6 +11,7 @@ Features - Inference : Loads ONNX models and performance inference using an array of input tensors. - ImageToTensor : Converts images to tensors for use with the Inference node. - TensorToImage : Converts tensors back to images following inference. +- RenderPassChooserWidget : Added a "Render Pass" menu to the Menu Bar that can be used to choose the current render pass from those available from the scene output from the focus node. Improvements ------------ diff --git a/python/GafferSceneUI/RenderPassEditor.py b/python/GafferSceneUI/RenderPassEditor.py index 8735a31a711..dbe1cf00d44 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 @@ -891,3 +896,245 @@ def __combiner( results ) : ) # Remove circular references that would keep the widget in limbo. e.__traceback__ = None + +class RenderPassChooserWidget( GafferUI.Widget ) : + + def __init__( self, topLevelWidget, **kw ) : + + self.__scriptNode = topLevelWidget["__scriptNode"].getInput().node() + self.__inputPlug = topLevelWidget["__renderPassMenuInputScene"] + + renderPassPlug = GafferSceneUI.ScriptNodeAlgo.acquireRenderPassPlug( self.__scriptNode ) + + self.__renderPassPlugValueWidget = _RenderPassPlugValueWidget( + renderPassPlug["value"], + topLevelWidget["__renderPassMenuAdaptedScene"]["globals"], + showLabel = True + ) + GafferUI.Widget.__init__( self, self.__renderPassPlugValueWidget, **kw ) + + self.__focusChangedConnection = self.__scriptNode.focusChangedSignal().connect( + Gaffer.WeakMethod( self.__focusChanged ), scoped = True + ) + + def __focusChanged( self, scriptNode, node ) : + + self.__updateInputPlug() + + def __updateInputPlug( self ) : + + self.__inputPlug.setInput( self.__scenePlugFromFocus() ) + + def __scenePlugFromFocus( self ) : + + focusNode = self.__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.RenderPassChooserWidget = RenderPassChooserWidget + +class _RenderPassPlugValueWidget( GafferUI.PlugValueWidget ) : + + def __init__( self, plug, globalsPlug = None, showLabel = False, **kw ) : + + self.__globalsPlug = globalsPlug + + 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.__updateMenuButton() + + def getToolTip( self ) : + + if self.__currentRenderPass == "" : + return "No render pass is active." + + if self.__currentRenderPass not in self.__renderPasses.get( "all", [] ) : + return "{} is not in the scene output from the focus node.".format( self.__currentRenderPass ) + else : + return "{} is the current render pass.".format( self.__currentRenderPass ) + + def _auxiliaryPlugs( self, plug ) : + + return [ self.__globalsPlug or self.__acquireGlobalsPlug() ] + + @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 __acquireGlobalsPlug( self ) : + + scriptWindow = GafferUI.ScriptWindow.acquire( self.getPlug().node().scriptNode() ) + return scriptWindow.getLayout().settings()["__renderPassMenuAdaptedScene"]["globals"] + + 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" : lambda checked : self.__setDisplayGrouped( checked ), + "description" : "Toggle grouped display of render passes." + } + ) + + result.append( + "/Hide Disabled", + { + "checkBox" : self.__hideDisabled, + "command" : lambda checked : self.__setHideDisabled( checked ), + "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 ) ) + +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..72249ced33d 100644 --- a/startup/gui/renderPassEditor.py +++ b/startup/gui/renderPassEditor.py @@ -38,6 +38,8 @@ import IECore import Gaffer +import GafferScene +import GafferUI import GafferSceneUI GafferSceneUI.RenderPassEditor.registerOption( "*", "renderPass:enabled" ) @@ -130,3 +132,24 @@ def __defaultPathGroupingFunction( renderPassName ) : return renderPassName.split( "_" )[0] if "_" in renderPassName else "" GafferSceneUI.RenderPassEditor.registerPathGroupingFunction( __defaultPathGroupingFunction ) + +def __compoundEditorCreated( editor ) : + + settingsNode = editor.settings() + settingsNode.addChild( GafferScene.ScenePlug( "__renderPassMenuInputScene" ) ) + settingsNode.addChild( GafferScene.ScenePlug( "__renderPassMenuAdaptedScene" ) ) + + settingsNode["__renderPassMenuAdaptors"] = GafferScene.SceneAlgo.createRenderAdaptors() + settingsNode["__renderPassMenuAdaptors"]["in"].setInput( settingsNode["__renderPassMenuInputScene"] ) + ## \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... + settingsNode["__renderPassMenuAdaptors"]["client"].setValue( "RenderPassWedge" ) + settingsNode["__renderPassMenuAdaptedScene"].setInput( settingsNode["__renderPassMenuAdaptors"]["out"] ) + +GafferUI.CompoundEditor.instanceCreatedSignal().connect( __compoundEditorCreated ) + +Gaffer.Metadata.registerValue( GafferUI.CompoundEditor.Settings, "layout:customWidget:renderPassSelector:widgetType", "GafferSceneUI.RenderPassEditor.RenderPassChooserWidget" ) +Gaffer.Metadata.registerValue( GafferUI.CompoundEditor.Settings, "layout:customWidget:renderPassSelector:section", "Settings" ) +Gaffer.Metadata.registerValue( GafferUI.CompoundEditor.Settings, "layout:customWidget:renderPassSelector:index", 0 ) +Gaffer.Metadata.registerValue( GafferUI.CompoundEditor.Settings, "layout:customWidget:renderPassSelector:width", 185 ) From d65b37a6e92c6369cad003f6ffac4313b2d0baed 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/10] _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 a39941af93fc03c60b44a7424d9e2af178e250bd 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/10] 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 | 56 ++++++++++++++++++- .../GafferSceneUITest/RenderPassEditorTest.py | 41 ++++++++++++++ resources/graphics.py | 1 + resources/graphics.svg | 34 +++++++++++ .../RenderPassEditorBinding.cpp | 34 ++++++++++- startup/gui/renderPassEditor.py | 6 +- 7 files changed, 164 insertions(+), 9 deletions(-) diff --git a/Changes.md b/Changes.md index 17428c6c7e9..6239e20468a 100644 --- a/Changes.md +++ b/Changes.md @@ -20,6 +20,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 / RenderPassChooserWidget : 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 dbe1cf00d44..d7e96462705 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 ) : @@ -224,6 +230,34 @@ 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["__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 + def __repr__( self ) : return "GafferSceneUI.RenderPassEditor( scriptNode )" @@ -265,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 ) : @@ -1005,8 +1039,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(), @@ -1074,6 +1113,7 @@ def __menuDefinition( self ) : { "command" : functools.partial( Gaffer.WeakMethod( self.__setCurrentRenderPass ), name ), "icon" : self.__renderPassIcon( name, activeIndicator = True ), + "description" : self.__renderPassDescription( name ) } ) @@ -1118,6 +1158,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 == "" : @@ -1129,6 +1181,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 2a8845a5043..c73f7a6786b 100644 --- a/resources/graphics.py +++ b/resources/graphics.py @@ -469,6 +469,7 @@ "ids" : [ "renderPass", "disabledRenderPass", + "adaptorDisabledRenderPass", "renderPassFolder", "activeRenderPass", "activeRenderPassFadedHighlighted", diff --git a/resources/graphics.svg b/resources/graphics.svg index 3116b21d337..98e62ff3e2a 100644 --- a/resources/graphics.svg +++ b/resources/graphics.svg @@ -3532,6 +3532,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 { diff --git a/startup/gui/renderPassEditor.py b/startup/gui/renderPassEditor.py index 72249ced33d..af3ed816e98 100644 --- a/startup/gui/renderPassEditor.py +++ b/startup/gui/renderPassEditor.py @@ -139,12 +139,8 @@ def __compoundEditorCreated( editor ) : settingsNode.addChild( GafferScene.ScenePlug( "__renderPassMenuInputScene" ) ) settingsNode.addChild( GafferScene.ScenePlug( "__renderPassMenuAdaptedScene" ) ) - settingsNode["__renderPassMenuAdaptors"] = GafferScene.SceneAlgo.createRenderAdaptors() + settingsNode["__renderPassMenuAdaptors"] = GafferSceneUI.RenderPassEditor._createRenderAdaptors() settingsNode["__renderPassMenuAdaptors"]["in"].setInput( settingsNode["__renderPassMenuInputScene"] ) - ## \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... - settingsNode["__renderPassMenuAdaptors"]["client"].setValue( "RenderPassWedge" ) settingsNode["__renderPassMenuAdaptedScene"].setInput( settingsNode["__renderPassMenuAdaptors"]["out"] ) GafferUI.CompoundEditor.instanceCreatedSignal().connect( __compoundEditorCreated ) From b40973a3138439ba02d57e8d8a08eacce33e691b 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/10] 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 6239e20468a..12c4e74f63b 100644 --- a/Changes.md +++ b/Changes.md @@ -20,7 +20,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 / RenderPassChooserWidget : 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 / RenderPassChooserWidget : 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 d7e96462705..edf5a4585ae 100644 --- a/python/GafferSceneUI/RenderPassEditor.py +++ b/python/GafferSceneUI/RenderPassEditor.py @@ -1036,11 +1036,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 9293fc32e4ee9dfb39659dbf9acea9b0a770c5d8 Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Mon, 9 Dec 2024 21:45:38 -0800 Subject: [PATCH 10/10] fixup! RenderPassEditor : Add RenderPassChooserWidget Whitespace fix --- python/GafferSceneUI/RenderPassEditor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/GafferSceneUI/RenderPassEditor.py b/python/GafferSceneUI/RenderPassEditor.py index edf5a4585ae..d7869dd0b83 100644 --- a/python/GafferSceneUI/RenderPassEditor.py +++ b/python/GafferSceneUI/RenderPassEditor.py @@ -965,7 +965,7 @@ def __scenePlugFromFocus( self ) : if focusNode is not None : outputScene = next( ( p for p in GafferScene.ScenePlug.RecursiveOutputRange( focusNode ) if not p.getName().startswith( "__" ) ), - None + None ) if outputScene is not None : return outputScene