// Copyright Epic Games, Inc. All Rights Reserved. #include "SSequencerTreeView.h" #include "SSequencerTrackLane.h" #include "EditorStyleSet.h" #include "Algo/BinarySearch.h" #include "Algo/Copy.h" #include "SequencerDisplayNodeDragDropOp.h" #include "Framework/Commands/GenericCommands.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Framework/Application/SlateApplication.h" #include "SSequencerTreeView.h" #include "ScopedTransaction.h" static FName TrackAreaName = "TrackArea"; SSequencerTreeViewRow::~SSequencerTreeViewRow() { const TSharedPtr& TreeView = StaticCastSharedPtr(OwnerTablePtr.Pin()); TSharedPtr PinnedNode = Node.Pin(); if (TreeView.IsValid() && PinnedNode.IsValid()) { TreeView->OnChildRowRemoved(PinnedNode.ToSharedRef()); } } /** Construct function for this widget */ void SSequencerTreeViewRow::Construct(const FArguments& InArgs, const TSharedRef& OwnerTableView, const FDisplayNodeRef& InNode) { Node = InNode; OnGenerateWidgetForColumn = InArgs._OnGenerateWidgetForColumn; bool bIsSelectable = InNode->IsSelectable(); SMultiColumnTableRow::Construct( SMultiColumnTableRow::FArguments() .OnDragDetected(this, &SSequencerTreeViewRow::OnDragDetected) .OnCanAcceptDrop(this, &SSequencerTreeViewRow::OnCanAcceptDrop) .OnAcceptDrop(this, &SSequencerTreeViewRow::OnAcceptDrop) .ShowSelection(bIsSelectable) .Padding(this, &SSequencerTreeViewRow::GetRowPadding), OwnerTableView); } FMargin SSequencerTreeViewRow::GetRowPadding() const { TSharedPtr PinnedNode = Node.Pin(); TSharedPtr ParentNode = PinnedNode ? PinnedNode->GetParentOrRoot() : nullptr; if (ParentNode.IsValid() && ParentNode->GetType() == ESequencerNode::Root && ParentNode->GetChildNodes()[0] != PinnedNode) { return FMargin(0.f, 1.f, 0.f, 0.f); } return FMargin(0.f, 0.f, 0.f, 0.f); } TSharedRef SSequencerTreeViewRow::GenerateWidgetForColumn(const FName& ColumnId) { TSharedPtr PinnedNode = Node.Pin(); if (PinnedNode.IsValid()) { return OnGenerateWidgetForColumn.Execute(PinnedNode.ToSharedRef(), ColumnId, SharedThis(this)); } return SNullWidget::NullWidget; } FReply SSequencerTreeViewRow::OnDragDetected( const FGeometry& InGeometry, const FPointerEvent& InPointerEvent ) { TSharedPtr DisplayNode = Node.Pin(); if ( DisplayNode.IsValid() ) { FSequencer& Sequencer = DisplayNode->GetParentTree().GetSequencer(); if ( Sequencer.GetSelection().GetSelectedOutlinerNodes().Num() > 0 ) { TArray > DraggableNodes; for ( const TSharedRef& SelectedNode : Sequencer.GetSelection().GetSelectedOutlinerNodes() ) { if ( SelectedNode->CanDrag() ) { DraggableNodes.Add(SelectedNode); } } // If there were no nodes selected, don't start a drag drop operation. if (DraggableNodes.Num() == 0) { return FReply::Unhandled(); } FText DefaultText = FText::Format( NSLOCTEXT( "SequencerTreeViewRow", "DefaultDragDropFormat", "Move {0} item(s)" ), FText::AsNumber( DraggableNodes.Num() ) ); TSharedRef DragDropOp = FSequencerDisplayNodeDragDropOp::New( DraggableNodes, DefaultText, nullptr ); return FReply::Handled().BeginDragDrop( DragDropOp ); } } return FReply::Unhandled(); } TOptional SSequencerTreeViewRow::OnCanAcceptDrop( const FDragDropEvent& DragDropEvent, EItemDropZone InItemDropZone, FDisplayNodeRef DisplayNode ) { TSharedPtr DragDropOp = DragDropEvent.GetOperationAs(); if ( DragDropOp.IsValid() ) { DragDropOp->ResetToDefaultToolTip(); TOptional AllowedDropZone = DisplayNode->CanDrop( *DragDropOp, InItemDropZone ); if ( AllowedDropZone.IsSet() == false ) { DragDropOp->CurrentIconBrush = FEditorStyle::GetBrush( TEXT( "Graph.ConnectorFeedback.Error" ) ); } return AllowedDropZone; } return TOptional(); } FReply SSequencerTreeViewRow::OnAcceptDrop( const FDragDropEvent& DragDropEvent, EItemDropZone InItemDropZone, FDisplayNodeRef DisplayNode ) { TSharedPtr DragDropOp = DragDropEvent.GetOperationAs(); if ( DragDropOp.IsValid()) { DisplayNode->Drop( DragDropOp->GetDraggedNodes(), InItemDropZone ); return FReply::Handled(); } return FReply::Unhandled(); } TSharedPtr SSequencerTreeViewRow::GetDisplayNode() const { return Node.Pin(); } void SSequencerTreeViewRow::AddTrackAreaReference(const TSharedPtr& Lane) { TrackLaneReference = Lane; } void SSequencerTreeViewRow::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { StaticCastSharedPtr(OwnerTablePtr.Pin())->ReportChildRowGeometry(Node.Pin().ToSharedRef(), AllottedGeometry); } const FSlateBrush* SSequencerTreeViewRow::GetBorder() const { TSharedPtr PinnedNode = Node.Pin(); bool bIsSelectable = PinnedNode->IsSelectable(); if (!bIsSelectable) { return nullptr; } return SMultiColumnTableRow::GetBorder(); } void SSequencerTreeView::Construct(const FArguments& InArgs, const TSharedRef& InNodeTree, const TSharedRef& InTrackArea) { SequencerNodeTree = InNodeTree; TrackArea = InTrackArea; bUpdatingSequencerSelection = false; bUpdatingTreeSelection = false; bSequencerSelectionChangeBroadcastWasSupressed = false; bPhysicalNodesNeedUpdate = false; bRightMouseButtonDown = false; bShowPinnedNodes = false; // We 'leak' these delegates (they'll get cleaned up automatically when the invocation list changes) // It's not safe to attempt their removal in ~SSequencerTreeView because SequencerNodeTree->GetSequencer() may not be valid FSequencer& Sequencer = InNodeTree->GetSequencer(); Sequencer.GetSelection().GetOnOutlinerNodeSelectionChanged().AddSP(this, &SSequencerTreeView::SynchronizeTreeSelectionWithSequencerSelection); HeaderRow = SNew(SHeaderRow).Visibility(EVisibility::Collapsed); OnGetContextMenuContent = InArgs._OnGetContextMenuContent; SetupColumns(InArgs); STreeView::Construct ( STreeView::FArguments() .TreeItemsSource(&RootNodes) .SelectionMode(ESelectionMode::Multi) .OnGenerateRow(this, &SSequencerTreeView::OnGenerateRow) .OnGetChildren(this, &SSequencerTreeView::OnGetChildren) .HeaderRow(HeaderRow) .ExternalScrollbar(InArgs._ExternalScrollbar) .OnExpansionChanged(this, &SSequencerTreeView::OnExpansionChanged) .AllowOverscroll(EAllowOverscroll::No) .OnContextMenuOpening( this, &SSequencerTreeView::OnContextMenuOpening ) .OnSetExpansionRecursive(this, &SSequencerTreeView::SetItemExpansionRecursive) .HighlightParentNodesForSelection(true) .AllowInvisibleItemSelection(true) //without this we deselect everything when we filter or we collapse, etc.. ); } void SSequencerTreeView::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { if (bSequencerSelectionChangeBroadcastWasSupressed && !FSlateApplication::Get().AnyMenusVisible()) { FSequencerSelection& SequencerSelection = SequencerNodeTree->GetSequencer().GetSelection(); if (SequencerSelection.IsBroadcasting()) { SequencerSelection.RequestOutlinerNodeSelectionChangedBroadcast(); bSequencerSelectionChangeBroadcastWasSupressed = false; } } STreeView::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); // These are updated in both tick and paint since both calls can cause changes to the cached rows and the data needs // to be kept synchronized so that external measuring calls get correct and reliable results. if (bPhysicalNodesNeedUpdate) { PhysicalNodes.Reset(); CachedRowGeometry.GenerateValueArray(PhysicalNodes); PhysicalNodes.Sort([](const FCachedGeometry& A, const FCachedGeometry& B) { return A.PhysicalTop < B.PhysicalTop; }); } HighlightRegion = TOptional(); if (SequencerNodeTree->GetHoveredNode().IsValid()) { TSharedRef OutermostParent = SequencerNodeTree->GetHoveredNode()->GetOutermostParent(); TOptional PhysicalTop = ComputeNodePosition(OutermostParent); if (PhysicalTop.IsSet()) { // Compute total height of the highlight float TotalHeight = 0.f; OutermostParent->TraverseVisible_ParentFirst([&](FSequencerDisplayNode& InNode){ TotalHeight += InNode.GetNodeHeight() + InNode.GetNodePadding().Combined(); return true; }); HighlightRegion = FHighlightRegion(PhysicalTop.GetValue(), PhysicalTop.GetValue() + TotalHeight); } } } int32 SSequencerTreeView::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const { LayerId = STreeView::OnPaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled); // These are updated in both tick and paint since both calls can cause changes to the cached rows and the data needs // to be kept synchronized so that external measuring calls get correct and reliable results. if (bPhysicalNodesNeedUpdate) { PhysicalNodes.Reset(); CachedRowGeometry.GenerateValueArray(PhysicalNodes); PhysicalNodes.Sort([](const FCachedGeometry& A, const FCachedGeometry& B) { return A.PhysicalTop < B.PhysicalTop; }); } if (HighlightRegion.IsSet()) { // Black tint for highlighted regions FSlateDrawElement::MakeBox( OutDrawElements, LayerId+1, AllottedGeometry.ToPaintGeometry(FVector2D(2.f, HighlightRegion->Top - 4.f), FVector2D(AllottedGeometry.Size.X - 4.f, 4.f)), FEditorStyle::GetBrush("Sequencer.TrackHoverHighlight_Top"), ESlateDrawEffect::None, FLinearColor::Black ); FSlateDrawElement::MakeBox( OutDrawElements, LayerId+1, AllottedGeometry.ToPaintGeometry(FVector2D(2.f, HighlightRegion->Bottom), FVector2D(AllottedGeometry.Size.X - 4.f, 4.f)), FEditorStyle::GetBrush("Sequencer.TrackHoverHighlight_Bottom"), ESlateDrawEffect::None, FLinearColor::Black ); } return LayerId + 1; } TOptional SSequencerTreeView::GetPhysicalGeometryForNode(const FDisplayNodeRef& InNode) const { if (const FCachedGeometry* FoundGeometry = CachedRowGeometry.Find(InNode)) { return *FoundGeometry; } return TOptional(); } TOptional SSequencerTreeView::ComputeNodePosition(const FDisplayNodeRef& InNode) const { // Positioning strategy: // Attempt to root out any visible node in the specified node's sub-hierarchy, and compute the node's offset from that float NegativeOffset = 0.f; TOptional Top; // Iterate parent first until we find a tree view row we can use for the offset height auto Iter = [this, &NegativeOffset, &Top](FSequencerDisplayNode& InDisplayNode) { TOptional ChildRowGeometry = this->GetPhysicalGeometryForNode(InDisplayNode.AsShared()); if (ChildRowGeometry.IsSet()) { Top = ChildRowGeometry->PhysicalTop; // Stop iterating return false; } NegativeOffset -= InDisplayNode.GetNodeHeight() + InDisplayNode.GetNodePadding().Combined(); return true; }; InNode->TraverseVisible_ParentFirst(Iter); if (Top.IsSet()) { return NegativeOffset + Top.GetValue(); } return Top; } void SSequencerTreeView::ReportChildRowGeometry(const FDisplayNodeRef& InNode, const FGeometry& InGeometry) { float ChildOffset = TransformPoint( Concatenate( InGeometry.GetAccumulatedLayoutTransform(), GetCachedGeometry().GetAccumulatedLayoutTransform().Inverse() ), FVector2D(0,0) ).Y; if (InNode->IsPinned() != bShowPinnedNodes) { CachedRowGeometry.Remove(InNode); } else { CachedRowGeometry.Add(InNode, FCachedGeometry(InNode, ChildOffset, InGeometry.Size.Y)); } bPhysicalNodesNeedUpdate = true; for (TSharedPtr SlaveTreeView : SlaveTreeViews) { SlaveTreeView->ReportChildRowGeometry(InNode, InGeometry); } } void SSequencerTreeView::OnChildRowRemoved(const FDisplayNodeRef& InNode) { CachedRowGeometry.Remove(InNode); bPhysicalNodesNeedUpdate = true; } TSharedPtr SSequencerTreeView::HitTestNode(float InPhysical) const { // Find the first node with a top after the specified value - the hit node must be the one preceeding this const int32 FoundIndex = Algo::UpperBoundBy(PhysicalNodes, InPhysical, &FCachedGeometry::PhysicalTop) - 1; if (FoundIndex >= 0) { return PhysicalNodes[FoundIndex].Node; } return nullptr; } float SSequencerTreeView::PhysicalToVirtual(float InPhysical) const { // Find the first node with a top after the specified value - the hit node must be the one preceeding this const int32 FoundIndex = Algo::UpperBoundBy(PhysicalNodes, InPhysical, &FCachedGeometry::PhysicalTop) - 1; if (FoundIndex >= 0) { const FCachedGeometry& Found = PhysicalNodes[FoundIndex]; const float FractionalHeight = (InPhysical - Found.PhysicalTop) / Found.PhysicalHeight; return Found.Node->GetVirtualTop() + (Found.Node->GetVirtualBottom() - Found.Node->GetVirtualTop()) * FractionalHeight; } if (PhysicalNodes.Num()) { const FCachedGeometry& First = PhysicalNodes[0]; if (InPhysical < First.PhysicalTop) { return First.Node->GetVirtualTop() + (InPhysical - First.PhysicalTop); } else { const FCachedGeometry& Last = PhysicalNodes.Last(); return Last.Node->GetVirtualTop() + (InPhysical - Last.PhysicalTop); } } return InPhysical; } float SSequencerTreeView::VirtualToPhysical(float InVirtual) const { auto GetVirtualTop = [](const FCachedGeometry& In) { return In.Node->GetVirtualTop(); }; // Find the first node with a top after the specified value - the hit node must be the one preceeding this const int32 FoundIndex = Algo::UpperBoundBy(PhysicalNodes, InVirtual, GetVirtualTop) - 1; if (FoundIndex >= 0) { const FCachedGeometry& Found = PhysicalNodes[FoundIndex]; const float FractionalHeight = (InVirtual - Found.Node->GetVirtualTop()) / (Found.Node->GetVirtualBottom() - Found.Node->GetVirtualTop()); return Found.PhysicalTop + Found.PhysicalHeight * FractionalHeight; } if (PhysicalNodes.Num()) { const FCachedGeometry& Last = PhysicalNodes.Last(); return Last.PhysicalTop + (InVirtual - Last.Node->GetVirtualTop()); } return InVirtual; } void SSequencerTreeView::SetupColumns(const FArguments& InArgs) { FSequencer& Sequencer = SequencerNodeTree->GetSequencer(); // Define a column for the Outliner auto GenerateOutliner = [](const FDisplayNodeRef& InNode, const TSharedRef& InRow) { return InNode->GenerateContainerWidgetForOutliner(InRow); }; Columns.Add("Outliner", FSequencerTreeViewColumn(GenerateOutliner, 1.f)); // Now populate the header row with the columns for (TTuple& Pair : Columns) { if (Pair.Key != TrackAreaName) { HeaderRow->AddColumn( SHeaderRow::Column(Pair.Key) .FillWidth(Pair.Value.Width) ); } } } void SSequencerTreeView::UpdateTrackArea() { FSequencer& Sequencer = SequencerNodeTree->GetSequencer(); // Add or remove the column if (const FSequencerTreeViewColumn* Column = Columns.Find(TrackAreaName)) { HeaderRow->AddColumn( SHeaderRow::Column(TrackAreaName) .FillWidth(Column->Width) ); } } void SSequencerTreeView::AddSlaveTreeView(TSharedPtr SlaveTreeView) { SlaveTreeViews.Add(SlaveTreeView); SlaveTreeView->SetMasterTreeView(SharedThis(this)); } void SSequencerTreeView::OnRightMouseButtonDown(const FPointerEvent& MouseEvent) { STreeView::OnRightMouseButtonDown(MouseEvent); bRightMouseButtonDown = true; } void SSequencerTreeView::OnRightMouseButtonUp(const FPointerEvent& MouseEvent) { STreeView::OnRightMouseButtonUp(MouseEvent); bRightMouseButtonDown = false; } FReply SSequencerTreeView::OnDragOver(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) { FReply Result = STreeView::OnDragOver(MyGeometry, DragDropEvent); if (!Result.IsEventHandled()) { TSharedPtr DragDropOp = DragDropEvent.GetOperationAs(); if (DragDropOp.IsValid()) { // Reset tooltip to indicate that the dragged objects can be moved to the root (ie. unparented) DragDropOp->ResetToDefaultToolTip(); } return Result.Handled(); } return Result; } FReply SSequencerTreeView::OnDrop(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) { FReply Result = STreeView::OnDrop(MyGeometry, DragDropEvent); TSharedPtr DragDropOp = DragDropEvent.GetOperationAs(); if (!Result.IsEventHandled() && DragDropOp.IsValid() && DragDropOp->GetDraggedNodes().Num()) { const FScopedTransaction Transaction(NSLOCTEXT("SequencerTrackNode", "MoveItems", "Move items.")); for (TSharedRef DraggedNode : DragDropOp->GetDraggedNodes()) { SequencerNodeTree->MoveDisplayNodeToRoot(DraggedNode); } SequencerNodeTree->GetSequencer().NotifyMovieSceneDataChanged(EMovieSceneDataChangeType::MovieSceneStructureItemsChanged); return FReply::Handled(); } return Result; } FReply SSequencerTreeView::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) { const TArray& ItemsSourceRef = (*this->ItemsSource); // Don't respond to key-presses containing "Alt" as a modifier if (ItemsSourceRef.Num() > 0 && !InKeyEvent.IsAltDown()) { bool bWasHandled = false; NullableItemType ItemNavigatedTo(nullptr); // Check for selection manipulation keys (Up, Down, Home, End, PageUp, PageDown) if (InKeyEvent.GetKey() == EKeys::Up) { int32 SelectionIndex = 0; if (TListTypeTraits::IsPtrValid(SelectorItem)) { SelectionIndex = ItemsSourceRef.Find(TListTypeTraits::NullableItemTypeConvertToItemType(SelectorItem)); } --SelectionIndex; for (; SelectionIndex >=0; --SelectionIndex) { if (ItemsSourceRef[SelectionIndex]->IsSelectable()) { ItemNavigatedTo = ItemsSourceRef[SelectionIndex]; break; } } bWasHandled = true; } else if (InKeyEvent.GetKey() == EKeys::Down) { int32 SelectionIndex = 0; if (TListTypeTraits::IsPtrValid(SelectorItem)) { SelectionIndex = ItemsSourceRef.Find(TListTypeTraits::NullableItemTypeConvertToItemType(SelectorItem)); } ++SelectionIndex; for (; SelectionIndex < ItemsSourceRef.Num(); ++SelectionIndex) { if (ItemsSourceRef[SelectionIndex]->IsSelectable()) { ItemNavigatedTo = ItemsSourceRef[SelectionIndex]; break; } } bWasHandled = true; } else if (InKeyEvent.GetKey() == EKeys::Home) { // Select the first item for (int32 SelectionIndex = 0; SelectionIndex < ItemsSourceRef.Num(); ++SelectionIndex) { if (ItemsSourceRef[SelectionIndex]->IsSelectable()) { ItemNavigatedTo = ItemsSourceRef[SelectionIndex]; break; } } bWasHandled = true; } else if (InKeyEvent.GetKey() == EKeys::End) { // Select the last item for (int32 SelectionIndex = ItemsSourceRef.Num() -1; SelectionIndex >=0 ; --SelectionIndex) { if (ItemsSourceRef[SelectionIndex]->IsSelectable()) { ItemNavigatedTo = ItemsSourceRef[SelectionIndex]; break; } } bWasHandled = true; } else if (InKeyEvent.GetKey() == EKeys::PageUp) { int32 SelectionIndex = 0; if (TListTypeTraits::IsPtrValid(SelectorItem)) { SelectionIndex = ItemsSourceRef.Find(TListTypeTraits::NullableItemTypeConvertToItemType(SelectorItem)); } int32 NumItemsInAPage = GetNumLiveWidgets(); int32 Remainder = NumItemsInAPage % GetNumItemsPerLine(); NumItemsInAPage -= Remainder; if (SelectionIndex >= NumItemsInAPage) { // Select an item on the previous page SelectionIndex = SelectionIndex - NumItemsInAPage; // Scan up for the first selectable node for (; SelectionIndex >= 0; --SelectionIndex) { if (ItemsSourceRef[SelectionIndex]->IsSelectable()) { ItemNavigatedTo = ItemsSourceRef[SelectionIndex]; break; } } } // If we had less than a page to jump, or we haven't found a selectable node yet, // scan back toward our current node until we find one. if (!ItemNavigatedTo) { SelectionIndex = 0; for (; SelectionIndex < ItemsSourceRef.Num(); ++SelectionIndex) { if (ItemsSourceRef[SelectionIndex]->IsSelectable()) { ItemNavigatedTo = ItemsSourceRef[SelectionIndex]; break; } } } bWasHandled = true; } else if (InKeyEvent.GetKey() == EKeys::PageDown) { int32 SelectionIndex = 0; if (TListTypeTraits::IsPtrValid(SelectorItem)) { SelectionIndex = ItemsSourceRef.Find(TListTypeTraits::NullableItemTypeConvertToItemType(SelectorItem)); } int32 NumItemsInAPage = GetNumLiveWidgets(); int32 Remainder = NumItemsInAPage % GetNumItemsPerLine(); NumItemsInAPage -= Remainder; if (SelectionIndex < ItemsSourceRef.Num() - NumItemsInAPage) { // Select an item on the next page SelectionIndex = SelectionIndex + NumItemsInAPage; for (; SelectionIndex < ItemsSourceRef.Num(); ++SelectionIndex) { if (ItemsSourceRef[SelectionIndex]->IsSelectable()) { ItemNavigatedTo = ItemsSourceRef[SelectionIndex]; break; } } } // If we had less than a page to jump, or we haven't found a selectable node yet, // scan back toward our current node until we find one. if (!ItemNavigatedTo) { SelectionIndex = ItemsSourceRef.Num() - 1; for (; SelectionIndex >= 0; --SelectionIndex) { if (ItemsSourceRef[SelectionIndex]->IsSelectable()) { ItemNavigatedTo = ItemsSourceRef[SelectionIndex]; break; } } } bWasHandled = true; } else if (InKeyEvent.GetKey() == EKeys::SpaceBar) { // SListView behavior is: Change selected status of item. Bypass that here. We don't want that, but instead want spacebar to go to SequencerCommands (toggle play) return FReply::Unhandled(); } if (TListTypeTraits::IsPtrValid(ItemNavigatedTo)) { FDisplayNodeRef ItemToSelect(TListTypeTraits::NullableItemTypeConvertToItemType(ItemNavigatedTo)); NavigationSelect(ItemToSelect, InKeyEvent); } if (bWasHandled) { return FReply::Handled(); } } return STreeView::OnKeyDown(MyGeometry, InKeyEvent); } void SSequencerTreeView::SynchronizeTreeSelectionWithSequencerSelection() { if ( bUpdatingSequencerSelection == false ) { bUpdatingTreeSelection = true; { const TArray& ItemsSourceRef = (*this->ItemsSource); FSequencer& Sequencer = SequencerNodeTree->GetSequencer(); FSequencerSelection& Selection = Sequencer.GetSelection(); bool bSelectionChanged = false; for (int32 ItemIndex = 0; ItemIndex < ItemsSourceRef.Num(); ++ItemIndex) { const bool bIsSelected = IsItemSelected(ItemsSourceRef[ItemIndex]); const bool bShouldBeSelected = Selection.IsSelected(ItemsSourceRef[ItemIndex]); if (bIsSelected != bShouldBeSelected) { bSelectionChanged = true; Private_SetItemSelection( ItemsSourceRef[ItemIndex], bShouldBeSelected, false ); } } if (bSelectionChanged) { Private_SignalSelectionChanged( ESelectInfo::Direct ); } } bUpdatingTreeSelection = false; } for (TSharedPtr SlaveTreeView : SlaveTreeViews) { SlaveTreeView->SynchronizeTreeSelectionWithSequencerSelection(); } } void SSequencerTreeView::Private_SetItemSelection( FDisplayNodeRef TheItem, bool bShouldBeSelected, bool bWasUserDirected ) { if (!TheItem->IsSelectable()) { return; } STreeView::Private_SetItemSelection( TheItem, bShouldBeSelected, bWasUserDirected ); if ( bUpdatingTreeSelection == false ) { // Don't broadcast the sequencer selection change on individual tree changes. Wait for signal selection changed. FSequencerSelection& SequencerSelection = SequencerNodeTree->GetSequencer().GetSelection(); SequencerSelection.SuspendBroadcast(); bSequencerSelectionChangeBroadcastWasSupressed = true; if ( bShouldBeSelected ) { SequencerSelection.AddToSelection( TheItem ); } else { SequencerSelection.RemoveFromSelection( TheItem ); } SequencerSelection.ResumeBroadcast(); } } void SSequencerTreeView::Private_ClearSelection() { STreeView::Private_ClearSelection(); if ( bUpdatingTreeSelection == false ) { // Don't broadcast the sequencer selection change on individual tree changes. Wait for signal selection changed. FSequencerSelection& SequencerSelection = SequencerNodeTree->GetSequencer().GetSelection(); SequencerSelection.SuspendBroadcast(); bSequencerSelectionChangeBroadcastWasSupressed = true; SequencerSelection.EmptySelectedOutlinerNodes(); SequencerSelection.ResumeBroadcast(); } } void SSequencerTreeView::Private_SelectRangeFromCurrentTo( FDisplayNodeRef InRangeSelectionEnd ) { STreeView::Private_SelectRangeFromCurrentTo( InRangeSelectionEnd ); if ( bUpdatingTreeSelection == false ) { // Don't broadcast the sequencer selection change on individual tree changes. Wait for signal selection changed. FSequencerSelection& SequencerSelection = SequencerNodeTree->GetSequencer().GetSelection(); SequencerSelection.SuspendBroadcast(); bSequencerSelectionChangeBroadcastWasSupressed = true; SynchronizeSequencerSelectionWithTreeSelection(); SequencerSelection.ResumeBroadcast(); } } void SSequencerTreeView::Private_SignalSelectionChanged(ESelectInfo::Type SelectInfo) { if ( bUpdatingTreeSelection == false && !bRightMouseButtonDown) { bUpdatingSequencerSelection = true; { FSequencerSelection& SequencerSelection = SequencerNodeTree->GetSequencer().GetSelection(); SequencerSelection.SuspendBroadcast(); bool bSelectionChanged = SynchronizeSequencerSelectionWithTreeSelection(); SequencerSelection.ResumeBroadcast(); if (bSequencerSelectionChangeBroadcastWasSupressed || bSelectionChanged) { if (SequencerSelection.IsBroadcasting()) { SequencerSelection.RequestOutlinerNodeSelectionChangedBroadcast(); bSequencerSelectionChangeBroadcastWasSupressed = false; } } } bUpdatingSequencerSelection = false; } STreeView::Private_SignalSelectionChanged(SelectInfo); } bool SSequencerTreeView::SynchronizeSequencerSelectionWithTreeSelection() { // If this is a slave SSequencerTreeView it only has a partial view of what is selected. The master should handle syncing the entire selection instead. if (MasterTreeView.IsValid()) { return MasterTreeView.Pin()->SynchronizeSequencerSelectionWithTreeSelection(); } bool bSelectionChanged = false; const TSet>& SequencerSelection = SequencerNodeTree->GetSequencer().GetSelection().GetSelectedOutlinerNodes(); TSet > AllSelectedItems(SelectedItems); // If we have slave SSequencerTreeViews, combine their selected items as well for (TSharedPtr SlaveTreeView : SlaveTreeViews) { AllSelectedItems.Append(SlaveTreeView->GetSelectedItems()); } if (AllSelectedItems.Num() != SequencerSelection.Num() || AllSelectedItems.Difference(SequencerSelection).Num() != 0 ) { FSequencer& Sequencer = SequencerNodeTree->GetSequencer(); FSequencerSelection& Selection = Sequencer.GetSelection(); const TArray& ItemsSourceRef = (*this->ItemsSource); for (int32 ItemIndex = 0; ItemIndex < ItemsSourceRef.Num(); ++ItemIndex) { const bool bShouldBeSelected = IsItemSelected(ItemsSourceRef[ItemIndex]); const bool bIsSelected = Selection.IsSelected(ItemsSourceRef[ItemIndex]); if (bIsSelected != bShouldBeSelected) { bSelectionChanged = true; if (bShouldBeSelected) { Selection.AddToSelection(ItemsSourceRef[ItemIndex]); } else { Selection.RemoveFromSelection(ItemsSourceRef[ItemIndex]); } } } } return bSelectionChanged; } TSharedPtr SSequencerTreeView::OnContextMenuOpening() { // Open a context menu for the first selected item if it is selectable for (TSharedRef SelectedNode : SequencerNodeTree->GetSequencer().GetSelection().GetSelectedOutlinerNodes()) { if (SelectedNode->IsSelectable()) { return SelectedNode->OnSummonContextMenu(); } break; } // Otherwise, add a general menu for options const bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, SequencerNodeTree->GetSequencer().GetCommandBindings()); OnGetContextMenuContent.ExecuteIfBound(MenuBuilder); MenuBuilder.BeginSection("Edit"); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Paste); MenuBuilder.EndSection(); return MenuBuilder.MakeWidget(); } void SSequencerTreeView::Refresh() { RootNodes.Reset(); for (auto& Item : SequencerNodeTree->GetRootNodes()) { if (Item->IsVisible() && Item->IsPinned() == bShowPinnedNodes) { RootNodes.Add(Item); } } // Reset item expansion since we don't know if any expansion states may have changed in-between refreshes { STreeView::OnExpansionChanged.Unbind(); ClearExpandedItems(); auto Traverse_SetExpansionStates = [this](FSequencerDisplayNode& InNode) { this->SetItemExpansion(InNode.AsShared(), InNode.IsExpanded()); return true; }; const bool bIncludeRootNode = false; SequencerNodeTree->GetRootNode()->Traverse_ParentFirst(Traverse_SetExpansionStates, bIncludeRootNode); STreeView::OnExpansionChanged.BindSP(this, &SSequencerTreeView::OnExpansionChanged); } // Force synchronization of selected tree view items here since the tree nodes may have been rebuilt // and the treeview's selection will now be invalid. bUpdatingTreeSelection = true; SynchronizeTreeSelectionWithSequencerSelection(); bUpdatingTreeSelection = false; RebuildList(); for (TSharedPtr SlaveTreeView : SlaveTreeViews) { SlaveTreeView->Refresh(); } } void SSequencerTreeView::ScrollByDelta(float DeltaInSlateUnits) { ScrollBy( GetCachedGeometry(), DeltaInSlateUnits, EAllowOverscroll::No ); } template bool ShouldExpand(const T& InContainer, ETreeRecursion Recursion) { bool bAllExpanded = true; for (auto& Item : InContainer) { bAllExpanded &= Item->IsExpanded(); if (Recursion == ETreeRecursion::Recursive) { Item->TraverseVisible_ParentFirst([&](FSequencerDisplayNode& InNode){ bAllExpanded &= InNode.IsExpanded(); return true; }); } } return !bAllExpanded; } void SSequencerTreeView::ToggleExpandCollapseNodes(ETreeRecursion Recursion, bool bExpandAll, bool bCollapseAll) { FSequencer& Sequencer = SequencerNodeTree->GetSequencer(); const TSet< FDisplayNodeRef >& SelectedNodes = Sequencer.GetSelection().GetSelectedOutlinerNodes(); if (SelectedNodes.Num() > 0 && !bExpandAll && !bCollapseAll) { const bool bExpand = ShouldExpand(SelectedNodes, Recursion); for (TSharedRef Item : SelectedNodes) { ExpandCollapseNode(Item, bExpand, Recursion); } } else if (SequencerNodeTree->GetRootNodes().Num() > 0) { bool bExpand = ShouldExpand(SequencerNodeTree->GetRootNodes(), Recursion); if (bExpandAll) { bExpand = true; } if (bCollapseAll) { bExpand = false; } for (TSharedRef Item : SequencerNodeTree->GetRootNodes()) { ExpandCollapseNode(Item, bExpand, Recursion); } } } void SSequencerTreeView::ExpandCollapseNode(const FDisplayNodeRef& InNode, bool bExpansionState, ETreeRecursion Recursion) { SetItemExpansion(InNode, bExpansionState); if (Recursion == ETreeRecursion::Recursive) { for (auto& Node : InNode->GetChildNodes()) { ExpandCollapseNode(Node, bExpansionState, ETreeRecursion::Recursive); } } } void SSequencerTreeView::OnExpansionChanged(FDisplayNodeRef InItem, bool bIsExpanded) { InItem->SetExpansionState(bIsExpanded); // Expand any children that are also expanded for (const FDisplayNodeRef& Child : InItem->GetChildNodes()) { if (Child->IsExpanded()) { SetItemExpansion(Child, true); } } } void SSequencerTreeView::SetItemExpansionRecursive(FDisplayNodeRef InItem, bool bIsExpanded) { ExpandCollapseNode(InItem, bIsExpanded, ETreeRecursion::Recursive); } void SSequencerTreeView::OnGetChildren(FDisplayNodeRef InParent, TArray& OutChildren) const { for (const auto& Node : InParent->GetChildNodes()) { if (!Node->IsHidden()) { OutChildren.Add(Node); } } } TSharedRef SSequencerTreeView::OnGenerateRow(FDisplayNodeRef InDisplayNode, const TSharedRef& OwnerTable) { TSharedRef Row = SNew(SSequencerTreeViewRow, OwnerTable, InDisplayNode) .OnGenerateWidgetForColumn(this, &SSequencerTreeView::GenerateWidgetForColumn); // Ensure the track area is kept up to date with the virtualized scroll of the tree view TSharedPtr SectionAuthority = InDisplayNode->GetSectionAreaAuthority(); if (SectionAuthority.IsValid()) { TSharedPtr TrackLane = TrackArea->FindTrackSlot(SectionAuthority.ToSharedRef()); if (!TrackLane.IsValid()) { // Add a track slot for the row TAttribute> ViewRange = FAnimatedRange::WrapAttribute( TAttribute::Create(TAttribute::FGetter::CreateSP(&SequencerNodeTree->GetSequencer(), &FSequencer::GetViewRange)) ); TrackLane = SNew(SSequencerTrackLane, SectionAuthority.ToSharedRef(), SharedThis(this)) //.IsEnabled(!InDisplayNode->GetSequencer().IsReadOnly()) [ SectionAuthority->GenerateWidgetForSectionArea(ViewRange) ]; TrackArea->AddTrackSlot(SectionAuthority.ToSharedRef(), TrackLane); } if (ensure(TrackLane.IsValid())) { Row->AddTrackAreaReference(TrackLane); } } return Row; } TSharedRef SSequencerTreeView::GenerateWidgetForColumn(const FDisplayNodeRef& InNode, const FName& ColumnId, const TSharedRef& Row) const { const auto* Definition = Columns.Find(ColumnId); if (ensureMsgf(Definition, TEXT("Invalid column name specified"))) { return Definition->Generator(InNode, Row); } return SNullWidget::NullWidget; }