From 3f25f5ded4390881cebedc5478fcdf061a03c8c4 Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:59:51 -0700 Subject: [PATCH 1/3] PathColumn : Add `keyPressSignal()` and `keyReleaseSignal()` --- Changes.md | 1 + include/GafferUI/PathColumn.h | 7 ++++++ src/GafferUI/PathColumn.cpp | 10 +++++++++ src/GafferUIModule/PathColumnBinding.cpp | 28 ++++++++++++++++++++++++ 4 files changed, 46 insertions(+) diff --git a/Changes.md b/Changes.md index f3c463f7aa..fd0d7c0eeb 100644 --- a/Changes.md +++ b/Changes.md @@ -56,6 +56,7 @@ API - 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. + - Added `keyPressSignal()` and `keyReleaseSignal()`, allowing a PathColumn to handle key events. - ArrayPlug : - It is now legal to construct an ArrayPlug with a minimum size of 0. Previously the minimum size was 1. - Added `elementPrototype()` method. diff --git a/include/GafferUI/PathColumn.h b/include/GafferUI/PathColumn.h index ab7989c932..723edd0e7b 100644 --- a/include/GafferUI/PathColumn.h +++ b/include/GafferUI/PathColumn.h @@ -39,6 +39,7 @@ #include "GafferUI/ButtonEvent.h" #include "GafferUI/EventSignalCombiner.h" #include "GafferUI/Export.h" +#include "GafferUI/KeyEvent.h" #include "Gaffer/Path.h" @@ -167,6 +168,10 @@ class GAFFERUI_API PathColumn : public IECore::RefCounted, public Gaffer::Signal /// To retain `widget` for use in MenuItem commands, use `PathListingWidgetPtr( &widget )`. ContextMenuSignal &contextMenuSignal(); + using KeySignal = Gaffer::Signals::Signal>; + KeySignal &keyPressSignal(); + KeySignal &keyReleaseSignal(); + /// Creation /// ======== @@ -183,6 +188,8 @@ class GAFFERUI_API PathColumn : public IECore::RefCounted, public Gaffer::Signal ButtonSignal m_buttonReleaseSignal; ButtonSignal m_buttonDoubleClickSignal; ContextMenuSignal m_contextMenuSignal; + KeySignal m_keyPressSignal; + KeySignal m_keyReleaseSignal; SizeMode m_sizeMode; diff --git a/src/GafferUI/PathColumn.cpp b/src/GafferUI/PathColumn.cpp index 70d21da61c..0dc12a9333 100644 --- a/src/GafferUI/PathColumn.cpp +++ b/src/GafferUI/PathColumn.cpp @@ -115,6 +115,16 @@ PathColumn::ContextMenuSignal &PathColumn::contextMenuSignal() return m_contextMenuSignal; } +PathColumn::KeySignal &PathColumn::keyPressSignal() +{ + return m_keyPressSignal; +} + +PathColumn::KeySignal &PathColumn::keyReleaseSignal() +{ + return m_keyReleaseSignal; +} + PathColumn::PathColumnSignal &PathColumn::instanceCreatedSignal() { static PathColumnSignal g_instanceCreatedSignal; diff --git a/src/GafferUIModule/PathColumnBinding.cpp b/src/GafferUIModule/PathColumnBinding.cpp index 2db8d7fad7..5d27291d21 100644 --- a/src/GafferUIModule/PathColumnBinding.cpp +++ b/src/GafferUIModule/PathColumnBinding.cpp @@ -443,6 +443,31 @@ struct ContextMenuSignalSlotCaller } }; +struct KeySignalCaller +{ + static bool call( PathColumn::KeySignal &s, PathColumn &column, object widget, const KeyEvent &event ) + { + PathListingWidgetAccessor accessor( widget ); + IECorePython::ScopedGILRelease gilRelease; + return s( column, accessor, event ); + } +}; + +struct KeySignalSlotCaller +{ + bool operator()( boost::python::object slot, PathColumn &column, PathListingWidget &widget, const KeyEvent &event ) + { + try + { + return slot( PathColumnPtr( &column ), static_cast( widget ).widget(), event ); + } + catch( const boost::python::error_already_set & ) + { + IECorePython::ExceptionAlgo::translatePythonException(); + } + } +}; + template const char *pathColumnProperty( const T &column ) { @@ -498,6 +523,7 @@ void GafferUIModule::bindPathColumn() SignalClass, ChangedSignalSlotCaller>( "PathColumnSignal" ); SignalClass( "ButtonSignal" ); SignalClass( "ContextMenuSignal" ); + SignalClass( "KeySignal" ); } pathColumnClass.def( init( arg( "sizeMode" ) = PathColumn::SizeMode::Default ) ) @@ -508,6 +534,8 @@ void GafferUIModule::bindPathColumn() .def( "buttonReleaseSignal", &PathColumn::buttonReleaseSignal, return_internal_reference<1>() ) .def( "buttonDoubleClickSignal", &PathColumn::buttonDoubleClickSignal, return_internal_reference<1>() ) .def( "contextMenuSignal", &PathColumn::contextMenuSignal, return_internal_reference<1>() ) + .def( "keyPressSignal", &PathColumn::keyPressSignal, return_internal_reference<1>() ) + .def( "keyReleaseSignal", &PathColumn::keyReleaseSignal, return_internal_reference<1>() ) .def( "instanceCreatedSignal", &PathColumn::instanceCreatedSignal, return_value_policy() ) .staticmethod( "instanceCreatedSignal" ) .def( "getSizeMode", (PathColumn::SizeMode (PathColumn::*)() const )&PathColumn::getSizeMode ) From dbd0a85f2912f7fd6849786ebb9b78bd009df2a5 Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Wed, 28 Aug 2024 15:00:15 -0700 Subject: [PATCH 2/3] PathListingWidget : Delegate keypress to column --- python/GafferUI/PathListingWidget.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/python/GafferUI/PathListingWidget.py b/python/GafferUI/PathListingWidget.py index 4779823a9b..51bdc22789 100644 --- a/python/GafferUI/PathListingWidget.py +++ b/python/GafferUI/PathListingWidget.py @@ -578,20 +578,21 @@ def __updateFinished( self ) : def __keyPress( self, widget, event ) : + # Use `__lastSelectedIndex` if available so that shift + keypress + # accumulates selection. + index = self.__lastSelectedIndex + assert( isinstance( index, ( type( None ), QtCore.QPersistentModelIndex ) ) ) + if index is not None and index.isValid() : + # Convert from persistent index + index = QtCore.QModelIndex( index ) + else : + index = self._qtWidget().currentIndex() + if ( event.key in ( "Up", "Down" ) or ( event.key in ( "Left", "Right" ) and self.__cellSelectionMode() ) ): - # Use `__lastSelectedIndex` if available so that shift + keypress - # accumulates selection. - index = self.__lastSelectedIndex - assert( isinstance( index, ( type( None ), QtCore.QPersistentModelIndex ) ) ) - if index is not None and index.isValid() : - # Convert from persistent index - index = QtCore.QModelIndex( index ) - else : - index = self._qtWidget().currentIndex() if not index.isValid() : return True @@ -637,6 +638,13 @@ def __keyPress( self, widget, event ) : self.__setSelectionInternal( selection, scrollToFirst=False ) return True + # Delegate the keyPress to the PathColumn, if it wants it. + + elif index.isValid() and self.getColumns()[index.column()].keyPressSignal()( + self.getColumns()[index.column()], self, event + ) : + return True + return False # Handles interactions for selection and expansion. Done at the level From a61490d8d1be2976db4d057f4a3e27346e1f1e16 Mon Sep 17 00:00:00 2001 From: Murray Stevenson <50844517+murraystevenson@users.noreply.github.com> Date: Wed, 28 Aug 2024 15:00:28 -0700 Subject: [PATCH 3/3] InspectorColumn : Migrate editor interactions to column This migrates the column-specific editing functionality from the Light Editor and Render Pass Editor to a common set of interactions on InspectorColumn itself. This is mostly a direct migration of the existing functionality from the two editors, with adjustments made where necessary to combine overlapping functionality. --- Changes.md | 1 + python/GafferSceneUI/LightEditor.py | 379 +---------------- python/GafferSceneUI/RenderPassEditor.py | 311 +------------- python/GafferSceneUI/_InspectorColumn.py | 448 ++++++++++++++++++++ python/GafferSceneUI/__init__.py | 1 + python/GafferSceneUITest/LightEditorTest.py | 7 +- src/GafferSceneUI/InspectorColumn.cpp | 4 +- 7 files changed, 463 insertions(+), 688 deletions(-) create mode 100644 python/GafferSceneUI/_InspectorColumn.py diff --git a/Changes.md b/Changes.md index fd0d7c0eeb..e40b241e1e 100644 --- a/Changes.md +++ b/Changes.md @@ -39,6 +39,7 @@ Fixes - Fixed error when `resize()` removed plugs with input connections. - Fixed error when `resize()` was used on an output plug. - CreateViews : Fixed redundant serialisation of internal connections. +- LightEditor, RenderPassEditor : Removed ambiguous `The selected cells cannot be edited in the current Edit Scope` message when attempting to edit non-editable columns, such as the `Name` column. API --- diff --git a/python/GafferSceneUI/LightEditor.py b/python/GafferSceneUI/LightEditor.py index 78b2710775..2247254b69 100644 --- a/python/GafferSceneUI/LightEditor.py +++ b/python/GafferSceneUI/LightEditor.py @@ -47,9 +47,6 @@ import GafferScene import GafferSceneUI -from GafferUI.PlugValueWidget import sole -from GafferSceneUI._HistoryWindow import _HistoryWindow - from . import ContextAlgo from . import _GafferSceneUI @@ -107,16 +104,12 @@ def __init__( self, scriptNode, **kw ) : horizontalScrollMode = GafferUI.ScrollMode.Automatic ) - self.__soloColumnIndex = 2 - self.__pathListing.setDragPointer( "objects" ) self.__pathListing.setSortable( False ) self.__selectionChangedConnection = self.__pathListing.selectionChangedSignal().connect( Gaffer.WeakMethod( self.__selectionChanged ), scoped = False ) - self.__pathListing.buttonDoubleClickSignal().connectFront( Gaffer.WeakMethod( self.__buttonDoubleClick ), scoped = False ) - self.__pathListing.keyPressSignal().connect( Gaffer.WeakMethod( self.__keyPress ), scoped = False ) - self.__pathListing.buttonPressSignal().connectFront( Gaffer.WeakMethod( self.__buttonPress ), scoped = False ) + self.__pathListing.columnContextMenuSignal().connect( Gaffer.WeakMethod( self.__columnContextMenuSignal ), scoped = False ) self._updateFromSet() self.__setPathListingPath() @@ -290,310 +283,14 @@ def __transferSelectionFromContext( self ) : selection = [selectedPaths] + ( [IECore.PathMatcher()] * ( len( self.__pathListing.getColumns() ) - 1 ) ) self.__pathListing.setSelection( selection, scrollToFirst=True ) - def __buttonDoubleClick( self, pathListing, event ) : - - # A small corner area below the vertical scroll bar may pass through - # to us, causing odd selection behavior. Check that we're within the - # scroll area. - if pathListing.pathAt( event.line.p0 ) is None : - return False - - if event.button == event.Buttons.Left : - self.__editSelectedCells( pathListing ) - - return True - - return False - - def __keyPress( self, pathListing, event ) : - - if event.modifiers == event.Modifiers.None_ : - - if event.key == "Return" or event.key == "Enter" : - self.__editSelectedCells( pathListing ) - return True - - if event.key == "D" and len( self.__disablableInspectionTweaks( pathListing ) ) > 0 : - self.__disableEdits( pathListing ) - return True - - if ( - ( event.key == "Backspace" or event.key == "Delete" ) and - len( self.__removableAttributeInspections( pathListing ) ) > 0 - ) : - self.__removeAttributes( pathListing ) - return True - - return False - - def __editSelectedCells( self, pathListing, quickBoolean = True ) : - - # A dictionary of the form : - # { inspector : { path1 : inspection, path2 : inspection, ... }, ... } - inspectors = {} - inspections = [] - - path = pathListing.getPath().copy() - for selection, column in zip( pathListing.getSelection(), pathListing.getColumns() ) : - if not isinstance( column, GafferSceneUI.Private.InspectorColumn ) : - continue - for pathString in selection.paths() : - path.setFromString( pathString ) - with path.inspectionContext() : - inspection = column.inspector().inspect() - - if inspection is not None : - inspectors.setdefault( column.inspector(), {} )[pathString] = inspection - inspections.append( inspection ) - - if len( inspectors ) == 0 : - with GafferUI.PopupWindow() as self.__popup : - with GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) : - GafferUI.Image( "warningSmall.png" ) - GafferUI.Label( "

