// Copyright Epic Games, Inc. All Rights Reserved. #include "TrackModelLayoutBuilder.h" #include "Channels/MovieSceneChannelEditorData.h" #include "Channels/MovieSceneChannelHandle.h" #include "Channels/MovieSceneChannelProxy.h" #include "Containers/UnrealString.h" #include "HAL/Platform.h" #include "ISequencerSection.h" #include "MVVM/Extensions/IOutlinerExtension.h" #include "MVVM/Extensions/IRecyclableExtension.h" #include "MVVM/ViewModels/CategoryModel.h" #include "MVVM/ViewModels/ChannelModel.h" #include "MVVM/ViewModels/SectionModel.h" #include "MVVM/ViewModels/TrackModel.h" #include "MVVM/ViewModels/ViewModel.h" #include "MVVM/ViewModels/ViewModelIterators.h" #include "Misc/AssertionMacros.h" #include "Misc/Optional.h" #include "SequencerCoreFwd.h" class FText; namespace UE { namespace Sequencer { FHierarchicalModelListRefresher::FListData::FListData(TSharedPtr InParent, const FViewModelChildren& InExistingChildren) : Parent(InParent) , Children(InExistingChildren) { } FHierarchicalModelListRefresher::FHierarchicalModelListRefresher() { } FHierarchicalModelListRefresher::FHierarchicalModelListRefresher(TSharedPtr InRoot, FViewModelChildren InExistingChildren) { // Add a recycled child list to the model. This will get cleaned up when // this FHierarchicalModelListRefresher instance is destroyed ConditionalRecycleChildren(InRoot, InExistingChildren); ListData.Add(FListData(InRoot, InExistingChildren)); } void FHierarchicalModelListRefresher::ConditionalRecycleChildren(const TSharedPtr& InModel, FViewModelChildren InExistingChildren) { TOptional RecycledChildren = InModel->FindChildList(EViewModelListType::Recycled); if (!RecycledChildren) { if (RecycledLists.Num() == 0 || RecycledLists.Last().GetSlack() == 0) { RecycledLists.Emplace(); RecycledLists.Last().Reserve(16); } RecycledLists.Last().Emplace(InModel, EViewModelListType::Recycled); RecycledChildren = InModel->GetChildList(EViewModelListType::Recycled); check(RecycledChildren.IsSet()); InExistingChildren.MoveChildrenTo(RecycledChildren.GetValue(), IRecyclableExtension::CallOnRecycle); } } bool FHierarchicalModelListRefresher::IsValid() const { return ListData.Num() > 0; } void FHierarchicalModelListRefresher::Reset() { check(ListData.Num() >= 1); ListData.RemoveAt(1, ListData.Num() - 1, EAllowShrinking::No); FListData& Last = ListData.Last(); Last.AttachTail = nullptr; } void FHierarchicalModelListRefresher::Link(TSharedPtr Item) { FListData& Last = ListData.Last(); if (Item != Last.AttachTail) { // Attach this item to the attach tail, or the head of the list Last.Children.InsertChild(Item, Last.AttachTail); Last.AttachTail = Item; } } void FHierarchicalModelListRefresher::RecurseInto(TSharedPtr Item, FViewModelChildren InExistingChildren) { // This item may already have recycled children if we're refreshing a track // or row that has multiple sections with different channel layouts. // We ensure that children remain eligible for recycling for the duration // of all sections by adding a temporary recycle list to the model while // the layout is being refreshed, but we create this recycle list only on the // first try to not recycle "legitimate" children we added in the previous // loop. ConditionalRecycleChildren(Item, InExistingChildren); ListData.Add(FListData(Item, InExistingChildren)); } TSharedPtr FHierarchicalModelListRefresher::GetCurrentParent() const { if (ensure(ListData.Num() >= 1)) { return ListData.Last().Parent; } return nullptr; } EViewModelListType FHierarchicalModelListRefresher::GetCurrentType() const { if (ensure(ListData.Num() >= 1)) { return ListData.Last().Children.GetType(); } return EViewModelListType::Invalid; } void FHierarchicalModelListRefresher::Pop() { if (ensure(ListData.Num() > 1)) { ListData.Pop(); } } FTrackModelLayoutBuilder::FTrackModelLayoutBuilder(TSharedPtr InSharedTreeRoot) : Root(InSharedTreeRoot) , OutlinerList(InSharedTreeRoot, InSharedTreeRoot->GetChildList(EViewModelListType::Outliner)) , Section(nullptr) { } FTrackModelLayoutBuilder::~FTrackModelLayoutBuilder() { // Recompute shared sizing for (TSharedPtr CompoundItem : Root->GetDescendantsOfType()) { CompoundItem->RecomputeSizing(); } for (TSharedPtr OutlinerChannelGroup : Root->GetDescendantsOfType()) { OutlinerChannelGroup->OnUpdated(); } } void FTrackModelLayoutBuilder::RefreshLayout(TSharedPtr InSection) { InSection->SetLinkedOutlinerItem(CastViewModelChecked(Root)); if (UMovieSceneSection* SectionObject = InSection->GetSection()) { InSection->SetLayoutChannelProxy(SectionObject->GetChannelProxy().AsWeak()); InSection->SetLayoutRowIndex(SectionObject->GetRowIndex()); } else { InSection->SetLayoutChannelProxy(nullptr); InSection->SetLayoutRowIndex(-1); } // Reset everything OutlinerList.Reset(); // Start off with the track area list adding to the top-level-channel list TrackAreaList = FHierarchicalModelListRefresher(InSection, InSection->GetChildList(FTrackModel::GetTopLevelChannelType())); Section = InSection; Section->GetSectionInterface()->GenerateSectionLayout(*this); Section = nullptr; TrackAreaList = FHierarchicalModelListRefresher(); } void FTrackModelLayoutBuilder::PushCategory(FName CategoryName, const FText& DisplayLabel, FGetMovieSceneTooltipText GetGroupTooltipTextDelegate, TFunction(FName, const FText&)> OptionalFactory) { check(TrackAreaList.IsValid() && Section); auto CategoryNamePredicate = [CategoryName](const auto& InModel){ return InModel.GetCategoryName() == CategoryName; }; // ------------------------------------ // Add a category group to the outliner // Note: the outliner can comprise the categories and channels of multile sections, potentially with different combinations // of channels. We have to be careful to ensure that we reuse existing items that may have been added to the outliner // from a previously built section. TSharedPtr OutlinerModel = OutlinerList.FindItem(CategoryNamePredicate); if (!OutlinerModel) { OutlinerModel = MakeShared(CategoryName, DisplayLabel, GetGroupTooltipTextDelegate); } OutlinerList.Link(OutlinerModel); OutlinerList.RecurseInto(OutlinerModel, OutlinerModel->GetChildList(EViewModelListType::Outliner)); // ------------------------------------------ // Add the actual category to the track model TSharedPtr TrackAreaModel = TrackAreaList.FindItem(CategoryNamePredicate); if (!TrackAreaModel) { if (OptionalFactory) { TrackAreaModel = OptionalFactory(CategoryName, DisplayLabel); } if (!TrackAreaModel) { TrackAreaModel = MakeShared(CategoryName); } } OutlinerModel->AddCategory(TrackAreaModel); TrackAreaModel->SetLinkedOutlinerItem(OutlinerModel); TrackAreaList.Link(TrackAreaModel); TrackAreaList.RecurseInto(TrackAreaModel, TrackAreaModel->GetChildList(EViewModelListType::Generic)); } void FTrackModelLayoutBuilder::PopCategory() { check(TrackAreaList.IsValid() && Section); TrackAreaList.Pop(); OutlinerList.Pop(); } void FTrackModelLayoutBuilder::SetTopLevelChannel(const FMovieSceneChannelHandle& Channel, TFunction(FName, const FSectionModel&, const FMovieSceneChannelHandle&)> OptionalFactory) { ensureAlwaysMsgf( OutlinerList.GetCurrentParent() == Root, TEXT("Attempting to assign a top level channel when a category node is active. Top level key nodes will always be added to the outermost track node.")); FViewModelChildren TopLevelGroup = Root->GetChildList(FTrackModel::GetTopLevelChannelGroupType()); TSharedPtr CurrentModel = OutlinerList.GetCurrentParent(); OutlinerList.RecurseInto(CurrentModel, TopLevelGroup); AddChannel(Channel, true, OptionalFactory); OutlinerList.Pop(); } void FTrackModelLayoutBuilder::AddChannel(const FMovieSceneChannelHandle& Channel, TFunction(FName, const FSectionModel&, const FMovieSceneChannelHandle&)> OptionalFactory) { check(TrackAreaList.IsValid() && Section); // Since we always start off adding to the top level channel, point the track area list at the generic child list now if (TrackAreaList.GetCurrentType() == FTrackModel::GetTopLevelChannelType()) { TSharedPtr CurrentModel = TrackAreaList.GetCurrentParent(); TrackAreaList.RecurseInto(CurrentModel, CurrentModel->GetChildList(EViewModelListType::Generic)); } AddChannel(Channel, false, OptionalFactory); } void FTrackModelLayoutBuilder::AddChannel(const FMovieSceneChannelHandle& Channel, bool bIsTopLevel, TFunction(FName, const FSectionModel&, const FMovieSceneChannelHandle&)> OptionalFactory) { check(TrackAreaList.IsValid() && Section); // @todo: this is all pretty crusty - we're currently linear-searching for both the child node, and the IKeyArea within that node // Performance is generally acceptible however since we are dealing with small numbers of children, but this may need to be revisited. const FMovieSceneChannelMetaData* MetaData = Channel.GetMetaData(); if (!ensureAlwaysMsgf(MetaData, TEXT("Attempting to add an expired channel handle to the node tree"))) { return; } FName ChannelName = MetaData->Name; auto ChannelNamePredicate = [ChannelName](const auto& InModel){ return InModel.GetChannelName() == ChannelName; }; // ----------------------------------- // Add a channel group to the outliner // Note: the outliner can comprise the channels of multile sections, potentially with different combinations // of channels. We have to be careful to ensure that we reuse existing items that may have been added // to the outliner from a previously built section. TViewModelPtr OutlinerModel; if (bIsTopLevel) { OutlinerModel = OutlinerList.FindItem(ChannelNamePredicate); } else { OutlinerModel = OutlinerList.FindItem(ChannelNamePredicate); } if (!OutlinerModel) { if (bIsTopLevel) { OutlinerModel = MakeShared(ChannelName, MetaData->DisplayText, MetaData->GetTooltipTextDelegate); } else { OutlinerModel = MakeShared(ChannelName, MetaData->DisplayText, MetaData->GetTooltipTextDelegate); } } OutlinerList.Link(OutlinerModel); // ---------------------------------------- // Add the channel itself to the track area // Note: The track area model must only ever add a single category or channel for each unique name. // Therefore we assert on the presence of an existing category in the track area to ensure that we highlight // bad FTrackModelLayoutBuilder logic or data that specifies duplicate names TSharedPtr TrackAreaModel = TrackAreaList.FindExistingItem(ChannelNamePredicate); if (ensureAlwaysMsgf(TrackAreaModel == nullptr, TEXT("Channel with identifier '%s' has already been added to the list for this section which is not allowed. Please give each channel a unique identifier."), *ChannelName.ToString())) { TrackAreaModel = TrackAreaList.FindRecycledItem(ChannelNamePredicate); if (!TrackAreaModel) { if (OptionalFactory) { TrackAreaModel = OptionalFactory(ChannelName, *Section, Channel); } if (!TrackAreaModel) { TrackAreaModel = MakeShared(ChannelName, Section->GetSectionInterface(), Channel); } } OutlinerModel->AddChannel(TrackAreaModel); if (bIsTopLevel) { TrackAreaModel->SetLinkedOutlinerItem(OutlinerModel->GetParent().ImplicitCastChecked()); } else { TrackAreaModel->SetLinkedOutlinerItem(OutlinerModel.ImplicitCastChecked()); } TrackAreaModel->Initialize(Section->GetSectionInterface(), Channel); TrackAreaList.Link(TrackAreaModel); } } } // namespace Sequencer } // namespace UE