// Copyright Epic Games, Inc. All Rights Reserved. #include "SequencerUtilities.h" #include "Misc/Attribute.h" #include "Misc/Paths.h" #include "Layout/Margin.h" #include "Fonts/SlateFontInfo.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Images/SImage.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/Input/SButton.h" #include "Styling/CoreStyle.h" #include "Styling/AppStyle.h" #include "MovieSceneTrack.h" #include "MovieSceneSection.h" #include "MovieSceneTimeHelpers.h" #include "MovieSceneFolder.h" #include "Compilation/MovieSceneCompiledDataManager.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "ISequencerTrackEditor.h" #include "ISequencer.h" #include "Sequencer.h" #include "SequencerNodeTree.h" #include "ScopedTransaction.h" #include "UObject/Package.h" #include "AssetRegistry/AssetRegistryModule.h" #include "FileHelpers.h" #include "LevelSequence.h" #include "MVVM/Extensions/IObjectBindingExtension.h" #include "MVVM/ViewModels/TrackModel.h" #include "MVVM/Views/ViewUtilities.h" #include "Framework/Notifications/NotificationManager.h" #include "Widgets/Notifications/SNotificationList.h" PRAGMA_DISABLE_OPTIMIZATION #define LOCTEXT_NAMESPACE "FSequencerUtilities" static EVisibility GetRolloverVisibility(TAttribute HoverState, TWeakPtr WeakButton) { TSharedPtr Button = WeakButton.Pin(); if (HoverState.Get()) { return EVisibility::SelfHitTestInvisible; } else { return EVisibility::Collapsed; } } TSharedRef FSequencerUtilities::MakeAddButton(FText HoverText, FOnGetContent MenuContent, const TAttribute& HoverState, TWeakPtr InSequencer) { TAttribute IsEnabled = MakeAttributeLambda([InSequencer]() -> bool { return InSequencer.IsValid() ? !InSequencer.Pin()->IsReadOnly() : false; }); return UE::Sequencer::MakeAddButton(HoverText, MenuContent, HoverState, IsEnabled); } TSharedRef FSequencerUtilities::MakeAddButton(FText HoverText, FOnClicked OnClicked, const TAttribute& HoverState, TWeakPtr InSequencer) { FSlateFontInfo SmallLayoutFont = FCoreStyle::GetDefaultFontStyle("Regular", 8); TSharedRef ButtonText = SNew(STextBlock) .Text(HoverText) .Font(SmallLayoutFont) .ColorAndOpacity(FSlateColor::UseForeground()); TSharedRef Button = SNew(SButton) .IsFocusable(true) .ButtonStyle(FAppStyle::Get(), "HoverHintOnly") .ForegroundColor(FSlateColor::UseForeground()) .IsEnabled_Lambda([=]() { return InSequencer.IsValid() ? !InSequencer.Pin()->IsReadOnly() : false; }) .OnClicked(OnClicked) .ContentPadding(FMargin(5, 2)) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .Content() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(FMargin(0, 0, 2, 0)) [ SNew(SImage) .ColorAndOpacity(FSlateColor::UseForeground()) .Image(FAppStyle::GetBrush("Plus")) ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() [ ButtonText ] ]; TAttribute Visibility = TAttribute::Create(TAttribute::FGetter::CreateStatic(GetRolloverVisibility, HoverState, TWeakPtr(Button))); ButtonText->SetVisibility(Visibility); return Button; } void FSequencerUtilities::CreateNewSection(UMovieSceneTrack* InTrack, TWeakPtr InSequencer, int32 InRowIndex, EMovieSceneBlendType InBlendType) { TSharedPtr Sequencer = InSequencer.Pin(); if (!Sequencer.IsValid()) { return; } FQualifiedFrameTime CurrentTime = Sequencer->GetLocalTime(); FFrameNumber PlaybackEnd = UE::MovieScene::DiscreteExclusiveUpper(Sequencer->GetFocusedMovieSceneSequence()->GetMovieScene()->GetPlaybackRange()); FScopedTransaction Transaction(LOCTEXT("AddSectionTransactionText", "Add Section")); if (UMovieSceneSection* NewSection = InTrack->CreateNewSection()) { int32 OverlapPriority = 0; for (UMovieSceneSection* Section : InTrack->GetAllSections()) { OverlapPriority = FMath::Max(Section->GetOverlapPriority() + 1, OverlapPriority); // Move existing sections on the same row or beyond so that they don't overlap with the new section if (Section != NewSection && Section->GetRowIndex() >= InRowIndex) { Section->SetRowIndex(Section->GetRowIndex() + 1); } } InTrack->Modify(); NewSection->SetRange(TRange(CurrentTime.Time.FrameNumber, PlaybackEnd)); NewSection->SetOverlapPriority(OverlapPriority); NewSection->SetRowIndex(InRowIndex); NewSection->SetBlendType(InBlendType); InTrack->AddSection(*NewSection); InTrack->UpdateEasing(); Sequencer->NotifyMovieSceneDataChanged(EMovieSceneDataChangeType::MovieSceneStructureItemAdded); Sequencer->EmptySelection(); Sequencer->SelectSection(NewSection); Sequencer->ThrobSectionSelection(); } else { Transaction.Cancel(); } } void FSequencerUtilities::PopulateMenu_CreateNewSection(FMenuBuilder& MenuBuilder, int32 RowIndex, UMovieSceneTrack* Track, TWeakPtr InSequencer) { if (!Track) { return; } auto CreateNewSection = [Track, InSequencer, RowIndex](EMovieSceneBlendType BlendType) { TSharedPtr Sequencer = InSequencer.Pin(); if (!Sequencer.IsValid()) { return; } FQualifiedFrameTime CurrentTime = Sequencer->GetLocalTime(); FFrameNumber PlaybackEnd = UE::MovieScene::DiscreteExclusiveUpper(Sequencer->GetFocusedMovieSceneSequence()->GetMovieScene()->GetPlaybackRange()); FScopedTransaction Transaction(LOCTEXT("AddSectionTransactionText", "Add Section")); if (UMovieSceneSection* NewSection = Track->CreateNewSection()) { int32 OverlapPriority = 0; for (UMovieSceneSection* Section : Track->GetAllSections()) { OverlapPriority = FMath::Max(Section->GetOverlapPriority() + 1, OverlapPriority); // Move existing sections on the same row or beyond so that they don't overlap with the new section if (Section != NewSection && Section->GetRowIndex() >= RowIndex) { Section->SetRowIndex(Section->GetRowIndex() + 1); } } Track->Modify(); NewSection->SetRange(TRange(CurrentTime.Time.FrameNumber, PlaybackEnd)); NewSection->SetOverlapPriority(OverlapPriority); NewSection->SetRowIndex(RowIndex); NewSection->SetBlendType(BlendType); Track->AddSection(*NewSection); Track->UpdateEasing(); Sequencer->NotifyMovieSceneDataChanged(EMovieSceneDataChangeType::MovieSceneStructureItemAdded); Sequencer->EmptySelection(); Sequencer->SelectSection(NewSection); Sequencer->ThrobSectionSelection(); } else { Transaction.Cancel(); } }; FText NameOverride = Track->GetSupportedBlendTypes().Num() == 1 ? LOCTEXT("AddSectionText", "Add New Section") : FText(); FText TooltipOverride = Track->GetSupportedBlendTypes().Num() == 1 ? LOCTEXT("AddSectionToolTip", "Adds a new section at the current time") : FText(); const UEnum* MovieSceneBlendType = FindObjectChecked(nullptr, TEXT("/Script/MovieScene.EMovieSceneBlendType")); for (EMovieSceneBlendType BlendType : Track->GetSupportedBlendTypes()) { FText DisplayName = MovieSceneBlendType->GetDisplayNameTextByValue((int64)BlendType); FName EnumValueName = MovieSceneBlendType->GetNameByValue((int64)BlendType); MenuBuilder.AddMenuEntry( NameOverride.IsEmpty() ? DisplayName : NameOverride, TooltipOverride.IsEmpty() ? FText::Format(LOCTEXT("AddSectionFormatToolTip", "Adds a new {0} section at the current time"), DisplayName) : TooltipOverride, FSlateIcon(FAppStyle::GetAppStyleSetName(), EnumValueName), FUIAction(FExecuteAction::CreateLambda(CreateNewSection, BlendType)) ); } } void FSequencerUtilities::PopulateMenu_SetBlendType(FMenuBuilder& MenuBuilder, UMovieSceneSection* Section, TWeakPtr InSequencer) { PopulateMenu_SetBlendType(MenuBuilder, TArray>({ Section }), InSequencer); } void FSequencerUtilities::PopulateMenu_SetBlendType(FMenuBuilder& MenuBuilder, const TArray>& InSections, TWeakPtr InSequencer) { using namespace UE::MovieScene; using namespace UE::Sequencer; auto Execute = [InSections, InSequencer](EMovieSceneBlendType BlendType) { FScopedTransaction Transaction(LOCTEXT("SetBlendType", "Set Blend Type")); for (TWeakObjectPtr WeakSection : InSections) { if (UMovieSceneSection* Section = WeakSection.Get()) { Section->Modify(); Section->SetBlendType(BlendType); } } TSharedPtr Sequencer = StaticCastSharedPtr(InSequencer.Pin()); if (Sequencer.IsValid()) { // If the blend type is changed to additive or relative, restore the state of the objects boud to this section before evaluating again. // This allows the additive or relative to evaluate based on the initial values of the object, rather than the current animated values. if (BlendType == EMovieSceneBlendType::Additive || BlendType == EMovieSceneBlendType::Relative) { TSet ObjectsToRestore; TSharedRef SequencerNodeTree = Sequencer->GetNodeTree(); for (TWeakObjectPtr WeakSection : InSections) { if (UMovieSceneSection* Section = WeakSection.Get()) { TSharedPtr SectionHandle = SequencerNodeTree->GetSectionModel(Section); if (!SectionHandle) { continue; } TSharedPtr ParentObjectBindingNode = SectionHandle->FindAncestorOfType(); if (!ParentObjectBindingNode.IsValid()) { continue; } for (TWeakObjectPtr<> BoundObject : Sequencer->FindObjectsInCurrentSequence(ParentObjectBindingNode->GetObjectGuid())) { if (AActor* BoundActor = Cast(BoundObject)) { for (UActorComponent* Component : TInlineComponentArray(BoundActor)) { if (Component) { ObjectsToRestore.Add(Component); } } } ObjectsToRestore.Add(BoundObject.Get()); } } } for (UObject* ObjectToRestore : ObjectsToRestore) { Sequencer->PreAnimatedState.RestorePreAnimatedState(*ObjectToRestore); } } Sequencer->NotifyMovieSceneDataChanged(EMovieSceneDataChangeType::TrackValueChanged); } }; const UEnum* MovieSceneBlendType = FindObjectChecked(nullptr, TEXT("/Script/MovieScene.EMovieSceneBlendType")); for (int32 NameIndex = 0; NameIndex < MovieSceneBlendType->NumEnums() - 1; ++NameIndex) { EMovieSceneBlendType BlendType = (EMovieSceneBlendType)MovieSceneBlendType->GetValueByIndex(NameIndex); // Include this if any section supports it bool bAnySupported = false; for (TWeakObjectPtr WeakSection : InSections) { UMovieSceneSection* Section = WeakSection.Get(); if (Section && Section->GetSupportedBlendTypes().Contains(BlendType)) { bAnySupported = true; break; } } if (!bAnySupported) { continue; } FName EnumValueName = MovieSceneBlendType->GetNameByIndex(NameIndex); MenuBuilder.AddMenuEntry( MovieSceneBlendType->GetDisplayNameTextByIndex(NameIndex), MovieSceneBlendType->GetToolTipTextByIndex(NameIndex), FSlateIcon(FAppStyle::GetAppStyleSetName(), EnumValueName), FUIAction( FExecuteAction::CreateLambda(Execute, BlendType), FCanExecuteAction(), FIsActionChecked::CreateLambda([InSections, BlendType] { int32 NumActiveBlendTypes = 0; for (TWeakObjectPtr WeakSection : InSections) { UMovieSceneSection* Section = WeakSection.Get(); if (Section && Section->GetBlendType() == BlendType) { ++NumActiveBlendTypes; } } return NumActiveBlendTypes == InSections.Num(); })), NAME_None, EUserInterfaceActionType::RadioButton ); } } FName FSequencerUtilities::GetUniqueName( FName CandidateName, const TArray& ExistingNames ) { if (!ExistingNames.Contains(CandidateName)) { return CandidateName; } FString CandidateNameString = CandidateName.ToString(); FString BaseNameString = CandidateNameString; if ( CandidateNameString.Len() >= 3 && CandidateNameString.Right(3).IsNumeric() ) { BaseNameString = CandidateNameString.Left( CandidateNameString.Len() - 3 ); } FName UniqueName = FName(*BaseNameString); int32 NameIndex = 1; while ( ExistingNames.Contains( UniqueName ) ) { UniqueName = FName( *FString::Printf(TEXT("%s%i"), *BaseNameString, NameIndex ) ); NameIndex++; } return UniqueName; } TArray FSequencerUtilities::GetAssociatedMapPackages(const ULevelSequence* InSequence) { if (!InSequence) { return TArray(); } FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); const FName LSMapPathName = *InSequence->GetOutermost()->GetPathName(); TArray AssociatedMaps; TArray AssociatedAssets; // This makes the assumption these functions will append the array, and not clear it. AssetRegistryModule.Get().GetReferencers(LSMapPathName, AssociatedAssets); AssetRegistryModule.Get().GetDependencies(LSMapPathName, AssociatedAssets); for (FAssetIdentifier& AssociatedMap : AssociatedAssets) { FString MapFilePath; FString LevelPath = AssociatedMap.PackageName.ToString(); if (FEditorFileUtils::IsMapPackageAsset(LevelPath, MapFilePath)) { AssociatedMaps.AddUnique(LevelPath); } } AssociatedMaps.Sort([](const FString& One, const FString& Two) { return FPaths::GetBaseFilename(One) < FPaths::GetBaseFilename(Two); }); return AssociatedMaps; } FGuid FSequencerUtilities::DoAssignActor(ISequencer * InSequencerPtr, AActor* Actor, FGuid InObjectBinding) { if (Actor == nullptr) { return FGuid(); } UMovieSceneSequence* OwnerSequence = InSequencerPtr->GetFocusedMovieSceneSequence(); UMovieScene* OwnerMovieScene = OwnerSequence->GetMovieScene(); if (OwnerMovieScene->IsReadOnly()) { FSequencerUtilities::ShowReadOnlyError(); return FGuid(); } FScopedTransaction AssignActor(LOCTEXT("AssignActor", "Assign Actor")); Actor->Modify(); OwnerSequence->Modify(); OwnerMovieScene->Modify(); TArrayView> RuntimeObjects = InSequencerPtr->FindObjectsInCurrentSequence(InObjectBinding); UObject* RuntimeObject = RuntimeObjects.Num() ? RuntimeObjects[0].Get() : nullptr; // Replace the object itself FMovieScenePossessable NewPossessableActor; FGuid NewGuid; { // Get the object guid to assign, remove the binding if it already exists FGuid ParentGuid = InSequencerPtr->FindObjectId(*Actor, InSequencerPtr->GetFocusedTemplateID()); FString NewActorLabel = Actor->GetActorLabel(); if (ParentGuid.IsValid()) { OwnerMovieScene->RemovePossessable(ParentGuid); OwnerSequence->UnbindPossessableObjects(ParentGuid); } // Add this object NewPossessableActor = FMovieScenePossessable(NewActorLabel, Actor->GetClass()); NewGuid = NewPossessableActor.GetGuid(); if (!NewPossessableActor.BindSpawnableObject(InSequencerPtr->GetFocusedTemplateID(), Actor, InSequencerPtr)) { OwnerSequence->BindPossessableObject(NewPossessableActor.GetGuid(), *Actor, InSequencerPtr->GetPlaybackContext()); } // Defer replacing this object until the components have been updated } auto UpdateComponent = [&](FGuid OldComponentGuid, UActorComponent* NewComponent) { FMovieSceneSequenceIDRef FocusedGuid = InSequencerPtr->GetFocusedTemplateID(); // Get the object guid to assign, remove the binding if it already exists FGuid NewComponentGuid = InSequencerPtr->FindObjectId(*NewComponent, FocusedGuid); if (NewComponentGuid.IsValid()) { OwnerMovieScene->RemovePossessable(NewComponentGuid); OwnerSequence->UnbindPossessableObjects(NewComponentGuid); } // Add this object FMovieScenePossessable NewPossessable(NewComponent->GetName(), NewComponent->GetClass()); OwnerSequence->BindPossessableObject(NewPossessable.GetGuid(), *NewComponent, Actor); // Replace OwnerMovieScene->ReplacePossessable(OldComponentGuid, NewPossessable); OwnerSequence->UnbindPossessableObjects(OldComponentGuid); InSequencerPtr->State.Invalidate(OldComponentGuid, FocusedGuid); InSequencerPtr->State.Invalidate(NewPossessable.GetGuid(), FocusedGuid); FMovieScenePossessable* ThisPossessable = OwnerMovieScene->FindPossessable(NewPossessable.GetGuid()); if (ensure(ThisPossessable)) { ThisPossessable->SetParent(NewGuid, OwnerMovieScene); } }; // Handle components AActor* ActorToReplace = Cast(RuntimeObject); if (ActorToReplace != nullptr && ActorToReplace->IsActorBeingDestroyed() == false) { for (UActorComponent* ComponentToReplace : ActorToReplace->GetComponents()) { if (ComponentToReplace != nullptr) { FGuid ComponentGuid = InSequencerPtr->FindObjectId(*ComponentToReplace, InSequencerPtr->GetFocusedTemplateID()); if (ComponentGuid.IsValid()) { bool bComponentWasUpdated = false; for (UActorComponent* NewComponent : Actor->GetComponents()) { if (NewComponent->GetFullName(Actor) == ComponentToReplace->GetFullName(ActorToReplace)) { UpdateComponent(ComponentGuid, NewComponent); bComponentWasUpdated = true; } } // Clear the parent guid since this possessable component doesn't match to any component on the new actor if (!bComponentWasUpdated) { FMovieScenePossessable* ThisPossessable = OwnerMovieScene->FindPossessable(ComponentGuid); ThisPossessable->SetParent(FGuid(), OwnerMovieScene); } } } } } else // If the actor didn't exist, try to find components who's parent guids were the previous actors guid. { TMap ComponentNameToComponent; for (UActorComponent* Component : Actor->GetComponents()) { ComponentNameToComponent.Add(Component->GetName(), Component); } for (int32 i = 0; i < OwnerMovieScene->GetPossessableCount(); i++) { FMovieScenePossessable& OldPossessable = OwnerMovieScene->GetPossessable(i); if (OldPossessable.GetParent() == InObjectBinding) { UActorComponent** ComponentPtr = ComponentNameToComponent.Find(OldPossessable.GetName()); if (ComponentPtr != nullptr) { UpdateComponent(OldPossessable.GetGuid(), *ComponentPtr); } } } } // Replace the actor itself after components have been updated OwnerMovieScene->ReplacePossessable(InObjectBinding, NewPossessableActor); OwnerSequence->UnbindPossessableObjects(InObjectBinding); InSequencerPtr->State.Invalidate(InObjectBinding, InSequencerPtr->GetFocusedTemplateID()); InSequencerPtr->State.Invalidate(NewPossessableActor.GetGuid(), InSequencerPtr->GetFocusedTemplateID()); // Try to fix up folders TArray FoldersToCheck; for (UMovieSceneFolder* Folder : InSequencerPtr->GetFocusedMovieSceneSequence()->GetMovieScene()->GetRootFolders()) { FoldersToCheck.Add(Folder); } bool bFolderFound = false; while (FoldersToCheck.Num() > 0 && bFolderFound == false) { UMovieSceneFolder* Folder = FoldersToCheck[0]; FoldersToCheck.RemoveAt(0); if (Folder->GetChildObjectBindings().Contains(InObjectBinding)) { Folder->RemoveChildObjectBinding(InObjectBinding); Folder->AddChildObjectBinding(NewGuid); bFolderFound = true; } for (UMovieSceneFolder* ChildFolder : Folder->GetChildFolders()) { FoldersToCheck.Add(ChildFolder); } } InSequencerPtr->RestorePreAnimatedState(); InSequencerPtr->NotifyMovieSceneDataChanged(EMovieSceneDataChangeType::MovieSceneStructureItemsChanged); return NewGuid; } void FSequencerUtilities::UpdateBindingIDs(ISequencer* InSequencerPtr, UMovieSceneCompiledDataManager* InCompiledDataManagerPtr, FGuid OldGuid, FGuid NewGuid) { FMovieSceneSequenceIDRef FocusedGuid = InSequencerPtr->GetFocusedTemplateID(); TMap OldFixedToNewFixedMap; OldFixedToNewFixedMap.Add(UE::MovieScene::FFixedObjectBindingID(OldGuid, FocusedGuid), UE::MovieScene::FFixedObjectBindingID(NewGuid, FocusedGuid)); const FMovieSceneSequenceHierarchy* Hierarchy = InCompiledDataManagerPtr->FindHierarchy(InSequencerPtr->GetEvaluationTemplate().GetCompiledDataID()); if (UMovieScene* MovieScene = InSequencerPtr->GetRootMovieSceneSequence()->GetMovieScene()) { for (UMovieSceneSection* Section : MovieScene->GetAllSections()) { if (Section) { Section->OnBindingIDsUpdated(OldFixedToNewFixedMap, InSequencerPtr->GetRootTemplateID(), Hierarchy, *InSequencerPtr); } } } if (Hierarchy) { for (const TTuple& Pair : Hierarchy->AllSubSequenceData()) { if (UMovieSceneSequence* Sequence = Pair.Value.GetSequence()) { if (UMovieScene* MovieScene = Sequence->GetMovieScene()) { for (UMovieSceneSection* Section : MovieScene->GetAllSections()) { if (Section) { Section->OnBindingIDsUpdated(OldFixedToNewFixedMap, Pair.Key, Hierarchy, *InSequencerPtr); } } } } } } } void FSequencerUtilities::ShowReadOnlyError() { FNotificationInfo Info(LOCTEXT("SequenceReadOnly", "Sequence is read only.")); Info.ExpireDuration = 5.0f; FSlateNotificationManager::Get().AddNotification(Info)->SetCompletionState(SNotificationItem::CS_Fail); } #undef LOCTEXT_NAMESPACE PRAGMA_ENABLE_OPTIMIZATION