diff --git a/Changes.md b/Changes.md
index 5727b48fbfb..fd43ddcc33c 100644
--- a/Changes.md
+++ b/Changes.md
@@ -6,6 +6,7 @@ Improvements
- Group : Added `sets` plug, to control what sets the group belongs to.
- USD : Added automatic render-time translation of UsdPreviewSurface shaders to Cycles.
+- SetEditor : Added support for dragging a set name onto a node in the Graph Editor to create or modify a connected `SetFilter` node. Holding Shift while dragging will add to the set expression. Holding Control will remove from the set expression. Only set expressions with a simple list of sets are supported. Expressions with boolean or hierarchy operators are not supported.
1.4.0.0 (relative to 1.3.16.0)
=======
diff --git a/python/GafferSceneUI/FilteredSceneProcessorUI.py b/python/GafferSceneUI/FilteredSceneProcessorUI.py
index ef3265b3407..7e84dd651db 100644
--- a/python/GafferSceneUI/FilteredSceneProcessorUI.py
+++ b/python/GafferSceneUI/FilteredSceneProcessorUI.py
@@ -88,6 +88,7 @@ def __nodeGadget( node ) :
nodeGadget = GafferUI.StandardNodeGadget( node )
GafferSceneUI.PathFilterUI.addObjectDropTarget( nodeGadget )
+ GafferSceneUI.SetFilterUI.addSetDropTarget( nodeGadget )
return nodeGadget
diff --git a/python/GafferSceneUI/PathFilterUI.py b/python/GafferSceneUI/PathFilterUI.py
index 55e5b0097b6..637eb33e462 100644
--- a/python/GafferSceneUI/PathFilterUI.py
+++ b/python/GafferSceneUI/PathFilterUI.py
@@ -316,6 +316,9 @@ def __dragLeave( nodeGadget, event ) :
global __originalDragPointer
+ if __originalDragPointer is None :
+ return False
+
GafferUI.Pointer.setCurrent( __originalDragPointer )
__originalDragPointer = None
diff --git a/python/GafferSceneUI/SetEditor.py b/python/GafferSceneUI/SetEditor.py
index 6dc344ecaa3..26012815f95 100644
--- a/python/GafferSceneUI/SetEditor.py
+++ b/python/GafferSceneUI/SetEditor.py
@@ -189,8 +189,12 @@ def __dragBegin( self, widget, event ) :
# prevent the path itself from being dragged
return IECore.StringVectorData()
- GafferUI.Pointer.setCurrent( "paths" )
column = self.__pathListing.columnAt( imath.V2f( event.line.p0.x, event.line.p0.y ) )
+ if isinstance( column, _GafferSceneUI._SetEditor.SetNameColumn ) :
+ GafferUI.Pointer.setCurrent( "sets" )
+ else :
+ GafferUI.Pointer.setCurrent( "paths" )
+
if column == self.__setMembersColumn :
return IECore.StringVectorData( self.__getSetMembers( setNames ).paths() )
elif column == self.__selectedSetMembersColumn :
diff --git a/python/GafferSceneUI/SetFilterUI.py b/python/GafferSceneUI/SetFilterUI.py
index 3a84460ce88..f0842900120 100644
--- a/python/GafferSceneUI/SetFilterUI.py
+++ b/python/GafferSceneUI/SetFilterUI.py
@@ -34,9 +34,15 @@
#
##########################################################################
+import enum
+import imath
+
import Gaffer
import GafferUI
import GafferScene
+import GafferSceneUI
+
+import IECore
##########################################################################
# Metadata
@@ -94,3 +100,163 @@
}
)
+
+##########################################################################
+# NodeGadget drop handler
+##########################################################################
+
+GafferUI.Pointer.registerPointer( "sets", GafferUI.Pointer( "pointerSets.png", imath.V2i( 53, 14 ) ) )
+GafferUI.Pointer.registerPointer( "addSets", GafferUI.Pointer( "pointerAddSets.png", imath.V2i( 53, 14 ) ) )
+GafferUI.Pointer.registerPointer( "removeSets", GafferUI.Pointer( "pointerRemoveSets.png", imath.V2i( 53, 14 ) ) )
+GafferUI.Pointer.registerPointer( "replaceSets", GafferUI.Pointer( "pointerReplaceSets.png", imath.V2i( 53, 14 ) ) )
+
+__DropMode = enum.Enum( "__DropMode", [ "None_", "Add", "Remove", "Replace" ] )
+
+__originalDragPointer = None
+
+def __setsPlug( node ) :
+
+ for plug in Gaffer.Plug.InputRange( node ) :
+ if Gaffer.Metadata.value( plug, "ui:scene:acceptsSetNames" ) or Gaffer.Metadata.value( plug, "ui:scene:acceptsSetExpression" ) :
+ return plug
+
+ return None
+
+def __editable( plug ) :
+ if Gaffer.MetadataAlgo.readOnly( plug ) or not plug.settable() :
+ return False
+
+ if Gaffer.Metadata.value( plug, "ui:scene:acceptsSetNames" ) or Gaffer.Metadata.value( plug, "ui:scene:acceptsSetExpression" ) :
+ plugValue = plug.getValue()
+ if any( i in plugValue for i in [ "(", ")", "|", "-", "&"] ) :
+ return False
+
+ plugTokens = plugValue.split( " " )
+ if any( i in plugTokens for i in [ "in", "containing" ] ) :
+ return False
+
+ return True
+
+def __dropMode( nodeGadget, event ) :
+
+ setsPlug = __setsPlug( nodeGadget.node() )
+ if setsPlug is None :
+ filter = None
+ if nodeGadget.node()["filter"].getInput() is not None :
+ filter = nodeGadget.node()["filter"].source().node()
+ if filter is None :
+ return __DropMode.Replace if __editable( nodeGadget.node()["filter"] ) else __DropMode.None_
+ elif not isinstance( filter, GafferScene.SetFilter ) :
+ return __DropMode.None_
+ setsPlug = filter["setExpression"]
+
+ if not __editable( setsPlug ) :
+ return __DropMode.None_
+
+ if event.modifiers & event.Modifiers.Shift :
+ return __DropMode.Add
+ elif event.modifiers & event.Modifiers.Control :
+ return __DropMode.Remove
+ else :
+ return __DropMode.Replace
+
+def __dragEnter( nodeGadget, event ) :
+
+ if not isinstance( event.data, IECore.StringVectorData ) :
+ return False
+
+ if not (
+ all( i.startswith( "/" ) for i in event.data ) or
+ event.sourceWidget.ancestor( GafferSceneUI.SetEditor ) is not None
+ ) :
+ return False
+
+ if not len ( event.data ) :
+ return False
+
+ if __dropMode( nodeGadget, event ) == __DropMode.None_ :
+ return False
+
+ global __originalDragPointer
+ __originalDragPointer = GafferUI.Pointer.getCurrent()
+
+ return True
+
+def __dragLeave( nodeGadget, event ) :
+
+ global __originalDragPointer
+
+ if __originalDragPointer is None :
+ return False
+
+ GafferUI.Pointer.setCurrent( __originalDragPointer )
+ __originalDragPointer = None
+
+ return True
+
+def __dragMove( nodeGadget, event ) :
+
+ global __originalDragPointer
+ if __originalDragPointer is None :
+ return False
+
+ GafferUI.Pointer.setCurrent( __dropMode( nodeGadget, event ).name.lower() + "Sets" )
+
+ return True
+
+def __drop( nodeGadget, event ) :
+
+ global __originalDragPointer
+ if __originalDragPointer is None :
+ return False
+
+ setsPlug = __setsPlug( nodeGadget.node() )
+ if setsPlug is None :
+ setsPlug = __setsPlug( nodeGadget.node()["filter"].source().node() )
+
+ dropSets = event.data
+
+ dropMode = __dropMode( nodeGadget, event )
+ if dropMode == __DropMode.Replace :
+ sets = sorted( dropSets )
+ elif dropMode == __DropMode.Add :
+ sets = set( setsPlug.getValue().split( " " ) )
+ sets.update( dropSets )
+ sets = sorted( sets )
+ else :
+ sets = set( setsPlug.getValue().split( " " ) )
+ sets.difference_update( dropSets )
+ sets = sorted( sets )
+
+ with Gaffer.UndoScope( nodeGadget.node().ancestor( Gaffer.ScriptNode ) ) :
+
+ if setsPlug is None :
+
+ setFilter = GafferScene.SetFilter()
+ nodeGadget.node().parent().addChild( setFilter )
+ nodeGadget.node()["filter"].setInput( setFilter["out"] )
+
+ setsPlug = setFilter["setExpression"]
+
+ setsPlug.setValue( " ".join( sets ) )
+
+ GafferUI.Pointer.setCurrent( __originalDragPointer )
+ __originalDragPointer = None
+
+ return True
+
+def addSetDropTarget( nodeGadget ) :
+
+ nodeGadget.dragEnterSignal().connect( __dragEnter, scoped = False )
+ nodeGadget.dragLeaveSignal().connect( __dragLeave, scoped = False )
+ nodeGadget.dragMoveSignal().connect( __dragMove, scoped = False )
+ nodeGadget.dropSignal().connect( __drop, scoped = False )
+
+def __nodeGadget( setFilter ) :
+
+ nodeGadget = GafferUI.StandardNodeGadget( setFilter )
+ addSetDropTarget( nodeGadget )
+
+ return nodeGadget
+
+GafferUI.NodeGadget.registerNodeGadget( GafferScene.SetFilter, __nodeGadget )
diff --git a/python/GafferSceneUI/SetUI.py b/python/GafferSceneUI/SetUI.py
index 501ef8926e9..18f2406cbd7 100644
--- a/python/GafferSceneUI/SetUI.py
+++ b/python/GafferSceneUI/SetUI.py
@@ -329,6 +329,7 @@ def __nodeGadget( node ) :
nodeGadget = GafferUI.StandardNodeGadget( node )
GafferSceneUI.PathFilterUI.addObjectDropTarget( nodeGadget )
+ GafferSceneUI.SetFilterUI.addSetDropTarget( nodeGadget )
return nodeGadget
diff --git a/resources/graphics.py b/resources/graphics.py
index c2ba048a6d5..3125d39a2f3 100644
--- a/resources/graphics.py
+++ b/resources/graphics.py
@@ -51,7 +51,11 @@
"objects", # \todo prefix with 'pointer'
"addObjects", # \todo prefix with 'pointer'
"removeObjects", # \todo prefix with 'pointer'
- "replaceObjects" # \todo prefix with 'pointer'
+ "replaceObjects", # \todo prefix with 'pointer'
+ "pointerSets",
+ "pointerReplaceSets",
+ "pointerAddSets",
+ "pointerRemoveSets",
]
},
diff --git a/resources/graphics.svg b/resources/graphics.svg
index d28532bb367..4e6a1f1f5c3 100644
--- a/resources/graphics.svg
+++ b/resources/graphics.svg
@@ -804,7 +804,8 @@
y="983"
style="font-size:24px;fill:#f4f4f4;fill-opacity:1" />POINTERS - PathFilterUI POINTERS - PathFilterUI, SetFilterUI
+
+ style="opacity:1;fill:none;fill-opacity:1;stroke:none;stroke-width:3.12102342;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.10173225;stroke-opacity:1;paint-order:markers stroke fill" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+