Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ContextTracker : Add new UI class for tracking active contexts #5961

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ Improvements
- Premultiply, Unpremultiply :
- Added `ignoreMissingAlpha` plug.
- Optimised the pass-through of the alpha channel.
- EditScope : Filtered the EditScope menu to show only nodes that are active in the relevant context.
- GraphGadget :
- Improved highlighting of active nodes, with more accurate tracking of Loop node iterations.
- Annotation `{plug}` substitutions are now evaluated in a context determined relative to the focus node.
- Spreadsheet : Added yellow underlining to the currently active row.

Fixes
-----
Expand All @@ -25,10 +30,12 @@ Fixes
API
---

- ContextTracker : Added a new class that determines what contexts nodes are evaluated in relative to the focus node. This allows UI components to provide improved context-sensitive feedback to the user.
- Editor :
- Added `settings()` method, which returns a node hosting plugs specifying settings for the editor.
- Added `_updateFromSettings()` method, which is called when a subclass should update to reflect changes to the settings.
- SceneEditor : Added new base class to simplify the creation of scene-specific editors.
- Loop : Added `previousIteration()` method.

Breaking Changes
----------------
Expand All @@ -45,6 +52,7 @@ Breaking Changes
- Editor, NodeToolbar, PlugLayout, PlugValueWidget :
- Removed `setContext()` methods.
- Deprecated `getContext()` methods. Use `context()` instead.
- Loop : Removed `nextIterationContext()` method.

1.4.x.x (relative to 1.4.9.0)
=======
Expand Down
7 changes: 4 additions & 3 deletions include/Gaffer/Loop.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,10 @@ class GAFFER_API Loop : public ComputeNode
Gaffer::Plug *correspondingInput( const Gaffer::Plug *output ) override;
const Gaffer::Plug *correspondingInput( const Gaffer::Plug *output ) const override;

/// Returns the context that will be used to evaluate `nextPlug()` in
/// the next iteration of the loop (relative to the current context).
ContextPtr nextIterationContext() const;
/// Returns the input plug and context that form the previous iteration of the loop
/// with respect to the `output` plug and the current context. Returns `{ nullptr, nullptr }`
/// if there is no such iteration.
std::pair<const ValuePlug *, ContextPtr> previousIteration( const ValuePlug *output ) const;

void affects( const Plug *input, DependencyNode::AffectedPlugsContainer &outputs ) const override;

Expand Down
6 changes: 4 additions & 2 deletions include/GafferUI/AnnotationsGadget.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ namespace GafferUI

class GraphGadget;
class NodeGadget;
IE_CORE_FORWARDDECLARE( ContextTracker );

