diff --git a/Changes.md b/Changes.md index 86a37a7777..8d3c0faae9 100644 --- a/Changes.md +++ b/Changes.md @@ -6,6 +6,7 @@ Improvements - LightEditor : Values of inherited attributes are now displayed in the Light Editor. These are presented as dimmed "fallback" values. - LightEditor, RenderPassEditor : Fallback values shown in the history window are displayed with the same dimmed text colour used for fallback values in editor columns. +- EditScope : Filtered the EditScope menu to show only nodes that are active in the relevant context. Fixes ----- diff --git a/python/GafferUI/EditScopeUI.py b/python/GafferUI/EditScopeUI.py index 7f0dc3e8ab..aa038021d9 100644 --- a/python/GafferUI/EditScopeUI.py +++ b/python/GafferUI/EditScopeUI.py @@ -150,17 +150,38 @@ def __init__( self, plug, **kw ) : # run the default dropSignal handler from PlugValueWidget. self.dropSignal().connectFront( Gaffer.WeakMethod( self.__drop ), scoped = False ) + self.__updatePlugInputChangedConnection() + self.__acquireContextTracker() + def hasLabel( self ) : return True + def setPlugs( self, plugs ) : + + GafferUI.PlugValueWidget.setPlugs( self, plugs ) + self.__updatePlugInputChangedConnection() + self.__acquireContextTracker() + + def getToolTip( self ) : + + editScope = self.__editScope() + if editScope is None : + return "Edits will be made using the last relevant node, including nodes not in any EditScope." + + unusableReason = self.__unusableReason( editScope ) + if unusableReason : + return unusableReason + else : + return "Edits will be made in {}.".format( editScope.getName() ) + # We don't actually display values, but this is also called whenever the # input changes, which is when we need to update. def _updateFromValues( self, values, exception ) : editScope = self.__editScope() editScopeActive = editScope is not None - self.__updateMenuButton( editScope ) + self.__updateMenuButton() self.__navigationMenuButton.setEnabled( editScopeActive ) if editScopeActive : self.__editScopeNameChangedConnection = editScope.nameChangedSignal().connect( @@ -177,19 +198,55 @@ def _updateFromValues( self, values, exception ) : self._qtWidget().setProperty( "editScopeActive", GafferUI._Variant.toVariant( editScopeActive ) ) self._repolish() - def __updateMenuButton( self, editScope ) : + def __updatePlugInputChangedConnection( self ) : + + self.__plugInputChangedConnection = self.getPlug().node().plugInputChangedSignal().connect( + Gaffer.WeakMethod( self.__plugInputChanged ), scoped = True + ) + + def __plugInputChanged( self, plug ) : + + if plug.getName() == "in" and plug.parent() == self.getPlug().node() : + # The result of `__inputNode()` will have changed. + self.__acquireContextTracker() + + def __acquireContextTracker( self ) : + self.__contextTracker = GafferUI.ContextTracker.acquire( self.__inputNode() ) + self.__contextTrackerChangedConnection = self.__contextTracker.changedSignal().connect( + Gaffer.WeakMethod( self.__contextTrackerChanged ), scoped = True + ) + + if not self.__contextTracker.updatePending() : + self.__updateMenuButton() + else : + # We'll update later in `__contextTrackerChanged()`. + pass + + def __updateMenuButton( self ) : + + editScope = self.__editScope() self.__menuButton.setText( editScope.getName() if editScope is not None else "None" ) - self.__menuButton.setImage( self.__editScopeSwatch( editScope ) if editScope is not None else None ) + + if editScope is not None : + self.__menuButton.setImage( + self.__editScopeSwatch( editScope ) if not self.__unusableReason( editScope ) else "warningSmall.png" + ) + else : + self.__menuButton.setImage( None ) def __editScopeNameChanged( self, editScope, oldName ) : - self.__updateMenuButton( editScope ) + self.__updateMenuButton() def __editScopeMetadataChanged( self, editScope, key, reason ) : if key == "nodeGadget:color" : - self.__updateMenuButton( editScope ) + self.__updateMenuButton() + + def __contextTrackerChanged( self, contextTracker ) : + + self.__updateMenuButton() def __editScope( self ) : @@ -231,6 +288,20 @@ def __inputNode( self ) : return inputNode + def __activeEditScopes( self ) : + + node = self.__inputNode() + if node is None : + return [] + + result = Gaffer.NodeAlgo.findAllUpstream( node, self.__editScopePredicate ) + if self.__editScopePredicate( node ) : + result.insert( 0, node ) + + result = [ n for n in result if self.__contextTracker.isTracked( n ) ] + + return result + def __buildMenu( self, result, path, currentEditScope ) : result = IECore.MenuDefinition() @@ -269,6 +340,8 @@ def __buildMenu( self, result, path, currentEditScope ) : "label" : itemName, "checkBox" : editScope == currentEditScope, "icon" : self.__editScopeSwatch( editScope ), + "active" : not self.__unusableReason( editScope ), + "description" : self.__unusableReason( editScope ), } ) else : @@ -290,16 +363,10 @@ def __menuDefinition( self ) : if self.getPlug().getInput() is not None : currentEditScope = self.getPlug().getInput().parent() - node = self.__inputNode() - if node is not None : - upstream = Gaffer.NodeAlgo.findAllUpstream( node, self.__editScopePredicate ) - if self.__editScopePredicate( node ) : - upstream.insert( 0, node ) + activeEditScopes = self.__activeEditScopes() - downstream = Gaffer.NodeAlgo.findAllDownstream( node, self.__editScopePredicate ) - else : - upstream = [] - downstream = [] + node = self.__inputNode() + downstream = Gaffer.NodeAlgo.findAllDownstream( node, self.__editScopePredicate ) if node is not None else [] # Each child of the root will get its own section in the menu # if it has children. The section will be preceded by a divider @@ -321,13 +388,11 @@ def addToMenuHierarchy( editScope, root ) : currentNode = currentNode.setdefault( n.getName(), {} ) currentNode[editScope.getName()] = editScope - if upstream : - for editScope in sorted( upstream, key = lambda e : e.relativeName( e.scriptNode() ) ) : - addToMenuHierarchy( editScope, "Upstream" ) + for editScope in reversed( activeEditScopes ) : + addToMenuHierarchy( editScope, "Upstream" ) - if downstream : - for editScope in sorted( downstream, key = lambda e : e.relativeName( e.scriptNode() ) ) : - addToMenuHierarchy( editScope, "Downstream" ) + for editScope in sorted( downstream, key = lambda e : e.relativeName( e.scriptNode() ) ) : + addToMenuHierarchy( editScope, "Downstream" ) menuPath = Gaffer.DictPath( menuHierarchy, "/" ) @@ -447,32 +512,37 @@ def __dragLeave( self, widget, event ) : def __drop( self, widget, event ) : - inputNode = self.__inputNode() dropNode = self.__dropNode( event ) - if inputNode is None : - with GafferUI.PopupWindow() as self.__popup : - with GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) : - GafferUI.Image( "warningSmall.png" ) - GafferUI.Label( "

