From 0a9e991402ee6077020bc4f0e899af1f269e8d1c Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Tue, 29 Oct 2024 09:47:56 -0400 Subject: [PATCH 01/33] ColorChooser : Constrain drag to axes --- Changes.md | 4 +- .../Interface/ControlsAndShortcuts/index.md | 6 + python/GafferUI/ColorChooser.py | 196 ++++++++++++++++-- 3 files changed, 187 insertions(+), 19 deletions(-) diff --git a/Changes.md b/Changes.md index b836392d358..fe6486342e3 100644 --- a/Changes.md +++ b/Changes.md @@ -10,7 +10,9 @@ Improvements - DeletePoints : Added modes for deleting points based on a list of ids. - Light Editor, Attribute Editor, Spreadsheet : Add original and current color swatches to color popups. - SceneView : Added fallback framing extents to create a reasonable view when `SceneGadget` is empty, for example if the grid is hidden. -- ColorChooser : Added an option to toggle the dynamic update of colors displayed in the slider and color field backgrounds. When enabled, the widget backgrounds update to show the color that will result from moving the indicator to a given position. When disabled, a static range of values is displayed instead. +- ColorChooser : + - Added an option to toggle the dynamic update of colors displayed in the slider and color field backgrounds. When enabled, the widget backgrounds update to show the color that will result from moving the indicator to a given position. When disabled, a static range of values is displayed instead. + - Holding the Control key now constrains dragging in the color field to a single axis. Fixes ----- diff --git a/doc/source/Interface/ControlsAndShortcuts/index.md b/doc/source/Interface/ControlsAndShortcuts/index.md index d758a346a8a..5b37d285bd8 100644 --- a/doc/source/Interface/ControlsAndShortcuts/index.md +++ b/doc/source/Interface/ControlsAndShortcuts/index.md @@ -438,3 +438,9 @@ Toggle cell selection | {kbd}`Ctrl` + {{leftClick Edit selected cells | {kbd}`Return`
or
{kbd}`Enter` Disable edit | {kbd}`D` Toggle a render pass as active | {kbd}`Return` or {{leftClick}} {{leftClick}} a cell within the {{activeRenderPass}} column + +## Color Chooser Color Field ## + +Action | Control or shortcut +-----------------------------------------------------|-------------------- +Constrain drag to single axis | {kbd}`Ctrl` diff --git a/python/GafferUI/ColorChooser.py b/python/GafferUI/ColorChooser.py index d9f9f19d9bb..c49a8789aa1 100644 --- a/python/GafferUI/ColorChooser.py +++ b/python/GafferUI/ColorChooser.py @@ -41,6 +41,7 @@ import math import sys import imath +import enum import IECore @@ -244,6 +245,8 @@ def _displayTransformChanged( self ) : class _ColorField( GafferUI.Widget ) : + __DragConstraints = enum.Flag( "__DragConstraints", [ "X", "Y" ] ) + def __init__( self, color = imath.Color3f( 1.0 ), staticComponent = "v", dynamicBackground = True, **kw ) : GafferUI.Widget.__init__( self, QtWidgets.QWidget(), **kw ) @@ -251,6 +254,7 @@ def __init__( self, color = imath.Color3f( 1.0 ), staticComponent = "v", dynamic self._qtWidget().paintEvent = Gaffer.WeakMethod( self.__paintEvent ) self.buttonPressSignal().connect( Gaffer.WeakMethod( self.__buttonPress ) ) + self.buttonReleaseSignal().connect( Gaffer.WeakMethod( self.__buttonRelease ) ) self.dragBeginSignal().connect( Gaffer.WeakMethod( self.__dragBegin ) ) self.dragEnterSignal().connect( Gaffer.WeakMethod( self.__dragEnter ) ) self.dragMoveSignal().connect( Gaffer.WeakMethod( self.__dragMove ) ) @@ -258,9 +262,14 @@ def __init__( self, color = imath.Color3f( 1.0 ), staticComponent = "v", dynamic self.__valueChangedSignal = Gaffer.Signals.Signal2() + self.__minDraggableHueRadius = 4.0 + self.__color = color self.__staticComponent = staticComponent self.__colorFieldToDraw = None + self.__dragConstraints = None + self.__constraintPosition = None + self.__constraintPath = None self.setColor( color, staticComponent ) self.setDynamicBackground( dynamicBackground ) @@ -354,6 +363,13 @@ def __cartesianToPolar( self, p, center, maxRadius ) : return imath.V2f( theta / twoPi, radius / maxRadius ) + def __polarToCartesian( self, p, center, maxRadius ) : + + a = p.x * math.pi * 2.0 + return imath.V2f( + math.cos( a ) * p.y * maxRadius + center.x, + math.sin( a ) * p.y * maxRadius + center.y + ) def __colorToPosition( self, color ) : @@ -389,7 +405,8 @@ def __positionToColor( self, position ) : if self.__useWheel() : halfSize = imath.V2f( size ) * 0.5 p = self.__cartesianToPolar( imath.V2f( position.x, size.y - position.y ), halfSize, halfSize.x ) - c[xIndex] = p.x + if p.y > 0 : + c[xIndex] = p.x c[yIndex] = p.y else : c[xIndex] = ( position.x / float( size.x ) ) * ( _ranges[xComponent].max - _ranges[xComponent].min ) + _ranges[xComponent].min @@ -397,41 +414,165 @@ def __positionToColor( self, position ) : return c + def __clampPosition( self, position ) : + + size = self.bound().size() + if self.__useWheel() : + halfSize = imath.V2f( size ) * 0.5 + centeredPosition = position - halfSize + radiusOriginal = centeredPosition.length() + + if radiusOriginal == 0 : + return halfSize + + return halfSize + ( centeredPosition / imath.V2f( radiusOriginal ) ) * min( radiusOriginal, halfSize.x ) + + return imath.V2f( min( size.x, max( 0.0, position.x ) ), min( size.y, max( 0.0, position.y ) ) ) + + def __constrainPosition( self, position ) : + + if self.__dragConstraints is None : + return position + + assert( bool( self.__dragConstraints ) ) + + if not self.__useWheel() : + # Determine if `position` is closer to the horizontal line going through + # `__constraintPosition` or the vertical line. The distance to the + # horizontal line is the vertical difference from `position` to + # `__constraintPosition`. The distance to the vertical line is the + # horizontal difference. + delta = self.__constraintPosition - position + delta.x = abs( delta.x ) + delta.y = abs( delta.y ) + + if delta.y < delta.x and ( + self.__DragConstraints.X in self.__dragConstraints or not self.__DragConstraints.Y in self.__dragConstraints + ) : + # Constrain to horizontal line + return imath.V2f( position.x, self.__constraintPosition.y ) + # Constrain to vertical line + return imath.V2f( self.__constraintPosition.x, position.y ) + + center = imath.V2f( self.bound().size() ) * 0.5 + distanceToCenter = ( position - center ).length() + + polarConstraintPosition = self.__cartesianToPolar( self.__constraintPosition, center, center.x ) + + # Extend line segment past the widget for distance check + extended = self.__polarToCartesian( imath.V2f( polarConstraintPosition.x, 2.0 ), center, center.x ) + + radiusLine = imath.Line3f( imath.V3f( center.x, center.y, 0 ), imath.V3f( extended.x, extended.y, 0 ) ) + position3 = imath.V3f( position.x, position.y, 0 ) + closestPointOnRadiusLine = radiusLine.closestPointTo( position3 ) + distanceToRadiusLine = ( closestPointOnRadiusLine - position3 ).length() + + constraintRadius = polarConstraintPosition.y * center.x + distanceToCircle = abs( distanceToCenter - constraintRadius ) + + dot = ( position - center ).dot( self.__constraintPosition - center ) + + polarPosition = self.__cartesianToPolar( position, center, center.x ) + + if self.__DragConstraints.X in self.__dragConstraints : + if ( + abs( distanceToCircle ) < distanceToRadiusLine or + ( dot < 0 and distanceToCenter > constraintRadius * 0.5 ) + ) : + # If we're closer to the circle than the radius line, or we're behind the center + # (from the point of view of `__constraintPosition`) and reasonably far from the center, + # constrain to the circle. + return self.__polarToCartesian( + imath.V2f( polarPosition.x, polarConstraintPosition.y ), center, center.x + ) + + if dot < 0 : + # If we're not constrained to the circle, but "behind" the center, snap to the center. + return center + + # If no constraints are enforced yet, constrain to the radius line. + polarClosestPoint = self.__cartesianToPolar( + imath.V2f( closestPointOnRadiusLine.x, closestPointOnRadiusLine.y ), center, center.x + ) + return self.__polarToCartesian( + imath.V2f( polarConstraintPosition.x, polarClosestPoint.y ), + center, + center.x + ) + + def __setupConstraints( self, event ) : + + c, staticComponent = self.getColor() + + self.__constraintPosition = self.__colorToPosition( c ) + + center = imath.V2f( self.bound().size() ) * 0.5 + + # Set `__dragConstraints` with `__constraintPosition` based on actual color + self.__dragConstraints = None + if event.modifiers == GafferUI.ButtonEvent.Modifiers.Control : + if not self.__useWheel() : + self.__dragConstraints = self.__DragConstraints.X | self.__DragConstraints.Y + else : + self.__dragConstraints = self.__DragConstraints.Y + + center = imath.V2f( self.bound().size() ) * 0.5 + if ( self.__constraintPosition - center ).length() > self.__minDraggableHueRadius : + self.__dragConstraints |= self.__DragConstraints.X + else : + # Adjust `__constraintPosition` to be along the hue angle, slightly off center. This + # allows the radius line constraint to work properly. + polarPosition = self.__cartesianToPolar( + self.__colorToPosition( imath.Color3f( c.r, 1.0, 1.0 ) ), center, center.x + ) + self.__constraintPosition = self.__polarToCartesian( imath.V2f( polarPosition.x, 1.0 / center.x ), center, center.x ) + def __buttonPress( self, widget, event ) : if event.buttons != GafferUI.ButtonEvent.Buttons.Left : return False - halfSize = imath.V2f( self.bound().size() ) * 0.5 - if ( imath.V2f( event.line.p0.x, event.line.p0.y ) - halfSize ).length() > halfSize.x : + center = imath.V2f( self.bound().size() ) * 0.5 + if ( imath.V2f( event.line.p0.x, event.line.p0.y ) - center ).length() > center.x : return False + self.__setupConstraints( event ) + + self.__constraintPath = None + c, staticComponent = self.getColor() - self.__setColorInternal( self.__positionToColor( event.line.p0 ), staticComponent, GafferUI.Slider.ValueChangedReason.Click ) - return True + self.__setColorInternal( + self.__positionToColor( + self.__clampPosition( self.__constrainPosition( imath.V2f( event.line.p0.x, event.line.p0.y ) ) ) + ), + staticComponent, + GafferUI.Slider.ValueChangedReason.Click + ) - def __clampPosition( self, position ) : + return True - size = self.bound().size() - if self.__useWheel() : - halfSize = imath.V2f( size ) * 0.5 - centeredPosition = imath.V2f( position.x, position.y ) - halfSize - radiusOriginal = centeredPosition.length() + def __buttonRelease( self, widget, event ) : - if radiusOriginal == 0 : - return halfSize + if event.buttons == GafferUI.ButtonEvent.Buttons.Left : + return False - return halfSize + ( centeredPosition / imath.V2f( radiusOriginal ) ) * min( radiusOriginal, halfSize.x ) + self.__constraintPosition = None + self.__dragConstraints = None + self.__constraintPath = None - return imath.V2f( min( size.x, max( 0.0, position.x ) ), min( size.y, max( 0.0, position.y ) ) ) + self._qtWidget().update() def __dragBegin( self, widget, event ) : if event.buttons == GafferUI.ButtonEvent.Buttons.Left : + self.__constraintPath = None + c, staticComponent = self.getColor() self.__setColorInternal( - self.__positionToColor( self.__clampPosition( event.line.p0 ) ), + self.__positionToColor( + self.__clampPosition( self.__constrainPosition( imath.V2f( event.line.p0.x, event.line.p0.y ) ) ) + ), staticComponent, GafferUI.Slider.ValueChangedReason.DragBegin ) @@ -449,8 +590,19 @@ def __dragEnter( self, widget, event ) : def __dragMove( self, widget, event ) : c, staticComponent = self.getColor() + + if self.__dragConstraints is None and event.modifiers == GafferUI.ButtonEvent.Modifiers.Control : + self.__setupConstraints( event ) + self.__constraintPath = None + elif self.__dragConstraints is not None and event.modifiers != GafferUI.ButtonEvent.Modifiers.Control : + self.__constraintPath = None + self.__dragConstraints = None + + self.__setColorInternal( - self.__positionToColor( self.__clampPosition( event.line.p0 ) ), + self.__positionToColor( + self.__clampPosition( self.__constrainPosition( imath.V2f( event.line.p0.x, event.line.p0.y ) ) ) + ), staticComponent, GafferUI.Slider.ValueChangedReason.DragMove ) @@ -459,11 +611,19 @@ def __dragMove( self, widget, event ) : def __dragEnd( self, widget, event ) : c, staticComponent = self.getColor() + self.__setColorInternal( - self.__positionToColor( self.__clampPosition( event.line.p0 ) ), + self.__positionToColor( + self.__clampPosition( self.__constrainPosition( imath.V2f( event.line.p0.x, event.line.p0.y ) ) ) + ), staticComponent, GafferUI.Slider.ValueChangedReason.DragEnd ) + + self.__constraintPosition = None + self.__dragConstraints = None + self.__constraintPath = None + return True def __drawBackground( self, painter ) : From ad5f804267839f33c24c940fcdeb0594e68d880f Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Wed, 30 Oct 2024 11:54:03 -0400 Subject: [PATCH 02/33] ColorChooser : Draw constraints on color field --- python/GafferUI/ColorChooser.py | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/python/GafferUI/ColorChooser.py b/python/GafferUI/ColorChooser.py index c49a8789aa1..fa2111aa2aa 100644 --- a/python/GafferUI/ColorChooser.py +++ b/python/GafferUI/ColorChooser.py @@ -691,6 +691,42 @@ def __drawBackground( self, painter ) : painter.drawImage( self._qtWidget().contentsRect(), self.__colorFieldToDraw ) + if self.__constraintPath is None and self.__dragConstraints is not None and self.__constraintPosition is not None : + path = QtGui.QPainterPath() + + if self.__useWheel() : + center = imath.V2f( self.bound().size() ) * 0.5 + + if self.__DragConstraints.X in self.__dragConstraints : + radius = ( self.__constraintPosition - center ).length() + path.addEllipse( QtCore.QPoint( center.x, center.y ), radius, radius ) + + if self.__DragConstraints.Y in self.__dragConstraints : + radiusEnd = center + ( self.__constraintPosition - center ).normalized() * center.x + + path.moveTo( center.x, center.y ) + path.lineTo( radiusEnd.x, radiusEnd.y ) + else : + if self.__dragConstraints.X in self.__dragConstraints : + path.moveTo( 0, self.__constraintPosition.y ) + path.lineTo( self.bound().max().x, self.__constraintPosition.y ) + if self.__DragConstraints.Y in self.__dragConstraints : + path.moveTo( self.__constraintPosition.x, 0 ) + path.lineTo( self.__constraintPosition.x, self.bound().max().y ) + + strokedPath = QtGui.QPainterPathStroker() + strokedPath.setWidth( 2 ) + + self.__constraintPath = strokedPath.createStroke( path ) + + if self.__constraintPath is not None : + pen = QtGui.QPen( QtGui.QColor( 0, 0, 0, 60 ) ) + pen.setWidth( 1 ) + painter.setPen( pen ) + painter.setBrush( QtGui.QBrush( QtGui.QColor( 255, 255, 255, 60 ) ) ) + + painter.drawPath( self.__constraintPath ) + def __drawValue( self, painter ) : position = self.__colorToPosition( self.__color ) From 595e3a6dc3d3e92b3d23cd1ccde31236ace48dd3 Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Sat, 2 Nov 2024 09:55:20 +1100 Subject: [PATCH 03/33] EditScopePlugValueWidget : Move navigation menu into main menu --- Changes.md | 1 + python/GafferUI/EditScopeUI.py | 58 ++++++++++++---------------------- 2 files changed, 21 insertions(+), 38 deletions(-) diff --git a/Changes.md b/Changes.md index 9411b75d752..233777b0949 100644 --- a/Changes.md +++ b/Changes.md @@ -13,6 +13,7 @@ Improvements - ColorChooser : - Added an option to toggle the dynamic update of colors displayed in the slider and color field backgrounds. When enabled, the widget backgrounds update to show the color that will result from moving the indicator to a given position. When disabled, a static range of values is displayed instead. - Holding the Control key now constrains dragging in the color field to a single axis. +- EditScope : Removed the "Navigation Arrow" button from the right side of the Edit Scope menu. Its actions have been relocated to a "Show Edits" submenu of the Edit Scope menu. Fixes ----- diff --git a/python/GafferUI/EditScopeUI.py b/python/GafferUI/EditScopeUI.py index d11dd83df0f..f038397f57e 100644 --- a/python/GafferUI/EditScopeUI.py +++ b/python/GafferUI/EditScopeUI.py @@ -136,11 +136,6 @@ def __init__( self, plug, **kw ) : ) # 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.__navigationMenuButton = GafferUI.MenuButton( - image = "navigationArrow.png", - hasFrame = False, - menu = GafferUI.Menu( Gaffer.WeakMethod( self.__navigationMenuDefinition ) ) - ) GafferUI.Spacer( imath.V2i( 4, 1 ), imath.V2i( 4, 1 ) ) self.buttonPressSignal().connect( Gaffer.WeakMethod( self.__buttonPress ) ) @@ -184,7 +179,6 @@ def _updateFromValues( self, values, exception ) : editScope = self.__editScope() editScopeActive = editScope is not None self.__updateMenuButton() - self.__navigationMenuButton.setEnabled( editScopeActive ) if editScopeActive : self.__editScopeNameChangedConnection = editScope.nameChangedSignal().connect( Gaffer.WeakMethod( self.__editScopeNameChanged ), scoped = True @@ -394,6 +388,26 @@ def __menuDefinition( self ) : "/None", { "command" : functools.partial( self.getPlug().setInput, None ) }, ) + if currentEditScope is not None : + result.append( "/__ActionsDivider__", { "divider" : True, "label" : "Actions" } ) + nodes = currentEditScope.processors() + nodes.extend( self.__userNodes( currentEditScope ) ) + + if nodes : + for node in nodes : + path = node.relativeName( currentEditScope ).replace( ".", "/" ) + result.append( + "/Show Edits/" + path, + { + "command" : functools.partial( GafferUI.NodeEditor.acquire, node ) + } + ) + else : + result.append( + "/Show Edits/EditScope is Empty", + { "active" : False }, + ) + return result def __refreshMenu( self ) : @@ -403,38 +417,6 @@ def __refreshMenu( self ) : # widget until it is done. self.__busyWidget.setVisible( True ) - def __navigationMenuDefinition( self ) : - - result = IECore.MenuDefinition() - - editScope = self.__editScope() - if editScope is None : - result.append( - "/No EditScope Selected", - { "active" : False }, - ) - return result - - nodes = editScope.processors() - nodes.extend( self.__userNodes( editScope ) ) - - if nodes : - for node in nodes : - path = node.relativeName( editScope ).replace( ".", "/" ) - result.append( - "/" + path, - { - "command" : functools.partial( GafferUI.NodeEditor.acquire, node ) - } - ) - else : - result.append( - "/EditScope is Empty", - { "active" : False }, - ) - - return result - def __editScopeSwatch( self, editScope ) : return GafferUI.Image.createSwatch( From 5e6fe8ae2bcbcce0c3b3cb8e227ee6a58d1bcde2 Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Thu, 7 Nov 2024 12:58:11 +1100 Subject: [PATCH 04/33] EditScopePlugValueWidget : Simplify UI Remove the outer frame and always colour the menu button blue. --- Changes.md | 6 +++- python/GafferSceneUI/AttributeEditor.py | 2 +- python/GafferSceneUI/LightEditor.py | 2 +- python/GafferSceneUI/RenderPassEditor.py | 2 +- python/GafferSceneUI/SceneViewUI.py | 4 +-- python/GafferUI/EditScopeUI.py | 41 ++++++++++-------------- python/GafferUI/_StyleSheet.py | 3 +- 7 files changed, 28 insertions(+), 32 deletions(-) diff --git a/Changes.md b/Changes.md index 233777b0949..ad3e2f2dafb 100644 --- a/Changes.md +++ b/Changes.md @@ -13,7 +13,11 @@ Improvements - ColorChooser : - Added an option to toggle the dynamic update of colors displayed in the slider and color field backgrounds. When enabled, the widget backgrounds update to show the color that will result from moving the indicator to a given position. When disabled, a static range of values is displayed instead. - Holding the Control key now constrains dragging in the color field to a single axis. -- EditScope : Removed the "Navigation Arrow" button from the right side of the Edit Scope menu. Its actions have been relocated to a "Show Edits" submenu of the Edit Scope menu. +- EditScope : + - Simplified the Edit Scope menu UI : + - Removed the dark background. + - Changed the menu button color to be always blue. + - Removed the "Navigation Arrow" button from the right side of the Edit Scope menu. Its actions have been relocated to a "Show Edits" submenu of the Edit Scope menu. Fixes ----- diff --git a/python/GafferSceneUI/AttributeEditor.py b/python/GafferSceneUI/AttributeEditor.py index fd19a6090fe..dec15a06503 100644 --- a/python/GafferSceneUI/AttributeEditor.py +++ b/python/GafferSceneUI/AttributeEditor.py @@ -322,7 +322,7 @@ def __transferSelectionFromScriptNode( self ) : "editScope" : [ "plugValueWidget:type", "GafferUI.EditScopeUI.EditScopePlugValueWidget", - "layout:width", 225, + "layout:width", 185, ], diff --git a/python/GafferSceneUI/LightEditor.py b/python/GafferSceneUI/LightEditor.py index 26af14dc1f9..c0e09a45601 100644 --- a/python/GafferSceneUI/LightEditor.py +++ b/python/GafferSceneUI/LightEditor.py @@ -414,7 +414,7 @@ def __deleteLights( self, *unused ) : "editScope" : [ "plugValueWidget:type", "GafferUI.EditScopeUI.EditScopePlugValueWidget", - "layout:width", 225, + "layout:width", 185, ], diff --git a/python/GafferSceneUI/RenderPassEditor.py b/python/GafferSceneUI/RenderPassEditor.py index 95dd27017a3..330a9f7c64b 100644 --- a/python/GafferSceneUI/RenderPassEditor.py +++ b/python/GafferSceneUI/RenderPassEditor.py @@ -638,7 +638,7 @@ def __updateButtonStatus( self, *unused ) : "editScope" : [ "plugValueWidget:type", "GafferUI.EditScopeUI.EditScopePlugValueWidget", - "layout:width", 225, + "layout:width", 185, ], diff --git a/python/GafferSceneUI/SceneViewUI.py b/python/GafferSceneUI/SceneViewUI.py index 93d365ce54c..fc6637e753b 100644 --- a/python/GafferSceneUI/SceneViewUI.py +++ b/python/GafferSceneUI/SceneViewUI.py @@ -105,7 +105,7 @@ def __rendererPlugActivator( plug ) : "plugValueWidget:type", "GafferUI.EditScopeUI.EditScopePlugValueWidget", "toolbarLayout:index", -1, - "toolbarLayout:width", 225, + "toolbarLayout:width", 185, ], @@ -1226,7 +1226,7 @@ class _EditScopeBalancingSpacer( GafferUI.Spacer ) : def __init__( self, sceneView, **kw ) : # EditScope width - pause button - spacer - spinner - renderer - width = 200 - 25 - 4 - 20 - 100 + width = 185 - 25 - 4 - 20 - 100 GafferUI.Spacer.__init__( self, diff --git a/python/GafferUI/EditScopeUI.py b/python/GafferUI/EditScopeUI.py index f038397f57e..1e89acbc719 100644 --- a/python/GafferUI/EditScopeUI.py +++ b/python/GafferUI/EditScopeUI.py @@ -120,23 +120,20 @@ class EditScopePlugValueWidget( GafferUI.PlugValueWidget ) : def __init__( self, plug, **kw ) : - self.__frame = GafferUI.Frame( borderWidth = 0 ) - GafferUI.PlugValueWidget.__init__( self, self.__frame, plug, **kw ) - - with self.__frame : - with GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) : - GafferUI.Spacer( imath.V2i( 4, 1 ), imath.V2i( 4, 1 ) ) - GafferUI.Label( "Edit Scope" ) - 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 ) - GafferUI.Spacer( imath.V2i( 4, 1 ), imath.V2i( 4, 1 ) ) + self.__listContainer = GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) + GafferUI.PlugValueWidget.__init__( self, self.__listContainer, plug, **kw ) + + with self.__listContainer : + GafferUI.Label( "Edit Scope" ) + 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.buttonPressSignal().connect( Gaffer.WeakMethod( self.__buttonPress ) ) self.dragBeginSignal().connect( Gaffer.WeakMethod( self.__dragBegin ) ) @@ -190,10 +187,6 @@ def _updateFromValues( self, values, exception ) : self.__editScopeNameChangedConnection = None self.__editScopeMetadataChangedConnection = None - if self._qtWidget().property( "editScopeActive" ) != editScopeActive : - self._qtWidget().setProperty( "editScopeActive", GafferUI._Variant.toVariant( editScopeActive ) ) - self._repolish() - def __updatePlugInputChangedConnection( self ) : self.__plugInputChangedConnection = self.getPlug().node().plugInputChangedSignal().connect( @@ -469,13 +462,13 @@ def __dragEnter( self, widget, event ) : return False if self.__dropNode( event ) : - self.__frame.setHighlighted( True ) + self.__menuButton.setHighlighted( True ) return True def __dragLeave( self, widget, event ) : - self.__frame.setHighlighted( False ) + self.__menuButton.setHighlighted( False ) return True @@ -494,7 +487,7 @@ def __drop( self, widget, event ) : GafferUI.Label( f"

{reason}

" ) self.__popup.popup( parent = self ) - self.__frame.setHighlighted( False ) + self.__menuButton.setHighlighted( False ) return True diff --git a/python/GafferUI/_StyleSheet.py b/python/GafferUI/_StyleSheet.py index 2e016adf93e..bdf01fa693f 100644 --- a/python/GafferUI/_StyleSheet.py +++ b/python/GafferUI/_StyleSheet.py @@ -1519,7 +1519,6 @@ def styleColor( key ) : *[gafferClass="GafferSceneUI.TransformToolUI._SelectionWidget"], *[gafferClass="GafferSceneUI.CropWindowToolUI._StatusWidget"], *[gafferClass="GafferSceneUI.TransformToolUI._TargetTipWidget"] > QFrame, - *[gafferClass="GafferUI.EditScopeUI.EditScopePlugValueWidget"] > QFrame, *[gafferClass="GafferSceneUI.InteractiveRenderUI._ViewRenderControlUI"] > QFrame, *[gafferClass="GafferSceneUI._SceneViewInspector"] > QFrame { @@ -1529,7 +1528,7 @@ def styleColor( key ) : padding: 2px; } - *[gafferClass="GafferUI.EditScopeUI.EditScopePlugValueWidget"][editScopeActive="true"] QPushButton[gafferWithFrame="true"][gafferMenuIndicator="true"] + *[gafferClass="GafferUI.EditScopeUI.EditScopePlugValueWidget"] QPushButton[gafferWithFrame="true"][gafferMenuIndicator="true"] { border: 1px solid rgb( 46, 75, 107 ); border-top-color: rgb( 75, 113, 155 ); From 4afbf3539d3717870600c0c44a9803c5806d29d1 Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Sat, 2 Nov 2024 19:03:42 +1100 Subject: [PATCH 05/33] EditScopePlugValueWidget : Show checkBox for `None` --- Changes.md | 1 + python/GafferUI/EditScopeUI.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Changes.md b/Changes.md index ad3e2f2dafb..28b01957f0d 100644 --- a/Changes.md +++ b/Changes.md @@ -18,6 +18,7 @@ Improvements - Removed the dark background. - Changed the menu button color to be always blue. - Removed the "Navigation Arrow" button from the right side of the Edit Scope menu. Its actions have been relocated to a "Show Edits" submenu of the Edit Scope menu. + - The "None" menu item now displays a checkbox when chosen. Fixes ----- diff --git a/python/GafferUI/EditScopeUI.py b/python/GafferUI/EditScopeUI.py index 1e89acbc719..936337102f2 100644 --- a/python/GafferUI/EditScopeUI.py +++ b/python/GafferUI/EditScopeUI.py @@ -257,6 +257,10 @@ def __connectEditScope( self, editScope, *ignored ) : self.getPlug().setInput( editScope["out"] ) + def __connectPlug( self, plug, *ignored ) : + + self.getPlug().setInput( plug ) + def __inputNode( self ) : node = self.getPlug().node() @@ -378,7 +382,11 @@ def __menuDefinition( self ) : result.append( "/__NoneDivider__", { "divider" : True } ) result.append( - "/None", { "command" : functools.partial( self.getPlug().setInput, None ) }, + "/None", + { + "command" : functools.partial( Gaffer.WeakMethod( self.__connectPlug ), None ), + "checkBox" : self.getPlug().getInput() == None, + }, ) if currentEditScope is not None : From 61955c867ab75a989865501f0d506a8ead3676b0 Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:28:52 +1100 Subject: [PATCH 06/33] EditScopePlugValueWidget : Rename "None" to "Source" In so doing, we change the "Edit Scope" label to "Edit Target" to clarify that "Source" mode doesn't target an Edit Scope. --- Changes.md | 3 ++- python/GafferUI/EditScopeUI.py | 11 ++++++----- resources/graphics.py | 1 + resources/graphics.svg | 15 +++++++++++++++ 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/Changes.md b/Changes.md index 28b01957f0d..ca7c4aa925b 100644 --- a/Changes.md +++ b/Changes.md @@ -18,7 +18,8 @@ Improvements - Removed the dark background. - Changed the menu button color to be always blue. - Removed the "Navigation Arrow" button from the right side of the Edit Scope menu. Its actions have been relocated to a "Show Edits" submenu of the Edit Scope menu. - - The "None" menu item now displays a checkbox when chosen. + - Renamed "None" mode to "Source" and added icon. + - The "Source" menu item now displays a checkbox when chosen. Fixes ----- diff --git a/python/GafferUI/EditScopeUI.py b/python/GafferUI/EditScopeUI.py index 936337102f2..ef242bbe9c7 100644 --- a/python/GafferUI/EditScopeUI.py +++ b/python/GafferUI/EditScopeUI.py @@ -124,7 +124,7 @@ def __init__( self, plug, **kw ) : GafferUI.PlugValueWidget.__init__( self, self.__listContainer, plug, **kw ) with self.__listContainer : - GafferUI.Label( "Edit Scope" ) + GafferUI.Label( "Edit Target" ) self.__busyWidget = GafferUI.BusyWidget( size = 18 ) self.__busyWidget.setVisible( False ) self.__menuButton = GafferUI.MenuButton( @@ -215,14 +215,14 @@ def __acquireContextTracker( self ) : def __updateMenuButton( self ) : editScope = self.__editScope() - self.__menuButton.setText( editScope.getName() if editScope is not None else "None" ) + self.__menuButton.setText( editScope.getName() if editScope is not None else "Source" ) if editScope is not None : self.__menuButton.setImage( self.__editScopeSwatch( editScope ) if not self.__unusableReason( editScope ) else "warningSmall.png" ) else : - self.__menuButton.setImage( None ) + self.__menuButton.setImage( "menuSource.png" ) def __editScopeNameChanged( self, editScope, oldName ) : @@ -380,12 +380,13 @@ def __menuDefinition( self ) : result.append( "/__RefreshDivider__", { "divider" : True } ) result.append( "/Refresh", { "command" : Gaffer.WeakMethod( self.__refreshMenu ) } ) - result.append( "/__NoneDivider__", { "divider" : True } ) + result.append( "/__SourceDivider__", { "divider" : True } ) result.append( - "/None", + "/Source", { "command" : functools.partial( Gaffer.WeakMethod( self.__connectPlug ), None ), "checkBox" : self.getPlug().getInput() == None, + "icon" : "menuSource.png", }, ) diff --git a/resources/graphics.py b/resources/graphics.py index 2f51a4e4914..5f065971bf3 100644 --- a/resources/graphics.py +++ b/resources/graphics.py @@ -413,6 +413,7 @@ "menuChecked", "menuIndicator", "menuIndicatorDisabled", + "menuSource", ] }, diff --git a/resources/graphics.svg b/resources/graphics.svg index 3061a6f3362..e064f6b487c 100644 --- a/resources/graphics.svg +++ b/resources/graphics.svg @@ -3457,6 +3457,15 @@ y="79" transform="matrix(0,1,1,0,0,0)" inkscape:label="menuBreadCrumb" /> + + Date: Tue, 5 Nov 2024 15:32:24 +1100 Subject: [PATCH 07/33] EditScopePlugValueWidget : Improve presentation of edit scope menu Add "Targets" divider, and "No EditScopes Available" menu item when no edit scopes are available. --- Changes.md | 1 + python/GafferUI/EditScopeUI.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/Changes.md b/Changes.md index ca7c4aa925b..953ee8e1d0c 100644 --- a/Changes.md +++ b/Changes.md @@ -20,6 +20,7 @@ Improvements - Removed the "Navigation Arrow" button from the right side of the Edit Scope menu. Its actions have been relocated to a "Show Edits" submenu of the Edit Scope menu. - Renamed "None" mode to "Source" and added icon. - The "Source" menu item now displays a checkbox when chosen. + - Added a "No EditScopes Available" menu item that is displayed when no upstream EditScopes are available. Fixes ----- diff --git a/python/GafferUI/EditScopeUI.py b/python/GafferUI/EditScopeUI.py index ef242bbe9c7..5d5572ff12e 100644 --- a/python/GafferUI/EditScopeUI.py +++ b/python/GafferUI/EditScopeUI.py @@ -299,6 +299,7 @@ def __activeEditScopes( self ) : def __buildMenu( self, path, currentEditScope ) : result = IECore.MenuDefinition() + result.append( "/__TargetsDivider__", { "divider" : True, "label" : "Edit Targets" } ) for childPath in path.children() : itemName = childPath[-1] @@ -345,6 +346,9 @@ def __buildMenu( self, path, currentEditScope ) : } ) + if result.size() == 1 : + result.append( "No EditScopes Available", { "active" : False } ) + return result def __menuDefinition( self ) : From d3911c3b7517851a15407c103f1f18d3bdc19434 Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:55:08 +1100 Subject: [PATCH 08/33] CompoundEditor : Add Settings node to hold a global choice of EditScope --- python/GafferUI/CompoundEditor.py | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/python/GafferUI/CompoundEditor.py b/python/GafferUI/CompoundEditor.py index eaada12bb6c..0ec4ac6f41f 100644 --- a/python/GafferUI/CompoundEditor.py +++ b/python/GafferUI/CompoundEditor.py @@ -53,6 +53,16 @@ class CompoundEditor( GafferUI.Editor ) : + class Settings( GafferUI.Editor.Settings ) : + + def __init__( self ) : + + GafferUI.Editor.Settings.__init__( self ) + + self["editScope"] = Gaffer.Plug() + + IECore.registerRunTimeTyped( Settings, typeName = "GafferUI::CompoundEditor::Settings" ) + # The CompoundEditor constructor args are considered 'private', used only # by the persistent layout system. def __init__( self, scriptNode, _state={}, **kw ) : @@ -1660,3 +1670,26 @@ def __getBookmarkSet( self ) : return nodeSet return None + +Gaffer.Metadata.registerNode( + + CompoundEditor.Settings, + + plugs = { + + "*" : [ + + "label", "", + + ], + + "editScope" : [ + + "plugValueWidget:type", "GafferUI.EditScopeUI.EditScopePlugValueWidget", + "layout:width", 185, + + ], + + } + +) From 1bc1318fdba384cfdb7ecba223de469cf4060215 Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:58:41 +1100 Subject: [PATCH 09/33] EditScopePlugValueWidget : Support settings nodes without an `in` plug Such as the one newly added to the CompoundEditor. --- python/GafferUI/EditScopeUI.py | 29 +++++++++++++++++++++-------- python/GafferUI/PlugValueWidget.py | 14 ++++++++++++-- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/python/GafferUI/EditScopeUI.py b/python/GafferUI/EditScopeUI.py index 5d5572ff12e..7de13ba2a22 100644 --- a/python/GafferUI/EditScopeUI.py +++ b/python/GafferUI/EditScopeUI.py @@ -201,7 +201,11 @@ def __plugInputChanged( self, plug ) : def __acquireContextTracker( self ) : - self.__contextTracker = GafferUI.ContextTracker.acquire( self.__inputNode() ) + if "in" in self.getPlug().node() : + self.__contextTracker = GafferUI.ContextTracker.acquire( self.__inputNode() ) + else : + self.__contextTracker = GafferUI.ContextTracker.acquireForFocus( self.getPlug() ) + self.__contextTrackerChangedConnection = self.__contextTracker.changedSignal().connect( Gaffer.WeakMethod( self.__contextTrackerChanged ), scoped = True ) @@ -265,20 +269,29 @@ def __inputNode( self ) : node = self.getPlug().node() # We assume that our plug is on a node dedicated to holding settings for the - # UI, and that it has an `in` plug that is connected to the node in the graph + # UI, and if the node has an `in` plug, it is connected to the node in the graph # that is being viewed. We start our node graph traversal at the viewed node # (we can't start at _this_ node, as then we will visit our own input connection # which may no longer be upstream of the viewed node). - if node["in"].getInput() is None : - return None + if "in" in node : + if node["in"].getInput() is None : + return None + + inputNode = node["in"].getInput().node() + else : + # Our node doesn't have an `in` plug so fall back to using the focus node + # as the starting point for node graph traversal. + inputNode = self.scriptNode().getFocus() - inputNode = node["in"].getInput().node() if not isinstance( inputNode, Gaffer.EditScope ) and isinstance( inputNode, Gaffer.SubGraph ) : # If we're starting from a SubGraph then attempt to begin the search from the # first input of the node's output so we can find any Edit Scopes within. - output = node["in"].getInput().getInput() - if output is not None and inputNode.isAncestorOf( output ) : - return output.node() + output = next( + ( p for p in Gaffer.Plug.RecursiveOutputRange( inputNode ) if not p.getName().startswith( "__" ) ), + None + ) + if output is not None and output.getInput() is not None and inputNode.isAncestorOf( output.getInput() ) : + return output.getInput().node() return inputNode diff --git a/python/GafferUI/PlugValueWidget.py b/python/GafferUI/PlugValueWidget.py index 253eed4e732..c4e1aca3304 100644 --- a/python/GafferUI/PlugValueWidget.py +++ b/python/GafferUI/PlugValueWidget.py @@ -148,8 +148,18 @@ def scriptNode( self ) : if not len( self.__plugs ) : return None - else : - return next( iter( self.__plugs ) ).ancestor( Gaffer.ScriptNode ) + + plug = next( iter( self.__plugs ) ) + scriptNode = plug.ancestor( Gaffer.ScriptNode ) + if scriptNode is not None : + return scriptNode + + # The plug may otherwise be on an `Editor.Settings` node, + # which receives a connection from the ScriptNode. + if "__scriptNode" in plug.node() and plug.node()["__scriptNode"].getInput() is not None : + return plug.node()["__scriptNode"].getInput().node() + + return None ## Should be reimplemented to return True if this widget includes # some sort of labelling for the plug. This is used to prevent From ab6d440c8c90e998bc3fc93a117905517db118c6 Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:42:51 +1100 Subject: [PATCH 10/33] SceneEditor : Add `editScope()` method We're about to add support for editors following the global edit target, which results in the Edit Scope node not being directly connected to the "editScope" plug on the Editor's Settings node. This centralises the behaviour so each editor doesn't need to implement its own search, though may be a little odd as not every SceneEditor has an "editScope" plug on its Settings node... --- Changes.md | 1 + python/GafferSceneUI/LightEditor.py | 7 +++---- python/GafferSceneUI/RenderPassEditor.py | 24 ++++++++++-------------- python/GafferSceneUI/SceneEditor.py | 10 ++++++++++ 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/Changes.md b/Changes.md index 953ee8e1d0c..655eb0e3dee 100644 --- a/Changes.md +++ b/Changes.md @@ -40,6 +40,7 @@ API - OpenColorIOConfigPlugUI : - Added `connectToApplication()` function. - Deprecated `connect()` function. Use `connectToApplication()` instead. +- SceneEditor : Added `editScope()` method. 1.5.0.1 (relative to 1.5.0.0) ======= diff --git a/python/GafferSceneUI/LightEditor.py b/python/GafferSceneUI/LightEditor.py index c0e09a45601..11e67106066 100644 --- a/python/GafferSceneUI/LightEditor.py +++ b/python/GafferSceneUI/LightEditor.py @@ -311,9 +311,8 @@ def __columnContextMenuSignal( self, column, pathListing, menuDefinition ) : # or unintuitive deleteEnabled = True inputNode = self.settings()["in"].getInput().node() - editScopeInput = self.settings()["editScope"].getInput() - if editScopeInput is not None : - editScopeNode = editScopeInput.node() + editScopeNode = self.editScope() + if editScopeNode is not None : if inputNode != editScopeNode and editScopeNode not in Gaffer.NodeAlgo.upstreamNodes( inputNode ) : # Edit scope is downstream of input deleteEnabled = False @@ -368,7 +367,7 @@ def __deleteLights( self, *unused ) : # There may be multiple columns with a selection, but we only operate on the name column. selection = self.__pathListing.getSelection()[0] - editScope = self.settings()["editScope"].getInput().node() + editScope = self.editScope() with Gaffer.UndoScope( editScope.ancestor( Gaffer.ScriptNode ) ) : GafferScene.EditScopeAlgo.setPruned( editScope, selection, True ) diff --git a/python/GafferSceneUI/RenderPassEditor.py b/python/GafferSceneUI/RenderPassEditor.py index 330a9f7c64b..690ebad746b 100644 --- a/python/GafferSceneUI/RenderPassEditor.py +++ b/python/GafferSceneUI/RenderPassEditor.py @@ -402,13 +402,11 @@ def __canEditRenderPasses( self, editScope = None ) : if editScope is None : # No edit scope provided so use the current selected - editScopeInput = self.settings()["editScope"].getInput() - if editScopeInput is None : + editScope = self.editScope() + if editScope is None : # No edit scope selected return False - editScope = editScopeInput.node() - if inputNode != editScope and editScope not in Gaffer.NodeAlgo.upstreamNodes( inputNode ) : # Edit scope is downstream of input return False @@ -456,11 +454,10 @@ def __deleteSelectedRenderPasses( self ) : if len( selectedRenderPasses ) == 0 : return - editScopeInput = self.settings()["editScope"].getInput() - if editScopeInput is None : + editScope = self.editScope() + if editScope is None : return - editScope = editScopeInput.node() localRenderPasses = [] renderPassesProcessor = editScope.acquireProcessor( "RenderPasses", createIfNecessary = False ) if renderPassesProcessor is not None : @@ -528,10 +525,9 @@ def __renderPassNames( self, plug ) : def __renderPassCreationDialogue( self ) : - editScopeInput = self.settings()["editScope"].getInput() - assert( editScopeInput is not None ) + editScope = self.editScope() + assert( editScope is not None ) - editScope = editScopeInput.node() dialogue = _RenderPassCreationDialogue( self.__renderPassNames( self.settings()["in"] ), editScope ) renderPassName = dialogue.waitForRenderPassName( parentWindow = self.ancestor( GafferUI.Window ) ) if renderPassName : @@ -564,14 +560,14 @@ def __removeButtonClicked( self, button ) : def __metadataChanged( self, nodeTypeId, key, node ) : - editScopeInput = self.settings()["editScope"].getInput() - if editScopeInput is None : + editScope = self.editScope() + if editScope is None : return - renderPassesProcessor = editScopeInput.node().acquireProcessor( "RenderPasses", createIfNecessary = False ) + renderPassesProcessor = editScope.acquireProcessor( "RenderPasses", createIfNecessary = False ) if ( - Gaffer.MetadataAlgo.readOnlyAffectedByChange( editScopeInput, nodeTypeId, key, node ) or + Gaffer.MetadataAlgo.readOnlyAffectedByChange( editScope, nodeTypeId, key, node ) or ( renderPassesProcessor and Gaffer.MetadataAlgo.readOnlyAffectedByChange( renderPassesProcessor, nodeTypeId, key, node ) ) ) : self.__updateButtonStatus() diff --git a/python/GafferSceneUI/SceneEditor.py b/python/GafferSceneUI/SceneEditor.py index 6e4cea1f6ea..cc28ed16605 100644 --- a/python/GafferSceneUI/SceneEditor.py +++ b/python/GafferSceneUI/SceneEditor.py @@ -65,6 +65,16 @@ def __init__( self, topLevelWidget, scriptNode, **kw ) : self.__parentingConnections = {} + def editScope( self ) : + + if not "editScope" in self.settings() : + return None + + return Gaffer.PlugAlgo.findSource( + self.settings()["editScope"], + lambda plug : plug.node() if isinstance( plug.node(), Gaffer.EditScope ) else None + ) + def _updateFromSet( self ) : # Find ScenePlugs and connect them to `settings()["in"]`. From 22910c81cb93c1d2badf3b61c761aa0ba3b05d2d Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Fri, 1 Nov 2024 11:09:39 +1100 Subject: [PATCH 11/33] Inspector, View : Find indirectly connected edit scopes The introduction of Editors following the global edit target will mean that the EditScope node may be indirectly connected via the CompoundEditor's Settings node, so we need to traverse to find it. --- src/GafferSceneUI/Inspector.cpp | 7 ++++++- src/GafferUI/View.cpp | 17 +++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/GafferSceneUI/Inspector.cpp b/src/GafferSceneUI/Inspector.cpp index 03d426da761..ce4f4b39eef 100644 --- a/src/GafferSceneUI/Inspector.cpp +++ b/src/GafferSceneUI/Inspector.cpp @@ -454,7 +454,12 @@ Gaffer::EditScope *Inspector::targetEditScope() const return nullptr; } - return runTimeCast( m_editScope->getInput()->node() ); + return PlugAlgo::findSource( + m_editScope.get(), + [] ( Plug *plug ) { + return runTimeCast( plug->node() ); + } + ); } void Inspector::editScopeInputChanged( const Gaffer::Plug *plug ) diff --git a/src/GafferUI/View.cpp b/src/GafferUI/View.cpp index 6f8bdc36dd2..091deb62f1e 100644 --- a/src/GafferUI/View.cpp +++ b/src/GafferUI/View.cpp @@ -153,14 +153,23 @@ const ToolContainer *View::tools() const Gaffer::EditScope *View::editScope() { - Plug *p = editScopePlug()->getInput(); - return p ? p->parent() : nullptr; + return PlugAlgo::findSource( + editScopePlug(), + [] ( Plug *plug ) { + return runTimeCast( plug->node() ); + } + ); } const Gaffer::EditScope *View::editScope() const { - const Plug *p = editScopePlug()->getInput(); - return p ? p->parent() : nullptr; + // Cheeky cast to avoid a duplicate `PlugAlgo::findSource( const... )` implementation + return PlugAlgo::findSource( + const_cast( editScopePlug() ), + [] ( Plug *plug ) { + return runTimeCast( plug->node() ); + } + ); } const Gaffer::Context *View::context() const From d53fae0d1c405f339854b878866c1f7aff6cd141 Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:56:39 +1100 Subject: [PATCH 12/33] ScriptWindow : Show CompoundEditor settings in menu bar --- Changes.md | 5 +++ python/GafferUI/ScriptWindow.py | 25 +++++++++++- python/GafferUI/_StyleSheet.py | 4 ++ python/GafferUITest/MenuBarTest.py | 61 ++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 2 deletions(-) diff --git a/Changes.md b/Changes.md index 655eb0e3dee..3f0d9effe76 100644 --- a/Changes.md +++ b/Changes.md @@ -1,6 +1,11 @@ 1.5.x.x (relative to 1.5.0.1) ======= +Features +-------- + +- EditScope : Introduced the Global Edit Target, providing script-level control over the target used by editors. The Global Edit Target can be set from a new "Edit Target" menu in the menu bar, which displays all available edit targets upstream of the focus node. + Improvements ------------ diff --git a/python/GafferUI/ScriptWindow.py b/python/GafferUI/ScriptWindow.py index f11753f6183..170b2bf6be9 100644 --- a/python/GafferUI/ScriptWindow.py +++ b/python/GafferUI/ScriptWindow.py @@ -37,6 +37,8 @@ import weakref +import imath + import IECore import Gaffer @@ -56,8 +58,14 @@ def __init__( self, script, **kw ) : self.__listContainer = GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Vertical, spacing = 0 ) + self.__menuContainer = GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) + self.__listContainer.append( self.__menuContainer ) + menuDefinition = self.menuDefinition( script.applicationRoot() ) if script.applicationRoot() else IECore.MenuDefinition() - self.__listContainer.append( GafferUI.MenuBar( menuDefinition ) ) + self.__menuBar = GafferUI.MenuBar( menuDefinition ) + self.__menuBar.addShortcutTarget( self ) + self.__menuContainer.append( self.__menuBar ) + self.__menuContainer._qtWidget().setObjectName( "gafferMenuBarWidgetContainer" ) # Must parent `__listContainer` to the window before setting the layout, # because `CompoundEditor.__parentChanged` needs to find the ancestor # ScriptWindow. @@ -76,7 +84,7 @@ def __init__( self, script, **kw ) : def menuBar( self ) : - return self.__listContainer[0] + return self.__menuContainer[0] def scriptNode( self ) : @@ -90,6 +98,19 @@ def setLayout( self, compoundEditor ) : assert( compoundEditor.scriptNode().isSame( self.scriptNode() ) ) self.__listContainer.append( compoundEditor, expand=True ) + if len( self.__menuContainer ) > 1 : + del self.__menuContainer[1:] + + if hasattr( compoundEditor, "settings" ) : + self.__menuContainer.append( + GafferUI.PlugLayout( + compoundEditor.settings(), + orientation = GafferUI.ListContainer.Orientation.Horizontal, + rootSection = "Settings" + ) + ) + self.__menuContainer.append( GafferUI.Spacer( imath.V2i( 0 ) ) ) + def getLayout( self ) : return self.__listContainer[1] diff --git a/python/GafferUI/_StyleSheet.py b/python/GafferUI/_StyleSheet.py index bdf01fa693f..3c8872c105c 100644 --- a/python/GafferUI/_StyleSheet.py +++ b/python/GafferUI/_StyleSheet.py @@ -283,6 +283,10 @@ def styleColor( key ) : padding: 5px 8px 5px 8px; } + #gafferMenuBarWidgetContainer { + background-color: $backgroundDarkest; + } + QMenu { border: 1px solid $backgroundDark; padding-bottom: 5px; diff --git a/python/GafferUITest/MenuBarTest.py b/python/GafferUITest/MenuBarTest.py index 22b88e9a8e0..7f860b9f05f 100644 --- a/python/GafferUITest/MenuBarTest.py +++ b/python/GafferUITest/MenuBarTest.py @@ -236,6 +236,67 @@ def buildMenu( identifier ) : self.assertEqual( callCounts, { "MenuA" : 1, "MenuB" : 0 } ) + def testAddShortcutTarget( self ) : + + commandInvocations = [] + def command( arg ) : + + commandInvocations.append( arg ) + + withTargetDefinition = IECore.MenuDefinition( [ + ( "/test/command", { "command" : functools.partial( command, "withTarget" ), "shortCut" : "Ctrl+A" } ), + ] ) + + withoutTargetDefinition = IECore.MenuDefinition( [ + ( "/test/command", { "command" : functools.partial( command, "withoutTarget" ), "shortCut" : "Ctrl+A" } ), + ] ) + + with GafferUI.Window() as window : + with GafferUI.ListContainer() : + with GafferUI.ListContainer() : + menu = GafferUI.MenuBar( withTargetDefinition ) + withTargetLabel = GafferUI.Label( "test" ) + with GafferUI.ListContainer() : + GafferUI.MenuBar( withoutTargetDefinition ) + withoutTargetLabel = GafferUI.Label( "test" ) + with GafferUI.ListContainer() : + otherLabel = GafferUI.Label( "test" ) + + window.setVisible( True ) + self.waitForIdle( 1000 ) + + self.__simulateShortcut( withTargetLabel ) + self.waitForIdle( 1000 ) + self.assertEqual( len( commandInvocations ), 1 ) + self.assertEqual( commandInvocations[0], "withTarget" ) + + self.__simulateShortcut( withoutTargetLabel ) + self.waitForIdle( 1000 ) + self.assertEqual( len( commandInvocations ), 2 ) + self.assertEqual( commandInvocations[1], "withoutTarget" ) + + self.__simulateShortcut( otherLabel ) + self.waitForIdle( 1000 ) + self.assertEqual( len( commandInvocations ), 2 ) + self.assertEqual( commandInvocations[1], "withoutTarget" ) + + self.__simulateShortcut( window ) + self.waitForIdle( 1000 ) + self.assertEqual( len( commandInvocations ), 2 ) + self.assertEqual( commandInvocations[1], "withoutTarget" ) + + menu.addShortcutTarget( window ) + + self.__simulateShortcut( otherLabel ) + self.waitForIdle( 1000 ) + self.assertEqual( len( commandInvocations ), 3 ) + self.assertEqual( commandInvocations[2], "withTarget" ) + + self.__simulateShortcut( window ) + self.waitForIdle( 1000 ) + self.assertEqual( len( commandInvocations ), 4 ) + self.assertEqual( commandInvocations[3], "withTarget" ) + def __simulateShortcut( self, widget ) : if Qt.__binding__ in ( "PySide2", "PyQt5" ) : From 3a8c5f8003ce37b4bc9843b2c7fb890fb72ece7c Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:50:55 +1100 Subject: [PATCH 13/33] ScriptWindow : Transfer EditScope when changing layout --- python/GafferUI/ScriptWindow.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/python/GafferUI/ScriptWindow.py b/python/GafferUI/ScriptWindow.py index 170b2bf6be9..6d715cc47f5 100644 --- a/python/GafferUI/ScriptWindow.py +++ b/python/GafferUI/ScriptWindow.py @@ -92,7 +92,11 @@ def scriptNode( self ) : def setLayout( self, compoundEditor ) : + # When changing layout we need to manually transfer the edit scope + # from an existing CompoundEditor to the new one. + currentEditScope = None if len( self.__listContainer ) > 1 : + currentEditScope = self.getLayout().settings()["editScope"].getInput() del self.__listContainer[1] assert( compoundEditor.scriptNode().isSame( self.scriptNode() ) ) @@ -101,15 +105,16 @@ def setLayout( self, compoundEditor ) : if len( self.__menuContainer ) > 1 : del self.__menuContainer[1:] - if hasattr( compoundEditor, "settings" ) : - self.__menuContainer.append( - GafferUI.PlugLayout( - compoundEditor.settings(), - orientation = GafferUI.ListContainer.Orientation.Horizontal, - rootSection = "Settings" - ) + if currentEditScope is not None : + compoundEditor.settings()["editScope"].setInput( currentEditScope ) + self.__menuContainer.append( + GafferUI.PlugLayout( + compoundEditor.settings(), + orientation = GafferUI.ListContainer.Orientation.Horizontal, + rootSection = "Settings" ) - self.__menuContainer.append( GafferUI.Spacer( imath.V2i( 0 ) ) ) + ) + self.__menuContainer.append( GafferUI.Spacer( imath.V2i( 0 ) ) ) def getLayout( self ) : From 9195afc6113cc966a1a65806607bb6abbda3d0cb Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:29:29 +1100 Subject: [PATCH 14/33] EditScopePlugValueWidget : Use metadata to control label visibility --- Changes.md | 1 + python/GafferSceneUI/AttributeEditor.py | 2 +- python/GafferSceneUI/LightEditor.py | 2 +- python/GafferSceneUI/RenderPassEditor.py | 2 +- python/GafferSceneUI/SceneViewUI.py | 4 ++-- python/GafferUI/CompoundEditor.py | 1 + python/GafferUI/EditScopeUI.py | 11 ++++++++++- 7 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Changes.md b/Changes.md index 3f0d9effe76..518952d52db 100644 --- a/Changes.md +++ b/Changes.md @@ -23,6 +23,7 @@ Improvements - Removed the dark background. - Changed the menu button color to be always blue. - Removed the "Navigation Arrow" button from the right side of the Edit Scope menu. Its actions have been relocated to a "Show Edits" submenu of the Edit Scope menu. + - Hid the label. It can be made visible for a specific plug by registering `editScopePlugValueWidget:showLabel` metadata with a value of `True`. - Renamed "None" mode to "Source" and added icon. - The "Source" menu item now displays a checkbox when chosen. - Added a "No EditScopes Available" menu item that is displayed when no upstream EditScopes are available. diff --git a/python/GafferSceneUI/AttributeEditor.py b/python/GafferSceneUI/AttributeEditor.py index dec15a06503..69928f9d5c1 100644 --- a/python/GafferSceneUI/AttributeEditor.py +++ b/python/GafferSceneUI/AttributeEditor.py @@ -322,7 +322,7 @@ def __transferSelectionFromScriptNode( self ) : "editScope" : [ "plugValueWidget:type", "GafferUI.EditScopeUI.EditScopePlugValueWidget", - "layout:width", 185, + "layout:width", 130, ], diff --git a/python/GafferSceneUI/LightEditor.py b/python/GafferSceneUI/LightEditor.py index 11e67106066..0eddd192804 100644 --- a/python/GafferSceneUI/LightEditor.py +++ b/python/GafferSceneUI/LightEditor.py @@ -413,7 +413,7 @@ def __deleteLights( self, *unused ) : "editScope" : [ "plugValueWidget:type", "GafferUI.EditScopeUI.EditScopePlugValueWidget", - "layout:width", 185, + "layout:width", 130, ], diff --git a/python/GafferSceneUI/RenderPassEditor.py b/python/GafferSceneUI/RenderPassEditor.py index 690ebad746b..e09423df49a 100644 --- a/python/GafferSceneUI/RenderPassEditor.py +++ b/python/GafferSceneUI/RenderPassEditor.py @@ -634,7 +634,7 @@ def __updateButtonStatus( self, *unused ) : "editScope" : [ "plugValueWidget:type", "GafferUI.EditScopeUI.EditScopePlugValueWidget", - "layout:width", 185, + "layout:width", 130, ], diff --git a/python/GafferSceneUI/SceneViewUI.py b/python/GafferSceneUI/SceneViewUI.py index fc6637e753b..b7df63e13ea 100644 --- a/python/GafferSceneUI/SceneViewUI.py +++ b/python/GafferSceneUI/SceneViewUI.py @@ -105,7 +105,7 @@ def __rendererPlugActivator( plug ) : "plugValueWidget:type", "GafferUI.EditScopeUI.EditScopePlugValueWidget", "toolbarLayout:index", -1, - "toolbarLayout:width", 185, + "toolbarLayout:width", 130, ], @@ -1226,7 +1226,7 @@ class _EditScopeBalancingSpacer( GafferUI.Spacer ) : def __init__( self, sceneView, **kw ) : # EditScope width - pause button - spacer - spinner - renderer - width = 185 - 25 - 4 - 20 - 100 + width = 130 - 25 - 4 - 20 - 100 GafferUI.Spacer.__init__( self, diff --git a/python/GafferUI/CompoundEditor.py b/python/GafferUI/CompoundEditor.py index 0ec4ac6f41f..bc96d623aa9 100644 --- a/python/GafferUI/CompoundEditor.py +++ b/python/GafferUI/CompoundEditor.py @@ -1687,6 +1687,7 @@ def __getBookmarkSet( self ) : "plugValueWidget:type", "GafferUI.EditScopeUI.EditScopePlugValueWidget", "layout:width", 185, + "editScopePlugValueWidget:showLabel", True, ], diff --git a/python/GafferUI/EditScopeUI.py b/python/GafferUI/EditScopeUI.py index 7de13ba2a22..f303de90968 100644 --- a/python/GafferUI/EditScopeUI.py +++ b/python/GafferUI/EditScopeUI.py @@ -124,7 +124,7 @@ def __init__( self, plug, **kw ) : GafferUI.PlugValueWidget.__init__( self, self.__listContainer, plug, **kw ) with self.__listContainer : - GafferUI.Label( "Edit Target" ) + self.__label = GafferUI.Label( "Edit Target" ) self.__busyWidget = GafferUI.BusyWidget( size = 18 ) self.__busyWidget.setVisible( False ) self.__menuButton = GafferUI.MenuButton( @@ -144,6 +144,7 @@ def __init__( self, plug, **kw ) : # run the default dropSignal handler from PlugValueWidget. self.dropSignal().connectFront( Gaffer.WeakMethod( self.__drop ) ) + self.__updateLabelVisibility() self.__updatePlugInputChangedConnection() self.__acquireContextTracker() @@ -169,6 +170,10 @@ def getToolTip( self ) : else : return "Edits will be made in {}.".format( editScope.getName() ) + def _updateFromMetadata( self ) : + + self.__updateLabelVisibility() + # 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 ) : @@ -216,6 +221,10 @@ def __acquireContextTracker( self ) : # We'll update later in `__contextTrackerChanged()`. pass + def __updateLabelVisibility( self ) : + + self.__label.setVisible( Gaffer.Metadata.value( self.getPlug(), "editScopePlugValueWidget:showLabel" ) or False ) + def __updateMenuButton( self ) : editScope = self.__editScope() From fd47954b666263644f4bf8a7b61bf0c14cc58d74 Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Mon, 11 Nov 2024 17:10:57 +1100 Subject: [PATCH 15/33] SceneViewUI : Improve balancing Spacers Add a balancing spacer to the right side of the toolbar, this is necessary now that the "editScope" plug's widget is narrower than the combined widgets on the left hand side. Make the balancing spacers respond better to changes in width of the "editScope" plug's widget, as this may be varied by studios... --- python/GafferSceneUI/SceneViewUI.py | 35 +++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/python/GafferSceneUI/SceneViewUI.py b/python/GafferSceneUI/SceneViewUI.py index b7df63e13ea..b3d34a1d92c 100644 --- a/python/GafferSceneUI/SceneViewUI.py +++ b/python/GafferSceneUI/SceneViewUI.py @@ -74,6 +74,9 @@ def __rendererPlugActivator( plug ) : "toolbarLayout:customWidget:StateWidget:section", "Top", "toolbarLayout:customWidget:StateWidget:index", 0, + ## \todo These balancing spacers are horrendous. We should be able to improve PlugLayout + # to support arranging plugs horizontally in sections with alignment as well as other + # niceties such as collapsible sections, etc. "toolbarLayout:customWidget:EditScopeBalancingSpacer:widgetType", "GafferSceneUI.SceneViewUI._EditScopeBalancingSpacer", "toolbarLayout:customWidget:EditScopeBalancingSpacer:section", "Top", "toolbarLayout:customWidget:EditScopeBalancingSpacer:index", 1, @@ -86,6 +89,10 @@ def __rendererPlugActivator( plug ) : "toolbarLayout:customWidget:CenterRightSpacer:section", "Top", "toolbarLayout:customWidget:CenterRightSpacer:index", -2, + "toolbarLayout:customWidget:RightEditScopeBalancingSpacer:widgetType", "GafferSceneUI.SceneViewUI._RightEditScopeBalancingSpacer", + "toolbarLayout:customWidget:RightEditScopeBalancingSpacer:section", "Top", + "toolbarLayout:customWidget:RightEditScopeBalancingSpacer:index", -2, + "nodeToolbar:right:type", "GafferUI.StandardNodeToolbar.right", "toolbarLayout:customWidget:InspectorTopSpacer:widgetType", "GafferSceneUI.SceneViewUI._InspectorTopSpacer", @@ -1221,13 +1228,37 @@ def __plugValueWidgetContextMenu( menuDefinition, plugValueWidget ) : # _Spacers ########################################################################## +# This Spacer balances the left side of the toolbar when +# the EditScope menu is wider than the tools on the left class _EditScopeBalancingSpacer( GafferUI.Spacer ) : def __init__( self, sceneView, **kw ) : - # EditScope width - pause button - spacer - spinner - renderer - width = 130 - 25 - 4 - 20 - 100 + editScopeWidth = Gaffer.Metadata.value( sceneView["editScope"], "toolbarLayout:width" ) or 130 + # EditScope width + spacer - pause button - spacer - spinner - renderer + width = max( editScopeWidth + 4 - 25 - 4 - 20 - 100, 0 ) + GafferUI.Spacer.__init__( + self, + imath.V2i( 0 ), # Minimum + preferredSize = imath.V2i( width, 1 ), + maximumSize = imath.V2i( width, 1 ) + ) + +# This Spacer balances the right side of the toolbar when +# the EditScope menu is narrower than the tools on the left +class _RightEditScopeBalancingSpacer( GafferUI.Spacer ) : + def __init__( self, sceneView, **kw ) : + + editScopeWidth = Gaffer.Metadata.value( sceneView["editScope"], "toolbarLayout:width" ) or 130 + # pause button + spacer + spinner + renderer - spacer - EditScope width + width = max( 25 + 4 + 20 + 100 - 4 - editScopeWidth, 0 ) + GafferUI.Spacer.__init__( + self, + imath.V2i( 0 ), # Minimum + preferredSize = imath.V2i( width, 1 ), + maximumSize = imath.V2i( width, 1 ) + ) GafferUI.Spacer.__init__( self, imath.V2i( 0 ), # Minimum From 416bbde37bcb9aa8ced561dae274dbd60e35545b Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:58:58 +1100 Subject: [PATCH 16/33] EditScopePlugValueWidget : Support following the global edit target --- Changes.md | 1 + python/GafferUI/EditScopeUI.py | 38 +++++++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/Changes.md b/Changes.md index 518952d52db..e204623b34c 100644 --- a/Changes.md +++ b/Changes.md @@ -5,6 +5,7 @@ Features -------- - EditScope : Introduced the Global Edit Target, providing script-level control over the target used by editors. The Global Edit Target can be set from a new "Edit Target" menu in the menu bar, which displays all available edit targets upstream of the focus node. + - Individual editors can be overridden to use a specific edit target where necessary. An overridden editor can return to following the Global Edit Target via the new "Follow Global Edit Target" menu item. Improvements ------------ diff --git a/python/GafferUI/EditScopeUI.py b/python/GafferUI/EditScopeUI.py index f303de90968..877f9565b71 100644 --- a/python/GafferUI/EditScopeUI.py +++ b/python/GafferUI/EditScopeUI.py @@ -225,6 +225,23 @@ def __updateLabelVisibility( self ) : self.__label.setVisible( Gaffer.Metadata.value( self.getPlug(), "editScopePlugValueWidget:showLabel" ) or False ) + def __followingGlobalEditTarget( self ) : + + input = self.getPlug().getInput() + + return ( + input is not None and input.getName() == "editScope" and + isinstance( input.node(), GafferUI.Editor.Settings ) + ) + + def __globalEditTargetPlug( self ) : + + compoundEditor = self.ancestor( GafferUI.CompoundEditor ) + if compoundEditor is None : + return None + + return compoundEditor.settings()["editScope"] + def __updateMenuButton( self ) : editScope = self.__editScope() @@ -253,8 +270,10 @@ def __contextTrackerChanged( self, contextTracker ) : def __editScope( self ) : - input = self.getPlug().getInput() - return input.ancestor( Gaffer.EditScope ) if input is not None else None + return Gaffer.PlugAlgo.findSource( + self.getPlug(), + lambda plug : plug.node() if isinstance( plug.node(), Gaffer.EditScope ) else None + ) def __editScopePredicate( self, node ) : @@ -377,7 +396,9 @@ def __menuDefinition( self ) : currentEditScope = None if self.getPlug().getInput() is not None : - currentEditScope = self.getPlug().getInput().parent() + input = self.getPlug().getInput().parent() + if isinstance( input, Gaffer.EditScope ) : + currentEditScope = input activeEditScopes = self.__activeEditScopes() @@ -416,6 +437,17 @@ def __menuDefinition( self ) : }, ) + if self.__globalEditTargetPlug() is not None : + result.append( "/__FollowDivider__", { "divider" : True, "label" : "Options" } ) + result.append( + "/Follow Global Edit Target", + { + "command" : functools.partial( Gaffer.WeakMethod( self.__connectPlug ), self.__globalEditTargetPlug() ), + "checkBox" : self.__followingGlobalEditTarget(), + "description" : "Always use the global edit target.", + } + ) + if currentEditScope is not None : result.append( "/__ActionsDivider__", { "divider" : True, "label" : "Actions" } ) nodes = currentEditScope.processors() From edab0e1ba50003fd0bb54258de65f44ef854e0e7 Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:57:14 +1100 Subject: [PATCH 17/33] SceneEditor, Viewer : Follow global edit target by default --- Changes.md | 1 + python/GafferSceneUI/SceneEditor.py | 13 +++++++++++++ python/GafferUI/Viewer.py | 17 +++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/Changes.md b/Changes.md index e204623b34c..06fdf8c3c3d 100644 --- a/Changes.md +++ b/Changes.md @@ -5,6 +5,7 @@ Features -------- - EditScope : Introduced the Global Edit Target, providing script-level control over the target used by editors. The Global Edit Target can be set from a new "Edit Target" menu in the menu bar, which displays all available edit targets upstream of the focus node. + - Editors now follow the Global Edit Target by default, allowing for a simpler experience when switching multiple editors to a common target. - Individual editors can be overridden to use a specific edit target where necessary. An overridden editor can return to following the Global Edit Target via the new "Follow Global Edit Target" menu item. Improvements diff --git a/python/GafferSceneUI/SceneEditor.py b/python/GafferSceneUI/SceneEditor.py index cc28ed16605..7ac10cb3286 100644 --- a/python/GafferSceneUI/SceneEditor.py +++ b/python/GafferSceneUI/SceneEditor.py @@ -65,6 +65,9 @@ def __init__( self, topLevelWidget, scriptNode, **kw ) : self.__parentingConnections = {} + self.__globalEditTargetLinked = False + self.parentChangedSignal().connect( Gaffer.WeakMethod( self.__parentChanged ) ) + def editScope( self ) : if not "editScope" in self.settings() : @@ -134,6 +137,16 @@ def _titleFormat( self ) : _ellipsis = False ) + def __parentChanged( self, widget ) : + + if self.__globalEditTargetLinked or not "editScope" in self.settings() : + return + + compoundEditor = self.ancestor( GafferUI.CompoundEditor ) + if compoundEditor : + self.settings()["editScope"].setInput( compoundEditor.settings()["editScope"] ) + self.__globalEditTargetLinked = True + def __scenePlugParentChanged( self, plug, newParent ) : self._updateFromSet() diff --git a/python/GafferUI/Viewer.py b/python/GafferUI/Viewer.py index 8894765a92f..39f1db0bd1f 100644 --- a/python/GafferUI/Viewer.py +++ b/python/GafferUI/Viewer.py @@ -150,10 +150,12 @@ def __init__( self, scriptNode, **kw ) : self.__views = [] self.__currentView = None + self.__globalEditTargetLinked = False self.keyPressSignal().connect( Gaffer.WeakMethod( self.__keyPress ) ) self.contextMenuSignal().connect( Gaffer.WeakMethod( self.__contextMenu ) ) self.nodeSetChangedSignal().connect( Gaffer.WeakMethod( self.__updateViewportMessage ) ) + self.parentChangedSignal().connect( Gaffer.WeakMethod( self.__parentChanged ) ) self._updateFromSet() @@ -202,6 +204,7 @@ def _updateFromSet( self ) : self.__currentView = GafferUI.View.create( plug ) if self.__currentView is not None: Gaffer.NodeAlgo.applyUserDefaults( self.__currentView ) + self.__connectGlobalEditTarget( self.__currentView ) self.__views.append( self.__currentView ) # if we succeeded in getting a suitable view, then # don't bother checking the other plugs @@ -231,6 +234,20 @@ def _titleFormat( self ) : return GafferUI.NodeSetEditor._titleFormat( self, _maxNodes = 1, _reverseNodes = True, _ellipsis = False ) + def __connectGlobalEditTarget( self, view ) : + + compoundEditor = self.ancestor( GafferUI.CompoundEditor ) + if "editScope" in view and compoundEditor is not None : + view["editScope"].setInput( compoundEditor.settings()["editScope"] ) + self.__globalEditTargetLinked = True + + def __parentChanged( self, widget ) : + + if self.__globalEditTargetLinked or self.__currentView is None : + return + + self.__connectGlobalEditTarget( self.__currentView ) + def __primaryToolChanged( self, *unused ) : for toolbar in self.__toolToolbars : From d7fd88b14389744c6619f0c2ef296a03127ab9e1 Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Thu, 7 Nov 2024 12:03:20 +1100 Subject: [PATCH 18/33] EditScopePlugValueWidget : Shrink when following global edit target This makes the change in state more distinct when following the global edit target, as well as reducing the prominence of the widget to guide users to interact with the more visible global edit target. --- Changes.md | 1 + python/GafferSceneUI/SceneViewUI.py | 22 ++++++++++++++++++++++ python/GafferUI/EditScopeUI.py | 20 +++++++++++++++++++- python/GafferUI/PlugLayout.py | 3 +++ 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/Changes.md b/Changes.md index 06fdf8c3c3d..3d2ba723d2b 100644 --- a/Changes.md +++ b/Changes.md @@ -7,6 +7,7 @@ Features - EditScope : Introduced the Global Edit Target, providing script-level control over the target used by editors. The Global Edit Target can be set from a new "Edit Target" menu in the menu bar, which displays all available edit targets upstream of the focus node. - Editors now follow the Global Edit Target by default, allowing for a simpler experience when switching multiple editors to a common target. - Individual editors can be overridden to use a specific edit target where necessary. An overridden editor can return to following the Global Edit Target via the new "Follow Global Edit Target" menu item. + - While following the Global Edit Target, an editor's Edit Scope menu will shrink to only display an icon. When an Editor is overridden to a specific edit target, the menu grows to display the name of the target. Improvements ------------ diff --git a/python/GafferSceneUI/SceneViewUI.py b/python/GafferSceneUI/SceneViewUI.py index b3d34a1d92c..4ddc2126060 100644 --- a/python/GafferSceneUI/SceneViewUI.py +++ b/python/GafferSceneUI/SceneViewUI.py @@ -66,6 +66,11 @@ def __rendererPlugActivator( plug ) : return plug.parent()["name"].getValue().lower() == plug.getName().lower() +def __condensedEditScopeSpacerActivator( node ) : + + input = node["editScope"].getInput() + return input is not None and input.getName() == "editScope" and isinstance( input.node(), GafferUI.Editor.Settings ) + Gaffer.Metadata.registerNode( GafferSceneUI.SceneView, @@ -93,6 +98,12 @@ def __rendererPlugActivator( plug ) : "toolbarLayout:customWidget:RightEditScopeBalancingSpacer:section", "Top", "toolbarLayout:customWidget:RightEditScopeBalancingSpacer:index", -2, + "toolbarLayout:activator:condensedEditScopeMenu", __condensedEditScopeSpacerActivator, + "toolbarLayout:customWidget:CondensedEditScopeBalancingSpacer:widgetType", "GafferSceneUI.SceneViewUI._CondensedEditScopeBalancingSpacer", + "toolbarLayout:customWidget:CondensedEditScopeBalancingSpacer:section", "Top", + "toolbarLayout:customWidget:CondensedEditScopeBalancingSpacer:index", -2, + "toolbarLayout:customWidget:CondensedEditScopeBalancingSpacer:visibilityActivator", "condensedEditScopeMenu", + "nodeToolbar:right:type", "GafferUI.StandardNodeToolbar.right", "toolbarLayout:customWidget:InspectorTopSpacer:widgetType", "GafferSceneUI.SceneViewUI._InspectorTopSpacer", @@ -1259,6 +1270,17 @@ def __init__( self, sceneView, **kw ) : preferredSize = imath.V2i( width, 1 ), maximumSize = imath.V2i( width, 1 ) ) + +# This Spacer balance the right side of the toolbar by +# preserving the width lost when the EditScope menu is +# displayed in condensed form. +class _CondensedEditScopeBalancingSpacer( GafferUI.Spacer ) : + + def __init__( self, sceneView, **kw ) : + + editScopeWidth = Gaffer.Metadata.value( sceneView["editScope"], "toolbarLayout:width" ) or 130 + # EditScope width - spacer - condensed EditScope width + width = max( editScopeWidth - 4 - 50, 0 ) GafferUI.Spacer.__init__( self, imath.V2i( 0 ), # Minimum diff --git a/python/GafferUI/EditScopeUI.py b/python/GafferUI/EditScopeUI.py index 877f9565b71..a1820bc069c 100644 --- a/python/GafferUI/EditScopeUI.py +++ b/python/GafferUI/EditScopeUI.py @@ -203,6 +203,10 @@ def __plugInputChanged( self, plug ) : if plug.getName() == "in" and plug.parent() == self.getPlug().node() : # The result of `__inputNode()` will have changed. self.__acquireContextTracker() + elif plug == self.getPlug() : + # Update menu button width immediately to prevent layout flicker + # caused by a deferred update from _updateFromValues. + self.__updateMenuButtonWidth() def __acquireContextTracker( self ) : @@ -242,10 +246,24 @@ def __globalEditTargetPlug( self ) : return compoundEditor.settings()["editScope"] + def __updateMenuButtonWidth( self ) : + + if self.__followingGlobalEditTarget() : + Gaffer.Metadata.registerValue( self.getPlug(), "layout:width", 50, persistent = False ) + Gaffer.Metadata.registerValue( self.getPlug(), "toolbarLayout:width", 50, persistent = False ) + else : + Gaffer.Metadata.deregisterValue( self.getPlug(), "layout:width" ) + Gaffer.Metadata.deregisterValue( self.getPlug(), "toolbarLayout:width" ) + def __updateMenuButton( self ) : editScope = self.__editScope() - self.__menuButton.setText( editScope.getName() if editScope is not None else "Source" ) + self.__updateMenuButtonWidth() + + if self.__followingGlobalEditTarget() : + self.__menuButton.setText( " " ) + else : + self.__menuButton.setText( editScope.getName() if editScope is not None else "Source" ) if editScope is not None : self.__menuButton.setImage( diff --git a/python/GafferUI/PlugLayout.py b/python/GafferUI/PlugLayout.py index 7d0a252ec4f..03686619eb1 100644 --- a/python/GafferUI/PlugLayout.py +++ b/python/GafferUI/PlugLayout.py @@ -274,6 +274,8 @@ def __updateLayout( self ) : self.__widgets[item] = widget else : widget = self.__widgets[item] + if self.__itemMetadataValue( item, "width" ) : + widget._qtWidget().setFixedWidth( self.__itemMetadataValue( item, "width" ) ) if widget is None : continue @@ -492,6 +494,7 @@ def __plugMetadataChanged( self, plug, key, reason ) : self.__layoutName + ":index", self.__layoutName + ":section", self.__layoutName + ":accessory", + self.__layoutName + ":width", "plugValueWidget:type" ) : # We often see sequences of several metadata changes, so From 130574ccc19c5b4652ba28059ac23151220f32f0 Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:20:21 +1100 Subject: [PATCH 19/33] OpenColorIOConfigPlugUI : Replace "Use" with "Follow" This keeps the language consistent with the "Follow Global Edit Target" menu item introduced on EditScopePlugValueWidget, and better fits with the idea that this is a persistent behaviour rather than a one-time action to reset to the default display and view for the current config. --- python/GafferImageUI/OpenColorIOConfigPlugUI.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/GafferImageUI/OpenColorIOConfigPlugUI.py b/python/GafferImageUI/OpenColorIOConfigPlugUI.py index bd433a0f447..996ff0a4375 100644 --- a/python/GafferImageUI/OpenColorIOConfigPlugUI.py +++ b/python/GafferImageUI/OpenColorIOConfigPlugUI.py @@ -261,7 +261,7 @@ def __menuDefinition( self ) : result.append( "/__OptionsDivider__", { "divider" : True, "label" : "Options" } ) result.append( - f"/Use Default Display And View", { + f"/Follow Default Display And View", { "command" : functools.partial( Gaffer.WeakMethod( self.__setToDefault ) ), "checkBox" : self.__currentValue == "__default__", "description" : "Always uses the default display and view for the current config. Useful when changing configs often, or using context-sensitive configs." From 69fc4edea9c056152801e8688a1f7d11cdacf11a Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Thu, 14 Nov 2024 21:51:24 +1100 Subject: [PATCH 20/33] Image : Increase swatch size to 14 --- Changes.md | 1 + python/GafferUI/Image.py | 4 ++-- python/GafferUITest/ImageTest.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Changes.md b/Changes.md index 3d2ba723d2b..f8da51a5470 100644 --- a/Changes.md +++ b/Changes.md @@ -30,6 +30,7 @@ Improvements - Renamed "None" mode to "Source" and added icon. - The "Source" menu item now displays a checkbox when chosen. - Added a "No EditScopes Available" menu item that is displayed when no upstream EditScopes are available. + - Increased menu item swatch size. Fixes ----- diff --git a/python/GafferUI/Image.py b/python/GafferUI/Image.py index 33e6331f794..a70c8353e44 100644 --- a/python/GafferUI/Image.py +++ b/python/GafferUI/Image.py @@ -76,14 +76,14 @@ def __init__( self, imagePrimitiveOrFileName, **kw ) : @staticmethod def createSwatch( color ) : - pixmap = QtGui.QPixmap( 10, 10 ) + pixmap = QtGui.QPixmap( 14, 14 ) pixmap.fill( QtGui.QColor( 0, 0, 0, 0 ) ) painter = QtGui.QPainter( pixmap ) painter.setRenderHint( QtGui.QPainter.Antialiasing ) painter.setPen( GafferUI._StyleSheet.styleColor( "backgroundDarkHighlight" ) ) painter.setBrush( QtGui.QColor.fromRgbF( color[0], color[1], color[2] ) ) - painter.drawRoundedRect( QtCore.QRectF( 0.5, 0.5, 9, 9 ), 2, 2 ) + painter.drawRoundedRect( QtCore.QRectF( 0.5, 0.5, 13, 13 ), 2, 2 ) del painter swatch = GafferUI.Image( None ) diff --git a/python/GafferUITest/ImageTest.py b/python/GafferUITest/ImageTest.py index 794e50ac69f..ee821a36464 100644 --- a/python/GafferUITest/ImageTest.py +++ b/python/GafferUITest/ImageTest.py @@ -70,8 +70,8 @@ def testCreateSwatch( self ) : s = GafferUI.Image.createSwatch( imath.Color3f( 1, 0, 0 ) ) - self.assertEqual( s._qtPixmap().width(), 10 ) - self.assertEqual( s._qtPixmap().height(), 10 ) + self.assertEqual( s._qtPixmap().width(), 14 ) + self.assertEqual( s._qtPixmap().height(), 14 ) if __name__ == "__main__": unittest.main() From d2ffd1ec3653a0d355fd0a3d87fa82eb15aa2bcc Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Fri, 15 Nov 2024 12:49:28 +1100 Subject: [PATCH 21/33] Graphics : Increase menuBreadCrumb size This keeps the breadcrumbs aligned with the larger 14 pixel swatches created by `Image.createSwatch()` --- Changes.md | 2 +- resources/graphics.svg | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Changes.md b/Changes.md index f8da51a5470..accb3e4a3b0 100644 --- a/Changes.md +++ b/Changes.md @@ -30,7 +30,7 @@ Improvements - Renamed "None" mode to "Source" and added icon. - The "Source" menu item now displays a checkbox when chosen. - Added a "No EditScopes Available" menu item that is displayed when no upstream EditScopes are available. - - Increased menu item swatch size. + - Increased menu item icon sizes. Fixes ----- diff --git a/resources/graphics.svg b/resources/graphics.svg index e064f6b487c..15bd6b37f65 100644 --- a/resources/graphics.svg +++ b/resources/graphics.svg @@ -3451,10 +3451,10 @@ + cy="2729.3623" + r="5.5" /> Date: Tue, 19 Nov 2024 13:31:34 +1100 Subject: [PATCH 22/33] _StyleSheet : Add 2px vertical margin to EditScopePlugValueWidget This margin is necessary to balance the widget vertically when it is used in the MenuBar. --- python/GafferUI/_StyleSheet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/GafferUI/_StyleSheet.py b/python/GafferUI/_StyleSheet.py index 3c8872c105c..7a558cea3e7 100644 --- a/python/GafferUI/_StyleSheet.py +++ b/python/GafferUI/_StyleSheet.py @@ -1538,6 +1538,8 @@ def styleColor( key ) : border-top-color: rgb( 75, 113, 155 ); border-left-color: rgb( 75, 113, 155 ); 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; } *[gafferClass="GafferSceneUI.InteractiveRenderUI._ViewRenderControlUI"] QPushButton[gafferWithFrame="true"] { From 28871619e5f5e3a1a4af97fef61e4ed84094890a Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Wed, 6 Nov 2024 13:21:41 +1100 Subject: [PATCH 23/33] Image : Allow swatches to be created with an icon overlaid --- Changes.md | 1 + python/GafferUI/Image.py | 31 +++++++++++++++++++++++++++++-- python/GafferUITest/ImageTest.py | 21 +++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/Changes.md b/Changes.md index accb3e4a3b0..d2b40218fb4 100644 --- a/Changes.md +++ b/Changes.md @@ -51,6 +51,7 @@ API - Added `connectToApplication()` function. - Deprecated `connect()` function. Use `connectToApplication()` instead. - SceneEditor : Added `editScope()` method. +- Image : Added optional `image` argument to `createSwatch()` static method. 1.5.0.1 (relative to 1.5.0.0) ======= diff --git a/python/GafferUI/Image.py b/python/GafferUI/Image.py index a70c8353e44..d6ad7d18f67 100644 --- a/python/GafferUI/Image.py +++ b/python/GafferUI/Image.py @@ -72,9 +72,10 @@ def __init__( self, imagePrimitiveOrFileName, **kw ) : self.__pixmapDisabled = None ## Creates an Image containing a color swatch useful for - # button and menu icons. + # button and menu icons. An `image` can be overlaid on + # top of the swatch, this will be scaled to fit. @staticmethod - def createSwatch( color ) : + def createSwatch( color, image = None ) : pixmap = QtGui.QPixmap( 14, 14 ) pixmap.fill( QtGui.QColor( 0, 0, 0, 0 ) ) @@ -84,6 +85,32 @@ def createSwatch( color ) : painter.setPen( GafferUI._StyleSheet.styleColor( "backgroundDarkHighlight" ) ) painter.setBrush( QtGui.QColor.fromRgbF( color[0], color[1], color[2] ) ) painter.drawRoundedRect( QtCore.QRectF( 0.5, 0.5, 13, 13 ), 2, 2 ) + + if image is not None : + + try : + iconImage = GafferUI.Image( image ) + except Exception as e: + IECore.msg( IECore.Msg.Level.Error, "GafferUI.Image", + "Could not read image for swatch icon : " + str( e ) + ) + iconImage = GafferUI.Image( "warningSmall.png" ) + + iconSize = pixmap.size() - QtCore.QSize( 2, 2 ) + iconPixmap = iconImage._qtPixmap() + if iconPixmap.width() > iconSize.width() or iconPixmap.height() > iconSize.height() : + iconPixmap = iconPixmap.scaled( + iconSize, + QtCore.Qt.AspectRatioMode.KeepAspectRatio, + QtCore.Qt.TransformationMode.SmoothTransformation + ) + + painter.drawPixmap( + ( pixmap.width() - iconPixmap.width() ) // 2, + ( pixmap.height() - iconPixmap.height() ) // 2, + iconPixmap + ) + del painter swatch = GafferUI.Image( None ) diff --git a/python/GafferUITest/ImageTest.py b/python/GafferUITest/ImageTest.py index ee821a36464..d7c14be1cc7 100644 --- a/python/GafferUITest/ImageTest.py +++ b/python/GafferUITest/ImageTest.py @@ -39,6 +39,8 @@ import imath +import IECore + import GafferUI import GafferUITest @@ -73,5 +75,24 @@ def testCreateSwatch( self ) : self.assertEqual( s._qtPixmap().width(), 14 ) self.assertEqual( s._qtPixmap().height(), 14 ) + def testCreateSwatchWithImage( self ) : + + s = GafferUI.Image.createSwatch( imath.Color3f( 1, 0, 0 ), image = "arrowRight10.png" ) + + self.assertEqual( s._qtPixmap().width(), 14 ) + self.assertEqual( s._qtPixmap().height(), 14 ) + + # Create a swatch with a large image. The swatch size should not increase. + s2 = GafferUI.Image.createSwatch( imath.Color3f( 1, 0, 0 ), image = "warningNotification.png" ) + + self.assertEqual( s2._qtPixmap().width(), 14 ) + self.assertEqual( s2._qtPixmap().height(), 14 ) + + with IECore.CapturingMessageHandler() as mh : + GafferUI.Image.createSwatch( imath.Color3f( 1, 0, 0 ), image = "iAmNotAFile" ) + + self.assertEqual( len( mh.messages ), 1 ) + self.assertIn( 'Unable to find file "iAmNotAFile"', mh.messages[0].message ) + if __name__ == "__main__": unittest.main() From e95675209dfd4eddd822082242691e31ed0da8dd Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:55:07 +1100 Subject: [PATCH 24/33] EditScopePlugValueWidget : Add lock icon to read-only EditScopes --- Changes.md | 1 + python/GafferUI/EditScopeUI.py | 33 +++++++++++++++++++++++++-------- resources/graphics.py | 1 + resources/graphics.svg | 15 +++++++++++++++ 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/Changes.md b/Changes.md index d2b40218fb4..ae92c966a41 100644 --- a/Changes.md +++ b/Changes.md @@ -31,6 +31,7 @@ Improvements - The "Source" menu item now displays a checkbox when chosen. - Added a "No EditScopes Available" menu item that is displayed when no upstream EditScopes are available. - Increased menu item icon sizes. + - A lock icon is now displayed next to read-only nodes. Fixes ----- diff --git a/python/GafferUI/EditScopeUI.py b/python/GafferUI/EditScopeUI.py index a1820bc069c..256160b29c6 100644 --- a/python/GafferUI/EditScopeUI.py +++ b/python/GafferUI/EditScopeUI.py @@ -144,6 +144,10 @@ def __init__( self, plug, **kw ) : # run the default dropSignal handler from PlugValueWidget. self.dropSignal().connectFront( Gaffer.WeakMethod( self.__drop ) ) + self.__nodeMetadataChangedConnection = Gaffer.Metadata.nodeValueChangedSignal().connect( + Gaffer.WeakMethod( self.__nodeMetadataChanged ), scoped = True + ) + self.__updateLabelVisibility() self.__updatePlugInputChangedConnection() self.__acquireContextTracker() @@ -165,8 +169,11 @@ def getToolTip( self ) : return "Edits will be made using the last relevant node found outside of an edit scope.\n\nTo make an edit in an edit scope, choose it from the menu." unusableReason = self.__unusableReason( editScope ) + readOnlyReason = self.__readOnlyReason( editScope ) if unusableReason : return unusableReason + elif readOnlyReason : + return readOnlyReason else : return "Edits will be made in {}.".format( editScope.getName() ) @@ -185,12 +192,8 @@ def _updateFromValues( self, values, exception ) : self.__editScopeNameChangedConnection = editScope.nameChangedSignal().connect( Gaffer.WeakMethod( self.__editScopeNameChanged ), scoped = True ) - self.__editScopeMetadataChangedConnection = Gaffer.Metadata.nodeValueChangedSignal( editScope ).connect( - Gaffer.WeakMethod( self.__editScopeMetadataChanged ), scoped = True - ) else : self.__editScopeNameChangedConnection = None - self.__editScopeMetadataChangedConnection = None def __updatePlugInputChangedConnection( self ) : @@ -276,9 +279,13 @@ def __editScopeNameChanged( self, editScope, oldName ) : self.__updateMenuButton() - def __editScopeMetadataChanged( self, editScope, key, reason ) : + def __nodeMetadataChanged( self, nodeTypeId, key, node ) : - if key == "nodeGadget:color" : + editScope = self.__editScope() + if ( + Gaffer.MetadataAlgo.readOnlyAffectedByChange( editScope, nodeTypeId, key, node ) or + node == editScope and key == "nodeGadget:color" + ) : self.__updateMenuButton() def __contextTrackerChanged( self, contextTracker ) : @@ -393,7 +400,7 @@ def __buildMenu( self, path, currentEditScope ) : "checkBox" : editScope == currentEditScope, "icon" : self.__editScopeSwatch( editScope ), "active" : not self.__unusableReason( editScope ), - "description" : self.__unusableReason( editScope ), + "description" : self.__unusableReason( editScope ) or self.__readOnlyReason( editScope ), } ) else : @@ -498,7 +505,8 @@ def __refreshMenu( self ) : def __editScopeSwatch( self, editScope ) : return GafferUI.Image.createSwatch( - Gaffer.Metadata.value( editScope, "nodeGadget:color" ) or imath.Color3f( 1 ) + Gaffer.Metadata.value( editScope, "nodeGadget:color" ) or imath.Color3f( 1 ), + image = "menuLock.png" if Gaffer.MetadataAlgo.readOnly( editScope ) else None ) @staticmethod @@ -590,6 +598,15 @@ def __unusableReason( self, editScope ) : else : return None + def __readOnlyReason( self, editScope ) : + + if Gaffer.MetadataAlgo.readOnly( editScope ) : + return "{} is locked.".format( + Gaffer.MetadataAlgo.readOnlyReason( editScope ).relativeName( editScope.scriptNode() ) + ) + + return None + # ProcessorWidget # =============== diff --git a/resources/graphics.py b/resources/graphics.py index 5f065971bf3..b3345204c5b 100644 --- a/resources/graphics.py +++ b/resources/graphics.py @@ -414,6 +414,7 @@ "menuIndicator", "menuIndicatorDisabled", "menuSource", + "menuLock", ] }, diff --git a/resources/graphics.svg b/resources/graphics.svg index 15bd6b37f65..10903ecaa32 100644 --- a/resources/graphics.svg +++ b/resources/graphics.svg @@ -3466,6 +3466,15 @@ y="95" transform="matrix(0,1,1,0,0,0)" inkscape:label="menuSource" /> + + Date: Wed, 20 Nov 2024 17:02:59 +1100 Subject: [PATCH 25/33] Graphics : Resize menuChecked to 14 pixels Qt draws the checked indicator at 14 pixels so our 15 pixel icon was appearing a bit soft. --- python/GafferUI/_StyleSheet.py | 4 ++-- resources/graphics.svg | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/python/GafferUI/_StyleSheet.py b/python/GafferUI/_StyleSheet.py index 7a558cea3e7..fc8181e41a3 100644 --- a/python/GafferUI/_StyleSheet.py +++ b/python/GafferUI/_StyleSheet.py @@ -362,7 +362,7 @@ def styleColor( key ) : } QMenu::indicator { - width: 15px; + width: 14px; padding: 0px 0px 0px 3px; /* Work around https://bugreports.qt.io/browse/QTBUG-90242. In Qt 5.12, @@ -373,7 +373,7 @@ def styleColor( key ) : between checkable and non-checkable items. This negative margin negates the shunt in Qt 5.15 and has no effect in Qt 5.12. */ - margin-right: -18px; + margin-right: -17px; } QMenu::indicator:non-exclusive:checked { diff --git a/resources/graphics.svg b/resources/graphics.svg index 15bd6b37f65..183a8175a85 100644 --- a/resources/graphics.svg +++ b/resources/graphics.svg @@ -3425,9 +3425,9 @@ inkscape:label="menuChecked" transform="matrix(0,1,1,0,0,0)" y="12" - x="2669" - height="15" - width="15" + x="2670" + height="14" + width="14" id="menuChecked" style="fill:none;fill-opacity:1;stroke:none" /> Date: Tue, 29 Oct 2024 15:42:01 +0000 Subject: [PATCH 26/33] ShadingEngine : Support 64 bit ints in `RendererServices::get_userdata()` This requires https://github.com/ImageEngine/cortex/pull/1439 to work. --- Changes.md | 1 + python/GafferOSLTest/ShadingEngineTest.py | 27 ++++++++++++ src/GafferOSL/ShadingEngine.cpp | 50 +++++++++++++++-------- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/Changes.md b/Changes.md index ae92c966a41..10cc16669d5 100644 --- a/Changes.md +++ b/Changes.md @@ -43,6 +43,7 @@ Fixes - `gaffer view` : Fixed default OpenColorIO display transform. - AnimationEditor : Fixed changing of the current frame by dragging the frame indicator or clicking on the time axis. - ImageWriter : Matched view metadata to Nuke when using the Nuke options for `layout`. This should address an issue where EXRs written from Gaffer using Nuke layouts sometimes did not load correctly in Nuke (#6120). In the unlikely situation that you were relying on the old behaviour, you can set the env var `GAFFERIMAGE_IMAGEWRITER_OMIT_DEFAULT_NUKE_VIEW = 1` in order to keep the old behaviour. +- OSLObject : Fixed `getattribute()` to support 64 bit integer data, such as an `instanceId` primitive variable loaded from USD. Since OSL doesn't provide a 64 bit integer type, values are truncated to 32 bits. API --- diff --git a/python/GafferOSLTest/ShadingEngineTest.py b/python/GafferOSLTest/ShadingEngineTest.py index 230e92a03dc..702a9cafa51 100644 --- a/python/GafferOSLTest/ShadingEngineTest.py +++ b/python/GafferOSLTest/ShadingEngineTest.py @@ -160,6 +160,33 @@ def testDoubleAsIntViaGetAttribute( self ) : for i, c in enumerate( p["Ci"] ) : self.assertEqual( c[0], float(i) ) + def testInt64GetAttribute( self ) : + + shader = self.compileShader( pathlib.Path( __file__ ).parent / "shaders" / "intAttribute.osl" ) + + for dataType in ( IECore.Int64VectorData, IECore.UInt64VectorData ) : + + with self.subTest( dataType = dataType ) : + + points = IECore.CompoundData( { + "P" : IECore.V3fVectorData( [ imath.V3f( i ) for i in range( 0, 10 ) ] ), + "int64Data" : dataType( range( 0, 10 ) ) + } ) + + engine = GafferOSL.ShadingEngine( IECoreScene.ShaderNetwork( + shaders = { + "output" : IECoreScene.Shader( shader, "osl:surface", { "name" : "int64Data" } ), + }, + output = "output" + ) ) + + self.assertTrue( engine.needsAttribute( "int64Data" ) ) + + points = engine.shade( points ) + + for i, c in enumerate( points["Ci"] ) : + self.assertEqual( c[0], float( i ) ) + def testUserDataViaGetAttribute( self ) : shader = self.compileShader( pathlib.Path( __file__ ).parent / "shaders" / "attribute.osl" ) diff --git a/src/GafferOSL/ShadingEngine.cpp b/src/GafferOSL/ShadingEngine.cpp index 0c40546c00b..b8c8dbeda77 100644 --- a/src/GafferOSL/ShadingEngine.cpp +++ b/src/GafferOSL/ShadingEngine.cpp @@ -215,6 +215,29 @@ DataPtr dataFromTypeDesc( TypeDesc type, void *&basePointer ) return nullptr; } +template +bool convertScalar( void *dst, TypeDesc dstType, const void *src ) +{ + const SourceType *typedSrc = reinterpret_cast( src ); + if( dstType == TypeDesc::FLOAT ) + { + if( typedSrc && dst ) + { + *((float*)dst) = static_cast( *typedSrc ); + } + return true; + } + else if( dstType == TypeDesc::INT ) + { + if( typedSrc && dst ) + { + *((int*)dst) = static_cast( *typedSrc ); + } + return true; + } + return false; +} + // Equivalent to `OSL::ShadingSystem:convert_value()`, but with support for // additional conversions. bool convertValue( void *dst, TypeDesc dstType, const void *src, TypeDesc srcType ) @@ -254,24 +277,15 @@ bool convertValue( void *dst, TypeDesc dstType, const void *src, TypeDesc srcTyp } else if( srcType.basetype == TypeDesc::DOUBLE && srcType.aggregate == TypeDesc::SCALAR ) { - const double *doubleCast = reinterpret_cast( src ); - if( dstType == TypeDesc::FLOAT ) - { - if( doubleCast && dst ) - { - *((float*)dst) = static_cast( *doubleCast ); - } - return true; - } - else if( dstType == TypeDesc::INT ) - { - if( doubleCast && dst ) - { - *((int*)dst) = static_cast( *doubleCast ); - } - return true; - } - return false; + return convertScalar( dst, dstType, src ); + } + else if( srcType.basetype == TypeDesc::INT64 && srcType.aggregate == TypeDesc::SCALAR ) + { + return convertScalar( dst, dstType, src ); + } + else if( srcType.basetype == TypeDesc::UINT64 && srcType.aggregate == TypeDesc::SCALAR ) + { + return convertScalar( dst, dstType, src ); } return false; From ed0cc2321ab7a959e2bef8ec679b5111f0047e3c Mon Sep 17 00:00:00 2001 From: John Haddon Date: Wed, 20 Nov 2024 22:29:10 +0000 Subject: [PATCH 27/33] CI : Update to GafferHQ/dependencies 9.1.0 --- .github/workflows/main/installDependencies.py | 2 +- Changes.md | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main/installDependencies.py b/.github/workflows/main/installDependencies.py index 7d22f2327e8..f2d1f7c6f28 100755 --- a/.github/workflows/main/installDependencies.py +++ b/.github/workflows/main/installDependencies.py @@ -49,7 +49,7 @@ # Determine default archive URL. -defaultURL = "https://github.com/GafferHQ/dependencies/releases/download/9.0.0/gafferDependencies-9.0.0-{platform}{buildEnvironment}.{extension}" +defaultURL = "https://github.com/GafferHQ/dependencies/releases/download/9.1.0/gafferDependencies-9.1.0-{platform}{buildEnvironment}.{extension}" # Parse command line arguments. diff --git a/Changes.md b/Changes.md index 10cc16669d5..5f54ad7ae84 100644 --- a/Changes.md +++ b/Changes.md @@ -44,6 +44,7 @@ Fixes - AnimationEditor : Fixed changing of the current frame by dragging the frame indicator or clicking on the time axis. - ImageWriter : Matched view metadata to Nuke when using the Nuke options for `layout`. This should address an issue where EXRs written from Gaffer using Nuke layouts sometimes did not load correctly in Nuke (#6120). In the unlikely situation that you were relying on the old behaviour, you can set the env var `GAFFERIMAGE_IMAGEWRITER_OMIT_DEFAULT_NUKE_VIEW = 1` in order to keep the old behaviour. - OSLObject : Fixed `getattribute()` to support 64 bit integer data, such as an `instanceId` primitive variable loaded from USD. Since OSL doesn't provide a 64 bit integer type, values are truncated to 32 bits. +- MeshSplit : Vertex order is now preserved. API --- @@ -55,6 +56,11 @@ API - SceneEditor : Added `editScope()` method. - Image : Added optional `image` argument to `createSwatch()` static method. +Build +----- + +- Cortex : Updated to version 10.5.11.0. + 1.5.0.1 (relative to 1.5.0.0) ======= From 23ceae7e7d2782439aa49ff6953934c8390428e4 Mon Sep 17 00:00:00 2001 From: ivanimanishi Date: Thu, 21 Nov 2024 11:20:58 -0800 Subject: [PATCH 28/33] DispatchDialogue : Remove `_DispatcherCreationWidget` from shown nodes --- Changes.md | 1 + python/GafferDispatchUI/DispatchDialogue.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Changes.md b/Changes.md index 5f54ad7ae84..22551a4a18b 100644 --- a/Changes.md +++ b/Changes.md @@ -45,6 +45,7 @@ Fixes - ImageWriter : Matched view metadata to Nuke when using the Nuke options for `layout`. This should address an issue where EXRs written from Gaffer using Nuke layouts sometimes did not load correctly in Nuke (#6120). In the unlikely situation that you were relying on the old behaviour, you can set the env var `GAFFERIMAGE_IMAGEWRITER_OMIT_DEFAULT_NUKE_VIEW = 1` in order to keep the old behaviour. - OSLObject : Fixed `getattribute()` to support 64 bit integer data, such as an `instanceId` primitive variable loaded from USD. Since OSL doesn't provide a 64 bit integer type, values are truncated to 32 bits. - MeshSplit : Vertex order is now preserved. +- DispatchDialogue : Removed `_DispatcherCreationWidget` from shown nodes. API --- diff --git a/python/GafferDispatchUI/DispatchDialogue.py b/python/GafferDispatchUI/DispatchDialogue.py index 854bcfe1cef..1d0eadcdb74 100644 --- a/python/GafferDispatchUI/DispatchDialogue.py +++ b/python/GafferDispatchUI/DispatchDialogue.py @@ -95,6 +95,8 @@ def __init__( self, tasks, dispatchers, nodesToShow, postDispatchBehaviour=PostD nodeFrame.addChild( self.__nodeEditor( node ) ) # remove the per-node execute button Gaffer.Metadata.registerValue( node, "layout:customWidget:dispatchButton:widgetType", "", persistent = False ) + # remove the per-node widget to create a dispatcher + Gaffer.Metadata.registerValue( node, "layout:customWidget:dispatcherCreationWidget:widgetType", "", persistent = False ) self.__tabs.setLabel( nodeFrame, node.relativeName( self.__script ) ) with GafferUI.ListContainer() as dispatcherTab : From f1ac8fe68b4ca08b84a1d3087f126e825196eedb Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Fri, 22 Nov 2024 21:52:07 +1100 Subject: [PATCH 29/33] Graphics : Reinstate "context yellow" current render pass indicator This was originally added in dced4d402f9456daccac9a5676d3ead718547b5b but was lost in some merge shenanigans along the way. --- Changes.md | 1 + resources/graphics.svg | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Changes.md b/Changes.md index 5f54ad7ae84..b159c663d40 100644 --- a/Changes.md +++ b/Changes.md @@ -32,6 +32,7 @@ Improvements - Added a "No EditScopes Available" menu item that is displayed when no upstream EditScopes are available. - Increased menu item icon sizes. - A lock icon is now displayed next to read-only nodes. +- RenderPassEditor : Changed the current render pass indicator to yellow to match other context-related UI elements. Fixes ----- diff --git a/resources/graphics.svg b/resources/graphics.svg index d61dfcad744..f94c73565e8 100644 --- a/resources/graphics.svg +++ b/resources/graphics.svg @@ -10150,7 +10150,7 @@ cy="2784.9451" cx="54.179211" id="circle6245-3" - style="display:inline;vector-effect:none;fill:#54ad5e;fill-opacity:1;fill-rule:nonzero;stroke:#3c3c3c;stroke-width:1.00157;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" /> + style="display:inline;vector-effect:none;fill:#f0dc28;fill-opacity:1;fill-rule:nonzero;stroke:#3c3c3c;stroke-width:1.00157;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" /> + style="display:inline;vector-effect:none;fill:#f0dc28;fill-opacity:0.38041458;fill-rule:nonzero;stroke:#779cbd;stroke-width:1.00157;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" /> Date: Mon, 4 Nov 2024 08:18:35 -0500 Subject: [PATCH 30/33] StandardNodeGadget : Persistent nodule labels --- Changes.md | 1 + include/GafferUI/StandardNodeGadget.h | 2 + python/GafferUITest/StandardNodeGadgetTest.py | 42 +++++++++++++ src/GafferUI/StandardNodeGadget.cpp | 59 +++++++++++++++---- 4 files changed, 92 insertions(+), 12 deletions(-) diff --git a/Changes.md b/Changes.md index f0d077aa999..e440e0560d6 100644 --- a/Changes.md +++ b/Changes.md @@ -57,6 +57,7 @@ API - Deprecated `connect()` function. Use `connectToApplication()` instead. - SceneEditor : Added `editScope()` method. - Image : Added optional `image` argument to `createSwatch()` static method. +- StandardNodeGadget : Added support for `nodeGadget:inputNoduleLabelsVisible` and `nodeGadget:outputNoduleLabelsVisible` metadata for setting nodule labels always on. If the metadata entry is not set or `False`, labels will be visible only when they are hovered over. Build ----- diff --git a/include/GafferUI/StandardNodeGadget.h b/include/GafferUI/StandardNodeGadget.h index 76be5283e40..9bf2f37d0a9 100644 --- a/include/GafferUI/StandardNodeGadget.h +++ b/include/GafferUI/StandardNodeGadget.h @@ -150,6 +150,7 @@ class GAFFERUI_API StandardNodeGadget : public NodeGadget bool dragMove( GadgetPtr gadget, const DragDropEvent &event ); bool dragLeave( GadgetPtr gadget, const DragDropEvent &event ); bool drop( GadgetPtr gadget, const DragDropEvent &event ); + void noduleAdded( Nodule *nodule ); ConnectionCreator *closestDragDestination( const DragDropEvent &event ) const; @@ -163,6 +164,7 @@ class GAFFERUI_API StandardNodeGadget : public NodeGadget bool updateShape(); void updateFocusGadgetVisibility(); void updateTextDimming(); + void applyNoduleLabelVisibilityMetadata(); IE_CORE_FORWARDDECLARE( ErrorGadget ); ErrorGadget *errorGadget( bool createIfMissing = true ); diff --git a/python/GafferUITest/StandardNodeGadgetTest.py b/python/GafferUITest/StandardNodeGadgetTest.py index b86ec30c173..bcea061c098 100644 --- a/python/GafferUITest/StandardNodeGadgetTest.py +++ b/python/GafferUITest/StandardNodeGadgetTest.py @@ -308,5 +308,47 @@ def assertMinWidth( gadget, minWidth ) : Gaffer.Metadata.deregisterValue( n, "nodeGadget:minWidth" ) assertMinWidth( g, 10.0 ) + def testNoduleLabelVisibility( self ) : + + n = Gaffer.Node() + n.addChild( Gaffer.FloatPlug( "fIn", direction = Gaffer.Plug.Direction.In ) ) + n.addChild( Gaffer.FloatPlug( "fOut", direction = Gaffer.Plug.Direction.Out ) ) + g = GafferUI.StandardNodeGadget( n ) + + fIn = g.nodule( n["fIn"] ) + fOut = g.nodule( n["fOut"] ) + + self.assertFalse( fIn.getLabelVisible() ) + self.assertFalse( fOut.getLabelVisible() ) + + Gaffer.Metadata.registerValue( Gaffer.Node, "nodeGadget:inputNoduleLabelsVisible", True ) + self.assertTrue( fIn.getLabelVisible() ) + self.assertFalse( fOut.getLabelVisible() ) + + Gaffer.Metadata.registerValue( Gaffer.Node, "nodeGadget:outputNoduleLabelsVisible", True ) + self.assertTrue( fIn.getLabelVisible() ) + self.assertTrue( fOut.getLabelVisible() ) + + n.addChild( Gaffer.IntPlug( "iIn", direction = Gaffer.Plug.Direction.In ) ) + n.addChild( Gaffer.IntPlug( "iOut", direction = Gaffer.Plug.Direction.Out ) ) + + iIn = g.nodule( n["iIn"] ) + iOut = g.nodule( n["iOut"] ) + + self.assertTrue( iIn.getLabelVisible() ) + self.assertTrue( iOut.getLabelVisible() ) + + Gaffer.Metadata.registerValue( Gaffer.Node, "nodeGadget:inputNoduleLabelsVisible", False ) + self.assertFalse( fIn.getLabelVisible() ) + self.assertFalse( iIn.getLabelVisible() ) + self.assertTrue( fOut.getLabelVisible() ) + self.assertTrue( iOut.getLabelVisible() ) + + Gaffer.Metadata.registerValue( Gaffer.Node, "nodeGadget:outputNoduleLabelsVisible", False ) + self.assertFalse( fIn.getLabelVisible() ) + self.assertFalse( iIn.getLabelVisible() ) + self.assertFalse( fOut.getLabelVisible() ) + self.assertFalse( iOut.getLabelVisible() ) + if __name__ == "__main__": unittest.main() diff --git a/src/GafferUI/StandardNodeGadget.cpp b/src/GafferUI/StandardNodeGadget.cpp index e07a42442b6..5c683b7e99d 100644 --- a/src/GafferUI/StandardNodeGadget.cpp +++ b/src/GafferUI/StandardNodeGadget.cpp @@ -566,6 +566,8 @@ static IECore::InternedString g_paddingKey( "nodeGadget:padding" ); static IECore::InternedString g_colorKey( "nodeGadget:color" ); static IECore::InternedString g_shapeKey( "nodeGadget:shape" ); static IECore::InternedString g_focusGadgetVisibleKey( "nodeGadget:focusGadgetVisible" ); +static IECore::InternedString g_inputNoduleLabelsVisibleKey( "nodeGadget:inputNoduleLabelsVisible" ); +static IECore::InternedString g_outputNoduleLabelsVisibleKey( "nodeGadget:outputNoduleLabelsVisible" ); static IECore::InternedString g_iconKey( "icon" ); static IECore::InternedString g_iconScaleKey( "iconScale" ); static IECore::InternedString g_errorGadgetName( "__error" ); @@ -700,6 +702,7 @@ StandardNodeGadget::StandardNodeGadget( Gaffer::NodePtr node, bool auxiliary ) dragMoveSignal().connect( boost::bind( &StandardNodeGadget::dragMove, this, ::_1, ::_2 ) ); dragLeaveSignal().connect( boost::bind( &StandardNodeGadget::dragLeave, this, ::_1, ::_2 ) ); dropSignal().connect( boost::bind( &StandardNodeGadget::drop, this, ::_1, ::_2 ) ); + noduleAddedSignal().connect( boost::bind( &StandardNodeGadget::noduleAdded, this, ::_2 ) ); for( int e = FirstEdge; e <= LastEdge; e++ ) { @@ -722,6 +725,7 @@ StandardNodeGadget::StandardNodeGadget( Gaffer::NodePtr node, bool auxiliary ) updateIcon(); updateShape(); updateFocusGadgetVisibility(); + applyNoduleLabelVisibilityMetadata(); } StandardNodeGadget::~StandardNodeGadget() @@ -1144,10 +1148,7 @@ void StandardNodeGadget::leave( Gadget *gadget ) { if( m_labelsVisibleOnHover ) { - for( StandardNodule::RecursiveIterator it( gadget ); !it.done(); ++it ) - { - (*it)->setLabelVisible( false ); - } + applyNoduleLabelVisibilityMetadata(); } } @@ -1202,10 +1203,7 @@ bool StandardNodeGadget::dragLeave( GadgetPtr gadget, const DragDropEvent &event if( m_dragDestination != event.destinationGadget ) { m_dragDestination->setHighlighted( false ); - for( StandardNodule::RecursiveIterator it( this ); !it.done(); ++it ) - { - (*it)->setLabelVisible( false ); - } + applyNoduleLabelVisibilityMetadata(); } m_dragDestination = nullptr; @@ -1222,14 +1220,25 @@ bool StandardNodeGadget::drop( GadgetPtr gadget, const DragDropEvent &event ) connect( event, m_dragDestination ); m_dragDestination->setHighlighted( false ); - for( StandardNodule::RecursiveIterator it( this ); !it.done(); ++it ) - { - (*it)->setLabelVisible( false ); - } + applyNoduleLabelVisibilityMetadata(); + m_dragDestination = nullptr; return true; } +void StandardNodeGadget::noduleAdded( Nodule *nodule ) +{ + if( auto standardNodule = IECore::runTimeCast( nodule ) ) + { + IECore::ConstBoolDataPtr d = standardNodule->plug()->direction() == Plug::Direction::In ? + Gaffer::Metadata::value( node(), g_inputNoduleLabelsVisibleKey ) : + Gaffer::Metadata::value( node(), g_outputNoduleLabelsVisibleKey ) + ; + + standardNodule->setLabelVisible( d ? d->readable() : false ); + } +} + ConnectionCreator *StandardNodeGadget::closestDragDestination( const DragDropEvent &event ) const { if( event.buttons != DragDropEvent::Left ) @@ -1303,6 +1312,10 @@ void StandardNodeGadget::nodeMetadataChanged( IECore::InternedString key ) { updateFocusGadgetVisibility(); } + else if( key == g_inputNoduleLabelsVisibleKey || key == g_outputNoduleLabelsVisibleKey ) + { + applyNoduleLabelVisibilityMetadata(); + } } bool StandardNodeGadget::updateUserColor() @@ -1464,6 +1477,28 @@ void StandardNodeGadget::updateFocusGadgetVisibility() m_focusGadget->setVisible( !d || d->readable() ); } +void StandardNodeGadget::applyNoduleLabelVisibilityMetadata() +{ + bool inputVisible = false; + if( IECore::ConstBoolDataPtr d = Gaffer::Metadata::value( node(), g_inputNoduleLabelsVisibleKey ) ) + { + inputVisible = d->readable(); + } + + bool outputVisible = false; + if( IECore::ConstBoolDataPtr d = Gaffer::Metadata::value( node(), g_outputNoduleLabelsVisibleKey ) ) + { + outputVisible = d->readable(); + } + + for( StandardNodule::RecursiveIterator it( this ); !it.done(); ++it ) + { + (*it)->setLabelVisible( + (*it)->plug()->direction() == Plug::Direction::In ? inputVisible : outputVisible + ); + } +} + StandardNodeGadget::ErrorGadget *StandardNodeGadget::errorGadget( bool createIfMissing ) { if( ErrorGadget *result = getChild( g_errorGadgetName ) ) From c61e6cf042196ce83724e7b5c3ada78882e8823f Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Tue, 19 Nov 2024 12:37:42 -0500 Subject: [PATCH 31/33] StandardNodule : Respect visibility metadata --- include/GafferUI/StandardNodeGadget.h | 4 ++ include/GafferUI/StandardNodule.h | 4 ++ src/GafferUI/StandardNodule.cpp | 58 ++++++++++++++++++++++++--- 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/include/GafferUI/StandardNodeGadget.h b/include/GafferUI/StandardNodeGadget.h index 9bf2f37d0a9..d593f6e40ea 100644 --- a/include/GafferUI/StandardNodeGadget.h +++ b/include/GafferUI/StandardNodeGadget.h @@ -46,6 +46,7 @@ namespace GafferUI class PlugAdder; class NoduleLayout; class ConnectionCreator; +class StandardNodule; /// The standard means of representing a Node in a GraphGadget. /// Nodes are represented as rectangular boxes with the name displayed @@ -164,6 +165,9 @@ class GAFFERUI_API StandardNodeGadget : public NodeGadget bool updateShape(); void updateFocusGadgetVisibility(); void updateTextDimming(); + + friend class StandardNodule; + // Set the visibility for all nodules based on the metadata registered for this node. void applyNoduleLabelVisibilityMetadata(); IE_CORE_FORWARDDECLARE( ErrorGadget ); diff --git a/include/GafferUI/StandardNodule.h b/include/GafferUI/StandardNodule.h index ec9da182f2f..0eec0acbc17 100644 --- a/include/GafferUI/StandardNodule.h +++ b/include/GafferUI/StandardNodule.h @@ -89,7 +89,11 @@ class GAFFERUI_API StandardNodule : public Nodule bool dragEnd( GadgetPtr gadget, const DragDropEvent &event ); bool drop( GadgetPtr gadget, const DragDropEvent &event ); + /// \deprecated Use overloaded method without `visible` when setting `visible = true` + /// or `StandardNodeGadget::applyNoduleLabelVisibilityMetadata()` to restore + /// metadata-aware visibility. void setCompatibleLabelsVisible( const DragDropEvent &event, bool visible ); + void setCompatibleLabelsVisible( const DragDropEvent &event ); private : diff --git a/src/GafferUI/StandardNodule.cpp b/src/GafferUI/StandardNodule.cpp index 016d1fef595..4e77f77c4bd 100644 --- a/src/GafferUI/StandardNodule.cpp +++ b/src/GafferUI/StandardNodule.cpp @@ -369,7 +369,7 @@ bool StandardNodule::dragEnter( GadgetPtr gadget, const DragDropEvent &event ) Nodule *prevDestination = IECore::runTimeCast( event.destinationGadget.get() ); if( !prevDestination || prevDestination->plug()->node() != plug()->node() ) { - setCompatibleLabelsVisible( event, true ); + setCompatibleLabelsVisible( event ); } dirty( DirtyType::Render ); @@ -397,19 +397,40 @@ bool StandardNodule::dragLeave( GadgetPtr gadget, const DragDropEvent &event ) { if( newDestination->plug()->node() != plug()->node() ) { - setCompatibleLabelsVisible( event, false ); + if( auto nodeGadget = ancestor() ) + { + nodeGadget->applyNoduleLabelVisibilityMetadata(); + } + else + { + setCompatibleLabelsVisible( event, false ); + } } } else if( NodeGadget *newDestination = IECore::runTimeCast( event.destinationGadget.get() ) ) { if( newDestination->node() != plug()->node() ) { - setCompatibleLabelsVisible( event, false ); + if( auto nodeGadget = ancestor() ) + { + nodeGadget->applyNoduleLabelVisibilityMetadata(); + } + else + { + setCompatibleLabelsVisible( event, false ); + } } } else { - setCompatibleLabelsVisible( event, false ); + if( auto nodeGadget = ancestor() ) + { + nodeGadget->applyNoduleLabelVisibilityMetadata(); + } + else + { + setCompatibleLabelsVisible( event, false ); + } } dirty( DirtyType::Render ); } @@ -434,7 +455,14 @@ bool StandardNodule::dragEnd( GadgetPtr gadget, const DragDropEvent &event ) bool StandardNodule::drop( GadgetPtr gadget, const DragDropEvent &event ) { setHighlighted( false ); - setCompatibleLabelsVisible( event, false ); + if( auto nodeGadget = ancestor() ) + { + nodeGadget->applyNoduleLabelVisibilityMetadata(); + } + else + { + setCompatibleLabelsVisible( event, false ); + } if( ConnectionCreator *creator = IECore::runTimeCast( event.sourceGadget.get() ) ) { @@ -474,6 +502,26 @@ void StandardNodule::setCompatibleLabelsVisible( const DragDropEvent &event, boo } } +void StandardNodule::setCompatibleLabelsVisible( const DragDropEvent &event ) +{ + NodeGadget *nodeGadget = ancestor(); + if( !nodeGadget ) + { + return; + } + + ConnectionCreator *creator = IECore::runTimeCast( event.sourceGadget.get() ); + if( !creator ) + { + return; + } + + for( StandardNodule::RecursiveIterator it( nodeGadget ); !it.done(); ++it ) + { + (*it)->setLabelVisible( creator->canCreateConnection( it->get()->plug() ) ? true : false ); + } +} + void StandardNodule::plugMetadataChanged( const Gaffer::Plug *plug, IECore::InternedString key ) { if( plug != this->plug() ) From 89154ca86b5cb894af781b9fe08e9cf47a2346f8 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Wed, 20 Nov 2024 12:56:27 -0500 Subject: [PATCH 32/33] GraphEditor : Add nodule label visibility menu items --- Changes.md | 1 + python/GafferUI/GraphEditor.py | 35 ++++++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/Changes.md b/Changes.md index e440e0560d6..aa730ffc8e8 100644 --- a/Changes.md +++ b/Changes.md @@ -33,6 +33,7 @@ Improvements - Increased menu item icon sizes. - A lock icon is now displayed next to read-only nodes. - RenderPassEditor : Changed the current render pass indicator to yellow to match other context-related UI elements. +- GraphEditor : Moved "Show Input Connections" and "Show Output Connections" to "Connections" sub-menu and added "Show Input Labels" and "Show Output Labels" items. Fixes ----- diff --git a/python/GafferUI/GraphEditor.py b/python/GafferUI/GraphEditor.py index abc955a31b7..4c292285ea3 100644 --- a/python/GafferUI/GraphEditor.py +++ b/python/GafferUI/GraphEditor.py @@ -215,7 +215,7 @@ def plugDirectionsWalk( gadget ) : if Gaffer.Plug.Direction.In in plugDirections : menuDefinition.append( - "/Show Input Connections", + "/Connections/Show Input Connections", { "checkBox" : functools.partial( cls.__getNodeInputConnectionsVisible, graphEditor.graphGadget(), node ), "command" : functools.partial( cls.__setNodeInputConnectionsVisible, graphEditor.graphGadget(), node ), @@ -225,7 +225,7 @@ def plugDirectionsWalk( gadget ) : if Gaffer.Plug.Direction.Out in plugDirections : menuDefinition.append( - "/Show Output Connections", + "/Connections/Show Output Connections", { "checkBox" : functools.partial( cls.__getNodeOutputConnectionsVisible, graphEditor.graphGadget(), node ), "command" : functools.partial( cls.__setNodeOutputConnectionsVisible, graphEditor.graphGadget(), node ), @@ -233,6 +233,26 @@ def plugDirectionsWalk( gadget ) : } ) + if Gaffer.Plug.Direction.In in plugDirections : + menuDefinition.append( + "/Connections/Show Input Labels", + { + "checkBox" : functools.partial( cls.__getNoduleLabelsVisible, node, "input" ), + "command" : functools.partial( cls.__setNoduleLabelsVisible, node, "input" ), + "active" : not readOnly, + } + ) + + if Gaffer.Plug.Direction.Out in plugDirections : + menuDefinition.append( + "/Connections/Show Output Labels", + { + "checkBox" : functools.partial( cls.__getNoduleLabelsVisible, node, "output" ), + "command" : functools.partial( cls.__setNoduleLabelsVisible, node, "output" ), + "active" : not readOnly, + } + ) + ## May be used from a slot attached to nodeContextMenuSignal() to install a # standard menu item for modifying the enabled state of a node. @classmethod @@ -729,6 +749,17 @@ def __setNodeInputConnectionsVisible( cls, graphGadget, node, value ) : with Gaffer.UndoScope( node.ancestor( Gaffer.ScriptNode ) ) : graphGadget.setNodeInputConnectionsMinimised( node, not value ) + @classmethod + def __getNoduleLabelsVisible( cls, node, direction ) : + + return Gaffer.Metadata.value( node, f"nodeGadget:{direction}NoduleLabelsVisible" ) or False + + @classmethod + def __setNoduleLabelsVisible( cls, node, direction, value ) : + + with Gaffer.UndoScope( node.ancestor( Gaffer.ScriptNode ) ) : + Gaffer.Metadata.registerValue( node, f"nodeGadget:{direction}NoduleLabelsVisible", value ) + @classmethod def __getNodeOutputConnectionsVisible( cls, graphGadget, node ) : From 58eaee11ebafd317a74deaf50c9911580854bb2d Mon Sep 17 00:00:00 2001 From: John Haddon Date: Fri, 22 Nov 2024 14:42:33 +0000 Subject: [PATCH 33/33] Bump version to 1.5.1.0 --- Changes.md | 7 ++++++- SConstruct | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Changes.md b/Changes.md index aa730ffc8e8..df8ced87413 100644 --- a/Changes.md +++ b/Changes.md @@ -1,4 +1,9 @@ -1.5.x.x (relative to 1.5.0.1) +1.5.x.x (relative to 1.5.1.0) +======= + + + +1.5.1.0 (relative to 1.5.0.1) ======= Features diff --git a/SConstruct b/SConstruct index 2e85847cc6d..246d37c3575 100644 --- a/SConstruct +++ b/SConstruct @@ -63,8 +63,8 @@ if codecs.lookup( locale.getpreferredencoding() ).name != "utf-8" : gafferMilestoneVersion = 1 # for announcing major milestones - may contain all of the below gafferMajorVersion = 5 # backwards-incompatible changes -gafferMinorVersion = 0 # new backwards-compatible features -gafferPatchVersion = 1 # bug fixes +gafferMinorVersion = 1 # new backwards-compatible features +gafferPatchVersion = 0 # bug fixes gafferVersionSuffix = "" # used for alpha/beta releases : "a1", "b2", etc. # All of the following must be considered when determining