diff --git a/Changes.md b/Changes.md index 6c4c7f7b783..8cd64cffc68 100644 --- a/Changes.md +++ b/Changes.md @@ -21,6 +21,7 @@ Improvements - Shaders (including light shaders) are only loaded from the `osl` subdirectory of the 3Delight installation. - Primitive variables named `uv` are now automatically renamed `st` for compatibility with the `uvCoord` shader's expectation. - Added a default `uvCoord` shader during internal shader network preprocessing to shader parameters that do not have an input connection. +- SetEditor : Added columns for controlling the Visible Set membership of set members. These allow the current members of a set to be included or excluded from the Visible Set by clicking within the Set Editor's Inclusions and Exclusions columns. Fixes ----- diff --git a/python/GafferSceneUI/SetEditor.py b/python/GafferSceneUI/SetEditor.py index 63195646e30..dbfebfa6d6f 100644 --- a/python/GafferSceneUI/SetEditor.py +++ b/python/GafferSceneUI/SetEditor.py @@ -71,11 +71,15 @@ def __init__( self, scriptNode, **kw ) : GafferUI.BasicPathFilterWidget( emptySetFilter ) self.__setMembersColumn = GafferUI.StandardPathColumn( "Members", "setPath:memberCount" ) + self.__includedSetMembersColumn = _GafferSceneUI._SetEditor.VisibleSetInclusionsColumn( scriptNode.context() ) + self.__excludedSetMembersColumn = _GafferSceneUI._SetEditor.VisibleSetExclusionsColumn( scriptNode.context() ) self.__pathListing = GafferUI.PathListingWidget( Gaffer.DictPath( {}, "/" ), # temp till we make a SetPath columns = [ _GafferSceneUI._SetEditor.SetNameColumn(), self.__setMembersColumn, + self.__includedSetMembersColumn, + self.__excludedSetMembersColumn, ], selectionMode = GafferUI.PathListingWidget.SelectionMode.Rows, displayMode = GafferUI.PathListingWidget.DisplayMode.Tree, @@ -164,23 +168,29 @@ def __selectedSetNames( self ) : def __dragBegin( self, widget, event ) : path = self.__pathListing.pathAt( imath.V2f( event.line.p0.x, event.line.p0.y ) ) - column = self.__pathListing.columnAt( imath.V2f( event.line.p0.x, event.line.p0.y ) ) selection = self.__pathListing.getSelection() - + setNames = [] if selection.match( str( path ) ) & IECore.PathMatcher.Result.ExactMatch : - if column == self.__setMembersColumn : - GafferUI.Pointer.setCurrent( "paths" ) - return IECore.StringVectorData( self.__getSetMembers().paths() ) - else : - selectedSetNames = self.__selectedSetNames() - if len( selectedSetNames ) > 0 : - GafferUI.Pointer.setCurrent( "paths" ) - return IECore.StringVectorData( selectedSetNames ) - else : - # prevent the path itself from being dragged - return IECore.StringVectorData() + setNames = self.__selectedSetNames() + else : + setName = path.property( "setPath:setName" ) + if setName is not None : + setNames.append( setName ) - return None + if len( setNames ) == 0 : + # prevent the path itself from being dragged + return IECore.StringVectorData() + + GafferUI.Pointer.setCurrent( "paths" ) + column = self.__pathListing.columnAt( imath.V2f( event.line.p0.x, event.line.p0.y ) ) + if column == self.__setMembersColumn : + return IECore.StringVectorData( self.__getSetMembers( setNames ).paths() ) + elif column == self.__includedSetMembersColumn : + return IECore.StringVectorData( self.__getIncludedSetMembers( setNames ).paths() ) + elif column == self.__excludedSetMembersColumn : + return IECore.StringVectorData( self.__getExcludedSetMembers( setNames ).paths() ) + else : + return IECore.StringVectorData( setNames ) def __keyPressSignal( self, widget, event ) : @@ -240,7 +250,7 @@ def __copySelectedSetNames( self, *unused ) : self.__plug.ancestor( Gaffer.ApplicationRoot ).setClipboardContents( data ) - def __getSetMembers( self, *unused ) : + def __getSetMembers( self, setNames, *unused ) : result = IECore.PathMatcher() @@ -248,21 +258,29 @@ def __getSetMembers( self, *unused ) : return result with Gaffer.Context( self.getContext() ) : - for setName in self.__selectedSetNames() : + for setName in setNames : result.addPaths( self.__plug.set( setName ).value ) return result + def __getIncludedSetMembers( self, setNames, *unused ) : + + return self.__getSetMembers( setNames ).intersection( ContextAlgo.getVisibleSet( self.getContext() ).inclusions ) + + def __getExcludedSetMembers( self, setNames, *unused ) : + + return self.__getSetMembers( setNames ).intersection( ContextAlgo.getVisibleSet( self.getContext() ).exclusions ) + def __selectSetMembers( self, *unused ) : - ContextAlgo.setSelectedPaths( self.getContext(), self.__getSetMembers() ) + ContextAlgo.setSelectedPaths( self.getContext(), self.__getSetMembers( self.__selectedSetNames() ) ) def __copySetMembers( self, *unused ) : data = IECore.StringVectorData() if self.__plug is not None : - data.extend( self.__getSetMembers().paths() ) + data.extend( self.__getSetMembers( self.__selectedSetNames() ).paths() ) self.__plug.ancestor( Gaffer.ApplicationRoot ).setClipboardContents( data ) diff --git a/src/GafferSceneUIModule/SetEditorBinding.cpp b/src/GafferSceneUIModule/SetEditorBinding.cpp index b1e70d21e9b..c3ae36e103a 100644 --- a/src/GafferSceneUIModule/SetEditorBinding.cpp +++ b/src/GafferSceneUIModule/SetEditorBinding.cpp @@ -407,6 +407,433 @@ class SetNameColumn : public StandardPathColumn }; +////////////////////////////////////////////////////////////////////////// +// VisibleSetInclusionsColumn - displays and modifies inclusions membership +// of the VisibleSet in the provided context. +////////////////////////////////////////////////////////////////////////// + +class VisibleSetInclusionsColumn : public PathColumn +{ + + public : + + IE_CORE_DECLAREMEMBERPTR( VisibleSetInclusionsColumn ) + + VisibleSetInclusionsColumn( ContextPtr context ) + : PathColumn(), m_context( context ) + { + buttonPressSignal().connect( boost::bind( &VisibleSetInclusionsColumn::buttonPress, this, ::_3 ) ); + buttonReleaseSignal().connect( boost::bind( &VisibleSetInclusionsColumn::buttonRelease, this, ::_1, ::_2, ::_3 ) ); + m_context->changedSignal().connect( boost::bind( &VisibleSetInclusionsColumn::contextChanged, this, ::_2 ) ); + } + + CellData cellData( const Gaffer::Path &path, const IECore::Canceller *canceller ) const override + { + CellData result; + + auto setPath = IECore::runTimeCast( &path ); + if( !setPath ) + { + return result; + } + + const auto setName = runTimeCast( setPath->property( g_setNamePropertyName ) ); + if( !setName ) + { + // We only interact with locations representing sets + return result; + } + + auto iconData = new CompoundData; + iconData->writable()["state:highlighted"] = g_setIncludedHighlightedTransparentIconName; + result.icon = iconData; + result.toolTip = g_inclusionToolTip; + + const auto visibleSet = ContextAlgo::getVisibleSet( m_context.get() ); + if( visibleSet.inclusions.isEmpty() ) + { + result.value = new IntData( 0 ); + return result; + } + + Context::Scope scopedContext( m_context.get() ); + const auto setMembers = setPath->getScene()->set( setName->readable() ); + const auto includedSetMembers = setMembers->readable().intersection( visibleSet.inclusions ); + result.value = new IntData( includedSetMembers.size() ); + if( includedSetMembers.isEmpty() ) + { + return result; + } + + size_t excludedSetMemberCount = 0; + if( !visibleSet.exclusions.isEmpty() ) + { + for( IECore::PathMatcher::Iterator it = includedSetMembers.begin(), eIt = includedSetMembers.end(); it != eIt; ++it ) + { + const auto visibility = visibleSet.visibility( *it ); + if( visibility.drawMode != GafferScene::VisibleSet::Visibility::Visible ) + { + excludedSetMemberCount++; + } + } + } + + iconData->writable()["state:highlighted"] = g_setIncludedHighlightedIconName; + const bool allSetMembersIncluded = includedSetMembers.size() == setMembers->readable().size(); + if( excludedSetMemberCount == 0 ) + { + iconData->writable()["state:normal"] = allSetMembersIncluded ? g_setIncludedIconName : g_setPartiallyIncludedIconName; + result.toolTip = allSetMembersIncluded ? g_setIncludedToolTip : g_setPartiallyIncludedToolTip; + } + else if( includedSetMembers.size() == excludedSetMemberCount ) + { + iconData->writable()["state:normal"] = g_setIncludedDisabledIconName; + result.toolTip = allSetMembersIncluded ? g_setIncludedOverrideToolTip : g_setPartiallyIncludedOverrideToolTip; + } + else + { + iconData->writable()["state:normal"] = g_setPartiallyDisabledIconName; + result.toolTip = allSetMembersIncluded ? g_setIncludedPartialOverrideToolTip : g_setPartiallyIncludedPartialOverrideToolTip; + } + + return result; + } + + CellData headerData( const IECore::Canceller *canceller ) const override + { + const auto visibleSet = ContextAlgo::getVisibleSet( m_context.get() ); + return CellData( /* value = */ nullptr, /* icon = */ visibleSet.inclusions.isEmpty() ? g_inclusionsEmptyIconName : g_setIncludedIconName, /* background = */ nullptr, /* tooltip = */ new StringData( "Visible Set Inclusions" ) ); + } + + private : + + void contextChanged( const IECore::InternedString &name ) + { + if( ContextAlgo::affectsVisibleSet( name ) ) + { + changedSignal()( this ); + } + } + + bool buttonPress( const ButtonEvent &event ) + { + if( event.buttons != ButtonEvent::Left ) + { + return false; + } + + return true; + } + + bool buttonRelease( const Gaffer::Path &path, const GafferUI::PathListingWidget &widget, const ButtonEvent &event ) + { + auto setPath = IECore::runTimeCast( &path ); + if( !setPath ) + { + return false; + } + + const auto setName = runTimeCast( setPath->property( g_setNamePropertyName ) ); + if( !setName ) + { + // We only interact with locations representing sets + return false; + } + + Context::Scope scopedContext( m_context.get() ); + const auto setMembers = setPath->getScene()->set( setName->readable() ); + auto pathsToInclude = IECore::PathMatcher( setMembers->readable() ); + const auto selection = widget.getSelection(); + if( std::holds_alternative( selection ) ) + { + // Permit bulk editing of a selection of set names when clicking on one of the selected set names + const auto selectedPaths = std::get( selection ); + if( selectedPaths.match( setPath->names() ) & IECore::PathMatcher::Result::ExactMatch ) + { + auto selectedSetPath = setPath->copy(); + for( IECore::PathMatcher::Iterator it = selectedPaths.begin(), eIt = selectedPaths.end(); it != eIt; ++it ) + { + selectedSetPath->setFromString( ScenePlug::pathToString( *it ) ); + const auto selectedSetName = runTimeCast( selectedSetPath->property( g_setNamePropertyName ) ); + if( selectedSetName && selectedSetName->readable() != setName->readable() ) + { + pathsToInclude.addPaths( setPath->getScene()->set( selectedSetName->readable() )->readable() ); + } + } + } + } + + bool update = false; + auto visibleSet = ContextAlgo::getVisibleSet( m_context.get() ); + if( event.button == ButtonEvent::Left && !event.modifiers ) + { + const auto includedSetMembers = setMembers->readable().intersection( visibleSet.inclusions ); + if( includedSetMembers.isEmpty() ) + { + update = visibleSet.inclusions.addPaths( pathsToInclude ); + } + else + { + update = visibleSet.inclusions.removePaths( pathsToInclude ); + } + } + else if( event.button == ButtonEvent::Left && event.modifiers == ButtonEvent::Modifiers::Shift ) + { + update = visibleSet.inclusions.addPaths( pathsToInclude ); + } + + if( update ) + { + ContextAlgo::setVisibleSet( m_context.get(), visibleSet ); + } + + return true; + } + + ContextPtr m_context; + + static IECore::StringDataPtr g_setIncludedIconName; + static IECore::StringDataPtr g_setIncludedDisabledIconName; + static IECore::StringDataPtr g_setIncludedHighlightedIconName; + static IECore::StringDataPtr g_setIncludedHighlightedTransparentIconName; + static IECore::StringDataPtr g_setPartiallyIncludedIconName; + static IECore::StringDataPtr g_setPartiallyDisabledIconName; + static IECore::StringDataPtr g_inclusionsEmptyIconName; + + static IECore::StringDataPtr g_inclusionToolTip; + static IECore::StringDataPtr g_setIncludedToolTip; + static IECore::StringDataPtr g_setIncludedOverrideToolTip; + static IECore::StringDataPtr g_setIncludedPartialOverrideToolTip; + static IECore::StringDataPtr g_setPartiallyIncludedToolTip; + static IECore::StringDataPtr g_setPartiallyIncludedOverrideToolTip; + static IECore::StringDataPtr g_setPartiallyIncludedPartialOverrideToolTip; + +}; + +StringDataPtr VisibleSetInclusionsColumn::g_setIncludedIconName = new StringData( "locationIncluded.png" ); +StringDataPtr VisibleSetInclusionsColumn::g_setIncludedDisabledIconName = new StringData( "locationIncludedDisabled.png" ); +StringDataPtr VisibleSetInclusionsColumn::g_setIncludedHighlightedIconName = new StringData( "locationIncludedHighlighted.png" ); +StringDataPtr VisibleSetInclusionsColumn::g_setIncludedHighlightedTransparentIconName = new StringData( "locationIncludedHighlightedTransparent.png" ); +StringDataPtr VisibleSetInclusionsColumn::g_setPartiallyIncludedIconName = new StringData( "descendantIncluded.png" ); +StringDataPtr VisibleSetInclusionsColumn::g_setPartiallyDisabledIconName = new StringData( "descendantIncludedTransparent.png" ); +StringDataPtr VisibleSetInclusionsColumn::g_inclusionsEmptyIconName = new StringData( "locationIncludedTransparent.png" ); + +StringDataPtr VisibleSetInclusionsColumn::g_inclusionToolTip = new StringData( "Click to include the current members of this set in the Visible Set, causing them to always appear in Viewers." ); +StringDataPtr VisibleSetInclusionsColumn::g_setIncludedToolTip = new StringData( + "All members are in the Visible Set, causing them to always appear in Viewers.\n\n" + "Click to remove members from the Visible Set." +); +StringDataPtr VisibleSetInclusionsColumn::g_setIncludedOverrideToolTip = new StringData( + "All members are in the Visible Set, but aren't visible due to being overridden by an exclusion.\n\n" + "Click to remove members from the Visible Set." +); +StringDataPtr VisibleSetInclusionsColumn::g_setIncludedPartialOverrideToolTip = new StringData( + "All members are in the Visible Set, but some aren't visible due to being overridden by an exclusion.\n\n" + "Click to remove members from the Visible Set." +); +StringDataPtr VisibleSetInclusionsColumn::g_setPartiallyIncludedToolTip = new StringData( + "Some members are in the Visible Set, causing them to always appear in Viewers.\n\n" + "Click to remove members from the Visible Set.\n" + "Shift-click to include members in the Visible Set." +); +StringDataPtr VisibleSetInclusionsColumn::g_setPartiallyIncludedOverrideToolTip = new StringData( + "Some members are in the Visible Set, but aren't visible due to being overridden by an exclusion.\n\n" + "Click to remove members from the Visible Set.\n" + "Shift-click to include members in the Visible Set." +); +StringDataPtr VisibleSetInclusionsColumn::g_setPartiallyIncludedPartialOverrideToolTip = new StringData( + "Some members are in the Visible Set, but some aren't visible due to being overridden by an exclusion.\n\n" + "Click to remove members from the Visible Set.\n" + "Shift-click to include members in the Visible Set." +); + +////////////////////////////////////////////////////////////////////////// +// VisibleSetExclusionsColumn - displays and modifies exclusions membership +// of the VisibleSet in the provided context. +////////////////////////////////////////////////////////////////////////// + +class VisibleSetExclusionsColumn : public PathColumn +{ + + public : + + IE_CORE_DECLAREMEMBERPTR( VisibleSetExclusionsColumn ) + + VisibleSetExclusionsColumn( ContextPtr context ) + : PathColumn(), m_context( context ) + { + buttonPressSignal().connect( boost::bind( &VisibleSetExclusionsColumn::buttonPress, this, ::_3 ) ); + buttonReleaseSignal().connect( boost::bind( &VisibleSetExclusionsColumn::buttonRelease, this, ::_1, ::_2, ::_3 ) ); + m_context->changedSignal().connect( boost::bind( &VisibleSetExclusionsColumn::contextChanged, this, ::_2 ) ); + } + + CellData cellData( const Gaffer::Path &path, const IECore::Canceller *canceller ) const override + { + CellData result; + + auto setPath = IECore::runTimeCast( &path ); + if( !setPath ) + { + return result; + } + + const auto setName = runTimeCast( setPath->property( g_setNamePropertyName ) ); + if( !setName ) + { + // We only interact with locations representing sets + return result; + } + + auto iconData = new CompoundData; + iconData->writable()["state:highlighted"] = g_setExcludedHighlightedTransparentIconName; + result.icon = iconData; + result.toolTip = g_exclusionToolTip; + + const auto visibleSet = ContextAlgo::getVisibleSet( m_context.get() ); + if( visibleSet.exclusions.isEmpty() ) + { + result.value = new IntData( 0 ); + return result; + } + + Context::Scope scopedContext( m_context.get() ); + const auto setMembers = setPath->getScene()->set( setName->readable() ); + const auto excludedSetMembers = setMembers->readable().intersection( visibleSet.exclusions ); + result.value = new IntData( excludedSetMembers.size() ); + if( excludedSetMembers.isEmpty() ) + { + return result; + } + + const bool allSetMembersExcluded = excludedSetMembers.size() == setMembers->readable().size(); + iconData->writable()["state:highlighted"] = g_setExcludedHighlightedIconName; + iconData->writable()["state:normal"] = allSetMembersExcluded ? g_setExcludedIconName : g_setPartiallyExcludedIconName; + result.toolTip = allSetMembersExcluded ? g_setExcludedToolTip : g_setPartiallyExcludedToolTip; + + return result; + } + + CellData headerData( const IECore::Canceller *canceller ) const override + { + const auto visibleSet = ContextAlgo::getVisibleSet( m_context.get() ); + return CellData( /* value = */ nullptr, /* icon = */ visibleSet.exclusions.isEmpty() ? g_exclusionsEmptyIconName : g_setExcludedIconName, /* background = */ nullptr, /* tooltip = */ new StringData( "Visible Set Exclusions" ) ); + } + + private : + + void contextChanged( const IECore::InternedString &name ) + { + if( ContextAlgo::affectsVisibleSet( name ) ) + { + changedSignal()( this ); + } + } + + bool buttonPress( const ButtonEvent &event ) + { + if( event.buttons != ButtonEvent::Left ) + { + return false; + } + + return true; + } + + bool buttonRelease( const Gaffer::Path &path, const GafferUI::PathListingWidget &widget, const ButtonEvent &event ) + { + auto setPath = IECore::runTimeCast( &path ); + if( !setPath ) + { + return false; + } + + const auto setName = runTimeCast( setPath->property( g_setNamePropertyName ) ); + if( !setName ) + { + // We only interact with locations representing sets + return false; + } + + Context::Scope scopedContext( m_context.get() ); + const auto setMembers = setPath->getScene()->set( setName->readable() ); + auto pathsToExclude = IECore::PathMatcher( setMembers->readable() ); + const auto selection = widget.getSelection(); + if( std::holds_alternative( selection ) ) + { + // Permit bulk editing of a selection of set names when clicking on one of the selected set names + const auto selectedPaths = std::get( selection ); + if( selectedPaths.match( setPath->names() ) & IECore::PathMatcher::Result::ExactMatch ) + { + auto selectedSetPath = setPath->copy(); + for( IECore::PathMatcher::Iterator it = selectedPaths.begin(), eIt = selectedPaths.end(); it != eIt; ++it ) + { + selectedSetPath->setFromString( ScenePlug::pathToString( *it ) ); + const auto selectedSetName = runTimeCast( selectedSetPath->property( g_setNamePropertyName ) ); + if( selectedSetName && selectedSetName->readable() != setName->readable() ) + { + pathsToExclude.addPaths( setPath->getScene()->set( selectedSetName->readable() )->readable() ); + } + } + } + } + + bool update = false; + auto visibleSet = ContextAlgo::getVisibleSet( m_context.get() ); + if( event.button == ButtonEvent::Left && !event.modifiers ) + { + const auto excludedSetMembers = setMembers->readable().intersection( visibleSet.exclusions ); + if( excludedSetMembers.isEmpty() ) + { + update = visibleSet.exclusions.addPaths( pathsToExclude ); + } + else + { + update = visibleSet.exclusions.removePaths( pathsToExclude ); + } + } + else if( event.button == ButtonEvent::Left && event.modifiers == ButtonEvent::Modifiers::Shift ) + { + update = visibleSet.exclusions.addPaths( pathsToExclude ); + } + + if( update ) + { + ContextAlgo::setVisibleSet( m_context.get(), visibleSet ); + } + + return true; + } + + ContextPtr m_context; + + static IECore::StringDataPtr g_setExcludedIconName; + static IECore::StringDataPtr g_setPartiallyExcludedIconName; + static IECore::StringDataPtr g_setExcludedHighlightedIconName; + static IECore::StringDataPtr g_setExcludedHighlightedTransparentIconName; + static IECore::StringDataPtr g_exclusionsEmptyIconName; + + static IECore::StringDataPtr g_exclusionToolTip; + static IECore::StringDataPtr g_setExcludedToolTip; + static IECore::StringDataPtr g_setPartiallyExcludedToolTip; +}; + +StringDataPtr VisibleSetExclusionsColumn::g_setExcludedIconName = new StringData( "locationExcluded.png" ); +StringDataPtr VisibleSetExclusionsColumn::g_setPartiallyExcludedIconName = new StringData( "descendantExcluded.png" ); +StringDataPtr VisibleSetExclusionsColumn::g_setExcludedHighlightedIconName = new StringData( "locationExcludedHighlighted.png" ); +StringDataPtr VisibleSetExclusionsColumn::g_setExcludedHighlightedTransparentIconName = new StringData( "locationExcludedHighlightedTransparent.png" ); +StringDataPtr VisibleSetExclusionsColumn::g_exclusionsEmptyIconName = new StringData( "locationExcludedTransparent.png" ); + +StringDataPtr VisibleSetExclusionsColumn::g_exclusionToolTip = new StringData( "Click to exclude the current members of this set from the Visible Set, causing them to not appear in Viewers." ); +StringDataPtr VisibleSetExclusionsColumn::g_setExcludedToolTip = new StringData( + "All members are excluded from the Visible Set, causing them to not appear in Viewers.\n\n" + "Click to remove the exclusion." +); +StringDataPtr VisibleSetExclusionsColumn::g_setPartiallyExcludedToolTip = new StringData( + "Some members are excluded from the Visible Set, causing them to not appear in Viewers.\n\n" + "Click to remove the exclusion.\n" + "Shift-click to exclude members from the Visible Set." +); + ////////////////////////////////////////////////////////////////////////// // SetEditorSearchFilter - filters based on a match pattern. This // removes non-leaf paths if all their children have also been @@ -604,4 +1031,12 @@ void GafferSceneUIModule::bindSetEditor() .def( init<>() ) ; + RefCountedClass( "VisibleSetInclusionsColumn" ) + .def( init< ContextPtr >() ) + ; + + RefCountedClass( "VisibleSetExclusionsColumn" ) + .def( init< ContextPtr >() ) + ; + }