The Edit Scope cannot be set while nothing is viewed

" ) - self.__popup.popup( parent = self ) - elif dropNode : - upstream = Gaffer.NodeAlgo.findAllUpstream( inputNode, self.__editScopePredicate ) - if self.__editScopePredicate( inputNode ) : - upstream.insert( 0, inputNode ) - - if dropNode in upstream : + if dropNode is not None : + + reason = self.__unusableReason( dropNode ) + if reason is None : self.__connectEditScope( dropNode ) else : with GafferUI.PopupWindow() as self.__popup : with GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) : GafferUI.Image( "warningSmall.png" ) - GafferUI.Label( "

{} cannot be used as it is not upstream of {}

".format( dropNode.getName(), inputNode.getName() ) ) + GafferUI.Label( f"

{reason}

" ) self.__popup.popup( parent = self ) self.__frame.setHighlighted( False ) return True + def __unusableReason( self, editScope ) : + + name = editScope.relativeName( editScope.scriptNode() ) + inputNode = self.__inputNode() + if inputNode is None : + return f"{name} cannot be used while nothing is viewed." + elif not self.__contextTracker.isTracked( editScope ) : + inputNodeName = inputNode.relativeName( inputNode.scriptNode() ) + return f"{name} cannot be used as it is not upstream of {inputNodeName}." + elif not self.__contextTracker.isEnabled( editScope ) : + return f"{name} cannot be used as it is disabled." + else : + return None + # ProcessorWidget # ===============