Skip to content

Commit

Permalink
Merge pull request #5985 from johnhaddon/pathColumnContextMenuSignal
Browse files Browse the repository at this point in the history
PathColumn : Add `contextMenuSignal()` and `instanceCreatedSignal()`
  • Loading branch information
murraystevenson committed Aug 23, 2024
2 parents 9599500 + 5ea05f7 commit c9b577f
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 15 deletions.
3 changes: 3 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ API
- A `DeprecationWarning` is now emitted by `_plugConnections()`. Use `_blockedUpdateFromValues()` instead.
- NodeGadget, ConnectionGadget : Added `updateFromContextTracker()` virtual methods.
- Path : Added `inspectionContext()` virtual method.
- PathColumn :
- Added `contextMenuSignal()`, allowing the creation of custom context menus.
- Added `instanceCreatedSignal()`, providing an opportunity to connect to the signals on _any_ column, no matter how it is created.

Breaking Changes
----------------
Expand Down
60 changes: 59 additions & 1 deletion include/GafferUI/PathColumn.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
namespace GafferUI
{

class MenuDefinition;
class PathListingWidget;

/// Abstract class for extracting properties from a Path in a form
Expand Down Expand Up @@ -162,13 +163,26 @@ class GAFFERUI_API PathColumn : public IECore::RefCounted, public Gaffer::Signal
ButtonSignal &buttonReleaseSignal();
ButtonSignal &buttonDoubleClickSignal();

using ContextMenuSignal = Gaffer::Signals::Signal<void ( PathColumn &column, PathListingWidget &widget, MenuDefinition &menuDefinition ), Gaffer::Signals::CatchingCombiner<void>>;
/// To retain `widget` for use in MenuItem commands, use `PathListingWidgetPtr( &widget )`.
ContextMenuSignal &contextMenuSignal();

/// Creation
/// ========

/// Signal emitted whenever a new PathColumn is created. This provides
/// an opportunity for the customisation of columns anywhere, no matter how
/// they are created or where they are hosted.
static PathColumnSignal &instanceCreatedSignal();

private :

PathColumnSignal m_changedSignal;

ButtonSignal m_buttonPressSignal;
ButtonSignal m_buttonReleaseSignal;
ButtonSignal m_buttonDoubleClickSignal;
ContextMenuSignal m_contextMenuSignal;

SizeMode m_sizeMode;

Expand Down Expand Up @@ -259,11 +273,13 @@ IE_CORE_DECLAREPTR( FileIconPathColumn )
/// C++ interface for the `GafferUI.PathListingWidget` Python class. Provided for
/// use in PathColumn event signals, so that event handling may be implemented
/// from C++ if desired.
class PathListingWidget
class PathListingWidget : public IECore::RefCounted
{

public :

IE_CORE_DECLAREMEMBERPTR( PathListingWidget )

using Columns = std::vector<PathColumnPtr>;
virtual void setColumns( const Columns &columns ) = 0;
virtual Columns getColumns() const = 0;
Expand All @@ -274,4 +290,46 @@ class PathListingWidget

};

IE_CORE_DECLAREPTR( PathListingWidget )

/// C++ interface for the `IECore.MenuDefinition` Python class. Provided for use
/// in `PathColumn::contextMenuSignal()`, so that event handling may be
/// implemented from C++ if desired.
class MenuDefinition
{

public :

struct MenuItem
{
using Command = std::function<void ()>;
Command command;
std::string description;
std::string icon;
std::string shortCut;
bool divider = false;
bool active = true;
};

virtual void append( const std::string &path, const MenuItem &item ) = 0;

};

/// Overload for the standard `intrusive_ptr_add_ref` defined in RefCounted.h.
/// This allows us to emit `instanceCreatedSignal()` once the object is fully
/// constructed and it is safe for slots (especially Python slots) to add
/// additional references.
///
/// > Caution : This won't be called if you assign a new PathColumn to
/// > RefCountedPtr rather than PathColumnPtr. Don't do that!
inline void intrusive_ptr_add_ref( PathColumn *column )
{
bool firstRef = column->refCount() == 0;
column->addRef();
if( firstRef )
{
PathColumn::instanceCreatedSignal()( column );
}
}

} // namespace GafferUI
8 changes: 4 additions & 4 deletions python/GafferUI/PathListingWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -766,17 +766,16 @@ def __dragEnd( self, widget, event ) :

