diff --git a/Changes.md b/Changes.md index 2534c825c6b..ca07af8878f 100644 --- a/Changes.md +++ b/Changes.md @@ -8,6 +8,11 @@ Features - USD Kind : When selecting, the first ancestor location with a `usd:kind` attribute matching the chosen list of USD Kind will ultimately be selected. USD's Kind Registry includes `Assembly`, `Component`, `Group`, `Model` and `SubComponent` by default and can be extended via USD startup scripts. - Shader Assignment : When selecting, the first ancestor location with a renderable and direct (not inherited) shader attribute will ultimately be selected. This can be used to select either surface or displacement shaders. +Improvements +------------ + +- EditScope : Added a summary of edits in the NodeEditor, with the ability to select the affected objects and quickly navigate to the processor nodes. + Fixes ----- @@ -21,6 +26,9 @@ API --- - SelectionTool : Added static `registerSelectMode()` method for registering a Python or C++ function that will modify a selected scene path location. Users can choose which mode is active when selecting. +- EditScopeUI : Added an API for customising the EditScope's NodeEditor with summaries for each processor : + - ProcessorWidget provides a base class for custom widgets, and a factory mechanism for registering them against processors. + - SimpleProcessorWidget provides a base class for widgets with a simple summary label and optional action links. 1.4.0.0b5 (relative to 1.4.0.0b4) ========= diff --git a/python/GafferSceneUI/EditScopeUI.py b/python/GafferSceneUI/EditScopeUI.py index 946d1ed6a28..d409c330090 100644 --- a/python/GafferSceneUI/EditScopeUI.py +++ b/python/GafferSceneUI/EditScopeUI.py @@ -41,6 +41,9 @@ import GafferScene import GafferSceneUI +# Pruning/Visibility hotkeys +# ========================== + def addPruningActions( editor ) : if isinstance( editor, GafferUI.Viewer ) : @@ -154,3 +157,149 @@ def __visibilityKeyPress( viewer, event ) : tweakPlug["value"].setValue( False ) return True + + +# Processor Widgets +# ================= + +class _SceneProcessorWidget( GafferUI.EditScopeUI.SimpleProcessorWidget ) : + + def _linkActivated( self, linkData ) : + + GafferSceneUI.ContextAlgo.setSelectedPaths( + self.processor().ancestor( Gaffer.ScriptNode ).context(), linkData + ) + +class __LocationEditsWidget( _SceneProcessorWidget ) : + + @staticmethod + def _summary( processor, linkCreator ) : + + # Get the locations being edited from the spreadsheet. + + canceller = Gaffer.Context.current().canceller() + activePathMatcher = IECore.PathMatcher() + disabledPathMatcher = IECore.PathMatcher() + for row in processor["edits"] : + IECore.Canceller.check( canceller ) + path = row["name"].getValue() + if not path : + continue + if row["enabled"].getValue() : + activePathMatcher.addPath( path ) + else : + disabledPathMatcher.addPath( path ) + + # Match those against the scene. + + activePaths = IECore.PathMatcher() + disabledPaths = IECore.PathMatcher() + GafferScene.SceneAlgo.matchingPaths( activePathMatcher, processor["in"], activePaths ) + GafferScene.SceneAlgo.matchingPaths( disabledPathMatcher, processor["in"], disabledPaths ) + + # Build a summary describing what we found. + + summaries = [] + if activePaths.size() : + activeLink = linkCreator( + "{} location{}".format( activePaths.size(), "s" if activePaths.size() > 1 else "" ), + activePaths + ) + summaries.append( f"edits on {activeLink}" ) + if disabledPaths.size() : + disabledLink = linkCreator( + "{} location{}".format( disabledPaths.size(), "s" if disabledPaths.size() > 1 else "" ), + disabledPaths + ) + summaries.append( f"disabled edits on {disabledLink}" ) + + if not summaries : + return "None" + + summaries[0] = summaries[0][0].upper() + summaries[0][1:] + return " and ".join( summaries ) + +GafferUI.EditScopeUI.ProcessorWidget.registerProcessorWidget( "AttributeEdits TransformEdits *LightEdits *SurfaceEdits", __LocationEditsWidget ) + +class __PruningEditsWidget( _SceneProcessorWidget ) : + + @staticmethod + def _summary( processor, linkCreator ) : + + paths = IECore.PathMatcher() + GafferScene.SceneAlgo.matchingPaths( processor["PathFilter"], processor["in"], paths ) + + if paths.isEmpty() : + return "None" + else : + link = linkCreator( + "{} location{}".format( paths.size(), "s" if paths.size() > 1 else "" ), + paths + ) + return f"{link} pruned" + +GafferUI.EditScopeUI.ProcessorWidget.registerProcessorWidget( "PruningEdits", __PruningEditsWidget ) + +class __RenderPassesWidget( GafferUI.EditScopeUI.SimpleProcessorWidget ) : + + @staticmethod + def _summary( processor, linkCreator ) : + + names = processor["names"].getValue() + return "{} render pass{} created".format( + len( names ) if names else "No", + "es" if len( names ) > 1 else "", + ) + +GafferUI.EditScopeUI.ProcessorWidget.registerProcessorWidget( "RenderPasses", __RenderPassesWidget ) + +class __RenderPassOptionEditsWidget( GafferUI.EditScopeUI.SimpleProcessorWidget ) : + + @staticmethod + def _summary( processor, linkCreator ) : + + enabledOptions = set() + enabledPasses = set() + disabledPasses = set() + for row in processor["edits"].children()[1:] : + renderPass = row["name"].getValue() + if not renderPass : + continue + if not row["enabled"].getValue() : + disabledPasses.add( renderPass ) + continue + + passEnabledOptions = { + cell["value"]["name"].getValue() + for cell in row["cells"] + if cell["value"]["enabled"].getValue() + } + + if passEnabledOptions : + enabledPasses.add( renderPass ) + enabledOptions = enabledOptions | passEnabledOptions + + summaries = [] + if enabledOptions : + summaries.append( + "edits to {} option{} in {} render pass{}".format( + len( enabledOptions ), "s" if len( enabledOptions ) > 1 else "", + len( enabledPasses ), "es" if len( enabledPasses ) > 1 else "", + ) + ) + + if disabledPasses : + summaries.append( + "disabled edits for {} render pass{}".format( + len( disabledPasses ), "es" if len( disabledPasses ) > 1 else "" + ) + ) + + if not summaries : + return "None" + + summaries[0] = summaries[0][0].upper() + summaries[0][1:] + return " and ".join( summaries ) + + +GafferUI.EditScopeUI.ProcessorWidget.registerProcessorWidget( "RenderPassOptionEdits", __RenderPassOptionEditsWidget ) diff --git a/python/GafferUI/EditScopeUI.py b/python/GafferUI/EditScopeUI.py index 275f86d5a41..d472104dc2e 100644 --- a/python/GafferUI/EditScopeUI.py +++ b/python/GafferUI/EditScopeUI.py @@ -45,6 +45,9 @@ import Gaffer import GafferUI +from GafferUI._StyleSheet import _styleColors +from Qt import QtGui + Gaffer.Metadata.registerNode( Gaffer.EditScope, @@ -75,6 +78,11 @@ "noduleLayout:customGadget:addButtonLeft:visible", lambda node : "in" in node, "noduleLayout:customGadget:addButtonRight:visible", lambda node : "in" in node, + # Add a custom widget for showing a summary of the processors within. + + "layout:customWidget:processors:widgetType", "GafferUI.EditScopeUI.__ProcessorsWidget", + "layout:customWidget:processors:section", "Edits", + plugs = { "in" : [ @@ -346,3 +354,226 @@ def __userNodes( editScope ) : nodes = Gaffer.Metadata.nodesWithMetadata( editScope, "editScope:includeInNavigationMenu" ) return [ n for n in nodes if n.ancestor( Gaffer.EditScope ).isSame( editScope ) ] + +# ProcessorWidget +# =============== + +class ProcessorWidget( GafferUI.Widget ) : + + def __init__( self, topLevelWidget, processor, **kw ) : + + GafferUI.Widget.__init__( self, topLevelWidget, **kw ) + + self.__processor = processor + + def processor( self ) : + + return self.__processor + + __widgetTypes = {} + @staticmethod + def registerProcessorWidget( processorType, widgetCreator ) : + + ProcessorWidget.__widgetTypes[processorType] = widgetCreator + + @staticmethod + def create( processor ) : + + processorType = Gaffer.Metadata.value( processor, "editScope:processorType" ) + creator = ProcessorWidget.__widgetTypes.get( processorType ) + if creator is None : + for name, candidate in ProcessorWidget.__widgetTypes.items() : + if IECore.StringAlgo.matchMultiple( processorType, name ) : + creator = candidate + break + + if creator is not None : + return creator( processor ) + + return None + +# SimpleProcessorWidget +# ===================== + +## Base class for creating simple summaries of Processors, including links +class SimpleProcessorWidget( ProcessorWidget ) : + + def __init__( self, processor, **kw ) : + + self.__column = GafferUI.ListContainer( spacing = 4 ) + ProcessorWidget.__init__( self, self.__column, processor, **kw ) + + with self.__column : + with GafferUI.ListContainer( orientation = GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) : + label = GafferUI.NameLabel( processor ) + label.setFormatter( lambda g : "