From 1448c9c59ad25035913a63efd36996acbdb2b50a Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Wed, 5 Feb 2025 16:31:49 -0500 Subject: [PATCH 1/7] GraphGadget : Add `annotationsGadget()` method --- Changes.md | 1 + include/GafferUI/GraphGadget.h | 5 ++++ python/GafferUI/GraphEditor.py | 4 +-- python/GafferUITest/AnnotationsGadgetTest.py | 26 ++++++++++---------- src/GafferUI/GraphGadget.cpp | 10 ++++++++ src/GafferUIModule/GraphGadgetBinding.cpp | 1 + startup/gui/performanceMonitor.py | 2 +- 7 files changed, 33 insertions(+), 16 deletions(-) diff --git a/Changes.md b/Changes.md index 04c007844fa..550e807fce7 100644 --- a/Changes.md +++ b/Changes.md @@ -43,6 +43,7 @@ API - EditScopeAlgo : Added `renameRenderPass()` and `renameRenderPassNonEditableReason()` functions. - SceneAlgo : Added `parallelGatherLocations()` function. +- GraphGadget : Added `annotationsGadget()` function. 1.5.4.1 (relative to 1.5.4.0) ======= diff --git a/include/GafferUI/GraphGadget.h b/include/GafferUI/GraphGadget.h index d2d3b02da26..f7a4c0f4ea6 100644 --- a/include/GafferUI/GraphGadget.h +++ b/include/GafferUI/GraphGadget.h @@ -37,6 +37,7 @@ #pragma once +#include "GafferUI/AnnotationsGadget.h" #include "GafferUI/ContainerGadget.h" #include "GafferUI/ContextTracker.h" @@ -133,6 +134,10 @@ class GAFFERUI_API GraphGadget : public ContainerGadget AuxiliaryConnectionsGadget *auxiliaryConnectionsGadget(); const AuxiliaryConnectionsGadget *auxiliaryConnectionsGadget() const; + /// Returns the Gadget responsible for drawing annotations. + AnnotationsGadget *annotationsGadget(); + const AnnotationsGadget *annotationsGadget() const; + /// Finds all the upstream NodeGadgets connected to the specified node /// and appends them to the specified vector. Returns the new size of the vector. /// \note Here "upstream" nodes are defined as nodes at the end of input diff --git a/python/GafferUI/GraphEditor.py b/python/GafferUI/GraphEditor.py index 4c292285ea3..2bccd6e5a83 100644 --- a/python/GafferUI/GraphEditor.py +++ b/python/GafferUI/GraphEditor.py @@ -661,7 +661,7 @@ def __preRender( self, viewportGadget ) : def __annotationsMenu( self ) : graphGadget = self.graphGadget() - annotationsGadget = graphGadget["__annotations"] + annotationsGadget = graphGadget.annotationsGadget() annotations = Gaffer.MetadataAlgo.annotationTemplates() + [ "user", annotationsGadget.untemplatedAnnotations ] visiblePattern = annotationsGadget.getVisibleAnnotations() @@ -734,7 +734,7 @@ def appendMenuItem( annotation, label = None ) : def __setVisibleAnnotations( self, unused, annotations ) : - annotationsGadget = self.graphGadget()["__annotations"] + annotationsGadget = self.graphGadget().annotationsGadget() pattern = " ".join( a.replace( " ", r"\ " ) for a in annotations ) annotationsGadget.setVisibleAnnotations( pattern ) diff --git a/python/GafferUITest/AnnotationsGadgetTest.py b/python/GafferUITest/AnnotationsGadgetTest.py index be3f8744bd7..7ce1fa3566e 100644 --- a/python/GafferUITest/AnnotationsGadgetTest.py +++ b/python/GafferUITest/AnnotationsGadgetTest.py @@ -55,7 +55,7 @@ def testSimpleText( self ) : script["node"] = Gaffer.Node() graphGadget = GafferUI.GraphGadget( script ) - gadget = graphGadget["__annotations"] + gadget = graphGadget.annotationsGadget() self.assertEqual( gadget.annotationText( script["node"] ), "" ) Gaffer.MetadataAlgo.addAnnotation( script["node"], "user", Gaffer.MetadataAlgo.Annotation( "test" ) ) @@ -73,7 +73,7 @@ def testExtendLifetimePastGraphGadget( self ) : script["node"] = Gaffer.Node() graphGadget = GafferUI.GraphGadget( script ) - gadget = graphGadget["__annotations"] + gadget = graphGadget.annotationsGadget() del graphGadget Gaffer.MetadataAlgo.addAnnotation( script["node"], "user", Gaffer.MetadataAlgo.Annotation( "test" ) ) @@ -85,7 +85,7 @@ def testSubstitutedText( self ) : script["node"] = GafferTest.AddNode() graphGadget = GafferUI.GraphGadget( script ) - gadget = graphGadget["__annotations"] + gadget = graphGadget.annotationsGadget() self.assertEqual( gadget.annotationText( script["node"] ), "" ) Gaffer.MetadataAlgo.addAnnotation( script["node"], "user", Gaffer.MetadataAlgo.Annotation( "test : {op1}" ) ) @@ -107,7 +107,7 @@ def testSubstitutionsByPlugType( self ) : Gaffer.MetadataAlgo.addAnnotation( script["node"], "user", Gaffer.MetadataAlgo.Annotation( "{plug}" ) ) graphGadget = GafferUI.GraphGadget( script ) - gadget = graphGadget["__annotations"] + gadget = graphGadget.annotationsGadget() for plugType, value, substitution in [ ( Gaffer.BoolPlug, True, "On" ), @@ -130,7 +130,7 @@ def testInitialSubstitutedText( self ) : Gaffer.MetadataAlgo.addAnnotation( script["node"], "user", Gaffer.MetadataAlgo.Annotation( "test : {op1}" ) ) graphGadget = GafferUI.GraphGadget( script ) - gadget = graphGadget["__annotations"] + gadget = graphGadget.annotationsGadget() self.assertEqual( gadget.annotationText( script["node"] ), "test : 0" ) def testComputedText( self ) : @@ -149,7 +149,7 @@ def error( *unused ) : with GafferTest.ParallelAlgoTest.UIThreadCallHandler() as callHandler : graphGadget = GafferUI.GraphGadget( script ) - gadget = graphGadget["__annotations"] + gadget = graphGadget.annotationsGadget() # Value must be computed in background, so initially we expect a placeholder self.assertEqual( gadget.annotationText( script["node"] ), "test : ---" ) @@ -221,7 +221,7 @@ def testComputedTextCancellation( self ) : graphGadget = GafferUI.GraphGadget( script ) viewportGadget = GafferUI.ViewportGadget( graphGadget ) - gadget = graphGadget["__annotations"] + gadget = graphGadget.annotationsGadget() # Value must be computed in background, so initially we expect a placeholder self.assertEqual( gadget.annotationText( script["node"] ), "---" ) @@ -286,7 +286,7 @@ def testContextSensitiveText( self ) : with GafferTest.ParallelAlgoTest.UIThreadCallHandler() as callHandler : graphGadget = GafferUI.GraphGadget( script ) - gadget = graphGadget["__annotations"] + gadget = graphGadget.annotationsGadget() # Value must be computed in background, so initially we expect a placeholder self.assertEqual( gadget.annotationText( script["node"] ), "---" ) @@ -320,7 +320,7 @@ def testContextTracking( self ) : with GafferTest.ParallelAlgoTest.UIThreadCallHandler() as callHandler : graphGadget = GafferUI.GraphGadget( script ) - gadget = graphGadget["__annotations"] + gadget = graphGadget.annotationsGadget() # Value must be computed in background, so initially we expect a placeholder self.assertEqual( gadget.annotationText( script["node"] ), "---" ) @@ -349,7 +349,7 @@ def testSubstitutedTextRenderRequests( self ) : script["node"] = GafferTest.AddNode() graphGadget = GafferUI.GraphGadget( script ) - gadget = graphGadget["__annotations"] + gadget = graphGadget.annotationsGadget() viewportGadget = GafferUI.ViewportGadget( graphGadget ) renderRequests = GafferTest.CapturingSlot( viewportGadget.renderRequestSignal() ) @@ -391,7 +391,7 @@ def testDestroyGadgetWhileBackgroundThreadRuns( self ) : with GafferTest.ParallelAlgoTest.UIThreadCallHandler() as callHandler : graphGadget = GafferUI.GraphGadget( script ) - gadget = graphGadget["__annotations"] + gadget = graphGadget.annotationsGadget() # Value must be computed in background, so initially we expect a placeholder. self.assertEqual( gadget.annotationText( script["node"], "user" ), "---" ) @@ -417,7 +417,7 @@ def testRemoveNodeWhileBackgroundThreadRuns( self ) : with GafferTest.ParallelAlgoTest.UIThreadCallHandler() as callHandler : graphGadget = GafferUI.GraphGadget( script ) - gadget = graphGadget["__annotations"] + gadget = graphGadget.annotationsGadget() # Value must be computed in background, so initially we expect a placeholder self.assertEqual( gadget.annotationText( script["node"], "test" ), "---" ) @@ -445,7 +445,7 @@ def testRemoveAnnotationWhileBackgroundThreadRuns( self ) : with GafferTest.ParallelAlgoTest.UIThreadCallHandler() as callHandler : graphGadget = GafferUI.GraphGadget( script ) - gadget = graphGadget["__annotations"] + gadget = graphGadget.annotationsGadget() # Value must be computed in background, so initially we expect a placeholder self.assertEqual( gadget.annotationText( script["node"], "test0" ), "---" ) diff --git a/src/GafferUI/GraphGadget.cpp b/src/GafferUI/GraphGadget.cpp index bb1ee4d31cb..97493052ae4 100644 --- a/src/GafferUI/GraphGadget.cpp +++ b/src/GafferUI/GraphGadget.cpp @@ -466,6 +466,16 @@ const AuxiliaryConnectionsGadget *GraphGadget::auxiliaryConnectionsGadget() cons return getChild( g_auxiliaryConnectionsGadgetName ); } +AnnotationsGadget *GraphGadget::annotationsGadget() +{ + return getChild( g_annotationsGadgetName ); +} + +const AnnotationsGadget *GraphGadget::annotationsGadget() const +{ + return getChild( g_annotationsGadgetName ); +} + size_t GraphGadget::upstreamNodeGadgets( const Gaffer::Node *node, std::vector &upstreamNodeGadgets, size_t degreesOfSeparation ) { NodeGadget *g = nodeGadget( node ); diff --git a/src/GafferUIModule/GraphGadgetBinding.cpp b/src/GafferUIModule/GraphGadgetBinding.cpp index e67ca5ef9e7..80dd605274a 100644 --- a/src/GafferUIModule/GraphGadgetBinding.cpp +++ b/src/GafferUIModule/GraphGadgetBinding.cpp @@ -279,6 +279,7 @@ void GafferUIModule::bindGraphGadget() .def( "connectionGadgets", &connectionGadgets1, ( arg_( "plug" ), arg_( "excludedNodes" ) = object() ) ) .def( "connectionGadgets", &connectionGadgets2, ( arg_( "node" ), arg_( "excludedNodes" ) = object() ) ) .def( "auxiliaryConnectionsGadget", (AuxiliaryConnectionsGadget *(GraphGadget::*)())&GraphGadget::auxiliaryConnectionsGadget, return_value_policy() ) + .def( "annotationsGadget", (AnnotationsGadget *(GraphGadget::*)())&GraphGadget::annotationsGadget, return_value_policy() ) .def( "upstreamNodeGadgets", &upstreamNodeGadgets, ( arg( "node" ), arg( "degreesOfSeparation" ) = std::numeric_limits::max() ) ) .def( "downstreamNodeGadgets", &downstreamNodeGadgets, ( arg( "node" ), arg( "degreesOfSeparation" ) = std::numeric_limits::max() ) ) .def( "connectedNodeGadgets", &connectedNodeGadgets, ( arg( "node" ), arg( "direction" ) = Gaffer::Plug::Invalid, arg( "degreesOfSeparation" ) = std::numeric_limits::max() ) ) diff --git a/startup/gui/performanceMonitor.py b/startup/gui/performanceMonitor.py index f4cccfb1cc3..1074a676d9e 100644 --- a/startup/gui/performanceMonitor.py +++ b/startup/gui/performanceMonitor.py @@ -238,7 +238,7 @@ def __graphEditorCreated( graphEditor ) : # What we really want is for Editors to have plugs (like Views do), and for # the visible annotations to be specified on a promoted plug. Then # we could just set a `userDefault` for that plug. - annotationsGadget = graphEditor.graphGadget()["__annotations"] + annotationsGadget = graphEditor.graphGadget().annotationsGadget() annotations = Gaffer.MetadataAlgo.annotationTemplates() + [ "user", annotationsGadget.untemplatedAnnotations ] visiblePattern = annotationsGadget.getVisibleAnnotations() From 5e433ba91cceb739f58a24f553778274cb3e48e9 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Fri, 7 Feb 2025 11:47:52 -0500 Subject: [PATCH 2/7] MetadataAlgo : Add `annotations()` variant --- Changes.md | 1 + include/Gaffer/MetadataAlgo.h | 3 +++ python/GafferTest/MetadataAlgoTest.py | 12 ++++++++++++ src/Gaffer/MetadataAlgo.cpp | 13 +++++++++++-- src/GafferModule/MetadataAlgoBinding.cpp | 8 ++++---- src/GafferUI/AnnotationsGadget.cpp | 4 +--- 6 files changed, 32 insertions(+), 9 deletions(-) diff --git a/Changes.md b/Changes.md index 550e807fce7..6f1f4917216 100644 --- a/Changes.md +++ b/Changes.md @@ -44,6 +44,7 @@ API - EditScopeAlgo : Added `renameRenderPass()` and `renameRenderPassNonEditableReason()` functions. - SceneAlgo : Added `parallelGatherLocations()` function. - GraphGadget : Added `annotationsGadget()` function. +- MetadataAlgo : Added `annotations()` variant accepting `Gaffer::Metadata::RegistrationTypes`. The default is `All` to match existing behavior and the previous `annotations()` variant is deprecated. 1.5.4.1 (relative to 1.5.4.0) ======= diff --git a/include/Gaffer/MetadataAlgo.h b/include/Gaffer/MetadataAlgo.h index 5973cdf6986..5232024e0c6 100644 --- a/include/Gaffer/MetadataAlgo.h +++ b/include/Gaffer/MetadataAlgo.h @@ -37,6 +37,7 @@ #pragma once #include "Gaffer/Export.h" +#include "Gaffer/Metadata.h" #include "Gaffer/Node.h" #include "IECore/SimpleTypedData.h" @@ -180,7 +181,9 @@ struct GAFFER_API Annotation GAFFER_API void addAnnotation( Node *node, const std::string &name, const Annotation &annotation, bool persistent = true ); GAFFER_API Annotation getAnnotation( const Node *node, const std::string &name, bool inheritTemplate = false ); GAFFER_API void removeAnnotation( Node *node, const std::string &name ); +[[deprecated( "Use alternative form with `RegistrationTypes` instead")]] GAFFER_API void annotations( const Node *node, std::vector &names ); +GAFFER_API std::vector annotations( const Node *node, Metadata::RegistrationTypes types = Metadata::RegistrationTypes::All ); /// Pass `user = false` for annotations not intended for creation directly by the user. GAFFER_API void addAnnotationTemplate( const std::string &name, const Annotation &annotation, bool user = true ); diff --git a/python/GafferTest/MetadataAlgoTest.py b/python/GafferTest/MetadataAlgoTest.py index f6de9e96aaf..b1debfd010e 100644 --- a/python/GafferTest/MetadataAlgoTest.py +++ b/python/GafferTest/MetadataAlgoTest.py @@ -704,6 +704,18 @@ def testAnnotations( self ) : Gaffer.MetadataAlgo.Annotation( "abc", imath.Color3f( 0, 1, 0 ) ) ) + def testAnnotationRegistrationTypes( self ) : + + n = Gaffer.Node() + Gaffer.MetadataAlgo.addAnnotation( n, "persistent", Gaffer.MetadataAlgo.Annotation( "Always here", imath.Color3f( 1, 0, 0 ) ) ) + Gaffer.MetadataAlgo.addAnnotation( n, "nonPersistent", Gaffer.MetadataAlgo.Annotation( "Ephemeral", imath.Color3f( 0, 1, 0 ) ), False ) + + Types = Gaffer.Metadata.RegistrationTypes + self.assertEqual( Gaffer.MetadataAlgo.annotations( n, Types.InstancePersistent ), ["persistent"] ) + self.assertEqual( Gaffer.MetadataAlgo.annotations( n, Types.InstanceNonPersistent ), ["nonPersistent"] ) + for t in [ Types.None_, Types.TypeId, Types.TypeIdDescendant ] : + self.assertEqual( Gaffer.MetadataAlgo.annotations( n, t ), [] ) + def testAnnotationWithoutColor( self ) : n = Gaffer.Node() diff --git a/src/Gaffer/MetadataAlgo.cpp b/src/Gaffer/MetadataAlgo.cpp index 1536064970d..e29368ba01f 100644 --- a/src/Gaffer/MetadataAlgo.cpp +++ b/src/Gaffer/MetadataAlgo.cpp @@ -452,15 +452,24 @@ void removeAnnotation( Node *node, const std::string &name ) void annotations( const Node *node, std::vector &names ) { - const vector keys = Metadata::registeredValues( node ); + names = annotations( node ); +} + +std::vector annotations( const Node *node, Gaffer::Metadata::RegistrationTypes types ) +{ + std::vector result; + + const vector keys = Metadata::registeredValues( node, types ); for( const auto &key : keys ) { if( boost::starts_with( key.string(), g_annotationPrefix ) && boost::ends_with( key.string(), ":text" ) ) { - names.push_back( key.string().substr( g_annotationPrefix.size(), key.string().size() - g_annotationPrefix.size() - 5 ) ); + result.push_back( key.string().substr( g_annotationPrefix.size(), key.string().size() - g_annotationPrefix.size() - 5 ) ); } } + + return result; } void addAnnotationTemplate( const std::string &name, const Annotation &annotation, bool user ) diff --git a/src/GafferModule/MetadataAlgoBinding.cpp b/src/GafferModule/MetadataAlgoBinding.cpp index a1c50d2dccc..0c135464eb9 100644 --- a/src/GafferModule/MetadataAlgoBinding.cpp +++ b/src/GafferModule/MetadataAlgoBinding.cpp @@ -39,6 +39,7 @@ #include "MetadataAlgoBinding.h" #include "Gaffer/GraphComponent.h" +#include "Gaffer/Metadata.h" #include "Gaffer/MetadataAlgo.h" #include "Gaffer/Node.h" #include "Gaffer/Plug.h" @@ -134,10 +135,9 @@ void removeAnnotationWrapper( Node &node, const std::string &name ) removeAnnotation( &node, name ); } -list annotationsWrapper( const Node &node ) +list annotationsWrapper( const Node &node, Metadata::RegistrationTypes types ) { - std::vector names; - annotations( &node, names ); + std::vector names = annotations( &node, types ); list result; for( const auto &n : names ) { @@ -287,7 +287,7 @@ void GafferModule::bindMetadataAlgo() def( "addAnnotation", &addAnnotationWrapper, ( arg( "node" ), arg( "name" ), arg( "annotation" ), arg( "persistent" ) = true ) ); def( "getAnnotation", &getAnnotationWrapper, ( arg( "node" ), arg( "name" ), arg( "inheritTemplate" ) = false ) ); def( "removeAnnotation", &removeAnnotationWrapper, ( arg( "node" ), arg( "name" ) ) ); - def( "annotations", &annotationsWrapper, ( arg( "node" ) ) ); + def( "annotations", &annotationsWrapper, ( arg( "node" ), arg( "types" ) = Metadata::RegistrationTypes::All ) ); def( "addAnnotationTemplate", &addAnnotationTemplate, ( arg( "name" ), arg( "annotation" ), arg( "user" ) = true ) ); def( "getAnnotationTemplate", &getAnnotationTemplateWrapper, ( arg( "name" ) ) ); diff --git a/src/GafferUI/AnnotationsGadget.cpp b/src/GafferUI/AnnotationsGadget.cpp index 69a4eba2d21..ca3c71e4d3d 100644 --- a/src/GafferUI/AnnotationsGadget.cpp +++ b/src/GafferUI/AnnotationsGadget.cpp @@ -504,7 +504,6 @@ void AnnotationsGadget::update() bool dependsOnContext = false; - vector names; for( auto &ga : m_annotations ) { const Node *node = ga.first->node(); @@ -533,8 +532,7 @@ void AnnotationsGadget::update() annotations.hasPlugValueSubstitutions = false; annotations.standardAnnotations.clear(); - names.clear(); - MetadataAlgo::annotations( node, names ); + vector names = MetadataAlgo::annotations( node ); for( const auto &name : names ) { if( !StringAlgo::matchMultiple( name, m_visibleAnnotations ) ) From e5e8b2351557d6c4fb870f59f238fa4bebe097fd Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Wed, 15 Jan 2025 15:11:08 -0500 Subject: [PATCH 3/7] AnnotationsGadget : Add `annotationAt()` --- Changes.md | 1 + include/GafferUI/AnnotationsGadget.h | 16 ++ python/GafferUITest/AnnotationsGadgetTest.py | 63 ++++++++ src/GafferUI/AnnotationsGadget.cpp | 155 ++++++++++++------- src/GafferUIModule/GraphGadgetBinding.cpp | 14 ++ 5 files changed, 197 insertions(+), 52 deletions(-) diff --git a/Changes.md b/Changes.md index 6f1f4917216..3c66e2aef29 100644 --- a/Changes.md +++ b/Changes.md @@ -45,6 +45,7 @@ API - SceneAlgo : Added `parallelGatherLocations()` function. - GraphGadget : Added `annotationsGadget()` function. - MetadataAlgo : Added `annotations()` variant accepting `Gaffer::Metadata::RegistrationTypes`. The default is `All` to match existing behavior and the previous `annotations()` variant is deprecated. +- AnnotationsGadget : Added `annotationAt()` function. 1.5.4.1 (relative to 1.5.4.0) ======= diff --git a/include/GafferUI/AnnotationsGadget.h b/include/GafferUI/AnnotationsGadget.h index 54d01797bc8..8a729af0e68 100644 --- a/include/GafferUI/AnnotationsGadget.h +++ b/include/GafferUI/AnnotationsGadget.h @@ -41,6 +41,8 @@ #include "GafferUI/Gadget.h" +#include "IECoreGL/Selector.h" + #include "IECore/StringAlgo.h" #include @@ -82,6 +84,11 @@ class GAFFERUI_API AnnotationsGadget : public Gadget bool acceptsParent( const GraphComponent *potentialParent ) const override; + // Identifies an annotation by the node and it's name. + using AnnotationIdentifier = std::pair; + // Returns the node and annotation name under the specified line. + std::optional annotationAt( const IECore::LineSegment3f &lineInGadgetSpace ) const; + protected : // Protected constructor and friend status so only GraphGadget can @@ -148,6 +155,15 @@ class GAFFERUI_API AnnotationsGadget : public Gadget // When we are hidden, we want to cancel all background tasks. void visibilityChanged(); + // Map associating an `IECoreGL::Selector::IDRender` entry with a `AnnotationIndex`. + using AnnotationBufferMap = std::unordered_map; + + // If given an `AnnotationBufferMap` and `Selector`, draws all annotations + // with a unique `IDRender` index per annotation and fills `selectionIds`. + // If they are not given, no modification to the selection buffer IDs are + // made (all annotations have the ID for this widget). + void renderAnnotations( const Style *style, AnnotationBufferMap *selectionIds = nullptr ) const; + struct StandardAnnotation : public Gaffer::MetadataAlgo::Annotation { StandardAnnotation( const Gaffer::MetadataAlgo::Annotation &a, IECore::InternedString name ) : Annotation( a ), name( name ) {} diff --git a/python/GafferUITest/AnnotationsGadgetTest.py b/python/GafferUITest/AnnotationsGadgetTest.py index 7ce1fa3566e..3e90d62b264 100644 --- a/python/GafferUITest/AnnotationsGadgetTest.py +++ b/python/GafferUITest/AnnotationsGadgetTest.py @@ -458,5 +458,68 @@ def testRemoveAnnotationWhileBackgroundThreadRuns( self ) : # And wait for the task to complete. callHandler.assertCalled() + def testAnnotationAt( self ) : + + with GafferUI.Window() as window : + gadgetWidget = GafferUI.GadgetWidget() + + viewportGadget = gadgetWidget.getViewportGadget() + + script = Gaffer.ScriptNode() + script["node1"] = GafferTest.AddNode() + script["node2"] = GafferTest.AddNode() + + graphGadget = GafferUI.GraphGadget( script ) + + node1Origin = imath.V2f( 0, 0 ) + node2Origin = imath.V2f( 0, 10 ) + graphGadget.setNodePosition( script["node1"], node1Origin ) + graphGadget.setNodePosition( script["node2"], node2Origin ) + + viewportGadget.setPrimaryChild( graphGadget ) + + for node, name, text in [ + ( script["node1"], "userA", "testNode1UserA" ), + ( script["node1"], "userB", "testNode1UserB" ), + ( script["node2"], "userA", "testNode2UserA" ), + ( script["node2"], "userC", "testNode2UserC" ), + ] : + Gaffer.MetadataAlgo.addAnnotation( node, name, Gaffer.MetadataAlgo.Annotation( text ) ) + + window.setVisible( True ) + self.waitForIdle( 1000 ) + + viewportGadget.frame( imath.Box3f( imath.V3f( -3 ), imath.V3f( 13 ) ) ) + + annotationsGadget = graphGadget.annotationsGadget() + + nodeGadget1 = graphGadget.nodeGadget( script["node1"] ) + nodeGadget2 = graphGadget.nodeGadget( script["node2"] ) + + node1Corner = imath.V2f( nodeGadget1.bound().max().x, nodeGadget1.bound().max().y ) + node1Origin + node2Corner = imath.V2f( nodeGadget2.bound().max().x, nodeGadget2.bound().max().y ) + node2Origin + + for gadgetPosition, desiredNode, desiredName in [ + ( imath.V2f( 0.0, 0.0 ), None, "" ), + ( node1Corner + imath.V2f( 2.0, -1.0 ), script["node1"], "userA" ), + ( node1Corner + imath.V2f( 2.0, -4.0 ), script["node1"], "userB" ), + ( node2Corner + imath.V2f( 2.0, -1.0 ), script["node2"], "userA" ), + ( node2Corner + imath.V2f( 2.0, -4.0 ), script["node2"], "userC" ), + ] : + with self.subTest( gadgetPosition = gadgetPosition, desiredNode = desiredNode, desiredName = desiredName ) : + + annotation = annotationsGadget.annotationAt( + IECore.LineSegment3f( + imath.V3f( gadgetPosition.x, gadgetPosition.y, 1000 ), + imath.V3f( gadgetPosition.x, gadgetPosition.y, -1000 ) + ) + ) + if desiredNode is not None : + node, name = annotation + self.assertEqual( node, desiredNode ) + self.assertEqual( name, desiredName ) + else : + self.assertIsNone( annotation ) + if __name__ == "__main__": unittest.main() diff --git a/src/GafferUI/AnnotationsGadget.cpp b/src/GafferUI/AnnotationsGadget.cpp index ca3c71e4d3d..4bd29454f9a 100644 --- a/src/GafferUI/AnnotationsGadget.cpp +++ b/src/GafferUI/AnnotationsGadget.cpp @@ -36,6 +36,8 @@ #include "GafferUI/AnnotationsGadget.h" +#include "GafferSceneUI/SceneGadget.h" + #include "GafferUI/GraphGadget.h" #include "GafferUI/ImageGadget.h" #include "GafferUI/NodeGadget.h" @@ -49,6 +51,10 @@ #include "Gaffer/ScriptNode.h" #include "Gaffer/StringPlug.h" +#include "IECoreGL/Camera.h" +#include "IECoreGL/Selector.h" +#include "IECoreGL/ToGLCameraConverter.h" + #include "boost/algorithm/string/predicate.hpp" #include "boost/bind/bind.hpp" #include "boost/bind/placeholders.hpp" @@ -349,58 +355,7 @@ void AnnotationsGadget::renderLayer( Layer layer, const Style *style, RenderReas return; } - const_cast( this )->update(); - - for( const auto &ga : m_annotations ) - { - const Annotations &annotations = ga.second; - assert( !annotations.dirty ); - if( !annotations.renderable ) - { - continue; - } - - const Box2f b = nodeFrame( ga.first ); - - V2f bookmarkIconPos( b.min.x, b.max.y ); - V2f annotationOrigin( b.max.x + g_offset, b.max.y ); - if( ga.first->node() == ga.first->node()->ancestor()->getFocus() ) - { - const StandardNodeGadget *standardNodeGadget = runTimeCast( ga.first ); - if( standardNodeGadget ) - { - float fbw = standardNodeGadget->focusBorderWidth(); - bookmarkIconPos += V2f( -fbw, fbw ); - annotationOrigin += V2f( fbw, 0.0f ); - } - } - - if( annotations.bookmarked ) - { - style->renderImage( Box2f( bookmarkIconPos - V2f( 1.0 ), bookmarkIconPos + V2f( 1.0 ) ), bookmarkTexture() ); - } - - if( annotations.numericBookmark.string().size() ) - { - if( !annotations.bookmarked ) - { - style->renderImage( Box2f( bookmarkIconPos - V2f( 1.0 ), bookmarkIconPos + V2f( 1.0 ) ), numericBookmarkTexture() ); - } - - const Box3f textBounds = style->textBound( Style::LabelText, annotations.numericBookmark.string() ); - - const Imath::Color4f textColor( 0.8f ); - glPushMatrix(); - IECoreGL::glTranslate( V2f( bookmarkIconPos.x - 0.9 - textBounds.size().x, bookmarkIconPos.y - textBounds.size().y * 0.5 - 0.2 ) ); - style->renderText( Style::LabelText, annotations.numericBookmark.string(), Style::NormalState, &textColor ); - glPopMatrix(); - } - - for( const auto &a : annotations.standardAnnotations ) - { - annotationOrigin = style->renderAnnotation( annotationOrigin, a.renderText, Style::NormalState, a.colorData ? &a.color() : nullptr ); - } - } + renderAnnotations( style ); } unsigned AnnotationsGadget::layerMask() const @@ -764,3 +719,99 @@ void AnnotationsGadget::visibilityChanged() } } } + +void AnnotationsGadget::renderAnnotations( const Style *style, AnnotationBufferMap *selectionIds ) const +{ + const_cast( this )->update(); + + IECoreGL::Selector *selector = IECoreGL::Selector::currentSelector(); + + for( const auto &ga : m_annotations ) + { + const Annotations &annotations = ga.second; + assert( !annotations.dirty ); + if( !annotations.renderable ) + { + continue; + } + + const Box2f b = nodeFrame( ga.first ); + + V2f bookmarkIconPos( b.min.x, b.max.y ); + V2f annotationOrigin( b.max.x + g_offset, b.max.y ); + if( ga.first->node() == ga.first->node()->ancestor()->getFocus() ) + { + const StandardNodeGadget *standardNodeGadget = runTimeCast( ga.first ); + if( standardNodeGadget ) + { + float fbw = standardNodeGadget->focusBorderWidth(); + bookmarkIconPos += V2f( -fbw, fbw ); + annotationOrigin += V2f( fbw, 0.0f ); + } + } + + if( !selectionIds ) + { + if( annotations.bookmarked ) + { + style->renderImage( Box2f( bookmarkIconPos - V2f( 1.0 ), bookmarkIconPos + V2f( 1.0 ) ), bookmarkTexture() ); + } + + if( annotations.numericBookmark.string().size() ) + { + if( !annotations.bookmarked ) + { + style->renderImage( Box2f( bookmarkIconPos - V2f( 1.0 ), bookmarkIconPos + V2f( 1.0 ) ), numericBookmarkTexture() ); + } + + const Box3f textBounds = style->textBound( Style::LabelText, annotations.numericBookmark.string() ); + + const Imath::Color4f textColor( 0.8f ); + glPushMatrix(); + IECoreGL::glTranslate( V2f( bookmarkIconPos.x - 0.9 - textBounds.size().x, bookmarkIconPos.y - textBounds.size().y * 0.5 - 0.2 ) ); + style->renderText( Style::LabelText, annotations.numericBookmark.string(), Style::NormalState, &textColor ); + glPopMatrix(); + } + } + + for( const auto &a : annotations.standardAnnotations ) + { + if( selectionIds && selector ) + { + unsigned int id = selector->loadName(); + (*selectionIds)[id] = AnnotationIdentifier( ga.first->node(), a.name ); + } + + annotationOrigin = style->renderAnnotation( annotationOrigin, a.renderText, Style::NormalState, a.colorData ? &a.color() : nullptr ); + } + } +} + +std::optional AnnotationsGadget::annotationAt( const LineSegment3f &lineInGadgetSpace ) const +{ + std::vector selection; + AnnotationBufferMap annotationBuffer; + { + ViewportGadget::SelectionScope selectionScope( lineInGadgetSpace, this, selection, IECoreGL::Selector::Mode::IDRender ); + + const Style *currentStyle = style(); + currentStyle->bind(); + + // See `ViewportGadget::renderInternal()` for reasoning behind disabling blending. + glDisable( GL_BLEND ); + + renderAnnotations( currentStyle, &annotationBuffer ); + } + + if( selection.empty() ) + { + return std::optional( std::nullopt ); + } + + auto result = annotationBuffer.find( selection[0].name ); + if( result == annotationBuffer.end() ) + { + return std::optional( std::nullopt ); + } + return result->second; +} diff --git a/src/GafferUIModule/GraphGadgetBinding.cpp b/src/GafferUIModule/GraphGadgetBinding.cpp index 80dd605274a..5c9e40c233c 100644 --- a/src/GafferUIModule/GraphGadgetBinding.cpp +++ b/src/GafferUIModule/GraphGadgetBinding.cpp @@ -57,6 +57,8 @@ #include "Gaffer/Node.h" #include "Gaffer/ScriptNode.h" +#include "boost/pointer_cast.hpp" + using namespace boost::python; using namespace IECorePython; using namespace Gaffer; @@ -195,6 +197,17 @@ const std::string &annotationTextWrapper( const AnnotationsGadget &gadget, const return gadget.annotationText( &node, annotation ); } +object annotationAtWrapper( const AnnotationsGadget &gadget, const IECore::LineSegment3f &lineInGadgetSpace ) +{ + std::optional a = gadget.annotationAt( lineInGadgetSpace ); + if( a ) + { + return boost::python::make_tuple( NodePtr( const_cast( a.value().first ) ), a.value().second ); + } + + return object(); +} + bool connectNode( const GraphLayout &layout, GraphGadget &graph, Gaffer::Node &node, Gaffer::Set &potentialInputs ) { IECorePython::ScopedGILRelease gilRelease; @@ -311,6 +324,7 @@ void GafferUIModule::bindGraphGadget() .def( "setVisibleAnnotations", &AnnotationsGadget::setVisibleAnnotations ) .def( "getVisibleAnnotations", &AnnotationsGadget::getVisibleAnnotations, return_value_policy() ) .def( "annotationText", &annotationTextWrapper, return_value_policy(), ( arg( "node" ), arg( "annotation" ) = "user" ) ) + .def( "annotationAt", &annotationAtWrapper ) ; IECorePython::RunTimeTypedClass() From 8ae873515e8ae5bbb12560e59360b120c75689b3 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Thu, 30 Jan 2025 17:19:55 -0500 Subject: [PATCH 4/7] AnnotationsUI : Add `contextMenuSignal()` --- Changes.md | 1 + python/GafferUI/AnnotationsUI.py | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/Changes.md b/Changes.md index 3c66e2aef29..240d2f13bb8 100644 --- a/Changes.md +++ b/Changes.md @@ -46,6 +46,7 @@ API - GraphGadget : Added `annotationsGadget()` function. - MetadataAlgo : Added `annotations()` variant accepting `Gaffer::Metadata::RegistrationTypes`. The default is `All` to match existing behavior and the previous `annotations()` variant is deprecated. - AnnotationsGadget : Added `annotationAt()` function. +- AnnotationUI : Added `contextMenuSignal()` allowing customisations to the context menu for annotations. 1.5.4.1 (relative to 1.5.4.0) ======= diff --git a/python/GafferUI/AnnotationsUI.py b/python/GafferUI/AnnotationsUI.py index d2864c2d147..26902172032 100644 --- a/python/GafferUI/AnnotationsUI.py +++ b/python/GafferUI/AnnotationsUI.py @@ -37,6 +37,7 @@ import functools import re import imath +import weakref import IECore @@ -72,6 +73,44 @@ def __annotate( node, name, menu ) : dialogue = __AnnotationsDialogue( node, name ) dialogue.wait( parentWindow = menu.ancestor( GafferUI.Window ) ) +# A signal emitted when a popup menu for an annotation is about to be shown. +# This provides an opportunity to customize the menu from external code. +# The signature for slots is ( menuDefinition, node, name ) where `node` and +# `name` identify which annotation the menu is being created for. Slots should +# modify `menuDefinition` in place. + +__contextMenuSignal = Gaffer.Signals.Signal3() + +def contextMenuSignal() : + return __contextMenuSignal + +def __buttonPress( editorWeakRef, annotationsGadget, event ) : + + if event.buttons & event.Buttons.Right : + annotation = annotationsGadget.annotationAt( event.line ) + if annotation is None : + return False + + node, name = annotation + + menuDefinition = IECore.MenuDefinition() + contextMenuSignal()( menuDefinition, node, name) + + global __popupMenu + __popupMenu = GafferUI.Menu( menuDefinition ) + __popupMenu.popup( editorWeakRef() ) + + return True + + return False + +def __graphEditorCreated( editor ) : + editor.graphGadget().annotationsGadget().buttonPressSignal().connect( + functools.partial( __buttonPress, weakref.ref( editor ) ) + ) + +GafferUI.GraphEditor.instanceCreatedSignal().connect( __graphEditorCreated ) + class _AnnotationsHighlighter( GafferUI.CodeWidget.Highlighter ) : __substitutionRe = re.compile( r"(\{[^}]+\})" ) From 688c02c2c16863887c238554d21d56b98c3224ea Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Mon, 3 Feb 2025 15:41:59 -0500 Subject: [PATCH 5/7] AnnotationsUI : Add copy / paste --- Changes.md | 1 + python/GafferUI/AnnotationsUI.py | 55 ++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/Changes.md b/Changes.md index 240d2f13bb8..6be5b068b38 100644 --- a/Changes.md +++ b/Changes.md @@ -11,6 +11,7 @@ Features - May be disabled entirely with `GafferScene.SceneAlgo.deregisterRenderAdaptor( "USDPointInstancerAdaptor" )`. - Viewer : Added "Expand USD Instancers" item to the Expansion menu. Defaults to on for all renderers except OpenGL. - PromotePointInstances : Added a new node for selectively converting a subset of a USD PointInstancer to expanded "hero" geometry. +- Annotations : Added copy and paste of annotations. The right-click menu of an annotation allows you to copy the annotation. Pressing Control + V in the Node Editor will paste the annotation to the selected nodes. Improvements ------------ diff --git a/python/GafferUI/AnnotationsUI.py b/python/GafferUI/AnnotationsUI.py index 26902172032..971a8d7f556 100644 --- a/python/GafferUI/AnnotationsUI.py +++ b/python/GafferUI/AnnotationsUI.py @@ -104,13 +104,68 @@ def __buttonPress( editorWeakRef, annotationsGadget, event ) : return False +def __clipboardIsAnnotation( clipboard ) : + + return ( + isinstance( clipboard, IECore.CompoundData ) and + [ "color", "name", "text" ] == sorted( clipboard.keys() ) and + isinstance( clipboard["color"], IECore.Color3fData ) and + isinstance( clipboard["name"], IECore.StringData ) and + isinstance( clipboard["text"], IECore.StringData ) + ) + +def __keyPress( editor, event ) : + + if event.key == "V" and event.modifiers == event.modifiers.Control : + scriptNode = editor.scriptNode() + clipboard = scriptNode.ancestor( Gaffer.ApplicationRoot ).getClipboardContents() + + if __clipboardIsAnnotation( clipboard ) : + with Gaffer.UndoScope( scriptNode ) : + editorSelection = [ i for i in scriptNode.selection() if editor.graphGadget().nodeGadget( i ) is not None ] + for n in editorSelection : + Gaffer.MetadataAlgo.addAnnotation( + n, + clipboard["name"].value, + Gaffer.MetadataAlgo.Annotation( clipboard["text"].value, clipboard["color"].value ) + ) + return True + + return False + +def __copyAnnotation( node, name ) : + + annotation = Gaffer.MetadataAlgo.getAnnotation( node, name, True ) + + data = IECore.CompoundData( + { + "color" : IECore.Color3fData( annotation.color() ), + "name" : IECore.StringData( name ), + "text" : IECore.StringData( annotation.text() ), + } + ) + + node.scriptNode().ancestor( Gaffer.ApplicationRoot ).setClipboardContents( data ) + +def __contextMenu( menuDefinition, node, name ) : + + menuDefinition.append( + "/Copy", + { + "command" : functools.partial( __copyAnnotation, node, name ), + }, + ) + def __graphEditorCreated( editor ) : editor.graphGadget().annotationsGadget().buttonPressSignal().connect( functools.partial( __buttonPress, weakref.ref( editor ) ) ) + editor.keyPressSignal().connect( __keyPress ) GafferUI.GraphEditor.instanceCreatedSignal().connect( __graphEditorCreated ) +contextMenuSignal().connect( __contextMenu ) + class _AnnotationsHighlighter( GafferUI.CodeWidget.Highlighter ) : __substitutionRe = re.compile( r"(\{[^}]+\})" ) From 53aea132e929c7121b0b301386a6950c7986f2e9 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Mon, 3 Feb 2025 17:15:12 -0500 Subject: [PATCH 6/7] AnnotationsUI : Double click to edit annotation --- Changes.md | 4 +++- python/GafferUI/AnnotationsUI.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Changes.md b/Changes.md index 6be5b068b38..051b268d2fb 100644 --- a/Changes.md +++ b/Changes.md @@ -11,7 +11,9 @@ Features - May be disabled entirely with `GafferScene.SceneAlgo.deregisterRenderAdaptor( "USDPointInstancerAdaptor" )`. - Viewer : Added "Expand USD Instancers" item to the Expansion menu. Defaults to on for all renderers except OpenGL. - PromotePointInstances : Added a new node for selectively converting a subset of a USD PointInstancer to expanded "hero" geometry. -- Annotations : Added copy and paste of annotations. The right-click menu of an annotation allows you to copy the annotation. Pressing Control + V in the Node Editor will paste the annotation to the selected nodes. +- Annotations : + - Added copy and paste of annotations. The right-click menu of an annotation allows you to copy the annotation. Pressing Control + V in the Node Editor will paste the annotation to the selected nodes. + - Double clicking on an annotation now pops up the annotation editor dialogue. Improvements ------------ diff --git a/python/GafferUI/AnnotationsUI.py b/python/GafferUI/AnnotationsUI.py index 971a8d7f556..be0ca6b3170 100644 --- a/python/GafferUI/AnnotationsUI.py +++ b/python/GafferUI/AnnotationsUI.py @@ -102,6 +102,21 @@ def __buttonPress( editorWeakRef, annotationsGadget, event ) : return True + return True # Needed for `__buttonDoubleClick()` to fire + +def __buttonDoubleClick( editorWeakRef, annotationsGadget, event ) : + + if event.buttons == event.Buttons.Left : + annotation = annotationsGadget.annotationAt( event.line ) + if annotation is None : + return False + + node, name = annotation + + __annotate( node, name, editorWeakRef() ) + + return True + return False def __clipboardIsAnnotation( clipboard ) : @@ -160,6 +175,9 @@ def __graphEditorCreated( editor ) : editor.graphGadget().annotationsGadget().buttonPressSignal().connect( functools.partial( __buttonPress, weakref.ref( editor ) ) ) + editor.graphGadget().annotationsGadget().buttonDoubleClickSignal().connect( + functools.partial( __buttonDoubleClick, weakref.ref( editor ) ) + ) editor.keyPressSignal().connect( __keyPress ) GafferUI.GraphEditor.instanceCreatedSignal().connect( __graphEditorCreated ) From 15bc013bb707f08c50843bbfaf572a9ae04a52b8 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Fri, 7 Feb 2025 12:49:12 -0500 Subject: [PATCH 7/7] AnnotationsUI : Only edit persistent annotations --- python/GafferUI/AnnotationsUI.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/python/GafferUI/AnnotationsUI.py b/python/GafferUI/AnnotationsUI.py index be0ca6b3170..1147fc3747d 100644 --- a/python/GafferUI/AnnotationsUI.py +++ b/python/GafferUI/AnnotationsUI.py @@ -75,15 +75,23 @@ def __annotate( node, name, menu ) : # A signal emitted when a popup menu for an annotation is about to be shown. # This provides an opportunity to customize the menu from external code. -# The signature for slots is ( menuDefinition, node, name ) where `node` and -# `name` identify which annotation the menu is being created for. Slots should -# modify `menuDefinition` in place. +# The signature for slots is ( menuDefinition, annotation, persistent ) where +# `annotation` is a tuple of `( node, name )` and `persistent` indicates whether +# or not the annotation will be serialised with the script. Slots should modify +# `menuDefinition` in place. __contextMenuSignal = Gaffer.Signals.Signal3() def contextMenuSignal() : return __contextMenuSignal +def __annotationIsPersistent( annotation ) : + + node, name = annotation + + persistentAnnotations = Gaffer.MetadataAlgo.annotations( node, Gaffer.Metadata.RegistrationTypes.InstancePersistent ) + return name in persistentAnnotations + def __buttonPress( editorWeakRef, annotationsGadget, event ) : if event.buttons & event.Buttons.Right : @@ -91,10 +99,8 @@ def __buttonPress( editorWeakRef, annotationsGadget, event ) : if annotation is None : return False - node, name = annotation - menuDefinition = IECore.MenuDefinition() - contextMenuSignal()( menuDefinition, node, name) + contextMenuSignal()( menuDefinition, annotation, __annotationIsPersistent( annotation ) ) global __popupMenu __popupMenu = GafferUI.Menu( menuDefinition ) @@ -108,7 +114,7 @@ def __buttonDoubleClick( editorWeakRef, annotationsGadget, event ) : if event.buttons == event.Buttons.Left : annotation = annotationsGadget.annotationAt( event.line ) - if annotation is None : + if annotation is None or not __annotationIsPersistent( annotation ) : return False node, name = annotation @@ -162,12 +168,14 @@ def __copyAnnotation( node, name ) : node.scriptNode().ancestor( Gaffer.ApplicationRoot ).setClipboardContents( data ) -def __contextMenu( menuDefinition, node, name ) : +def __contextMenu( menuDefinition, annotation, persistent ) : + node, name = annotation menuDefinition.append( "/Copy", { "command" : functools.partial( __copyAnnotation, node, name ), + "active" : persistent }, )