def __contextMenu( self, widget ) :

if not self.columnContextMenuSignal().numSlots() :
mousePosition = GafferUI.Widget.mousePosition( relativeTo = self )
column = self.columnAt( mousePosition )
if not column.contextMenuSignal().numSlots() and not self.columnContextMenuSignal().numSlots() :
# Allow legacy clients connected to `Widget.contextMenuSignal()` to
# do their own thing instead.
return False

# Select the path under the mouse, if it's not already selected.
# The user will expect to be operating on the thing under the mouse.

mousePosition = GafferUI.Widget.mousePosition( relativeTo = self )
column = self.columnAt( mousePosition )

path = self.pathAt( mousePosition )
if path is not None :
path = str( path )
Expand All @@ -797,6 +796,7 @@ def __contextMenu( self, widget ) :
# Use signals to build menu and display it.

menuDefinition = IECore.MenuDefinition()
column.contextMenuSignal()( column, self, menuDefinition )
self.columnContextMenuSignal()( column, self, menuDefinition )

if menuDefinition.size() :
Expand Down
43 changes: 43 additions & 0 deletions python/GafferUITest/PathColumnTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

import IECore

import GafferTest
import GafferUI
import GafferUITest

Expand Down Expand Up @@ -136,5 +137,47 @@ def testIconPathColumnConstructors( self ) :
self.assertEqual( c.headerData().value, "label" )
self.assertEqual( c.headerData().toolTip, "help!" )

def testInstanceCreatedSignal( self ) :

cs = GafferTest.CapturingSlot( GafferUI.PathColumn.instanceCreatedSignal() )

column1 = GafferUI.StandardPathColumn( "l1", "p1" )
column2 = GafferUI.StandardPathColumn( "l2", "p2" )
column3 = GafferUI.StandardPathColumn( "l3", "p3" )

self.assertEqual( cs, [ ( column1, ), ( column2, ), ( column3, ) ] )

def testInstanceCreatedSignalWithPythonColumn( self ) :

class PythonColumn( GafferUI.PathColumn ) :

def __init__( self ) :

self.member = "preInitValue"
GafferUI.PathColumn.__init__( self )
self.member = "postInitValue"

columnCreated = None

def instanceCreated( column ) :

nonlocal columnCreated
columnCreated = column

# We can query the full derived type of the column in `instanceCreated()`.
self.assertIsInstance( column, PythonColumn )
# But we see the object in the state in which is called the base
# class `__init__()`. Column subclasses which need to be seen in a
# fully constructed state should do all their initialisation before
# calling the base class `__init__()`. Hopefully this is not too
# onerous.
self.assertEqual( column.member, "preInitValue" )

connection = GafferUI.PathColumn.instanceCreatedSignal().connect( instanceCreated, scoped = True )
column = PythonColumn()

self.assertIs( column, columnCreated )
self.assertEqual( column.member, "postInitValue" )

if __name__ == "__main__":
unittest.main()
11 changes: 11 additions & 0 deletions src/GafferUI/PathColumn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ PathColumn::ButtonSignal &PathColumn::buttonDoubleClickSignal()
return m_buttonDoubleClickSignal;
}

PathColumn::ContextMenuSignal &PathColumn::contextMenuSignal()
{
return m_contextMenuSignal;
}

PathColumn::PathColumnSignal &PathColumn::instanceCreatedSignal()
{
static PathColumnSignal g_instanceCreatedSignal;
return g_instanceCreatedSignal;
}

//////////////////////////////////////////////////////////////////////////
// StandardPathColumn
//////////////////////////////////////////////////////////////////////////
Expand Down
Loading

0 comments on commit c9b577f

Please sign in to comment.