The selected cells cannot be edited in the current Edit Scope

" ) - - self.__popup.popup( parent = self ) - - return - - nonEditable = [ i for i in inspections if not i.editable() ] - - if len( nonEditable ) == 0 : - with Gaffer.Context( self.context() ) as context : - if not quickBoolean or not self.__toggleBoolean( inspectors, inspections ) : - edits = [ i.acquireEdit() for i in inspections ] - warnings = "\n".join( [ i.editWarning() for i in inspections if i.editWarning() != "" ] ) - # The plugs are either not boolean, boolean with mixed values, - # or attributes that don't exist and are not boolean. Show the popup. - self.__popup = GafferUI.PlugPopup( edits, warning = warnings ) - - if isinstance( self.__popup.plugValueWidget(), GafferUI.TweakPlugValueWidget ) : - self.__popup.plugValueWidget().setNameVisible( False ) - - self.__popup.popup( parent = self ) - - else : - - with GafferUI.PopupWindow() as self.__popup : - with GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) : - GafferUI.Image( "warningSmall.png" ) - GafferUI.Label( "

{}

".format( nonEditable[0].nonEditableReason() ) ) - - self.__popup.popup( parent = self ) - - def __toggleBoolean( self, inspectors, inspections ) : - - plugs = [ i.acquireEdit() for i in inspections ] - # Make sure all the plugs either contain, or are themselves a BoolPlug or can be edited - # by `SetMembershipInspector.editSetMembership()` - if not all ( - ( - isinstance( plug, ( Gaffer.TweakPlug, Gaffer.NameValuePlug ) ) and - isinstance( plug["value"], Gaffer.BoolPlug ) - ) or ( - isinstance( plug, ( Gaffer.BoolPlug ) ) - ) or ( - isinstance( inspector, GafferSceneUI.Private.SetMembershipInspector ) - ) - for plug, inspector in zip( plugs, inspectors ) - ) : - return False - - currentValues = [] - - # Use a single new value for all plugs. - # First we need to find out what the new value would be for each plug in isolation. - for inspector, pathInspections in inspectors.items() : - for path, inspection in pathInspections.items() : - currentValues.append( inspection.value().value if inspection.value() is not None else False ) - - # Now set the value for all plugs, defaulting to `True` if they are not - # currently all the same. - newValue = not sole( currentValues ) - - with Gaffer.UndoScope( self.scriptNode() ) : - for inspector, pathInspections in inspectors.items() : - for path, inspection in pathInspections.items() : - if isinstance( inspector, GafferSceneUI.Private.SetMembershipInspector ) : - inspector.editSetMembership( - inspection, - path, - GafferScene.EditScopeAlgo.SetMembership.Added if newValue else GafferScene.EditScopeAlgo.SetMembership.Removed - ) - - else : - plug = inspection.acquireEdit() - if isinstance( plug, ( Gaffer.TweakPlug, Gaffer.NameValuePlug ) ) : - plug["value"].setValue( newValue ) - plug["enabled"].setValue( True ) - if isinstance( plug, Gaffer.TweakPlug ) : - plug["mode"].setValue( Gaffer.TweakPlug.Mode.Create ) - else : - plug.setValue( newValue ) - - return True - - def __disablableInspectionTweaks( self, pathListing ) : - - tweaks = [] - - path = pathListing.getPath().copy() - for columnSelection, column in zip( pathListing.getSelection(), pathListing.getColumns() ) : - if not isinstance( column, GafferSceneUI.Private.InspectorColumn ) : - continue - for pathString in columnSelection.paths() : - path.setFromString( pathString ) - with path.inspectionContext() : - inspection = column.inspector().inspect() - if inspection is not None and inspection.editable() : - source = inspection.source() - editScope = self.settings()["editScope"].getInput() - if ( - ( - ( - isinstance( source, ( Gaffer.TweakPlug, Gaffer.NameValuePlug ) ) and - source["enabled"].getValue() - ) or - isinstance( column.inspector(), GafferSceneUI.Private.SetMembershipInspector ) - ) and - ( editScope is None or editScope.node().isAncestorOf( source ) ) - ) : - tweaks.append( ( pathString, column.inspector() ) ) - else : - return [] - else : - return [] - - return tweaks - - def __disableEdits( self, pathListing ) : - - edits = self.__disablableInspectionTweaks( pathListing ) - - path = pathListing.getPath().copy() - with Gaffer.UndoScope( self.scriptNode() ) : - for pathString, inspector in edits : - path.setFromString( pathString ) - with path.inspectionContext() : - inspection = inspector.inspect() - if inspection is not None and inspection.editable() : - source = inspection.source() - - if isinstance( source, ( Gaffer.TweakPlug, Gaffer.NameValuePlug ) ) : - source["enabled"].setValue( False ) - elif isinstance( inspector, GafferSceneUI.Private.SetMembershipInspector ) : - inspector.editSetMembership( inspection, pathString, GafferScene.EditScopeAlgo.SetMembership.Unchanged ) - - def __removableAttributeInspections( self, pathListing ) : - - inspections = [] - - path = pathListing.getPath().copy() - for columnSelection, column in zip( pathListing.getSelection(), pathListing.getColumns() ) : - if not isinstance( column, GafferSceneUI.Private.InspectorColumn ) : - continue - elif not columnSelection.isEmpty() and type( column.inspector() ) != GafferSceneUI.Private.AttributeInspector : - return [] - for pathString in columnSelection.paths() : - path.setFromString( pathString ) - with path.inspectionContext() : - inspection = column.inspector().inspect() - if inspection is not None and inspection.editable() : - source = inspection.source() - editScope = self.settings()["editScope"].getInput() - if ( - ( isinstance( source, Gaffer.TweakPlug ) and source["mode"].getValue() != Gaffer.TweakPlug.Mode.Remove ) or - ( isinstance( source, Gaffer.ValuePlug ) and len( source.children() ) == 2 and "Added" in source and "Removed" in source ) or - editScope is not None - ) : - inspections.append( inspection ) - else : - return [] - else : - return [] - - return inspections - - def __removeAttributes( self, pathListing ) : - - inspections = self.__removableAttributeInspections( pathListing ) - - with Gaffer.UndoScope( self.scriptNode() ) : - for inspection in inspections : - tweak = inspection.acquireEdit() - tweak["enabled"].setValue( True ) - tweak["mode"].setValue( Gaffer.TweakPlug.Mode.Remove ) - - def __selectedSetExpressions( self, pathListing ) : - - # A dictionary of the form : - # { light1 : set( setExpression1, setExpression2 ), light2 : set( setExpression1 ), ... } - result = {} - - lightPath = pathListing.getPath().copy() - for columnSelection, column in zip( pathListing.getSelection(), pathListing.getColumns() ) : - if ( - not columnSelection.isEmpty() and ( - not isinstance( column, GafferSceneUI.Private.InspectorColumn ) or - not ( - Gaffer.Metadata.value( "attribute:" + column.inspector().name(), "ui:scene:acceptsSetName" ) or - Gaffer.Metadata.value( "attribute:" + column.inspector().name(), "ui:scene:acceptsSetNames" ) or - Gaffer.Metadata.value( "attribute:" + column.inspector().name(), "ui:scene:acceptsSetExpression" ) - ) - ) - ) : - # We only return set expressions if all selected paths are in - # columns that accept set names or set expressions. - return {} - - for path in columnSelection.paths() : - lightPath.setFromString( path ) - cellValue = column.cellData( lightPath ).value - if cellValue is not None : - result.setdefault( path, set() ).add( cellValue ) - else : - # We only return set expressions if all selected paths are render passes. - return {} - - return result - - def __selectAffected( self, pathListing ) : - - result = IECore.PathMatcher() - - with Gaffer.Context( self.context() ) as context : - for light, setExpressions in self.__selectedSetExpressions( pathListing ).items() : - for setExpression in setExpressions : - result.addPaths( GafferScene.SetAlgo.evaluateSetExpression( setExpression, self.settings()["in"] ) ) - - GafferSceneUI.ContextAlgo.setSelectedPaths( self.context(), result ) - - def __buttonPress( self, pathListing, event ) : - - if event.button != event.Buttons.Right or event.modifiers != event.Modifiers.None_ : - return False - - selection = pathListing.getSelection() + def __columnContextMenuSignal( self, column, pathListing, menuDefinition ) : columns = pathListing.getColumns() - cellColumn = pathListing.columnAt( event.line.p0 ) columnIndex = -1 for i in range( 0, len( columns ) ) : - if cellColumn == columns[i] : + if column == columns[i] : columnIndex = i - cellPath = pathListing.pathAt( event.line.p0 ) - if cellPath is None : - return False - - if not selection[columnIndex].match( str( cellPath ) ) & IECore.PathMatcher.Result.ExactMatch : - for p in selection : - p.clear() - selection[columnIndex].addPath( str( cellPath ) ) - pathListing.setSelection( selection, scrollToFirst = False ) - - menuDefinition = IECore.MenuDefinition() - if columnIndex == 0 : # Whole light operations @@ -641,54 +338,6 @@ def __buttonPress( self, pathListing, event ) : } ) - else : - # Parameter cells - - menuDefinition.append( - "Show History...", - { - "command" : Gaffer.WeakMethod( self.__showHistory ) - } - ) - menuDefinition.append( - "Edit...", - { - "command" : functools.partial( self.__editSelectedCells, pathListing, False ), - "active" : pathListing.getSelection()[self.__soloColumnIndex].isEmpty(), - } - ) - menuDefinition.append( - "Disable Edit", - { - "command" : functools.partial( self.__disableEdits, pathListing ), - "active" : len( self.__disablableInspectionTweaks( pathListing ) ) > 0, - "shortCut" : "D", - } - ) - menuDefinition.append( - "Remove Attribute", - { - "command" : functools.partial( self.__removeAttributes, pathListing ), - "active" : len( self.__removableAttributeInspections( pathListing ) ) > 0, - "shortCut" : "Backspace, Delete", - } - ) - if len( self.__selectedSetExpressions( pathListing ) ) > 0 : - menuDefinition.append( - "SelectAffectedObjectsDivider", { "divider" : True } - ) - menuDefinition.append( - "Select Affected Objects", - { - "command" : functools.partial( self.__selectAffected, pathListing ), - } - ) - - self.__contextMenu = GafferUI.Menu( menuDefinition ) - self.__contextMenu.popup( pathListing ) - - return True - def __selectLinked (self, *unused ) : context = self.context() @@ -720,28 +369,6 @@ def __deleteLights( self, *unused ) : with Gaffer.UndoScope( editScope.ancestor( Gaffer.ScriptNode ) ) : GafferScene.EditScopeAlgo.setPruned( editScope, selection, True ) - def __showHistory( self, *unused ) : - - selection = self.__pathListing.getSelection() - columns = self.__pathListing.getColumns() - - for i in range( 0, len( columns ) ) : - column = columns[ i ] - if not isinstance( column, GafferSceneUI.Private.InspectorColumn ) : - continue - - for path in selection[i].paths() : - window = _HistoryWindow( - column.inspector(), - path, - self.context(), - self.ancestor( GafferUI.ScriptWindow ).scriptNode(), - "History : {} : {}".format( path, column.headerData().value ) - ) - self.ancestor( GafferUI.Window ).addChildWindow( window, removeOnClose = True ) - window.setVisible( True ) - - GafferUI.Editor.registerType( "LightEditor", LightEditor ) ########################################################################## diff --git a/python/GafferSceneUI/RenderPassEditor.py b/python/GafferSceneUI/RenderPassEditor.py index 85897766dd..46af5cfdee 100644 --- a/python/GafferSceneUI/RenderPassEditor.py +++ b/python/GafferSceneUI/RenderPassEditor.py @@ -35,7 +35,6 @@ ########################################################################## import collections -import functools import imath import traceback @@ -48,9 +47,6 @@ from . import _GafferSceneUI -from GafferUI.PlugValueWidget import sole -from GafferSceneUI._HistoryWindow import _HistoryWindow - from Qt import QtWidgets class RenderPassEditor( GafferSceneUI.SceneEditor ) : @@ -134,7 +130,7 @@ def __init__( self, scriptNode, **kw ) : self.__pathListing.buttonDoubleClickSignal().connectFront( Gaffer.WeakMethod( self.__buttonDoubleClick ), scoped = False ) self.__pathListing.keyPressSignal().connect( Gaffer.WeakMethod( self.__keyPress ), scoped = False ) - self.__pathListing.buttonPressSignal().connectFront( Gaffer.WeakMethod( self.__buttonPress ), scoped = False ) + self.__pathListing.columnContextMenuSignal().connect( Gaffer.WeakMethod( self.__columnContextMenuSignal ), scoped = False ) self.__pathListing.selectionChangedSignal().connect( Gaffer.WeakMethod( self.__selectionChanged ), scoped = False ) self.__pathListing.dragBeginSignal().connectFront( Gaffer.WeakMethod( self.__dragBegin ), scoped = False ) @@ -306,10 +302,7 @@ def __buttonDoubleClick( self, pathListing, event ) : column = pathListing.columnAt( event.line.p0 ) if column == self.__renderPassActiveColumn : self.__setActiveRenderPass( pathListing ) - else : - self.__editSelectedCells( pathListing ) - - return True + return True return False @@ -321,13 +314,7 @@ def __keyPress( self, pathListing, event ) : selection = pathListing.getSelection() if len( selection[1].paths() ) : self.__setActiveRenderPass( pathListing ) - else : - self.__editSelectedCells( pathListing ) - return True - - if event.key == "D" and len( self.__disablableInspectionTweaks( pathListing ) ) > 0 : - self.__disableEdits( pathListing ) - return True + return True def __selectedRenderPasses( self, columns = [ 0 ] ) : @@ -385,235 +372,14 @@ def __setActiveRenderPass( self, pathListing ) : renderPassPlug["value"].setValue( selectedPassNames[0] if selectedPassNames[0] != currentRenderPass else "" ) - ## \todo Replace this and `LightEditor.__editSelectedCells()` with common editing - # functionality to be provided by InspectorColumn in the future. - def __editSelectedCells( self, pathListing, quickBoolean = True ) : - - # A dictionary of the form : - # { inspector : { path1 : inspection, path2 : inspection, ... }, ... } - inspectors = {} - inspections = [] - - path = pathListing.getPath().copy() - for selection, column in zip( pathListing.getSelection(), pathListing.getColumns() ) : - if not isinstance( column, GafferSceneUI.Private.InspectorColumn ) : - continue - for pathString in selection.paths() : - path.setFromString( pathString ) - inspectionContext = path.inspectionContext() - if inspectionContext is None : - continue - - with inspectionContext : - inspection = column.inspector().inspect() - - if inspection is not None : - inspectors.setdefault( column.inspector(), {} )[pathString] = inspection - inspections.append( inspection ) - - if len( inspectors ) == 0 : - with GafferUI.PopupWindow() as self.__popup : - with GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) : - GafferUI.Image( "warningSmall.png" ) - GafferUI.Label( "