class GAFFERUI_API AnnotationsGadget : public Gadget
{
Expand Down Expand Up @@ -132,7 +133,7 @@ class GAFFERUI_API AnnotationsGadget : public Gadget
void plugDirtied( const Gaffer::Plug *plug, Annotations *annotations );
// If the substitutions are from computed plugs, then we also need to
// update when the context changes.
void scriptContextChanged();
void contextTrackerChanged();
// Some plug substitutions may depend on computes, in which case we must
// perform the substitutions in a BackgroundTask to avoid blocking the
// UI. This function schedules such a task, or if the values are not
Expand Down Expand Up @@ -169,7 +170,8 @@ class GAFFERUI_API AnnotationsGadget : public Gadget

Gaffer::Signals::ScopedConnection m_graphGadgetChildAddedConnection;
Gaffer::Signals::ScopedConnection m_graphGadgetChildRemovedConnection;
Gaffer::Signals::ScopedConnection m_scriptContextChangedConnection;
ContextTrackerPtr m_contextTracker;
Gaffer::Signals::ScopedConnection m_contextTrackerChangedConnection;

using AnnotationsContainer = std::unordered_map<const NodeGadget *, Annotations>;
AnnotationsContainer m_annotations;
Expand Down
178 changes: 178 additions & 0 deletions include/GafferUI/ContextTracker.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2024, Cinesite VFX Ltd. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above
// copyright notice, this list of conditions and the following
// disclaimer.
//
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials provided with
// the distribution.
//
// * Neither the name of John Haddon nor the names of
// any other contributors to this software may be used to endorse or
// promote products derived from this software without specific prior
// written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
//////////////////////////////////////////////////////////////////////////

#pragma once

#include "GafferUI/Export.h"

#include "Gaffer/Context.h"
#include "Gaffer/Set.h"
#include "Gaffer/Signals.h"

#include "IECore/Canceller.h"
#include "IECore/RefCounted.h"

#include <unordered_map>
#include <unordered_set>

namespace Gaffer
{

class BackgroundTask;
class Plug;
class ScriptNode;
IE_CORE_FORWARDDECLARE( Node );
IE_CORE_FORWARDDECLARE( Context )

} // namespace Gaffer

namespace GafferUI
{

/// Utility class for UI components which display context-sensitive information
/// to users. This tracks which upstream nodes contribute to the result at a
/// particular target node, and also what context they should be evaluated in
/// with respect to that node.
class GAFFERUI_API ContextTracker final : public IECore::RefCounted, public Gaffer::Signals::Trackable
{

public :

/// Constructs an instance that will track the graph upstream of the
/// target `node`, taking into account what connections are active in
/// the target `context`.
ContextTracker( const Gaffer::NodePtr &node, const Gaffer::ContextPtr &context );
~ContextTracker() override;

IE_CORE_DECLAREMEMBERPTR( ContextTracker );

/// Shared instances
/// ================
///
/// Tracking the upstream contexts can involve significant computation,
/// so it is recommended that ContextTracker instances are shared
/// between UI components. The `aquire()` methods maintain a pool of
/// instances for this purpose. Acquisition and destruction of shared
/// instances is not threadsafe, and must always be done on the UI
/// thread.

/// Returns a shared instance for the target `node`. The node must
/// belong to a ScriptNode, so that `ScriptNode::context()` can be used
/// to provide the target context.
static Ptr acquire( const Gaffer::NodePtr &node );
/// Returns an shared instance that will automatically track the focus
/// node in the specified `script`.
static Ptr acquireForFocus( Gaffer::ScriptNode *script );

/// Target
/// ======

const Gaffer::Node *targetNode() const;
const Gaffer::Context *targetContext() const;

/// Update and signalling
/// =====================
///
/// Updates are performed asynchronously in background tasks so that
/// the UI is never blocked. Clients should connect to `changedSignal()`
/// to be notified when updates are complete.

/// Returns true if an update is in-progress, in which case queries will
/// return stale values.
bool updatePending() const;
using Signal = Gaffer::Signals::Signal<void ( ContextTracker & ), Gaffer::Signals::CatchingCombiner<void>>;
/// Signal emitted when the results of any queries have changed.
Signal &changedSignal();

/// Queries
/// =======
///
/// Queries return immediately so will not block the UI waiting for computation.
/// But while `updatePending()` is `true` they will return stale values.

/// Returns true if the specified plug or node is active with respect to
/// the target node and context.
bool isActive( const Gaffer::Plug *plug ) const;
bool isActive( const Gaffer::Node *node ) const;

/// Returns the most suitable context for the UI to evaluate a plug or
/// node in. This will always return a valid context, even if
/// `isActive()` returns false.
Gaffer::ConstContextPtr context( const Gaffer::Plug *plug ) const;
Gaffer::ConstContextPtr context( const Gaffer::Node *node ) const;

private :

void updateNode( const Gaffer::NodePtr &node );
void plugDirtied( const Gaffer::Plug *plug );
void contextChanged( IECore::InternedString variable );
void scheduleUpdate();
void updateInBackground();
const Gaffer::Context *findPlugContext( const Gaffer::Plug *plug ) const;

Gaffer::ConstNodePtr m_node;
Gaffer::ConstContextPtr m_context;
Gaffer::Signals::ScopedConnection m_plugDirtiedConnection;

Gaffer::Signals::ScopedConnection m_idleConnection;
std::unique_ptr<Gaffer::BackgroundTask> m_updateTask;
Signal m_changedSignal;

struct NodeData
{
Gaffer::ConstContextPtr context = nullptr;
// If `true`, then all input plugs on the node are assumed to be
// active in the Node's context. This is just an optimisation that
// allows us to keep the size of `m_plugContexts` to a minimum.
bool allInputsActive = false;
};

using NodeContexts = std::unordered_map<Gaffer::ConstNodePtr, NodeData>;
NodeContexts m_nodeContexts;
using PlugContexts = std::unordered_map<Gaffer::ConstPlugPtr, Gaffer::ConstContextPtr>;
// Stores plug-specific contexts, which take precedence over `m_nodeContexts`.
PlugContexts m_plugContexts;

static void visit(
std::deque<std::pair<const Gaffer::Plug *, Gaffer::ConstContextPtr>> &toVisit,
NodeContexts &nodeContexts, PlugContexts &plugContexts, const IECore::Canceller *canceller
);

};

IE_CORE_DECLAREPTR( ContextTracker )

} // namespace GafferUI
49 changes: 6 additions & 43 deletions include/GafferUI/GraphGadget.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@
#pragma once

#include "GafferUI/ContainerGadget.h"
#include "GafferUI/ContextTracker.h"

#include "Gaffer/CompoundNumericPlug.h"
#include "Gaffer/Context.h"
#include "Gaffer/Plug.h"

#include "Gaffer/BackgroundTask.h"
Expand All @@ -56,11 +56,6 @@ IE_CORE_FORWARDDECLARE( ScriptNode );
IE_CORE_FORWARDDECLARE( Set );
}

namespace GafferUIModule
{
class ActivePlugsWrapperClassToUseAsFriend;
}

