You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
Copying //Tasks/UE5/Dev-SequencerMVVM2 to Main (//UE5/Main) @20364093 #preflight 628866dfb94f739b152c1e29 #preflight 628866e4585e8f793ee80943 #rb ludovic.chabant, andrew.rodham #fyi ludovic.chabant, andrew.rodham, andrew.porter #jira UE-105322 [CL 20364493 by Max Chen in ue5-main branch]
1109 lines
34 KiB
C++
1109 lines
34 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "SequencerNodeTree.h"
|
|
#include "MVVM/ViewModels/ViewModelHierarchy.h"
|
|
#include "MovieSceneBinding.h"
|
|
#include "GameFramework/Actor.h"
|
|
#include "MovieScene.h"
|
|
#include "MVVM/ViewModels/ViewModel.h"
|
|
#include "MVVM/Extensions/IOutlinerExtension.h"
|
|
#include "MVVM/Extensions/ICurveEditorTreeItemExtension.h"
|
|
#include "MVVM/Extensions/IPinnableExtension.h"
|
|
#include "MVVM/Extensions/ISoloableExtension.h"
|
|
#include "MVVM/Extensions/IMutableExtension.h"
|
|
#include "MVVM/ViewModels/SequencerEditorViewModel.h"
|
|
#include "MVVM/ViewModels/OutlinerViewModel.h"
|
|
#include "MVVM/ViewModels/CategoryModel.h"
|
|
#include "MVVM/ViewModels/FolderModel.h"
|
|
#include "MVVM/ViewModels/ViewModelIterators.h"
|
|
#include "MVVM/ViewModels/ObjectBindingModel.h"
|
|
#include "MVVM/ViewModels/OutlinerSpacer.h"
|
|
#include "MVVM/ViewModels/SectionModel.h"
|
|
#include "MVVM/ViewModels/SequenceModel.h"
|
|
#include "MVVM/ViewModels/TrackModel.h"
|
|
#include "MVVM/CurveEditorIntegrationExtension.h"
|
|
#include "MVVM/ObjectBindingModelStorageExtension.h"
|
|
#include "MVVM/SectionModelStorageExtension.h"
|
|
#include "MVVM/TrackModelStorageExtension.h"
|
|
#include "IKeyArea.h"
|
|
#include "ISequencerSection.h"
|
|
#include "MovieSceneSequence.h"
|
|
#include "Tracks/MovieSceneCinematicShotTrack.h"
|
|
#include "Tracks/MovieSceneSubTrack.h"
|
|
#include "Sequencer.h"
|
|
#include "MovieSceneFolder.h"
|
|
#include "ISequencerTrackEditor.h"
|
|
#include "Widgets/Views/STableRow.h"
|
|
#include "CurveEditor.h"
|
|
#include "SequencerTrackFilters.h"
|
|
#include "Channels/MovieSceneChannel.h"
|
|
#include "ScopedTransaction.h"
|
|
#include "SequencerUtilities.h"
|
|
#include "SequencerLog.h"
|
|
#include "SequencerCommonHelpers.h"
|
|
|
|
FSequencerNodeTree::~FSequencerNodeTree()
|
|
{
|
|
if (TrackFilters.IsValid())
|
|
{
|
|
TrackFilters->OnChanged().RemoveAll(this);
|
|
}
|
|
if (TrackFilterLevelFilter.IsValid())
|
|
{
|
|
TrackFilterLevelFilter->OnChanged().RemoveAll(this);
|
|
}
|
|
}
|
|
|
|
FSequencerNodeTree::FSequencerNodeTree(FSequencer& InSequencer)
|
|
: Sequencer(InSequencer)
|
|
, bFilterUpdateRequested(false)
|
|
{
|
|
TrackFilters = MakeShared<FSequencerTrackFilterCollection>();
|
|
TrackFilters->OnChanged().AddRaw(this, &FSequencerNodeTree::RequestFilterUpdate);
|
|
TrackFilterLevelFilter = MakeShared< FSequencerTrackFilter_LevelFilter>();
|
|
TrackFilterLevelFilter->OnChanged().AddRaw(this, &FSequencerNodeTree::RequestFilterUpdate);
|
|
}
|
|
|
|
TSharedPtr<UE::Sequencer::FObjectBindingModel> FSequencerNodeTree::FindObjectBindingNode(const FGuid& BindingID) const
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
const FObjectBindingModelStorageExtension* ObjectBindingStorage = RootNode->CastDynamic<FObjectBindingModelStorageExtension>();
|
|
if (ObjectBindingStorage)
|
|
{
|
|
TSharedPtr<FObjectBindingModel> Model = ObjectBindingStorage->FindModelForObjectBinding(BindingID);
|
|
return Model;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
bool FSequencerNodeTree::HasActiveFilter() const
|
|
{
|
|
return (!FilterString.IsEmpty()
|
|
|| TrackFilters->Num() > 0
|
|
|| TrackFilterLevelFilter->IsActive()
|
|
|| Sequencer.GetSequencerSettings()->GetShowSelectedNodesOnly()
|
|
|| Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene()->GetNodeGroups().HasAnyActiveFilter());
|
|
}
|
|
|
|
bool FSequencerNodeTree::UpdateFiltersOnTrackValueChanged()
|
|
{
|
|
// If filters are already scheduled for update, we can defer until the next update
|
|
if (bFilterUpdateRequested)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (TSharedPtr< FSequencerTrackFilter > TrackFilter : *TrackFilters)
|
|
{
|
|
if (TrackFilter->ShouldUpdateOnTrackValueChanged())
|
|
{
|
|
// UpdateFilters will only run if bFilterUpdateRequested is true
|
|
bFilterUpdateRequested = true;
|
|
bool bFiltersUpdated = UpdateFilters();
|
|
|
|
// If the filter list was modified, set bFilterUpdateRequested to suppress excessive re-filters between tree update
|
|
bFilterUpdateRequested = bFiltersUpdated;
|
|
return bFiltersUpdated;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void FSequencerNodeTree::Update()
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
FViewModelHierarchyOperation UpdateOp(RootNode);
|
|
|
|
FilteredNodes.Empty();
|
|
|
|
if (!ensure(RootNode))
|
|
{
|
|
return;
|
|
}
|
|
|
|
TSharedPtr<FSequenceModel> SequenceModel = RootNode->CastThisShared<FSequenceModel>();
|
|
check(SequenceModel);
|
|
|
|
UMovieSceneSequence* CurrentSequence = SequenceModel->GetSequence();
|
|
check(CurrentSequence);
|
|
|
|
UMovieScene* MovieScene = CurrentSequence->GetMovieScene();
|
|
CleanupMuteSolo(MovieScene);
|
|
|
|
// Re-filter the tree after updating
|
|
// @todo sequencer: Newly added sections may need to be visible even when there is a filter
|
|
FilterNodes(FilterString);
|
|
bFilterUpdateRequested = true;
|
|
UpdateFilters();
|
|
|
|
// Sort all nodes
|
|
const bool bIncludeRootNode = true;
|
|
for (TSharedPtr<ISortableExtension> SortableChild : RootNode->GetDescendantsOfType<ISortableExtension>(bIncludeRootNode))
|
|
{
|
|
SortableChild->SortChildren();
|
|
}
|
|
|
|
// Update all virtual geometries
|
|
// This must happen after the sorting
|
|
IGeometryExtension::UpdateVirtualGeometry(0.f, RootNode);
|
|
|
|
// Cache pinned state of nodes, needs to happen after OnTreeRefreshed
|
|
FPinnableExtensionShim::UpdateCachedPinnedState(RootNode);
|
|
|
|
// Update mute/solo markers and cache final state
|
|
UpdateMuteSolo(MovieScene);
|
|
|
|
// Update curve editor tree based on new filtered hierarchy
|
|
auto CurveEditorIntegration = SequenceModel->CastDynamic<FCurveEditorIntegrationExtension>();
|
|
if (CurveEditorIntegration)
|
|
{
|
|
CurveEditorIntegration->UpdateCurveEditor();
|
|
}
|
|
|
|
OnUpdatedDelegate.Broadcast();
|
|
}
|
|
|
|
UE::Sequencer::TViewModelPtr<UE::Sequencer::IOutlinerExtension> FindNodeWithPath(UE::Sequencer::TViewModelPtr<UE::Sequencer::IOutlinerExtension> InNode, const FString& NodePath)
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
if (!InNode)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
FString HeadPath, TailPath;
|
|
const bool bHasDelimiter = NodePath.Split(".", &HeadPath, &TailPath);
|
|
const FString NodeIdentifier = InNode->GetIdentifier().ToString();
|
|
|
|
if (bHasDelimiter)
|
|
{
|
|
if (NodeIdentifier != HeadPath)
|
|
{
|
|
// The node we're looking for is not in this sub-branch.
|
|
return nullptr;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// NodePath is just a name, so simply check with our node's name.
|
|
return (NodeIdentifier == NodePath) ? InNode : nullptr;
|
|
}
|
|
|
|
check(bHasDelimiter && !TailPath.IsEmpty());
|
|
|
|
for (TViewModelPtr<IOutlinerExtension> Child : InNode.AsModel()->GetChildrenOfType<IOutlinerExtension>())
|
|
{
|
|
TViewModelPtr<IOutlinerExtension> FoundNode = FindNodeWithPath(Child, TailPath);
|
|
if (FoundNode)
|
|
{
|
|
return FoundNode;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
UE::Sequencer::TViewModelPtr<UE::Sequencer::IOutlinerExtension> FSequencerNodeTree::GetNodeAtPath(const FString& NodePath) const
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
for (const TViewModelPtr<IOutlinerExtension>& RootChild : RootNode->GetChildrenOfType<IOutlinerExtension>())
|
|
{
|
|
TViewModelPtr<IOutlinerExtension> NodeAtPath = FindNodeWithPath(RootChild, NodePath);
|
|
if (NodeAtPath)
|
|
{
|
|
return NodeAtPath;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void FSequencerNodeTree::SetRootNode(const UE::Sequencer::FViewModelPtr& InRootNode)
|
|
{
|
|
ensureMsgf(!RootNode, TEXT("Re-assinging the root node is currently an undefined behavior"));
|
|
RootNode = InRootNode;
|
|
}
|
|
|
|
UE::Sequencer::FViewModelPtr FSequencerNodeTree::GetRootNode() const
|
|
{
|
|
return RootNode;
|
|
}
|
|
|
|
TArray<UE::Sequencer::TViewModelPtr<UE::Sequencer::IOutlinerExtension>> FSequencerNodeTree::GetRootNodes() const
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
TArray<TViewModelPtr<IOutlinerExtension>> RootNodes;
|
|
for (const TViewModelPtr<IOutlinerExtension>& Child : RootNode->GetChildrenOfType<IOutlinerExtension>())
|
|
{
|
|
RootNodes.Add(Child);
|
|
}
|
|
return RootNodes;
|
|
}
|
|
|
|
void FSequencerNodeTree::SortAllNodesAndDescendants()
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
const bool bIncludeRootNode = true;
|
|
TArray<ISortableExtension*> SortableChildren;
|
|
for (TSharedPtr<FViewModel> Child : RootNode->GetDescendants(bIncludeRootNode))
|
|
{
|
|
if (ISortableExtension* SortableExtension = Child->CastThis<ISortableExtension>())
|
|
{
|
|
SortableExtension->SetCustomOrder(-1);
|
|
SortableChildren.Add(SortableExtension);
|
|
}
|
|
}
|
|
for (ISortableExtension* SortableChild : SortableChildren)
|
|
{
|
|
SortableChild->SortChildren();
|
|
}
|
|
|
|
// Refresh the tree so that our changes are visible.
|
|
// @todo: Is this necessary any more?
|
|
//GetSequencer().RefreshTree();
|
|
}
|
|
|
|
void FSequencerNodeTree::AddFilter(TSharedPtr<FSequencerTrackFilter> TrackFilter)
|
|
{
|
|
TrackFilters->Add(TrackFilter);
|
|
}
|
|
|
|
int32 FSequencerNodeTree::RemoveFilter(TSharedPtr<FSequencerTrackFilter> TrackFilter)
|
|
{
|
|
return TrackFilters->Remove(TrackFilter);
|
|
}
|
|
|
|
void FSequencerNodeTree::RemoveAllFilters()
|
|
{
|
|
TrackFilters->RemoveAll();
|
|
TrackFilterLevelFilter->ResetFilter();
|
|
}
|
|
|
|
bool FSequencerNodeTree::IsTrackFilterActive(TSharedPtr<FSequencerTrackFilter> TrackFilter) const
|
|
{
|
|
return TrackFilters->Contains(TrackFilter);
|
|
}
|
|
|
|
void FSequencerNodeTree::AddLevelFilter(const FString& LevelName)
|
|
{
|
|
TrackFilterLevelFilter->UnhideLevel(LevelName);
|
|
}
|
|
|
|
void FSequencerNodeTree::RemoveLevelFilter(const FString& LevelName)
|
|
{
|
|
TrackFilterLevelFilter->HideLevel(LevelName);
|
|
}
|
|
|
|
bool FSequencerNodeTree::IsTrackLevelFilterActive(const FString& LevelName) const
|
|
{
|
|
return !TrackFilterLevelFilter->IsLevelHidden(LevelName);
|
|
}
|
|
|
|
bool FSequencerNodeTree::IsNodeSolo(const UE::Sequencer::TViewModelPtr<UE::Sequencer::IOutlinerExtension>& InNode) const
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
const TArray<FString>& SoloNodes = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene()->GetSoloNodes();
|
|
const FString NodePath = IOutlinerExtension::GetPathName(InNode);
|
|
|
|
if (SoloNodes.Contains(NodePath))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Children should follow their parent's behavior unless told otherwise.
|
|
TViewModelPtr<IOutlinerExtension> ParentNode = InNode.AsModel()->FindAncestorOfType<IOutlinerExtension>();
|
|
if (ParentNode)
|
|
{
|
|
return IsNodeSolo(ParentNode);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool FSequencerNodeTree::HasSoloNodes() const
|
|
{
|
|
return bHasSoloNodes;
|
|
}
|
|
|
|
bool FSequencerNodeTree::IsSelectedNodesSolo() const
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
const TSet<TWeakPtr<FViewModel>> SelectedNodes = Sequencer.GetSelection().GetSelectedOutlinerItems();
|
|
|
|
if (SelectedNodes.Num() == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const TArray<FString>& SoloNodes = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene()->GetSoloNodes();
|
|
|
|
bool bIsSolo = true;
|
|
for (const TWeakPtr<FViewModel>& Node : SelectedNodes)
|
|
{
|
|
if (!SoloNodes.Contains(IOutlinerExtension::GetPathName(Node)))
|
|
{
|
|
bIsSolo = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return bIsSolo;
|
|
}
|
|
|
|
void FSequencerNodeTree::ToggleSelectedNodesSolo()
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
UMovieScene* MovieScene = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene();
|
|
|
|
if (MovieScene->IsReadOnly())
|
|
{
|
|
return;
|
|
}
|
|
|
|
TArray<FString>& SoloNodes = MovieScene->GetSoloNodes();
|
|
|
|
const TSet<TWeakPtr<FViewModel>> SelectedNodes = Sequencer.GetSelection().GetSelectedOutlinerItems();
|
|
if (SelectedNodes.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// First, determine if any of the selected nodes are not marked as solo
|
|
// If we have a mix, we should default to setting them all as solo
|
|
bool bIsSolo = true;
|
|
for (const TWeakPtr<const FViewModel> Node : SelectedNodes)
|
|
{
|
|
if (!SoloNodes.Contains(IOutlinerExtension::GetPathName(Node)))
|
|
{
|
|
bIsSolo = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const FScopedTransaction Transaction(NSLOCTEXT("Sequencer", "ToggleSolo", "Toggle Solo"));
|
|
|
|
MovieScene->Modify();
|
|
|
|
for (const TWeakPtr<FViewModel>& Node : SelectedNodes)
|
|
{
|
|
FString NodePath = IOutlinerExtension::GetPathName(Node);
|
|
if (bIsSolo)
|
|
{
|
|
// If we're currently solo, unsolo
|
|
SoloNodes.Remove(NodePath);
|
|
}
|
|
else
|
|
{
|
|
// Mark solo, being careful as we might be re-marking an already solo node
|
|
SoloNodes.AddUnique(NodePath);
|
|
}
|
|
}
|
|
|
|
GetSequencer().RefreshTree();
|
|
}
|
|
|
|
bool FSequencerNodeTree::IsNodeMute(const UE::Sequencer::TViewModelPtr<UE::Sequencer::IOutlinerExtension>& InNode) const
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
const TArray<FString>& MuteNodes = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene()->GetMuteNodes();
|
|
const FString NodePath = IOutlinerExtension::GetPathName(InNode);
|
|
|
|
if (MuteNodes.Contains(NodePath))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Children should follow their parent's behavior unless told otherwise.
|
|
TViewModelPtr<IOutlinerExtension> ParentNode(InNode.AsModel()->GetParent());
|
|
if (ParentNode)
|
|
{
|
|
return IsNodeMute(ParentNode);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool FSequencerNodeTree::IsSelectedNodesMute() const
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
const TSet<TWeakPtr<FViewModel>> SelectedNodes = Sequencer.GetSelection().GetSelectedOutlinerItems();
|
|
|
|
if (SelectedNodes.Num() == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const TArray<FString>& MuteNodes = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene()->GetMuteNodes();
|
|
|
|
bool bIsMute = true;
|
|
for (const TWeakPtr<const FViewModel> Node : SelectedNodes)
|
|
{
|
|
if (!MuteNodes.Contains(IOutlinerExtension::GetPathName(Node)))
|
|
{
|
|
bIsMute = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return bIsMute;
|
|
}
|
|
|
|
void FSequencerNodeTree::ToggleSelectedNodesMute()
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
UMovieScene* MovieScene = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene();
|
|
|
|
if (MovieScene->IsReadOnly())
|
|
{
|
|
return;
|
|
}
|
|
|
|
TArray<FString>& MuteNodes = MovieScene->GetMuteNodes();
|
|
|
|
const TSet<TWeakPtr<FViewModel>> SelectedNodes = Sequencer.GetSelection().GetSelectedOutlinerItems();
|
|
if (SelectedNodes.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// First, determine if any of the selected nodes are not marked as Mute
|
|
// If we have a mix, we should default to setting them all as Mute
|
|
bool bIsMute = true;
|
|
for (const TWeakPtr<const FViewModel> Node : SelectedNodes)
|
|
{
|
|
if (!MuteNodes.Contains(IOutlinerExtension::GetPathName(Node)))
|
|
{
|
|
bIsMute = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const FScopedTransaction Transaction(NSLOCTEXT("Sequencer", "ToggleMute", "Toggle Mute"));
|
|
|
|
MovieScene->Modify();
|
|
|
|
for (const TWeakPtr<FViewModel>& Node : SelectedNodes)
|
|
{
|
|
FString NodePath = IOutlinerExtension::GetPathName(Node);
|
|
if (bIsMute)
|
|
{
|
|
// If we're currently Mute, unMute
|
|
MuteNodes.Remove(NodePath);
|
|
}
|
|
else
|
|
{
|
|
// Mark Mute, being careful as we might be re-marking an already Mute node
|
|
MuteNodes.AddUnique(NodePath);
|
|
}
|
|
}
|
|
|
|
GetSequencer().RefreshTree();
|
|
}
|
|
|
|
void FSequencerNodeTree::SaveExpansionState(const UE::Sequencer::FViewModel& Node, bool bExpanded)
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
// @todo Sequencer - This should be moved to the sequence level
|
|
UMovieScene* MovieScene = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene();
|
|
FMovieSceneEditorData& EditorData = MovieScene->GetEditorData();
|
|
|
|
EditorData.ExpansionStates.Add(IOutlinerExtension::GetPathName(Node), FMovieSceneExpansionState(bExpanded));
|
|
}
|
|
|
|
TOptional<bool> FSequencerNodeTree::GetSavedExpansionState( const FViewModel& Node )
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
UMovieScene* MovieScene = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene();
|
|
FMovieSceneEditorData& EditorData = MovieScene->GetEditorData();
|
|
FMovieSceneExpansionState* ExpansionState = EditorData.ExpansionStates.Find(IOutlinerExtension::GetPathName(Node));
|
|
|
|
return ExpansionState ? ExpansionState->bExpanded : TOptional<bool>();
|
|
}
|
|
|
|
void FSequencerNodeTree::SavePinnedState(const UE::Sequencer::FViewModel& Node, bool bPinned)
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
UMovieScene* MovieScene = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene();
|
|
FMovieSceneEditorData& EditorData = MovieScene->GetEditorData();
|
|
|
|
if (bPinned)
|
|
{
|
|
EditorData.PinnedNodes.AddUnique(IOutlinerExtension::GetPathName(Node));
|
|
}
|
|
else
|
|
{
|
|
EditorData.PinnedNodes.RemoveSingle(IOutlinerExtension::GetPathName(Node));
|
|
}
|
|
}
|
|
|
|
|
|
bool FSequencerNodeTree::GetSavedPinnedState(const UE::Sequencer::FViewModel& Node) const
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
UMovieScene* MovieScene = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene();
|
|
FMovieSceneEditorData& EditorData = MovieScene->GetEditorData();
|
|
bool bPinned = EditorData.PinnedNodes.Contains(IOutlinerExtension::GetPathName(Node));
|
|
|
|
return bPinned;
|
|
}
|
|
|
|
bool FSequencerNodeTree::IsNodeFiltered(const TSharedPtr<UE::Sequencer::FViewModel>& Node) const
|
|
{
|
|
return FilteredNodes.Contains(Node);
|
|
}
|
|
|
|
TSharedPtr<UE::Sequencer::FSectionModel> FSequencerNodeTree::GetSectionModel(const UMovieSceneSection* Section) const
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
FSectionModelStorageExtension* SectionStorage = RootNode->CastThis<FSectionModelStorageExtension>();
|
|
if (ensure(SectionStorage))
|
|
{
|
|
return SectionStorage->FindModelForSection(Section);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
static void AddChildNodes(const UE::Sequencer::TViewModelPtr<UE::Sequencer::IOutlinerExtension>& StartNode, TSet<UE::Sequencer::TWeakViewModelPtr<UE::Sequencer::IOutlinerExtension>>& OutFilteredNodes)
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
for (TViewModelPtr<IOutlinerExtension> ChildNode : StartNode.AsModel()->GetDescendantsOfType<IOutlinerExtension>())
|
|
{
|
|
OutFilteredNodes.Add(ChildNode);
|
|
}
|
|
}
|
|
|
|
static void AddParentNodes(const UE::Sequencer::TViewModelPtr<UE::Sequencer::IOutlinerExtension>& StartNode, TSet<UE::Sequencer::TWeakViewModelPtr<UE::Sequencer::IOutlinerExtension>>& OutFilteredNodes)
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
// Gather parent folders up the chain
|
|
for (TViewModelPtr<IOutlinerExtension> ParentNode : StartNode.AsModel()->GetAncestorsOfType<IOutlinerExtension>())
|
|
{
|
|
OutFilteredNodes.Add(ParentNode);
|
|
}
|
|
}
|
|
|
|
static bool PassesFilterStrings(FSequencer& Sequencer, const UE::Sequencer::TViewModelPtr<UE::Sequencer::IOutlinerExtension>& StartNode, const TArray<FString>& FilterStrings)
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
// If we have a filter string, make sure we match
|
|
if (FilterStrings.Num() > 0)
|
|
{
|
|
TSharedPtr<FOutlinerViewModel> Outliner = Sequencer.GetViewModel()->GetOutliner();
|
|
|
|
if (!Outliner)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FString DisplayLabel = StartNode->GetLabel().ToString();
|
|
|
|
// check each string in the filter strings list against
|
|
for (const FString& String : FilterStrings)
|
|
{
|
|
if (!DisplayLabel.Contains(String))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (TViewModelPtr<IOutlinerExtension> Parent : StartNode.AsModel()->GetAncestorsOfType<IOutlinerExtension>())
|
|
{
|
|
if (!Parent->IsExpanded())
|
|
{
|
|
Parent->SetExpansion(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Recursively filters nodes
|
|
*
|
|
* @param StartNode The node to start from
|
|
* @param Filters The filter collection to test against
|
|
* @param OutFilteredNodes The list of all filtered nodes
|
|
*
|
|
* @return Whether this node passed filtering
|
|
*/
|
|
|
|
static bool FilterNodesRecursive(
|
|
FSequencer& Sequencer
|
|
, const UE::Sequencer::TViewModelPtr<UE::Sequencer::IOutlinerExtension>& StartNode
|
|
, TSharedPtr<FSequencerTrackFilterCollection> Filters, const TArray<FString>& FilterStrings
|
|
, TSharedPtr<FSequencerTrackFilter_LevelFilter> LevelTrackFilter
|
|
, TSet<UE::Sequencer::TWeakViewModelPtr<UE::Sequencer::IOutlinerExtension>>& OutFilteredNodes)
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
bool bAnyChildPassed = false;
|
|
|
|
// Special case: If a parent node matches an active text search filter, the children should also pass the text search filter
|
|
// so we stop filtering them based on text to eliminate special case conflicts with non-object binding nodes
|
|
// with potential child object bindings nodes when only showing selected bindings. This is also faster.
|
|
bool bPassedFilterStrings = PassesFilterStrings(Sequencer, StartNode, FilterStrings);
|
|
const TArray<FString>& ChildFilterStrings = bPassedFilterStrings ? TArray<FString>() : FilterStrings;
|
|
|
|
// Special case: Child nodes should always be processed, as they may force their parents to pass
|
|
for (TViewModelPtr<IOutlinerExtension> Node : StartNode.AsModel()->GetChildrenOfType<IOutlinerExtension>())
|
|
{
|
|
if (FilterNodesRecursive(Sequencer, Node, Filters, ChildFilterStrings, LevelTrackFilter, OutFilteredNodes))
|
|
{
|
|
bAnyChildPassed = true;
|
|
}
|
|
}
|
|
|
|
// After child nodes are processed, if this node didn't pass text filtering, fail it
|
|
if (!bPassedFilterStrings)
|
|
{
|
|
return bAnyChildPassed;
|
|
}
|
|
|
|
// TODO: we can probably completely rewrite this logic by taking advantage of the new modular extensions.
|
|
|
|
bool bPassedAnyFilters = false;
|
|
bool bIsTrackOrObjectBinding = false;
|
|
bool bIsSubTrack = false;
|
|
|
|
TSharedPtr<IPinnableExtension> Pinnable = StartNode.ImplicitCast();
|
|
const bool bIsPinned = Pinnable && Pinnable->IsPinned();
|
|
|
|
if (TSharedPtr<FTrackModel> TrackModel = StartNode.ImplicitCast())
|
|
{
|
|
bIsTrackOrObjectBinding = true;
|
|
|
|
UMovieSceneTrack* Track = TrackModel->GetTrack();
|
|
|
|
// Always show subsequence tracks so that the user can navigate up and down the hierarchy with the filter
|
|
if (Track && Track->IsA<UMovieSceneSubTrack>())
|
|
{
|
|
bPassedAnyFilters = true;
|
|
bIsSubTrack = true;
|
|
goto NextStep;
|
|
}
|
|
|
|
for (TSharedPtr<FChannelGroupModel> ChannelGroupModel : StartNode.AsModel()->GetDescendantsOfType<FChannelGroupModel>())
|
|
{
|
|
for (const TWeakViewModelPtr<FChannelModel>& ChannelModel : ChannelGroupModel->GetChannels())
|
|
{
|
|
const TSharedPtr<IKeyArea>& KeyArea = ChannelModel.Pin()->GetKeyArea();
|
|
FMovieSceneChannel* Channel = KeyArea->ResolveChannel();
|
|
if (Channel)
|
|
{
|
|
if (Filters->Num() == 0 || Filters->PassesAnyFilters(Channel))
|
|
{
|
|
bPassedAnyFilters = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bPassedAnyFilters || Filters->Num() == 0 || Filters->PassesAnyFilters(Track, TrackModel->GetLabel()))
|
|
{
|
|
bPassedAnyFilters = true;
|
|
|
|
// Track nodes do not belong to a level, but might be a child of an objectbinding node that does
|
|
if (LevelTrackFilter->IsActive())
|
|
{
|
|
TSharedPtr<IObjectBindingExtension> ObjectNode = StartNode.AsModel()->FindAncestorOfType<IObjectBindingExtension>();
|
|
if (ObjectNode)
|
|
{
|
|
// The track belongs to an objectbinding node, start by assuming it doesn't match the level filter
|
|
bPassedAnyFilters = false;
|
|
|
|
for (TWeakObjectPtr<>& Object : Sequencer.FindObjectsInCurrentSequence(ObjectNode->GetObjectGuid()))
|
|
{
|
|
if (Object.IsValid() && LevelTrackFilter->PassesFilter(Object.Get()))
|
|
{
|
|
// If at least one of the objects on the objectbinding node pass the level filter, show the track
|
|
bPassedAnyFilters = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bPassedAnyFilters && Sequencer.GetSequencerSettings()->GetShowSelectedNodesOnly())
|
|
{
|
|
TSharedPtr<IObjectBindingExtension> ObjectNode = StartNode.AsModel()->FindAncestorOfType<IObjectBindingExtension>();
|
|
// Always show pinned items
|
|
if (!bIsPinned && ObjectNode)
|
|
{
|
|
const FMovieSceneBinding* Binding = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene()->FindBinding(ObjectNode->GetObjectGuid());
|
|
if (!(Binding && Sequencer.IsBindingVisible(*Binding)))
|
|
{
|
|
return bAnyChildPassed;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (TViewModelPtr<FObjectBindingModel> ObjectNode = StartNode.ImplicitCast())
|
|
{
|
|
bIsTrackOrObjectBinding = true;
|
|
|
|
for (TWeakObjectPtr<>& Object : Sequencer.FindObjectsInCurrentSequence(ObjectNode->GetObjectGuid()))
|
|
{
|
|
if (Object.IsValid() && (Filters->Num() == 0 || Filters->PassesAnyFilters(Object.Get(), StartNode->GetLabel()))
|
|
&& LevelTrackFilter->PassesFilter(Object.Get()))
|
|
{
|
|
bPassedAnyFilters = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (bPassedAnyFilters && Sequencer.GetSequencerSettings()->GetShowSelectedNodesOnly() && !bIsPinned)
|
|
{
|
|
UMovieScene* MovieScene = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene();
|
|
const FMovieSceneBinding* Binding = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene()->FindBinding(ObjectNode->GetObjectGuid());
|
|
if (Binding && !Sequencer.IsBindingVisible(*Binding))
|
|
{
|
|
return bAnyChildPassed;
|
|
}
|
|
}
|
|
}
|
|
else if (TViewModelPtr<FCategoryModel> CategoryModel = StartNode.ImplicitCast())
|
|
{
|
|
if (TSharedPtr<FTrackModel> TrackNode = CategoryModel->FindAncestorOfType<FTrackModel>())
|
|
{
|
|
UMovieSceneTrack* Track = TrackNode->GetTrack();
|
|
if (Filters->Num() == 0 || Filters->PassesAnyFilters(Track, StartNode->GetLabel()))
|
|
{
|
|
bPassedAnyFilters = true;
|
|
}
|
|
}
|
|
if (bPassedAnyFilters && Sequencer.GetSequencerSettings()->GetShowSelectedNodesOnly())
|
|
{
|
|
TSharedPtr<IObjectBindingExtension> ParentObjectNode = StartNode.AsModel()->FindAncestorOfType<IObjectBindingExtension>();
|
|
// Always show pinned items
|
|
if (!bIsPinned && ParentObjectNode)
|
|
{
|
|
const FMovieSceneBinding* Binding = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene()->FindBinding(ParentObjectNode->GetObjectGuid());
|
|
if (!(Binding && Sequencer.IsBindingVisible(*Binding)))
|
|
{
|
|
return bAnyChildPassed;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (TViewModelPtr<FCategoryGroupModel> CategoryGroupModel = StartNode.ImplicitCast())
|
|
{
|
|
if (TSharedPtr<FTrackModel> TrackNode = CategoryGroupModel->FindAncestorOfType<FTrackModel>())
|
|
{
|
|
UMovieSceneTrack* Track = TrackNode->GetTrack();
|
|
if (Filters->Num() == 0 || Filters->PassesAnyFilters(Track, FText::FromName(CategoryGroupModel->GetCategoryName())))
|
|
{
|
|
bPassedAnyFilters = true;
|
|
}
|
|
}
|
|
if (bPassedAnyFilters && Sequencer.GetSequencerSettings()->GetShowSelectedNodesOnly())
|
|
{
|
|
TSharedPtr<IObjectBindingExtension> ParentObjectNode = StartNode.AsModel()->FindAncestorOfType<IObjectBindingExtension>();
|
|
// Always show pinned items
|
|
if (!bIsPinned && ParentObjectNode)
|
|
{
|
|
const FMovieSceneBinding* Binding = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene()->FindBinding(ParentObjectNode->GetObjectGuid());
|
|
if (!(Binding && Sequencer.IsBindingVisible(*Binding)))
|
|
{
|
|
return bAnyChildPassed;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (TViewModelPtr<FChannelGroupModel> ChannelGroupModel = StartNode.ImplicitCast())
|
|
{
|
|
for (const TWeakViewModelPtr<FChannelModel>& ChannelModel : ChannelGroupModel->GetChannels())
|
|
{
|
|
const TSharedPtr<IKeyArea>& KeyArea = ChannelModel.Pin()->GetKeyArea();
|
|
FMovieSceneChannel* Channel = KeyArea->ResolveChannel();
|
|
if (Channel)
|
|
{
|
|
if (Filters->Num() == 0 || Filters->PassesAnyFilters(Channel))
|
|
{
|
|
bPassedAnyFilters = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (bPassedAnyFilters && Sequencer.GetSequencerSettings()->GetShowSelectedNodesOnly())
|
|
{
|
|
TSharedPtr<IObjectBindingExtension> ParentObjectNode = StartNode.AsModel()->FindAncestorOfType<IObjectBindingExtension>();
|
|
// Always show pinned items
|
|
if (!bIsPinned && ParentObjectNode)
|
|
{
|
|
const FMovieSceneBinding* Binding = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene()->FindBinding(ParentObjectNode->GetObjectGuid());
|
|
if (!(Binding && Sequencer.IsBindingVisible(*Binding)))
|
|
{
|
|
bPassedAnyFilters = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (TViewModelPtr<FFolderModel> FolderModel = StartNode.ImplicitCast())
|
|
{
|
|
// Special case: If we're pinned, then we should pass regardless
|
|
if (bIsPinned)
|
|
{
|
|
bPassedAnyFilters = true;
|
|
}
|
|
|
|
// Special case: If we're only filtering on text search, include folders and key areas in the search
|
|
if (!bPassedAnyFilters && Filters->Num() == 0 && FilterStrings.Num() > 0)
|
|
{
|
|
bPassedAnyFilters = true;
|
|
|
|
// Special case: but don't include if only showing selected bindings and we don't have child that passed
|
|
if (Sequencer.GetSequencerSettings()->GetShowSelectedNodesOnly() && !bAnyChildPassed)
|
|
{
|
|
bPassedAnyFilters = false;
|
|
|
|
// Special case: unless we're the child of a node that is a selected binding
|
|
TSharedPtr<IObjectBindingExtension> ParentObjectNode = StartNode.AsModel()->FindAncestorOfType<IObjectBindingExtension>();
|
|
// Always show pinned items
|
|
if (ParentObjectNode)
|
|
{
|
|
const FMovieSceneBinding* Binding = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene()->FindBinding(ParentObjectNode->GetObjectGuid());
|
|
if (Binding && Sequencer.IsBindingVisible(*Binding))
|
|
{
|
|
bPassedAnyFilters = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
NextStep:
|
|
|
|
if (bPassedAnyFilters && !bIsSubTrack)
|
|
{
|
|
// If filtering on selection set is enabled, we need to run another pass to verify we're in an enabled node group
|
|
UMovieSceneNodeGroupCollection& NodeGroups = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene()->GetNodeGroups();
|
|
if (NodeGroups.HasAnyActiveFilter())
|
|
{
|
|
bPassedAnyFilters = false;
|
|
|
|
constexpr bool bIncludeThis = true;
|
|
for (TViewModelPtr<IOutlinerExtension> Parent : StartNode.AsModel()->GetAncestorsOfType<IOutlinerExtension>(bIncludeThis))
|
|
{
|
|
// Special case: Pinned tracks should be visible whether in the node group or not
|
|
if (bIsPinned)
|
|
{
|
|
bPassedAnyFilters = true;
|
|
break;
|
|
}
|
|
|
|
for (const UMovieSceneNodeGroup* NodeGroup : NodeGroups)
|
|
{
|
|
if (NodeGroup->GetEnableFilter() && NodeGroup->ContainsNode(IOutlinerExtension::GetPathName(Parent.AsModel())))
|
|
{
|
|
bPassedAnyFilters = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bPassedAnyFilters)
|
|
{
|
|
OutFilteredNodes.Add(StartNode);
|
|
AddParentNodes(StartNode, OutFilteredNodes);
|
|
|
|
// Special case: When only showing selected bindings, and a non-object passes text filtering
|
|
// don't add it's children, as they may be a binding that is not seleceted. Selected child nodes will add themselves.
|
|
if (!(Sequencer.GetSequencerSettings()->GetShowSelectedNodesOnly() && FilterStrings.Num() > 0 && !bIsTrackOrObjectBinding))
|
|
{
|
|
AddChildNodes(StartNode, OutFilteredNodes);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return bAnyChildPassed;
|
|
}
|
|
|
|
bool FSequencerNodeTree::UpdateFilters()
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
if (!bFilterUpdateRequested)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
TSet<TWeakViewModelPtr<IOutlinerExtension>> PreviousFilteredNodes(FilteredNodes);
|
|
|
|
FilteredNodes.Empty();
|
|
const bool bHasActiveFilter = HasActiveFilter();
|
|
|
|
UObject* PlaybackContext = Sequencer.GetPlaybackContext();
|
|
UWorld* World = PlaybackContext ? PlaybackContext->GetWorld() : nullptr;
|
|
TrackFilterLevelFilter->UpdateWorld(World);
|
|
|
|
if (bHasActiveFilter)
|
|
{
|
|
// Build a list of strings that must be matched
|
|
TArray<FString> FilterStrings;
|
|
|
|
// Remove whitespace from the front and back of the string
|
|
FilterString.TrimStartAndEndInline();
|
|
FilterString.ParseIntoArray(FilterStrings, TEXT(" "), true /*bCullEmpty*/);
|
|
|
|
for (const TViewModelPtr<IOutlinerExtension>& Node : GetRootNodes())
|
|
{
|
|
// Recursively filter all nodes, matching them against the list of filter strings. All filter strings must be matched
|
|
FilterNodesRecursive(Sequencer, Node, TrackFilters, FilterStrings, TrackFilterLevelFilter, FilteredNodes);
|
|
}
|
|
}
|
|
|
|
bFilteringOnNodeGroups = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene()->GetNodeGroups().HasAnyActiveFilter();
|
|
bFilterUpdateRequested = false;
|
|
|
|
// Always include the bottom spacer
|
|
if (FSequenceModel* SequenceModel = RootNode->CastThis<FSequenceModel>())
|
|
{
|
|
FilteredNodes.Add(SequenceModel->GetBottomSpacer());
|
|
}
|
|
|
|
// Count the total number of display nodes, and update filtered state.
|
|
DisplayNodeCount = 0;
|
|
for (const TViewModelPtr<IOutlinerExtension>& Item : RootNode->GetDescendantsOfType<IOutlinerExtension>())
|
|
{
|
|
const bool bIsNodeFilteredIn = !bHasActiveFilter || FilteredNodes.Contains(Item);
|
|
Item->SetFilteredOut(!bIsNodeFilteredIn);
|
|
|
|
if (FOutlinerItemModel* OutlinerItemModel = Item.AsModel()->CastThis<FOutlinerItemModel>())
|
|
{
|
|
if (OutlinerItemModel->IsForceFilteredOut())
|
|
{
|
|
// Skip things that are force-filtered-out because we don't want to see or count them
|
|
// in the outliner hierarchy.
|
|
continue;
|
|
}
|
|
}
|
|
|
|
++DisplayNodeCount;
|
|
}
|
|
|
|
// Return whether the new list of FilteredNodes is different than the previous list
|
|
return (PreviousFilteredNodes.Num() != FilteredNodes.Num() || !PreviousFilteredNodes.Includes(FilteredNodes));
|
|
}
|
|
|
|
void FSequencerNodeTree::CleanupMuteSolo(UMovieScene* MovieScene)
|
|
{
|
|
// Remove mute/solo markers for any nodes that no longer exist
|
|
if (!MovieScene->IsReadOnly())
|
|
{
|
|
for (auto It = MovieScene->GetSoloNodes().CreateIterator(); It; ++It)
|
|
{
|
|
if (!GetNodeAtPath(*It))
|
|
{
|
|
It.RemoveCurrent();
|
|
}
|
|
}
|
|
|
|
for (auto It = MovieScene->GetMuteNodes().CreateIterator(); It; ++It)
|
|
{
|
|
if (!GetNodeAtPath(*It))
|
|
{
|
|
It.RemoveCurrent();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FSequencerNodeTree::UpdateMuteSolo(UMovieScene* MovieScene)
|
|
{
|
|
// Muting overrides soloing, so a solo node only counts if it's not muted.
|
|
bHasSoloNodes = false;
|
|
TArray<FString>& SoloNodes = Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene()->GetSoloNodes();
|
|
for (const FString& NodePath : SoloNodes)
|
|
{
|
|
UE::Sequencer::TViewModelPtr<UE::Sequencer::IOutlinerExtension> Node = GetNodeAtPath(NodePath);
|
|
if (Node)
|
|
{
|
|
if (!IsNodeMute(Node))
|
|
{
|
|
bHasSoloNodes = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int32 FSequencerNodeTree::GetTotalDisplayNodeCount() const
|
|
{
|
|
// Subtract 1 for the spacer node which is always added
|
|
return DisplayNodeCount - 1;
|
|
}
|
|
|
|
int32 FSequencerNodeTree::GetFilteredDisplayNodeCount() const
|
|
{
|
|
// Subtract 1 for the spacer node which is always added
|
|
return FilteredNodes.Num() - 1;
|
|
}
|
|
|
|
void FSequencerNodeTree::FilterNodes(const FString& InFilter)
|
|
{
|
|
if (InFilter != FilterString)
|
|
{
|
|
FilterString = InFilter;
|
|
bFilterUpdateRequested = true;
|
|
}
|
|
}
|
|
|
|
void FSequencerNodeTree::NodeGroupsCollectionChanged()
|
|
{
|
|
if (Sequencer.GetFocusedMovieSceneSequence()->GetMovieScene()->GetNodeGroups().HasAnyActiveFilter() || bFilteringOnNodeGroups)
|
|
{
|
|
RequestFilterUpdate();
|
|
}
|
|
}
|
|
|
|
void FSequencerNodeTree::GetAllNodes(TArray<TSharedPtr<UE::Sequencer::FViewModel>>& OutNodes) const
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
const bool bIncludeRootNode = false;
|
|
for (const FViewModelPtr& It : RootNode->GetDescendants(bIncludeRootNode))
|
|
{
|
|
OutNodes.Add(It.AsModel());
|
|
}
|
|
}
|
|
|
|
void FSequencerNodeTree::GetAllNodes(TArray<TSharedRef<UE::Sequencer::FViewModel>>& OutNodes) const
|
|
{
|
|
using namespace UE::Sequencer;
|
|
|
|
const bool bIncludeRootNode = false;
|
|
for (const FViewModelPtr& It : RootNode->GetDescendants(bIncludeRootNode))
|
|
{
|
|
OutNodes.Add(It.AsModel().ToSharedRef());
|
|
}
|
|
}
|
|
|