The selected cells cannot be edited in the current Edit Scope

" ) - - self.__popup.popup( parent = self ) - - return - - nonEditable = [ i for i in inspections if not i.editable() ] - - if len( nonEditable ) == 0 : - with Gaffer.Context( self.context() ) as context : - if not quickBoolean or not self.__toggleBoolean( inspectors, inspections ) : - edits = [ i.acquireEdit() for i in inspections ] - warnings = "\n".join( [ i.editWarning() for i in inspections if i.editWarning() != "" ] ) - # The plugs are either not boolean, boolean with mixed values, - # or attributes that don't exist and are not boolean. Show the popup. - self.__popup = GafferUI.PlugPopup( edits, warning = warnings ) - - if isinstance( self.__popup.plugValueWidget(), GafferUI.TweakPlugValueWidget ) : - self.__popup.plugValueWidget().setNameVisible( False ) - - ## \todo : Adjust popup width based on the inspector column width(s) to improve - # editing of long paths, similar to how we handle this in the Spreadsheet. - self.__popup.popup( parent = self ) - - else : - - with GafferUI.PopupWindow() as self.__popup : - with GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) : - GafferUI.Image( "warningSmall.png" ) - GafferUI.Label( "

{}

".format( nonEditable[0].nonEditableReason() ) ) - - self.__popup.popup( parent = self ) - - def __toggleBoolean( self, inspectors, inspections ) : - - plugs = [ i.acquireEdit() for i in inspections ] - # Make sure all the plugs either contain, or are themselves a BoolPlug - if not all ( - ( - isinstance( plug, ( Gaffer.TweakPlug, Gaffer.NameValuePlug ) ) and - isinstance( plug["value"], Gaffer.BoolPlug ) - ) or ( - isinstance( plug, ( Gaffer.BoolPlug ) ) - ) - for plug, inspector in zip( plugs, inspectors ) - ) : - return False - - currentValues = [] - - # Use a single new value for all plugs. - # First we need to find out what the new value would be for each plug in isolation. - for inspector, pathInspections in inspectors.items() : - for path, inspection in pathInspections.items() : - currentValue = inspection.value().value if inspection.value() is not None else None - currentValues.append( currentValue ) - - # Now set the value for all plugs, defaulting to `True` if they are not - # currently all the same. - newValue = not sole( currentValues ) - - with Gaffer.UndoScope( self.scriptNode() ) : - for inspector, pathInspections in inspectors.items() : - for path, inspection in pathInspections.items() : - plug = inspection.acquireEdit() - if isinstance( plug, ( Gaffer.TweakPlug, Gaffer.NameValuePlug ) ) : - plug["value"].setValue( newValue ) - plug["enabled"].setValue( True ) - if isinstance( plug, Gaffer.TweakPlug ) : - plug["mode"].setValue( Gaffer.TweakPlug.Mode.Create ) - else : - plug.setValue( newValue ) - - return True - - def __disablableInspectionTweaks( self, pathListing ) : - - tweaks = [] - - path = pathListing.getPath().copy() - for columnSelection, column in zip( pathListing.getSelection(), pathListing.getColumns() ) : - if not isinstance( column, GafferSceneUI.Private.InspectorColumn ) : - continue - for pathString in columnSelection.paths() : - path.setFromString( pathString ) - inspectionContext = path.inspectionContext() - if inspectionContext is None : - continue - - with inspectionContext : - inspection = column.inspector().inspect() - if inspection is not None and inspection.editable() : - source = inspection.source() - editScope = self.settings()["editScope"].getInput() - if ( - ( - isinstance( source, ( Gaffer.TweakPlug, Gaffer.NameValuePlug ) ) and - source["enabled"].getValue() - ) and - ( editScope is None or editScope.node().isAncestorOf( source ) ) - ) : - tweaks.append( ( pathString, column.inspector() ) ) - else : - return [] - else : - return [] - - return tweaks - - def __disableEdits( self, pathListing ) : - - edits = self.__disablableInspectionTweaks( pathListing ) - - path = pathListing.getPath().copy() - with Gaffer.UndoScope( self.scriptNode() ) : - for pathString, inspector in edits : - path.setFromString( pathString ) - with path.inspectionContext() : - inspection = inspector.inspect() - if inspection is not None and inspection.editable() : - source = inspection.source() - - if isinstance( source, ( Gaffer.TweakPlug, Gaffer.NameValuePlug ) ) : - source["enabled"].setValue( False ) - - def __selectedSetExpressions( self, pathListing ) : - - # A dictionary of the form : - # { path1 : set( setExpression1, setExpression2 ), path2 : set( setExpression1 ), ... } - result = {} - - renderPassPath = pathListing.getPath().copy() - for columnSelection, column in zip( pathListing.getSelection(), pathListing.getColumns() ) : - if ( - not columnSelection.isEmpty() and ( - not isinstance( column, GafferSceneUI.Private.InspectorColumn ) or - not ( - Gaffer.Metadata.value( "option:" + column.inspector().name(), "ui:scene:acceptsSetName" ) or - Gaffer.Metadata.value( "option:" + column.inspector().name(), "ui:scene:acceptsSetNames" ) or - Gaffer.Metadata.value( "option:" + column.inspector().name(), "ui:scene:acceptsSetExpression" ) - ) - ) - ) : - # We only return set expressions if all selected paths are in - # columns that accept set names or set expressions. - return {} - - for path in columnSelection.paths() : - renderPassPath.setFromString( path ) - cellValue = column.cellData( renderPassPath ).value - if cellValue is not None : - result.setdefault( path, set() ).add( cellValue ) - else : - # We only return set expressions if all selected paths are render passes. - return {} - - return result - - def __selectAffected( self, pathListing ) : - - result = IECore.PathMatcher() - - renderPassPath = pathListing.getPath().copy() - for path, setExpressions in self.__selectedSetExpressions( pathListing ).items() : - # Evaluate set expressions within their render pass in the context - # as set membership could vary based on the render pass. - renderPassPath.setFromString( path ) - with renderPassPath.inspectionContext() : - for setExpression in setExpressions : - result.addPaths( GafferScene.SetAlgo.evaluateSetExpression( setExpression, self.settings()["in"] ) ) - - GafferSceneUI.ContextAlgo.setSelectedPaths( self.context(), result ) - - def __buttonPress( self, pathListing, event ) : - - if event.button != event.Buttons.Right or event.modifiers != event.Modifiers.None_ : - return False - - selection = pathListing.getSelection() + def __columnContextMenuSignal( self, column, pathListing, menuDefinition ) : columns = pathListing.getColumns() - cellColumn = pathListing.columnAt( event.line.p0 ) columnIndex = -1 for i in range( 0, len( columns ) ) : - if cellColumn == columns[i] : + if column == columns[i] : columnIndex = i - cellPath = pathListing.pathAt( event.line.p0 ) - if cellPath is None : - return False - - if not selection[columnIndex].match( str( cellPath ) ) & IECore.PathMatcher.Result.ExactMatch : - for p in selection : - p.clear() - selection[columnIndex].addPath( str( cellPath ) ) - pathListing.setSelection( selection, scrollToFirst = False ) - - menuDefinition = IECore.MenuDefinition() - if columnIndex == 0 : # Render pass operations @@ -625,73 +391,6 @@ def __buttonPress( self, pathListing, event ) : } ) - elif columnIndex > 1 : - # Option cells - - menuDefinition.append( - "Show History...", - { - "command" : Gaffer.WeakMethod( self.__showEditHistory ) - } - ) - menuDefinition.append( - "Edit...", - { - "command" : functools.partial( self.__editSelectedCells, pathListing, False ), - "active" : pathListing.getSelection()[0].isEmpty(), - } - ) - menuDefinition.append( - "Disable Edit", - { - "command" : functools.partial( self.__disableEdits, pathListing ), - "active" : len( self.__disablableInspectionTweaks( pathListing ) ) > 0, - "shortCut" : "D", - } - ) - if len( self.__selectedSetExpressions( pathListing ) ) > 0 : - menuDefinition.append( - "SelectAffectedObjectsDivider", { "divider" : True } - ) - menuDefinition.append( - "Select Affected Objects", - { - "command" : functools.partial( self.__selectAffected, pathListing ), - } - ) - - self.__contextMenu = GafferUI.Menu( menuDefinition ) - self.__contextMenu.popup( pathListing ) - - return True - - def __showEditHistory( self, *unused ) : - - selection = self.__pathListing.getSelection() - columns = self.__pathListing.getColumns() - renderPassPath = self.__pathListing.getPath().copy() - - for i in range( 0, len( columns ) ) : - column = columns[ i ] - if not isinstance( column, GafferSceneUI.Private.InspectorColumn ) : - continue - - for path in selection[i].paths() : - renderPassPath.setFromString( path ) - inspectionContext = renderPassPath.inspectionContext() - if inspectionContext is None : - continue - - window = _HistoryWindow( - column.inspector(), - "/", - inspectionContext, - self.ancestor( GafferUI.ScriptWindow ).scriptNode(), - "History : {} : {}".format( inspectionContext["renderPass"], column.headerData().value ) - ) - self.ancestor( GafferUI.Window ).addChildWindow( window, removeOnClose = True ) - window.setVisible( True ) - def __canEditRenderPasses( self, editScope = None ) : input = self.settings()["in"].getInput() diff --git a/python/GafferSceneUI/_InspectorColumn.py b/python/GafferSceneUI/_InspectorColumn.py new file mode 100644 index 0000000000..dbfa6ccc30 --- /dev/null +++ b/python/GafferSceneUI/_InspectorColumn.py @@ -0,0 +1,448 @@ +########################################################################## +# +# 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. +# +########################################################################## + +import functools + +import IECore + +import Gaffer +import GafferUI +import GafferScene +import GafferSceneUI + +from GafferUI.PlugValueWidget import sole +from GafferSceneUI._HistoryWindow import _HistoryWindow + +# This file extends the C++ functionality of InspectorColumn with functionality +# that is easier to implement in Python. This should all be considered as one +# component. + +def __toggleBoolean( pathListing, inspectors, inspections ) : + + plugs = [ i.acquireEdit() for i in inspections ] + # Make sure all the plugs either contain, or are themselves a BoolPlug or can be edited + # by `SetMembershipInspector.editSetMembership()` + if not all ( + ( + isinstance( plug, ( Gaffer.TweakPlug, Gaffer.NameValuePlug ) ) and + isinstance( plug["value"], Gaffer.BoolPlug ) + ) or ( + isinstance( plug, ( Gaffer.BoolPlug ) ) + ) or ( + isinstance( inspector, GafferSceneUI.Private.SetMembershipInspector ) + ) + for plug, inspector in zip( plugs, inspectors ) + ) : + return False + + currentValues = [] + + # Use a single new value for all plugs. + # First we need to find out what the new value would be for each plug in isolation. + for inspector, pathInspections in inspectors.items() : + for path, inspection in pathInspections.items() : + currentValues.append( inspection.value().value if inspection.value() is not None else False ) + + # Now set the value for all plugs, defaulting to `True` if they are not + # currently all the same. + newValue = not sole( currentValues ) + + with Gaffer.UndoScope( pathListing.ancestor( GafferUI.Editor ).scriptNode() ) : + for inspector, pathInspections in inspectors.items() : + for path, inspection in pathInspections.items() : + if isinstance( inspector, GafferSceneUI.Private.SetMembershipInspector ) : + inspector.editSetMembership( + inspection, + path, + GafferScene.EditScopeAlgo.SetMembership.Added if newValue else GafferScene.EditScopeAlgo.SetMembership.Removed + ) + + else : + plug = inspection.acquireEdit() + if isinstance( plug, ( Gaffer.TweakPlug, Gaffer.NameValuePlug ) ) : + plug["value"].setValue( newValue ) + plug["enabled"].setValue( True ) + if isinstance( plug, Gaffer.TweakPlug ) : + plug["mode"].setValue( Gaffer.TweakPlug.Mode.Create ) + else : + plug.setValue( newValue ) + + return True + +def __editSelectedCells( pathListing, quickBoolean = True ) : + + global __inspectorColumnPopup + + # A dictionary of the form : + # { inspector : { path1 : inspection, path2 : inspection, ... }, ... } + inspectors = {} + inspections = [] + + path = pathListing.getPath().copy() + for selection, column in zip( pathListing.getSelection(), pathListing.getColumns() ) : + for pathString in selection.paths() : + path.setFromString( pathString ) + inspectionContext = path.inspectionContext() + if inspectionContext is None : + continue + + with inspectionContext : + inspection = column.inspector().inspect() + + if inspection is not None : + inspectors.setdefault( column.inspector(), {} )[pathString] = inspection + inspections.append( inspection ) + + if len( inspectors ) == 0 : + with GafferUI.PopupWindow() as __inspectorColumnPopup : + with GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) : + GafferUI.Image( "warningSmall.png" ) + GafferUI.Label( "

