From c0a07f4a4e788a024a7651a7b5a8ef069c4cc2ff Mon Sep 17 00:00:00 2001 From: John Haddon Date: Wed, 31 Jul 2024 16:15:07 +0100 Subject: [PATCH 1/2] PathListingWidget : Add column context menu signal This follows the pattern established by other context menus, in allowing multiple client slots to collaborate on the building of a context menu. I debated whether or not the signal should pass the `path` that is currently under the mouse, but decided against. That would encourage the creation of menu items that operate only on that path, but with multiple selection I believe the user expectation would be that the entire selection is operated on. By forcing clients to go fetch the selection themselves they are more likely to do the expected thing with it. --- Changes.md | 5 +++ python/GafferUI/PathListingWidget.py | 56 ++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/Changes.md b/Changes.md index 5d179bf0b9..c50fc51896 100644 --- a/Changes.md +++ b/Changes.md @@ -14,6 +14,11 @@ Fixes - WidgetAlgo : Fixed issue preventing `grab()` from capturing popup menus on Windows. - ShowURL : Fixed opening of "file://" URLs on Windows (#5861). +API +--- + +- PathListingWidget : Added `columnContextMenuSignal()`, allowing multiple clients to collaborate on the creation of a column-specific context menu. + Documentation ------------- diff --git a/python/GafferUI/PathListingWidget.py b/python/GafferUI/PathListingWidget.py index ea5774c6c4..03a25c2375 100644 --- a/python/GafferUI/PathListingWidget.py +++ b/python/GafferUI/PathListingWidget.py @@ -148,6 +148,7 @@ def __init__( self.__displayModeChangedSignal = GafferUI.WidgetSignal() self.__expansionChangedSignal = GafferUI.WidgetSignal() self.__updateFinishedSignal = GafferUI.WidgetSignal() + self.__columnContextMenuSignal = Gaffer.Signal3() # Connections for implementing selection and drag and drop. self.keyPressSignal().connect( Gaffer.WeakMethod( self.__keyPress ), scoped = False ) @@ -157,6 +158,7 @@ def __init__( self.mouseMoveSignal().connect( Gaffer.WeakMethod( self.__mouseMove ), scoped = False ) self.dragBeginSignal().connect( Gaffer.WeakMethod( self.__dragBegin ), scoped = False ) self.dragEndSignal().connect( Gaffer.WeakMethod( self.__dragEnd ), scoped = False ) + self.contextMenuSignal().connect( Gaffer.WeakMethod( self.__contextMenu ), scoped = False ) self.__dragPointer = "paths" self.__path = None @@ -423,6 +425,19 @@ def pathSelectedSignal( self ) : return self.__pathSelectedSignal + ## Signal emitted to generate a context menu for a column. This allows + # multiple clients to collaborate in the construction of a menu, with each + # providing different items. It should be preferred to the generic + # `Widget.contextMenuSignal()`. + # + # Slots must have the following signature, with the `menuDefinition` being + # edited directly in place : + # + # `slot( column, pathListingWidget, menuDefinition )` + def columnContextMenuSignal( self ) : + + return self.__columnContextMenuSignal + def setDragPointer( self, dragPointer ) : self.__dragPointer = dragPointer @@ -749,6 +764,47 @@ def __dragEnd( self, widget, event ) : GafferUI.Pointer.setCurrent( None ) + def __contextMenu( self, widget ) : + + if 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 ) + selection = self.getSelection() + if isinstance( selection, IECore.PathMatcher ) : + # Row or Rows mode. + if not selection.match( path ) & IECore.PathMatcher.Result.ExactMatch : + selection = IECore.PathMatcher( [ path ] ) + self.setSelection( selection ) + else : + # Cell or Cells mode. + columnIndex = self.getColumns().index( column ) + if not selection[columnIndex].match( path ) & IECore.PathMatcher.Result.ExactMatch : + for i in range( 0, len( selection ) ) : + selection[i] = IECore.PathMatcher() if columnIndex != i else IECore.PathMatcher( [ path ] ) + self.setSelection( selection ) + + # Use signals to build menu and display it. + + menuDefinition = IECore.MenuDefinition() + self.columnContextMenuSignal()( column, self, menuDefinition ) + + if menuDefinition.size() : + self.__columnContextMenu = GafferUI.Menu( menuDefinition ) + self.__columnContextMenu.popup( self ) + + return True + def __indexAt( self, position ) : point = self._qtWidget().viewport().mapFrom( From 1ed69bfd0cbc42080411515aa7e91f7b9f073d90 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Wed, 31 Jul 2024 16:36:15 +0100 Subject: [PATCH 2/2] HierarchyView : Add `sceneListing()` method This is intended to give clients access to the `columnContextMenuSignal()` to customise the context menu. Currently it returns a bare PathListingWidget which gives clients more privilege than is ideal (we don't want them to change the path!), but in future should return a more specialised SceneListing widget. That won't appear till Gaffer 1.5 though, and we want to allow the menu to be customised in 1.4. So for now we content ourselves with a little warning that folks shouldn't get addicted to anything other than the context menu signal. --- Changes.md | 1 + python/GafferSceneUI/HierarchyView.py | 24 ++++++++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Changes.md b/Changes.md index c50fc51896..931acac957 100644 --- a/Changes.md +++ b/Changes.md @@ -18,6 +18,7 @@ API --- - PathListingWidget : Added `columnContextMenuSignal()`, allowing multiple clients to collaborate on the creation of a column-specific context menu. +- HierarchyView : Added `sceneListing()` method, to allow the context menu to be customised. Documentation ------------- diff --git a/python/GafferSceneUI/HierarchyView.py b/python/GafferSceneUI/HierarchyView.py index dd2d11040d..47c3495152 100644 --- a/python/GafferSceneUI/HierarchyView.py +++ b/python/GafferSceneUI/HierarchyView.py @@ -88,7 +88,7 @@ def __init__( self, scriptNode, **kw ) : self.__selectionChangedConnection = self.__pathListing.selectionChangedSignal().connect( Gaffer.WeakMethod( self.__selectionChanged ), scoped = False ) self.__expansionChangedConnection = self.__pathListing.expansionChangedSignal().connect( Gaffer.WeakMethod( self.__expansionChanged ), scoped = False ) - self.__pathListing.contextMenuSignal().connect( Gaffer.WeakMethod( self.__contextMenuSignal ), scoped = False ) + self.__pathListing.columnContextMenuSignal().connect( Gaffer.WeakMethod( self.__columnContextMenuSignal ), scoped = False ) self.keyPressSignal().connect( Gaffer.WeakMethod( self.__keyPressSignal ), scoped = False ) self.__plug = None @@ -100,6 +100,17 @@ def scene( self ) : return self.__plug + ## Returns the widget used for showing the main scene listing, with the + # intention that clients can add custom context menu items via + # `sceneListing.columnContextMenuSignal()`. + # + # > Caution : This currently returns a PathListingWidget, but in future + # > will probably return a more specialised widget with fewer privileges. + # > Please limit usage to `columnContextMenuSignal()`. + def sceneListing( self ) : + + return self.__pathListing + def __repr__( self ) : return "GafferSceneUI.HierarchyView( scriptNode )" @@ -209,11 +220,9 @@ def __keyPressSignal( self, widget, event ) : return False - def __contextMenuSignal( self, widget ) : - - menuDefinition = IECore.MenuDefinition() + def __columnContextMenuSignal( self, column, pathListing, menuDefinition ) : - selection = self.__pathListing.getSelection() + selection = pathListing.getSelection() menuDefinition.append( "Copy Path%s" % ( "" if selection.size() == 1 else "s" ), { @@ -231,11 +240,6 @@ def __contextMenuSignal( self, widget ) : } ) - self.__contextMenu = GafferUI.Menu( menuDefinition ) - self.__contextMenu.popup( widget ) - - return True - def __copySelectedPaths( self, *unused ) : if self.__plug is None :