// Copyright Epic Games, Inc. All Rights Reserved. #include "AnimTimeline/SAnimOutliner.h" #include "AnimTimeline/AnimModel.h" #include "AnimTimeline/AnimTimelineTrack.h" #include "AnimTimeline/SAnimOutlinerItem.h" #include "AnimTimeline/SAnimTrackArea.h" #include "Widgets/Input/SButton.h" #include "AnimTimeline/SAnimTrack.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Misc/TextFilterExpressionEvaluator.h" #define LOCTEXT_NAMESPACE "SAnimOutliner" SAnimOutliner::~SAnimOutliner() { if(AnimModel.IsValid()) { AnimModel.Pin()->OnTracksChanged().Remove(TracksChangedDelegateHandle); } } void SAnimOutliner::Construct(const FArguments& InArgs, const TSharedRef& InAnimModel, const TSharedRef& InTrackArea) { AnimModel = InAnimModel; TrackArea = InTrackArea; FilterText = InArgs._FilterText; bPhysicalTracksNeedUpdate = false; TracksChangedDelegateHandle = InAnimModel->OnTracksChanged().AddSP(this, &SAnimOutliner::HandleTracksChanged); TextFilter = MakeShareable(new FTextFilterExpressionEvaluator(ETextFilterExpressionEvaluatorMode::BasicString)); HeaderRow = SNew(SHeaderRow) .Visibility(EVisibility::Collapsed); HeaderRow->AddColumn( SHeaderRow::Column(TEXT("Outliner")) .FillWidth(1.0f) ); STreeView::Construct ( STreeView::FArguments() .TreeItemsSource(&InAnimModel->GetAllRootTracks()) .SelectionMode(ESelectionMode::Multi) .OnGenerateRow(this, &SAnimOutliner::HandleGenerateRow) .OnGetChildren(this, &SAnimOutliner::HandleGetChildren) .HeaderRow(HeaderRow) .ExternalScrollbar(InArgs._ExternalScrollbar) .OnExpansionChanged(this, &SAnimOutliner::HandleExpansionChanged) .AllowOverscroll(EAllowOverscroll::No) .OnContextMenuOpening(this, &SAnimOutliner::HandleContextMenuOpening) ); // expand all InAnimModel->ForEachRootTrack([this](FAnimTimelineTrack& InRootTrack) { InRootTrack.Traverse_ParentFirst([this](FAnimTimelineTrack& InTrack){ SetItemExpansion(InTrack.AsShared(), InTrack.IsExpanded()); return true; }); }); } void SAnimOutliner::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { 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 (bPhysicalTracksNeedUpdate) { PhysicalTracks.Reset(); CachedTrackGeometry.GenerateValueArray(PhysicalTracks); PhysicalTracks.Sort([](const FCachedGeometry& A, const FCachedGeometry& B) { return A.Top < B.Top; }); bPhysicalTracksNeedUpdate = false; } } int32 SAnimOutliner::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 (bPhysicalTracksNeedUpdate) { PhysicalTracks.Reset(); CachedTrackGeometry.GenerateValueArray(PhysicalTracks); PhysicalTracks.Sort([](const FCachedGeometry& A, const FCachedGeometry& B) { return A.Top < B.Top; }); bPhysicalTracksNeedUpdate = false; } return LayerId + 1; } TSharedRef SAnimOutliner::HandleGenerateRow(TSharedRef InTrack, const TSharedRef& OwnerTable) { TSharedRef Row = SNew(SAnimOutlinerItem, OwnerTable, InTrack) .OnGenerateWidgetForColumn(this, &SAnimOutliner::GenerateWidgetForColumn) .HighlightText(FilterText); // Ensure the track area is kept up to date with the virtualized scroll of the tree view TSharedPtr TrackWidget = TrackArea->FindTrackSlot(InTrack); if (!TrackWidget.IsValid()) { // Add a track slot for the row TrackWidget = SNew(SAnimTrack, InTrack, SharedThis(this)) .ViewRange(AnimModel.Pin().Get(), &FAnimModel::GetViewRange) [ InTrack->GenerateContainerWidgetForTimeline() ]; TrackArea->AddTrackSlot(InTrack, TrackWidget); } if (ensure(TrackWidget.IsValid())) { Row->AddTrackAreaReference(TrackWidget); } return Row; } TSharedRef SAnimOutliner::GenerateWidgetForColumn(const TSharedRef& InTrack, const FName& ColumnId, const TSharedRef& Row) const { return InTrack->GenerateContainerWidgetForOutliner(Row); } void SAnimOutliner::HandleGetChildren(TSharedRef Item, TArray>& OutChildren) { if(!FilterText.Get().IsEmpty()) { for(const TSharedRef& Child : Item->GetChildren()) { if(!Child->SupportsFiltering() || TextFilter->TestTextFilter(FBasicStringFilterExpressionContext(Child->GetLabel().ToString()))) { OutChildren.Add(Child); } } } else { OutChildren.Append(Item->GetChildren()); } } void SAnimOutliner::HandleExpansionChanged(TSharedRef InTrack, bool bIsExpanded) { InTrack->SetExpanded(bIsExpanded); // Expand any children that are also expanded for (const TSharedRef& Child : InTrack->GetChildren()) { if (Child->IsExpanded()) { SetItemExpansion(Child, true); } } } TSharedPtr SAnimOutliner::HandleContextMenuOpening() { const bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, AnimModel.Pin()->GetCommandList()); AnimModel.Pin()->BuildContextMenu(MenuBuilder); // > 1 because the search widget is always added return MenuBuilder.GetMultiBox()->GetBlocks().Num() > 1 ? MenuBuilder.MakeWidget() : TSharedPtr(); } void SAnimOutliner::HandleTracksChanged() { RequestTreeRefresh(); } void SAnimOutliner::ReportChildRowGeometry(const TSharedRef& InTrack, const FGeometry& InGeometry) { const float ChildOffset = static_cast(TransformPoint( Concatenate( InGeometry.GetAccumulatedLayoutTransform(), GetCachedGeometry().GetAccumulatedLayoutTransform().Inverse() ), FVector2D(0,0) ).Y); const FCachedGeometry* ExistingGeometry = CachedTrackGeometry.Find(InTrack); if(ExistingGeometry == nullptr || (ExistingGeometry->Top != ChildOffset || ExistingGeometry->Height != InGeometry.Size.Y)) { CachedTrackGeometry.Add(InTrack, FCachedGeometry(InTrack, ChildOffset, static_cast(InGeometry.Size.Y))); bPhysicalTracksNeedUpdate = true; } } void SAnimOutliner::OnChildRowRemoved(const TSharedRef& InTrack) { CachedTrackGeometry.Remove(InTrack); bPhysicalTracksNeedUpdate = true; } TOptional SAnimOutliner::GetCachedGeometryForTrack(const TSharedRef& InTrack) const { if (const FCachedGeometry* FoundGeometry = CachedTrackGeometry.Find(InTrack)) { return *FoundGeometry; } return TOptional(); } TOptional SAnimOutliner::ComputeTrackPosition(const TSharedRef& InTrack) const { // Positioning strategy: // Attempt to root out any visible track in the specified track's sub-hierarchy, and compute the track'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 = [&](FAnimTimelineTrack& InTrack) { TOptional ChildRowGeometry = GetCachedGeometryForTrack(InTrack.AsShared()); if (ChildRowGeometry.IsSet()) { Top = ChildRowGeometry->Top; // Stop iterating return false; } NegativeOffset -= InTrack.GetHeight() + InTrack.GetPadding().Combined(); return true; }; InTrack->TraverseVisible_ParentFirst(Iter); if (Top.IsSet()) { return NegativeOffset + Top.GetValue(); } return Top; } void SAnimOutliner::ScrollByDelta(float DeltaInSlateUnits) { ScrollBy(GetCachedGeometry(), DeltaInSlateUnits, EAllowOverscroll::No); } void SAnimOutliner::Private_SetItemSelection( TSharedRef TheItem, bool bShouldBeSelected, bool bWasUserDirected ) { if(TheItem->SupportsSelection()) { AnimModel.Pin()->SetTrackSelected(TheItem, bShouldBeSelected); STreeView::Private_SetItemSelection(TheItem, bShouldBeSelected, bWasUserDirected); } } void SAnimOutliner::Private_ClearSelection() { AnimModel.Pin()->ClearTrackSelection(); STreeView::Private_ClearSelection(); } void SAnimOutliner::Private_SelectRangeFromCurrentTo( TSharedRef InRangeSelectionEnd ) { STreeView::Private_SelectRangeFromCurrentTo(InRangeSelectionEnd); for(TSet>::TIterator Iter = SelectedItems.CreateIterator(); Iter; ++Iter) { if(!(*Iter)->SupportsSelection()) { Iter.RemoveCurrent(); } } for(const TSharedRef& SelectedItem : SelectedItems) { AnimModel.Pin()->SetTrackSelected(SelectedItem, true); } } void SAnimOutliner::RefreshFilter() { TextFilter->SetFilterText(FilterText.Get()); RequestTreeRefresh(); } #undef LOCTEXT_NAMESPACE