The selected cells cannot be edited in the current Edit Scope

" ) + + __inspectorColumnPopup.popup( parent = pathListing ) + + return + + nonEditable = [ i for i in inspections if not i.editable() ] + + if len( nonEditable ) == 0 : + ## \todo Consider removal of this usage of the Editor's context when + # the toggling code is moved the inspectors. + with pathListing.ancestor( GafferUI.Editor ).context() : + if not quickBoolean or not __toggleBoolean( pathListing, inspectors, inspections ) : + edits = [ i.acquireEdit() for i in inspections ] + warnings = "\n".join( [ i.editWarning() for i in inspections if i.editWarning() != "" ] ) + # The plugs are either not boolean, boolean with mixed values, + # or attributes that don't exist and are not boolean. Show the popup. + __inspectorColumnPopup = GafferUI.PlugPopup( edits, warning = warnings ) + + if isinstance( __inspectorColumnPopup.plugValueWidget(), GafferUI.TweakPlugValueWidget ) : + __inspectorColumnPopup.plugValueWidget().setNameVisible( False ) + + __inspectorColumnPopup.popup( parent = pathListing ) + + else : + with GafferUI.PopupWindow() as __inspectorColumnPopup : + with GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) : + GafferUI.Image( "warningSmall.png" ) + GafferUI.Label( "