namespace GafferUI
{

Expand Down Expand Up @@ -214,10 +209,6 @@ class GAFFERUI_API GraphGadget : public ContainerGadget
void rootChildRemoved( Gaffer::GraphComponent *root, Gaffer::GraphComponent *child );
void selectionMemberAdded( Gaffer::Set *set, IECore::RunTimeTyped *member );
void selectionMemberRemoved( Gaffer::Set *set, IECore::RunTimeTyped *member );
void updateFocusPlugDirtiedConnection();
void focusChanged();
void focusPlugDirtied( Gaffer::Plug *plug );
void scriptContextChanged( const Gaffer::Context *context, const IECore::InternedString& );
void filterMemberAdded( Gaffer::Set *set, IECore::RunTimeTyped *member );
void filterMemberRemoved( Gaffer::Set *set, IECore::RunTimeTyped *member );
void inputChanged( Gaffer::Plug *dstPlug );
Expand Down Expand Up @@ -268,14 +259,10 @@ class GAFFERUI_API GraphGadget : public ContainerGadget
Gaffer::NodePtr m_root;
Gaffer::ScriptNodePtr m_scriptNode;
RootChangedSignal m_rootChangedSignal;
IECore::MurmurHash m_scriptContextHash;
Gaffer::Signals::ScopedConnection m_rootChildAddedConnection;
Gaffer::Signals::ScopedConnection m_rootChildRemovedConnection;
Gaffer::Signals::ScopedConnection m_selectionMemberAddedConnection;
Gaffer::Signals::ScopedConnection m_selectionMemberRemovedConnection;
Gaffer::Signals::ScopedConnection m_focusChangedConnection;
Gaffer::Signals::ScopedConnection m_focusPlugDirtiedConnection;
Gaffer::Signals::ScopedConnection m_scriptContextChangedConnection;

Gaffer::SetPtr m_filter;
Gaffer::Signals::ScopedConnection m_filterMemberAddedConnection;
Expand Down Expand Up @@ -315,35 +302,11 @@ class GAFFERUI_API GraphGadget : public ContainerGadget

GraphLayoutPtr m_layout;

// Track if we need to run an active state update. If true, then if there isn't already an
// m_activeTask running, we need to start one. ( Helpful to track separately so we know
// we need to restart if the task gets cancelled somehow )
bool m_activeStateDirty;
void dirtyActive();

// Used to run updateActive()
std::shared_ptr<Gaffer::BackgroundTask> m_activeStateTask;

// Does the actual calculation of the active state, then calls applyActive.
// Should be run on background thread
void updateActive();

// Applies the active state to the child Gadgets ( must be called on UI thread )
void applyActive(
std::shared_ptr< std::unordered_set<const Gaffer::Plug*> > activePlugs,
std::shared_ptr< std::unordered_set<const Gaffer::Node*> > activeNodes
);

// Given a plug and context, returns all nodes and plugs which contribute to evaluating that
// plug
static void activePlugsAndNodes(
const Gaffer::Plug *plug,
const Gaffer::Context *context,
std::unordered_set<const Gaffer::Plug*> &activePlugs,
std::unordered_set<const Gaffer::Node*> &activeNodes
);

friend GafferUIModule::ActivePlugsWrapperClassToUseAsFriend;
void applyFocusContexts();

ContextTrackerPtr m_contextTracker;
Gaffer::Signals::ScopedConnection m_contextTrackerChangedConnection;

};

IE_CORE_DECLAREPTR( GraphGadget );
Expand Down
37 changes: 12 additions & 25 deletions python/GafferTest/LoopTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,38 +322,25 @@ def testEmptyLoopVariable( self ) :
loop["indexVariable"].setValue( "" )
self.assertEqual( loop["out"].getValue(), 0 )

def testNextLoopIteration( self ) :

loop = Gaffer.Loop()
self.assertIsNone( loop.nextIterationContext() )
def testPreviousIteration( self ) :

loop = self.intLoop()
loop["next"].setInput( loop["previous"] )
loop["iterations"].setValue( 10 )

index = 0
context = Gaffer.Context()
while context is not None :
with context :
context = loop.nextIterationContext()
if context is not None :
self.assertEqual( context["loop:index"], index )
index += 1

self.assertEqual( index, loop["iterations"].getValue() )
iteration = loop.previousIteration( loop["out"] )
self.assertTrue( iteration[0].isSame( loop["next"] ) )
self.assertEqual( iteration[1]["loop:index"], 9 )

loop["iterations"].setValue( 0 )
self.assertIsNone( loop.nextIterationContext() )
for i in range( 0, 9 ) :
with iteration[1] :
iteration = loop.previousIteration( loop["previous"] )
self.assertTrue( iteration[0].isSame( loop["next"] ) )
self.assertEqual( iteration[1]["loop:index"], 8 - i )

loop["iterations"].setValue( 2 )
self.assertIsNotNone( loop.nextIterationContext() )
loop["enabled"].setValue( False )
self.assertIsNone( loop.nextIterationContext() )

loop["enabled"].setValue( True )
self.assertIsNotNone( loop.nextIterationContext() )
loop["indexVariable"].setValue( "" )
self.assertIsNone( loop.nextIterationContext() )
iteration = loop.previousIteration( loop["previous"] )
self.assertTrue( iteration[0].isSame( loop["in"] ) )
self.assertNotIn( "loop:index", iteration[1] )

if __name__ == "__main__":
unittest.main()
Loading
Loading