From 5ac2886f992977fab3715c8d27a58d3645091aa8 Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Fri, 17 Jan 2025 13:28:33 -0800 Subject: [PATCH] RenderPassEditor : Support registering columns with a specific index Columns are usually displayed in the order that they are registered. This change allows custom columns registered after Gaffer's standard columns to be displayed ahead of or intermixed within Gaffer's standard columns. `index` specifies the column's position to the right of the "Active" column, as we consider the "Name" and "Active" columns to be a primary part of the editor with fixed positions. A negative index can be used to position a column at the end of the list. If multiple columns are registered with the same index, they are ordered within that index based on their registration order. So if columns A and B are both registered with an index of -1, they will appear as the rightmost columns, with B positioned after A as it was registered second. --- Changes.md | 5 + python/GafferSceneUI/RenderPassEditor.py | 30 ++++- .../GafferSceneUITest/RenderPassEditorTest.py | 109 ++++++++++++++++++ 3 files changed, 138 insertions(+), 6 deletions(-) diff --git a/Changes.md b/Changes.md index 88d27dd1f9..fd41ac9c49 100644 --- a/Changes.md +++ b/Changes.md @@ -9,6 +9,11 @@ Fixes - Fixed warning messages when attempting to disable a non-existent edit. - Fixed warning message which referred to "None" rather than the "Source" scope. +API +--- + +- RenderPassEditor : Added optional `index` argument to `registerOption()` and `registerColumn()`. This can be used to specify the column's position in the UI. + 1.5.3.0 (relative to 1.5.2.0) ======= diff --git a/python/GafferSceneUI/RenderPassEditor.py b/python/GafferSceneUI/RenderPassEditor.py index d96335acc7..d3a8c91900 100644 --- a/python/GafferSceneUI/RenderPassEditor.py +++ b/python/GafferSceneUI/RenderPassEditor.py @@ -153,7 +153,7 @@ def __init__( self, scriptNode, **kw ) : __columnRegistry = collections.OrderedDict() @classmethod - def registerOption( cls, groupKey, optionName, section = "Main", columnName = None ) : + def registerOption( cls, groupKey, optionName, section = "Main", columnName = None, index = None ) : optionLabel = Gaffer.Metadata.value( "option:" + optionName, "label" ) if not columnName : @@ -173,7 +173,8 @@ def registerOption( cls, groupKey, optionName, section = "Main", columnName = No columnName, toolTip ), - section + section, + index ) # Registers a column in the Render Pass Editor. @@ -181,12 +182,12 @@ def registerOption( cls, groupKey, optionName, section = "Main", columnName = No # `inspectorFunction( scene, editScope )` returning a # `GafferSceneUI.Private.InspectorColumn` object. @classmethod - def registerColumn( cls, groupKey, columnKey, inspectorFunction, section = "Main" ) : + def registerColumn( cls, groupKey, columnKey, inspectorFunction, section = "Main", index = None ) : sections = cls.__columnRegistry.setdefault( groupKey, collections.OrderedDict() ) section = sections.setdefault( section, collections.OrderedDict() ) - section[columnKey] = inspectorFunction + section[columnKey] = ( inspectorFunction, index ) @classmethod def deregisterColumn( cls, groupKey, columnKey, section = "Main" ) : @@ -287,9 +288,26 @@ def __updateColumns( self ) : for groupKey, sections in self.__columnRegistry.items() : if IECore.StringAlgo.match( tabGroup, groupKey ) : section = sections.get( currentSection or None, {} ) - sectionColumns += [ c( self.settings()["in"], self.settings()["editScope"] ) for c in section.values() ] + sectionColumns += [ ( c( self.settings()["in"], self.settings()["editScope"] ), index ) for ( c, index ) in section.values() ] - self.__pathListing.setColumns( [ self.__renderPassNameColumn, self.__renderPassActiveColumn ] + sectionColumns ) + self.__pathListing.setColumns( [ self.__renderPassNameColumn, self.__renderPassActiveColumn ] + self.__orderedColumns( sectionColumns ) ) + + @staticmethod + def __orderedColumns( columnsAndIndices ) : + + for i, ( column, index ) in enumerate( columnsAndIndices ) : + if index is not None : + # Negative indices are remapped to their absolute position in the column list. + columnsAndIndices[i] = ( column, index if index >= 0 else len( columnsAndIndices ) + index ) + + # As column indices may be sparse, we fill in the gaps with any unspecified indices before sorting. + availableIndices = iter( sorted( set( range( len( columnsAndIndices ) ) ) - { x[1] for x in columnsAndIndices } ) ) + orderedColumns = sorted( + [ ( column, index if index is not None else next( availableIndices ) ) for column, index in columnsAndIndices ], + key = lambda x: x[1] + ) + + return [ x[0] for x in orderedColumns ] @GafferUI.LazyMethod( deferUntilPlaybackStops = True ) def __setPathListingPath( self ) : diff --git a/python/GafferSceneUITest/RenderPassEditorTest.py b/python/GafferSceneUITest/RenderPassEditorTest.py index 83c7e1d97e..365dfed168 100644 --- a/python/GafferSceneUITest/RenderPassEditorTest.py +++ b/python/GafferSceneUITest/RenderPassEditorTest.py @@ -434,3 +434,112 @@ def testFn( name ) : self.assertEqual( [ str( c ) for c in path.children() ], [ "/A/A_A", "/A/A_B" ] ) path.setFromString( "/B" ) self.assertEqual( [ str( c ) for c in path.children() ], [ "/B/B_C", "/B/B_D" ] ) + + def testRegisterOption( self ) : + + for columnName in [ "A", "B", "C", "D", "E" ] : + GafferSceneUI.RenderPassEditor.registerOption( "*", columnName, "Test" ) + self.addCleanup( GafferSceneUI.RenderPassEditor.deregisterColumn, "*", columnName, "Test" ) + + script = Gaffer.ScriptNode() + + editor = GafferSceneUI.RenderPassEditor( script ) + editor.settings()["section"].setValue( "Test" ) + + GafferSceneUI.RenderPassEditor._RenderPassEditor__updateColumns.flush( editor ) + + pathListing = editor._RenderPassEditor__pathListing + + columnNames = [ c.headerData().value for c in pathListing.getColumns() ] + for columnName in [ "A", "B", "C", "D", "E" ] : + self.assertIn( columnName, columnNames ) + + GafferSceneUI.RenderPassEditor.deregisterColumn( "*", "B", "Test" ) + + editor._RenderPassEditor__updateColumns() + GafferSceneUI.RenderPassEditor._RenderPassEditor__updateColumns.flush( editor ) + + columnNames = [ c.headerData().value for c in pathListing.getColumns() ] + self.assertNotIn( "B", columnNames ) + self.assertIn( "A", columnNames ) + self.assertIn( "C", columnNames ) + self.assertIn( "D", columnNames ) + self.assertIn( "E", columnNames ) + + def testColumnOrder( self ) : + + script = Gaffer.ScriptNode() + editor = GafferSceneUI.RenderPassEditor( script ) + editor.settings()["section"].setValue( "Test" ) + + def assertColumnOrder( columns, order ) : + + for columnName, index in columns : + GafferSceneUI.RenderPassEditor.registerOption( "*", columnName, "Test", index = index ) + self.addCleanup( GafferSceneUI.RenderPassEditor.deregisterColumn, "*", columnName, "Test" ) + + editor._RenderPassEditor__updateColumns() + GafferSceneUI.RenderPassEditor._RenderPassEditor__updateColumns.flush( editor ) + columnNames = [ c.headerData().value for c in editor._RenderPassEditor__pathListing.getColumns() if isinstance( c, GafferSceneUI.Private.InspectorColumn ) ] + self.assertEqual( columnNames, order ) + + for columnName, _ in columns : + GafferSceneUI.RenderPassEditor.deregisterColumn( "*", columnName, "Test" ) + + assertColumnOrder( + [ ( "A", None ), ( "B", None ), ( "C", None ), ( "D", None ), ( "E", None ) ], + [ "A", "B", "C", "D", "E" ] + ) + + assertColumnOrder( + [ ( "E", None ), ( "D", None ), ( "C", None ), ( "B", None ), ( "A", None ) ], + [ "E", "D", "C", "B", "A" ] + ) + + assertColumnOrder( + [ ( "A", 0 ), ( "B", 1 ), ( "C", 2 ), ( "D", 3 ), ( "E", 4 ) ], + [ "A", "B", "C", "D", "E" ] + ) + + assertColumnOrder( + [ ( "A", 4 ), ( "B", 3 ), ( "C", 2 ), ( "D", 1 ), ( "E", 0 ) ], + [ "E", "D", "C", "B", "A" ] + ) + + assertColumnOrder( + [ ( "A", None ), ( "B", None ), ( "C", None ), ( "D", 0 ), ( "E", 2 ) ], + [ "D", "A", "E", "B", "C" ] + ) + + assertColumnOrder( + [ ( "A", None ), ( "B", None ), ( "C", 0 ), ( "D", -1 ), ( "E", None ) ], + [ "C", "A", "B", "E", "D" ] + ) + + assertColumnOrder( + [ ( "A", None ), ( "B", None ), ( "C", -1 ), ( "D", -2 ), ( "E", None ) ], + [ "A", "B", "E", "D", "C" ] + ) + + assertColumnOrder( + [ ( "A", None ), ( "B", -1 ), ( "C", None ), ( "D", -1 ), ( "E", None ) ], + [ "A", "C", "E", "B", "D" ] + ) + + assertColumnOrder( + [ ( "A", None ), ( "B", 0 ), ( "C", 0 ), ( "D", None ), ( "E", 2 ) ], + [ "B", "C", "A", "E", "D" ] + ) + + assertColumnOrder( + [ ( "A", None ), ( "B", 0 ), ( "C", 0 ), ( "D", 0 ), ( "E", 2 ) ], + [ "B", "C", "D", "A", "E" ] + ) + + assertColumnOrder( + [ ( "A", None ), ( "B", 0 ), ( "C", 0 ), ( "D", 0 ), ( "E", 1 ) ], + [ "B", "C", "D", "E", "A" ] + ) + +if __name__ == "__main__" : + unittest.main()