{}

".format( nonEditable[0].nonEditableReason() ) ) + + __inspectorColumnPopup.popup( parent = pathListing ) + +def __disablableInspectionTweaks( pathListing ) : + + tweaks = [] + + path = pathListing.getPath().copy() + for columnSelection, column in zip( pathListing.getSelection(), pathListing.getColumns() ) : + for pathString in columnSelection.paths() : + path.setFromString( pathString ) + inspectionContext = path.inspectionContext() + if inspectionContext is None : + continue + + with inspectionContext : + inspection = column.inspector().inspect() + if inspection is not None and inspection.editable() : + source = inspection.source() + editScope = inspection.editScope() + if ( + ( + ( + isinstance( source, ( Gaffer.TweakPlug, Gaffer.NameValuePlug ) ) and + source["enabled"].getValue() + ) or + isinstance( column.inspector(), GafferSceneUI.Private.SetMembershipInspector ) + ) and + ( editScope is None or editScope.isAncestorOf( source ) ) + ) : + tweaks.append( ( pathString, column.inspector() ) ) + else : + return [] + else : + return [] + + return tweaks + +def __disableEdits( pathListing ) : + + edits = __disablableInspectionTweaks( pathListing ) + path = pathListing.getPath().copy() + with Gaffer.UndoScope( pathListing.ancestor( GafferUI.Editor ).scriptNode() ) : + for pathString, inspector in edits : + path.setFromString( pathString ) + with path.inspectionContext() : + inspection = inspector.inspect() + if inspection is not None and inspection.editable() : + source = inspection.source() + + if isinstance( source, ( Gaffer.TweakPlug, Gaffer.NameValuePlug ) ) : + source["enabled"].setValue( False ) + elif isinstance( inspector, GafferSceneUI.Private.SetMembershipInspector ) : + inspector.editSetMembership( inspection, pathString, GafferScene.EditScopeAlgo.SetMembership.Unchanged ) + +def __removableAttributeInspections( pathListing ) : + + inspections = [] + + path = pathListing.getPath().copy() + for columnSelection, column in zip( pathListing.getSelection(), pathListing.getColumns() ) : + if not columnSelection.isEmpty() and type( column.inspector() ) != GafferSceneUI.Private.AttributeInspector : + return [] + for pathString in columnSelection.paths() : + path.setFromString( pathString ) + inspectionContext = path.inspectionContext() + if inspectionContext is None : + continue + + with inspectionContext : + inspection = column.inspector().inspect() + if inspection is not None and inspection.editable() : + source = inspection.source() + if ( + ( isinstance( source, Gaffer.TweakPlug ) and source["mode"].getValue() != Gaffer.TweakPlug.Mode.Remove ) or + ( isinstance( source, Gaffer.ValuePlug ) and len( source.children() ) == 2 and "Added" in source and "Removed" in source ) or + inspection.editScope() is not None + ) : + inspections.append( inspection ) + else : + return [] + else : + return [] + + return inspections + +def __removeAttributes( pathListing ) : + + inspections = __removableAttributeInspections( pathListing ) + + with Gaffer.UndoScope( pathListing.ancestor( GafferUI.Editor ).scriptNode() ) : + for inspection in inspections : + tweak = inspection.acquireEdit() + tweak["enabled"].setValue( True ) + tweak["mode"].setValue( Gaffer.TweakPlug.Mode.Remove ) + +def __selectedSetExpressions( pathListing ) : + + # A dictionary of the form : + # { path1 : set( setExpression1, setExpression2 ), path2 : set( setExpression1 ), ... } + result = {} + + # Map of Inspectors to metadata prefixes. + prefixMap = { + GafferSceneUI.Private.OptionInspector : "option:", + GafferSceneUI.Private.AttributeInspector : "attribute:" + } + + path = pathListing.getPath().copy() + for columnSelection, column in zip( pathListing.getSelection(), pathListing.getColumns() ) : + if ( + not columnSelection.isEmpty() and ( + type( column.inspector() ) not in prefixMap.keys() or + not ( + Gaffer.Metadata.value( prefixMap.get( type( column.inspector() ) ) + column.inspector().name(), "ui:scene:acceptsSetName" ) or + Gaffer.Metadata.value( prefixMap.get( type( column.inspector() ) ) + column.inspector().name(), "ui:scene:acceptsSetNames" ) or + Gaffer.Metadata.value( prefixMap.get( type( column.inspector() ) ) + column.inspector().name(), "ui:scene:acceptsSetExpression" ) + ) + ) + ) : + # We only return set expressions if all selected paths are in + # columns that accept set names or set expressions. + return {} + + for pathString in columnSelection.paths() : + path.setFromString( pathString ) + cellValue = column.cellData( path ).value + if cellValue is not None : + result.setdefault( pathString, set() ).add( cellValue ) + else : + # We only return set expressions if all selected paths have values. + return {} + + return result + +def __selectAffected( pathListing ) : + + result = IECore.PathMatcher() + + editor = pathListing.ancestor( GafferUI.Editor ) + path = pathListing.getPath().copy() + + for pathString, setExpressions in __selectedSetExpressions( pathListing ).items() : + # Evaluate set expressions within their path's inspection context + # as set membership could vary based on the context. + path.setFromString( pathString ) + with path.inspectionContext() : + for setExpression in setExpressions : + result.addPaths( GafferScene.SetAlgo.evaluateSetExpression( setExpression, editor.settings()["in"] ) ) + + GafferSceneUI.ContextAlgo.setSelectedPaths( editor.scriptNode().context(), result ) + +def __showHistory( pathListing ) : + + columns = pathListing.getColumns() + selection = pathListing.getSelection() + path = pathListing.getPath().copy() + + for i, column in enumerate( columns ) : + for pathString in selection[i].paths() : + path.setFromString( pathString ) + inspectionContext = path.inspectionContext() + if inspectionContext is None : + continue + + window = _HistoryWindow( + column.inspector(), + pathString, + inspectionContext, + pathListing.ancestor( GafferUI.Editor ).scriptNode(), + "History : {} : {}".format( pathString, column.headerData().value ) + ) + pathListing.ancestor( GafferUI.Window ).addChildWindow( window, removeOnClose = True ) + window.setVisible( True ) + +def __validateSelection( pathListing ) : + + selection = pathListing.getSelection() + # We only operate on PathListingWidgets + # with `Cell` or `Cells` selection modes. + if not isinstance( selection, list ) : + return False + + if all( [ x.isEmpty() for x in selection ] ) : + return False + + for columnSelection, column in zip( selection, pathListing.getColumns() ) : + if not columnSelection.isEmpty() and not isinstance( column, GafferSceneUI.Private.InspectorColumn ) : + return False + + return True + +def __buttonDoubleClick( path, pathListing, event ) : + + # We only support doubleClick events when all of the selected + # cells are in InspectorColumns. + if not __validateSelection( pathListing ) : + return False + + if event.button == event.Buttons.Left : + __editSelectedCells( pathListing ) + return True + + return False + +def __contextMenu( column, pathListing, menuDefinition ) : + + # We only add context menu items when all of the selected + # cells are in InspectorColumns. + if not __validateSelection( pathListing ) : + return + + menuDefinition.append( + "Show History...", + { + "command" : functools.partial( __showHistory, pathListing ) + } + ) + + toggleOnly = isinstance( column.inspector(), GafferSceneUI.Private.SetMembershipInspector ) + menuDefinition.append( + "Toggle" if toggleOnly else "Edit...", + { + "command" : functools.partial( __editSelectedCells, pathListing, toggleOnly ), + } + ) + menuDefinition.append( + "Disable Edit", + { + "command" : functools.partial( __disableEdits, pathListing ), + "active" : len( __disablableInspectionTweaks( pathListing ) ) > 0, + "shortCut" : "D", + } + ) + + if len( __removableAttributeInspections( pathListing ) ) > 0 : + menuDefinition.append( + "Remove Attribute", + { + "command" : functools.partial( __removeAttributes, pathListing ), + "shortCut" : "Backspace, Delete", + } + ) + + if len( __selectedSetExpressions( pathListing ) ) > 0 : + menuDefinition.append( + "SelectAffectedObjectsDivider", { "divider" : True } + ) + menuDefinition.append( + "Select Affected Objects", + { + "command" : functools.partial( __selectAffected, pathListing ), + } + ) + +def __keyPress( column, pathListing, event ) : + + # We only support keyPress events when all of the selected + # cells are in InspectorColumns. + if not __validateSelection( pathListing ) : + return + + if event.modifiers == event.Modifiers.None_ : + if event.key in ( "Return", "Enter" ) : + __editSelectedCells( pathListing ) + return True + + if event.key == "D" : + if len( __disablableInspectionTweaks( pathListing ) ) > 0 : + __disableEdits( pathListing ) + return True + + if event.key in ( "Backspace", "Delete" ) : + if len( __removableAttributeInspections( pathListing ) ) > 0 : + __removeAttributes( pathListing ) + return True + + return False + +def __inspectorColumnCreated( column ) : + + if isinstance( column, GafferSceneUI.Private.InspectorColumn ) : + column.buttonDoubleClickSignal().connectFront( __buttonDoubleClick, scoped = False ) + column.contextMenuSignal().connectFront( __contextMenu, scoped = False ) + column.keyPressSignal().connectFront( __keyPress, scoped = False ) + +GafferSceneUI.Private.InspectorColumn.instanceCreatedSignal().connect( __inspectorColumnCreated, scoped = False ) diff --git a/python/GafferSceneUI/__init__.py b/python/GafferSceneUI/__init__.py index 5a5ef42518..2a71956551 100644 --- a/python/GafferSceneUI/__init__.py +++ b/python/GafferSceneUI/__init__.py @@ -54,6 +54,7 @@ from .RenderPassEditor import RenderPassEditor from . import SceneHistoryUI from . import EditScopeUI +from . import _InspectorColumn from . import SceneNodeUI from . import SceneReaderUI diff --git a/python/GafferSceneUITest/LightEditorTest.py b/python/GafferSceneUITest/LightEditorTest.py index a875a73961..78b70bf985 100644 --- a/python/GafferSceneUITest/LightEditorTest.py +++ b/python/GafferSceneUITest/LightEditorTest.py @@ -48,6 +48,7 @@ import GafferSceneTest import GafferUITest +from GafferSceneUI._InspectorColumn import __editSelectedCells as editSelectedCells class LightEditorTest( GafferUITest.TestCase ) : @@ -659,10 +660,10 @@ def testLightMuteAttribute( toggleCount, toggleLocation, newStates ) : widget = editor._LightEditor__pathListing self.setLightEditorMuteSelection( widget, togglePaths ) - editor._LightEditor__editSelectedCells( widget ) + editSelectedCells( widget ) testLightMuteAttribute( 1, togglePaths, firstNewStates ) - editor._LightEditor__editSelectedCells( widget ) + editSelectedCells( widget ) testLightMuteAttribute( 2, togglePaths, secondNewStates ) del widget, editor @@ -714,7 +715,7 @@ def testToggleContext( self ) : self.setLightEditorMuteSelection( widget, ["/group/light"] ) # This will raise an exception if the context is not scoped correctly. - editor._LightEditor__editSelectedCells( + editSelectedCells( widget, True # quickBoolean ) diff --git a/src/GafferSceneUI/InspectorColumn.cpp b/src/GafferSceneUI/InspectorColumn.cpp index 173f04ef9e..483cf8731e 100644 --- a/src/GafferSceneUI/InspectorColumn.cpp +++ b/src/GafferSceneUI/InspectorColumn.cpp @@ -115,9 +115,7 @@ PathColumn::CellData InspectorColumn::cellData( const Gaffer::Path &path, const toolTip = "Source : " + source->relativeName( source->ancestor() ); } - /// \todo Adding these "Double-click" prompts really only makes sense - /// once the column itself handles editing. Should we have the ability - /// to create read-only columns? + /// \todo Should we have the ability to create read-only columns? if( inspectorResult->editable() ) { toolTip += !toolTip.empty() ? "\n\n" : "";