// Copyright Epic Games, Inc. All Rights Reserved. #include "MVVM/ViewModels/OutlinerItemModel.h" #include "MVVM/ViewModels/ViewModelIterators.h" #include "MVVM/ViewModels/SequenceModel.h" #include "MVVM/ViewModels/FolderModel.h" #include "MVVM/Extensions/IDraggableOutlinerExtension.h" #include "MVVM/Extensions/ITrackExtension.h" #include "MVVM/Extensions/IObjectBindingExtension.h" #include "MVVM/ViewModels/OutlinerViewModel.h" #include "MVVM/ViewModels/SequencerEditorViewModel.h" #include "MVVM/SharedViewModelData.h" #include "MVVM/Selection/Selection.h" #include "CurveEditor.h" #include "Framework/Commands/GenericCommands.h" #include "Framework/Commands/UIAction.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "ISettingsModule.h" #include "MovieScene.h" #include "MovieSceneSequence.h" #include "ScopedTransaction.h" #include "Sequencer.h" #include "SequencerSelectionCurveFilter.h" #include "SequencerSettings.h" #include "Styling/AppStyle.h" #include "Tree/CurveEditorTreeFilter.h" #include "Tree/SCurveEditorTreePin.h" #include "Tree/SCurveEditorTreeSelect.h" #include "Widgets/Images/SImage.h" #include "Widgets/Layout/SSpacer.h" #include "SequencerCommonHelpers.h" #include "MVVM/ViewModels/TrackRowModel.h" #include "PropertyEditorModule.h" #include "IStructureDetailsView.h" #include "IStructureDataProvider.h" #include "Conditions/MovieSceneConditionCustomization.h" #include "Conditions/MovieSceneDirectorBlueprintConditionCustomization.h" #define LOCTEXT_NAMESPACE "OutlinerItemModel" namespace UE { namespace Sequencer { static bool NodeMatchesTextFilterTerm(TViewModelPtr Node, const FCurveEditorTreeTextFilterTerm& Term) { using namespace UE::Sequencer; FCurveEditorTreeTextFilterTerm::FMatchResult Match(Term.ChildToParentTokens); while (Node && Match.IsPartialMatch()) { FCurveEditorTreeTextFilterTerm::FMatchResult NewMatch = Match.Match(Node->GetLabel().ToString()); if (NewMatch.IsAnyMatch()) { // If we matched, keep searching parents using the remaining match result Match = NewMatch; } Node = Node.AsModel()->FindAncestorOfType(); } return Match.IsTotalMatch(); } void FOutlinerItemModelMixin::AddEvalOptionsPropertyMenuItem(FMenuBuilder& InMenuBuilder, const FBoolProperty* InProperty, TFunction InValidator) { auto IsChecked = [InProperty, InValidator](const TArray& InTracks) -> bool { return InTracks.ContainsByPredicate( [InValidator, InProperty](UMovieSceneTrack* InTrack) { return (!InValidator || InValidator(InTrack)) && InProperty->GetPropertyValue(InProperty->ContainerPtrToValuePtr(&InTrack->EvalOptions)); }); }; InMenuBuilder.AddMenuEntry( InProperty->GetDisplayNameText(), InProperty->GetToolTipText(), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([this, InProperty, InValidator, IsChecked] { FScopedTransaction Transaction(FText::Format(NSLOCTEXT("Sequencer", "TrackNodeSetRoundEvaluation", "Set '{0}'"), InProperty->GetDisplayNameText())); const TArray AllTracks = GetSelectedTracks(); for (UMovieSceneTrack* Track : AllTracks) { if (InValidator && !InValidator(Track)) { continue; } void* PropertyContainer = InProperty->ContainerPtrToValuePtr(&Track->EvalOptions); Track->Modify(); InProperty->SetPropertyValue(PropertyContainer, !IsChecked(AllTracks)); } }), FCanExecuteAction::CreateLambda([this] { const TSharedPtr Sequencer = GetEditor()->GetSequencerImpl(); if (Sequencer) { return !Sequencer->IsReadOnly(); } return false; }), FIsActionChecked::CreateLambda([this, IsChecked] { const TArray AllTracks = GetSelectedTracks(); return IsChecked(AllTracks); }) ), NAME_None, EUserInterfaceActionType::Check ); } void FOutlinerItemModelMixin::AddDisplayOptionsPropertyMenuItem(FMenuBuilder& InMenuBuilder, const FBoolProperty* InProperty, TFunction InValidator) { auto IsChecked = [InProperty, InValidator](const TArray& InTracks) -> bool { return InTracks.ContainsByPredicate( [InValidator, InProperty](UMovieSceneTrack* InTrack) { return (!InValidator || InValidator(InTrack)) && InProperty->GetPropertyValue(InProperty->ContainerPtrToValuePtr(&InTrack->DisplayOptions)); }); }; InMenuBuilder.AddMenuEntry( InProperty->GetDisplayNameText(), InProperty->GetToolTipText(), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([this, InProperty, InValidator, IsChecked] { FScopedTransaction Transaction(FText::Format(NSLOCTEXT("Sequencer", "TrackNodeSetDisplayOption", "Set '{0}'"), InProperty->GetDisplayNameText())); const TArray AllTracks = GetSelectedTracks(); for (UMovieSceneTrack* Track : AllTracks) { if (InValidator && !InValidator(Track)) { continue; } void* PropertyContainer = InProperty->ContainerPtrToValuePtr(&Track->DisplayOptions); Track->Modify(); InProperty->SetPropertyValue(PropertyContainer, !IsChecked(AllTracks)); } }), FCanExecuteAction::CreateLambda([this] { const TSharedPtr Sequencer = GetEditor()->GetSequencerImpl(); if (Sequencer) { return !Sequencer->IsReadOnly(); } return false; }), FIsActionChecked::CreateLambda([this, IsChecked] { const TArray AllTracks = GetSelectedTracks(); return IsChecked(AllTracks); }) ), NAME_None, EUserInterfaceActionType::Check ); } FOutlinerItemModelMixin::FOutlinerItemModelMixin() : OutlinerChildList(EViewModelListType::Outliner) , bInitializedExpansion(false) , bInitializedPinnedState(false) { } TSharedPtr FOutlinerItemModelMixin::GetEditor() const { TSharedPtr SequenceModel = AsViewModel()->FindAncestorOfType(); return SequenceModel ? SequenceModel->GetEditor() : nullptr; } FName FOutlinerItemModelMixin::GetIdentifier() const { return TreeItemIdentifier; } void FOutlinerItemModelMixin::SetIdentifier(FName InNewIdentifier) { TreeItemIdentifier = InNewIdentifier; const FViewModel* ViewModel = AsViewModel(); if (ViewModel && ViewModel->IsConstructed()) { TSharedPtr EditorViewModel = GetEditor(); if (EditorViewModel) { EditorViewModel->HandleDataHierarchyChanged(); } } } bool FOutlinerItemModelMixin::IsExpanded() const { const FViewModel* ViewModel = AsViewModel(); if (!bInitializedExpansion) { bInitializedExpansion = true; TStringBuilder<256> StringBuilder; IOutlinerExtension::GetPathName(*ViewModel, StringBuilder); TSharedPtr SequenceModel = ViewModel->FindAncestorOfType(); UMovieSceneSequence* Sequence = SequenceModel ? SequenceModel->GetSequence() : nullptr; UMovieScene* MovieScene = Sequence ? Sequence->GetMovieScene() : nullptr; if (MovieScene) { FStringView StringView = StringBuilder.ToView(); FMovieSceneEditorData& EditorData = MovieScene->GetEditorData(); if (const FMovieSceneExpansionState* Expansion = EditorData.ExpansionStates.FindByHash(GetTypeHash(StringView), StringView)) { const_cast(this)->bIsExpanded = Expansion->bExpanded; } else { const_cast(this)->bIsExpanded = GetDefaultExpansionState(); } } } if (bIsExpanded) { // If there are no children, no need to allow this to be expanded FViewModelListIterator OutlinerChildren = ViewModel->GetChildren(EViewModelListType::Outliner); if (OutlinerChildren) { return true; } } return false; } bool FOutlinerItemModelMixin::GetDefaultExpansionState() const { return false; } void FOutlinerItemModelMixin::SetExpansion(bool bInIsExpanded) { FViewModel* ViewModel = AsViewModel(); // If no children, there's no need to set this expanded FViewModelListIterator OutlinerChildren = ViewModel->GetChildren(EViewModelListType::Outliner); if (!OutlinerChildren) { return; } SetExpansionWithoutSaving(bInIsExpanded); if (ViewModel->GetParent()) { // Expansion state has changed, save it to the movie scene now TSharedPtr SequenceModel = ViewModel->FindAncestorOfType(); if (SequenceModel) { TSharedPtr Sequencer = SequenceModel->GetSequencerImpl(); if (Sequencer) { Sequencer->GetNodeTree()->SaveExpansionState(*ViewModel, bInIsExpanded); } } } } void FOutlinerItemModelMixin::SetExpansionWithoutSaving(bool bInIsExpanded) { FOutlinerExtensionShim::SetExpansion(bInIsExpanded); // Force this flag in case a sub-class wants a given expansion state before the // getter is called. bInitializedExpansion = true; } bool FOutlinerItemModelMixin::IsFilteredOut() const { return bIsFilteredOut; } bool FOutlinerItemModelMixin::IsPinned() const { if (bInitializedPinnedState) { return FPinnableExtensionShim::IsPinned(); } bInitializedPinnedState = true; // Initialize expansion states for tree items // Assign the saved expansion state when this node is initialized for the first time const bool bIsRootModel = (AsViewModel()->GetHierarchicalDepth() == 1); if (bIsRootModel) { TSharedPtr SequenceModel = AsViewModel()->FindAncestorOfType(); TSharedPtr Sequencer = SequenceModel->GetSequencerImpl(); if (Sequencer) { const bool bWasPinned = Sequencer->GetNodeTree()->GetSavedPinnedState(*AsViewModel()); const_cast(this)->FPinnableExtensionShim::SetPinned(bWasPinned); } } return FPinnableExtensionShim::IsPinned(); } bool FOutlinerItemModelMixin::IsDimmed() const { const FViewModel* const ViewModel = AsViewModel(); const TSharedPtr SharedData = ViewModel->GetSharedData(); if (!SharedData.IsValid()) { return false; } const FDeactiveStateCacheExtension* const DeactiveState = SharedData->CastThis(); const FMuteStateCacheExtension* const MuteState = SharedData->CastThis(); const FSoloStateCacheExtension* const SoloState = SharedData->CastThis(); check(DeactiveState && MuteState && SoloState); const uint32 ModelID = ViewModel->GetModelID(); const ECachedDeactiveState DeactiveFlags = DeactiveState->GetCachedFlags(ModelID); const ECachedMuteState MuteFlags = MuteState->GetCachedFlags(ModelID); const ECachedSoloState SoloFlags = SoloState->GetCachedFlags(ModelID); const bool bIsDeactive = EnumHasAnyFlags(DeactiveFlags, ECachedDeactiveState::Deactivated | ECachedDeactiveState::ImplicitlyDeactivatedByParent); const bool bAnySoloNodes = EnumHasAnyFlags(SoloState->GetRootFlags(), ECachedSoloState::Soloed | ECachedSoloState::PartiallySoloedChildren); const bool bIsMuted = EnumHasAnyFlags(MuteFlags, ECachedMuteState::Muted | ECachedMuteState::ImplicitlyMutedByParent); const bool bIsSoloed = EnumHasAnyFlags(SoloFlags, ECachedSoloState::Soloed | ECachedSoloState::ImplicitlySoloedByParent); const bool bDisableEval = bIsDeactive || bIsMuted || (bAnySoloNodes && !bIsSoloed); return bDisableEval; } bool FOutlinerItemModelMixin::IsRootModelPinned() const { TSharedPtr PinnableParent = AsViewModel()->FindAncestorOfType(true); return PinnableParent && PinnableParent->IsPinned(); } void FOutlinerItemModelMixin::ToggleRootModelPinned() { FSequenceModel* RootModel = AsViewModel()->GetRoot()->CastThis(); TSharedPtr PinnableParent = AsViewModel()->FindAncestorOfType(true); if (RootModel && PinnableParent) { TSharedPtr Outliner = RootModel->GetEditor()->GetOutliner(); Outliner->UnpinAllNodes(); const bool bShouldPin = !PinnableParent->IsPinned(); PinnableParent->SetPinned(bShouldPin); TSharedPtr Sequencer = RootModel->GetSequencerImpl(); if (Sequencer) { Sequencer->GetNodeTree()->SavePinnedState(*AsViewModel(), bShouldPin); Sequencer->RefreshTree(); } } } ECheckBoxState FOutlinerItemModelMixin::SelectedModelsSoloState() const { FSoloStateCacheExtension* SoloStateCache = AsViewModel()->GetSharedData()->CastThis(); check(SoloStateCache); int32 NumSoloables = 0; int32 NumSoloed = 0; for (FViewModelPtr Soloable : GetEditor()->GetSelection()->Outliner.Filter()) { ++NumSoloables; if (EnumHasAnyFlags(SoloStateCache->GetCachedFlags(Soloable), ECachedSoloState::Soloed)) { ++NumSoloed; } } if (NumSoloed == 0) { return ECheckBoxState::Unchecked; } return NumSoloables == NumSoloed ? ECheckBoxState::Checked : ECheckBoxState::Undetermined; } void FOutlinerItemModelMixin::ToggleSelectedModelsSolo() { ECheckBoxState CurrentState = SelectedModelsSoloState(); const bool bNewSoloState = CurrentState != ECheckBoxState::Checked; const FScopedTransaction Transaction(NSLOCTEXT("Sequencer", "ToggleSolo", "Toggle Solo")); TSharedPtr EditorViewModel = GetEditor(); for (TViewModelPtr Soloable : EditorViewModel->GetSelection()->Outliner.Filter()) { Soloable->SetIsSoloed(bNewSoloState); } TSharedPtr Sequencer = EditorViewModel->GetSequencer(); if (Sequencer) { Sequencer->RefreshTree(); } } ECheckBoxState FOutlinerItemModelMixin::SelectedModelsMuteState() const { FMuteStateCacheExtension* MuteStateCache = AsViewModel()->GetSharedData()->CastThis(); check(MuteStateCache); int32 NumMutables = 0; int32 NumMuted = 0; for (FViewModelPtr Mutable : GetEditor()->GetSelection()->Outliner.Filter()) { ++NumMutables; if (EnumHasAnyFlags(MuteStateCache->GetCachedFlags(Mutable), ECachedMuteState::Muted)) { ++NumMuted; } } if (NumMuted == 0) { return ECheckBoxState::Unchecked; } return NumMutables == NumMuted ? ECheckBoxState::Checked : ECheckBoxState::Undetermined; } void FOutlinerItemModelMixin::ToggleSelectedModelsMuted() { ECheckBoxState CurrentState = SelectedModelsMuteState(); const bool bNewMuteState = CurrentState != ECheckBoxState::Checked; const FScopedTransaction Transaction(NSLOCTEXT("Sequencer", "ToggleMute", "Toggle Mute")); TSharedPtr EditorViewModel = GetEditor(); for (TViewModelPtr Muteable : EditorViewModel->GetSelection()->Outliner.Filter()) { Muteable->SetIsMuted(bNewMuteState); } TSharedPtr Sequencer = EditorViewModel->GetSequencer(); if (Sequencer) { Sequencer->RefreshTree(); } } TSharedPtr FOutlinerItemModelMixin::CreateContextMenuWidget(const FCreateOutlinerContextMenuWidgetParams& InParams) { TSharedPtr EditorViewModel = GetEditor(); TSharedPtr Sequencer = EditorViewModel->GetSequencer(); if (Sequencer) { const bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, Sequencer->GetCommandBindings()); BuildContextMenu(MenuBuilder); return MenuBuilder.MakeWidget(); } return nullptr; } FSlateColor FOutlinerItemModelMixin::GetLabelColor() const { if (TViewModelPtr SequenceModel = AsViewModel()->FindAncestorOfType()) { if (TSharedPtr SequencerModel = SequenceModel->GetEditor()) { if (IMovieScenePlayer* Player = SequencerModel->GetSequencer().Get()) { if (TViewModelPtr ObjectBindingModel = AsViewModel()->FindAncestorOfType()) { // If the object binding model has an invalid binding, we want to use its label color, as it may be red or gray depending on situation // and we want the children of that to have the same color. // Otherwise, we can use the track's label color below TArrayView > BoundObjects = Player->FindBoundObjects(ObjectBindingModel->GetObjectGuid(), SequenceModel->GetSequenceID()); if (BoundObjects.Num() == 0) { return ObjectBindingModel->GetLabelColor(); } } } } } return IOutlinerExtension::GetLabelColor(); } void FOutlinerItemModelMixin::BuildContextMenu(FMenuBuilder& MenuBuilder) { TSharedPtr Sequencer = StaticCastSharedPtr(GetEditor()->GetSequencer()); if (!Sequencer) { return; } TSharedRef SharedThis(AsViewModel()->AsShared(), this); const bool bIsReadOnly = Sequencer->IsReadOnly(); FCanExecuteAction CanExecute = FCanExecuteAction::CreateLambda([bIsReadOnly]{ return !bIsReadOnly; }); MenuBuilder.BeginSection("Edit", LOCTEXT("EditContextMenuSectionName", "Edit")); { MenuBuilder.AddMenuEntry( LOCTEXT("ToggleNodeLock", "Locked"), LOCTEXT("ToggleNodeLockTooltip", "Lock or unlock this node or selected tracks"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(Sequencer.Get(), &FSequencer::ToggleNodeLocked), CanExecute, FIsActionChecked::CreateSP(Sequencer.Get(), &FSequencer::IsNodeLocked) ), NAME_None, EUserInterfaceActionType::ToggleButton ); // Only support pinning root nodes const bool bIsRootModel = (AsViewModel()->GetHierarchicalDepth() == 1); if (bIsRootModel) { MenuBuilder.AddMenuEntry( LOCTEXT("ToggleNodePin", "Pinned"), LOCTEXT("ToggleNodePinTooltip", "Pin or unpin this node or selected tracks"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(SharedThis, &FOutlinerItemModelMixin::ToggleRootModelPinned), FCanExecuteAction(), FIsActionChecked::CreateSP(SharedThis, &FOutlinerItemModelMixin::IsRootModelPinned) ), NAME_None, EUserInterfaceActionType::ToggleButton ); } // We already know we are soloable and mutable MenuBuilder.AddMenuEntry( LOCTEXT("ToggleNodeSolo", "Solo"), LOCTEXT("ToggleNodeSoloTooltip", "Solo or unsolo this node or selected tracks"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(SharedThis, &FOutlinerItemModelMixin::ToggleSelectedModelsSolo), CanExecute, FGetActionCheckState::CreateSP(SharedThis, &FOutlinerItemModelMixin::SelectedModelsSoloState) ), NAME_None, EUserInterfaceActionType::ToggleButton ); MenuBuilder.AddMenuEntry( LOCTEXT("ToggleNodeMute", "Mute"), LOCTEXT("ToggleNodeMuteTooltip", "Mute or unmute this node or selected tracks"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(SharedThis, &FOutlinerItemModelMixin::ToggleSelectedModelsMuted), CanExecute, FGetActionCheckState::CreateSP(SharedThis, &FOutlinerItemModelMixin::SelectedModelsMuteState) ), NAME_None, EUserInterfaceActionType::ToggleButton ); // Add cut, copy and paste functions to the tracks MenuBuilder.AddMenuEntry(FGenericCommands::Get().Cut); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Copy); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Paste); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Duplicate); TSharedRef ThisNode = AsViewModel()->AsShared(); MenuBuilder.AddMenuEntry( LOCTEXT("DeleteNode", "Delete"), LOCTEXT("DeleteNodeTooltip", "Delete this or selected tracks"), FSlateIcon(FAppStyle::GetAppStyleSetName(), "ContentBrowser.AssetActions.Delete"), FUIAction(FExecuteAction::CreateSP(Sequencer.Get(), &FSequencer::DeleteNode, ThisNode, false), CanExecute) ); if (ThisNode->IsA()) { MenuBuilder.AddMenuEntry( LOCTEXT("DeleteNodeAndKeepState", "Delete and Keep State"), LOCTEXT("DeleteNodeAndKeepStateTooltip", "Delete this object's tracks and keep its current animated state"), FSlateIcon(FAppStyle::GetAppStyleSetName(), "ContentBrowser.AssetActions.Delete"), FUIAction(FExecuteAction::CreateSP(Sequencer.Get(), &FSequencer::DeleteNode, ThisNode, true), CanExecute) ); } MenuBuilder.AddMenuEntry(FGenericCommands::Get().Rename); } MenuBuilder.EndSection(); MenuBuilder.BeginSection("Organize", LOCTEXT("OrganizeContextMenuSectionName", "Organize")); BuildOrganizeContextMenu(MenuBuilder); MenuBuilder.EndSection(); const TArray AllTracks = GetSelectedTracks(); if (AllTracks.Num()) { BuildTrackOptionsMenu(MenuBuilder, AllTracks); BuildTrackRowOptionsMenu(MenuBuilder); BuildDisplayOptionsMenu(MenuBuilder); } } void FOutlinerItemModelMixin::BuildOrganizeContextMenu(FMenuBuilder& MenuBuilder) { const TSharedPtr EditorViewModel = GetEditor(); if (!EditorViewModel.IsValid()) { return; } const TSharedPtr Sequencer = EditorViewModel->GetSequencerImpl(); if (!Sequencer.IsValid()) { return; } FSequencer* const SequencerRaw = Sequencer.Get(); TSharedRef ThisNode = AsViewModel()->AsShared(); const bool bFilterableNode = (ThisNode->IsA() || ThisNode->IsA() || ThisNode->IsA()); const bool bIsReadOnly = Sequencer->IsReadOnly(); TArray AllTracks; TArray > DraggableNodes; for (const FViewModelPtr Node : EditorViewModel->GetSelection()->Outliner) { if (ITrackExtension* TrackExtension = Node->CastThis()) { UMovieSceneTrack* Track = TrackExtension->GetTrack(); if (Track) { AllTracks.Add(Track); } } if (IDraggableOutlinerExtension* DraggableExtension = Node->CastThis()) { if (DraggableExtension->CanDrag()) { DraggableNodes.Add(Node); } } } if (bFilterableNode && !bIsReadOnly) { MenuBuilder.AddSubMenu( LOCTEXT("AddNodesToNodeGroup", "Add to Group"), LOCTEXT("AddNodesToNodeGroupTooltip", "Add selected nodes to a group"), FNewMenuDelegate::CreateSP(SequencerRaw, &FSequencer::BuildAddSelectedToNodeGroupMenu)); } if (DraggableNodes.Num() && !bIsReadOnly) { MenuBuilder.AddSubMenu( LOCTEXT("MoveToFolder", "Move to Folder"), LOCTEXT("MoveToFolderTooltip", "Move the selected nodes to a folder"), FNewMenuDelegate::CreateSP(SequencerRaw, &FSequencer::BuildAddSelectedToFolderMenu)); MenuBuilder.AddMenuEntry( LOCTEXT("RemoveFromFolder", "Remove from Folder"), LOCTEXT("RemoveFromFolderTooltip", "Remove selected nodes from their folders"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(SequencerRaw, &FSequencer::RemoveSelectedNodesFromFolders), FCanExecuteAction::CreateLambda([SequencerRaw] { return SequencerRaw->GetSelectedNodesInFolders().Num() > 0; } ))); } if (!bIsReadOnly) { MenuBuilder.AddSubMenu( LOCTEXT("SortBy", "Sort by"), LOCTEXT("SortByTooltip", "Sort the selected tracks by start time of the first layer bar"), FNewMenuDelegate::CreateSP(SequencerRaw, &FSequencer::BuildSortMenu)); } } void FOutlinerItemModelMixin::BuildDisplayOptionsMenu(FMenuBuilder& MenuBuilder) { const TSharedPtr EditorViewModel = GetEditor(); if (!EditorViewModel.IsValid()) { return; } const TSharedPtr Sequencer = EditorViewModel->GetSequencer(); if (!Sequencer.IsValid()) { return; } const TSharedRef SharedThis(AsViewModel()->AsShared(), this); const bool bIsReadOnly = Sequencer->IsReadOnly(); const FCanExecuteAction CanExecute = FCanExecuteAction::CreateLambda([bIsReadOnly]{ return !bIsReadOnly; }); TArray AllTracks = GetSelectedTracks(); if (AllTracks.IsEmpty()) { return; } MenuBuilder.BeginSection(TEXT("TrackDisplayOptions"), LOCTEXT("TrackNodeDisplayOptions", "Display Options")); { MenuBuilder.AddSubMenu( LOCTEXT("SetColorTint", "Set Color Tint"), LOCTEXT("SetColorTintTooltip", "Set color tint from the preferences for the selected sections or the track's sections"), FNewMenuDelegate::CreateSP(SharedThis, &FOutlinerItemModelMixin::BuildSectionColorTintsMenu)); UStruct* const DisplayOptionsStruct = FMovieSceneTrackDisplayOptions::StaticStruct(); const FBoolProperty* const ShowVerticalFramesProperty = CastField(DisplayOptionsStruct->FindPropertyByName(GET_MEMBER_NAME_CHECKED(FMovieSceneTrackDisplayOptions, bShowVerticalFrames))); if (ShowVerticalFramesProperty) { AddDisplayOptionsPropertyMenuItem(MenuBuilder, ShowVerticalFramesProperty); } } MenuBuilder.EndSection(); } void FOutlinerItemModelMixin::BuildTrackOptionsMenu(FMenuBuilder& MenuBuilder, const TArray& InTracks) { if (InTracks.IsEmpty()) { return; } MenuBuilder.BeginSection(TEXT("GeneralTrackOptions"), LOCTEXT("TrackNodeGeneralOptions", "Track Options")); { UStruct* const EvalOptionsStruct = FMovieSceneTrackEvalOptions::StaticStruct(); const FBoolProperty* const NearestSectionProperty = CastField(EvalOptionsStruct->FindPropertyByName(GET_MEMBER_NAME_CHECKED(FMovieSceneTrackEvalOptions, bEvalNearestSection))); auto CanEvaluateNearest = [](const UMovieSceneTrack* const InTrack) { return InTrack->EvalOptions.bCanEvaluateNearestSection != 0; }; if (NearestSectionProperty && InTracks.ContainsByPredicate(CanEvaluateNearest)) { TFunction Validator = CanEvaluateNearest; AddEvalOptionsPropertyMenuItem(MenuBuilder, NearestSectionProperty, Validator); } const FBoolProperty* const PrerollProperty = CastField(EvalOptionsStruct->FindPropertyByName(GET_MEMBER_NAME_CHECKED(FMovieSceneTrackEvalOptions, bEvaluateInPreroll))); if (PrerollProperty) { AddEvalOptionsPropertyMenuItem(MenuBuilder, PrerollProperty); } const FBoolProperty* const PostrollProperty = CastField(EvalOptionsStruct->FindPropertyByName(GET_MEMBER_NAME_CHECKED(FMovieSceneTrackEvalOptions, bEvaluateInPostroll))); if (PostrollProperty) { AddEvalOptionsPropertyMenuItem(MenuBuilder, PostrollProperty); } } MenuBuilder.EndSection(); } void FOutlinerItemModelMixin::BuildTrackRowOptionsMenu(FMenuBuilder& MenuBuilder) { // Don't show track row metadata if we don't allow conditions, as for now this is the only item in track row metadata FViewModel* ViewModel = AsViewModel(); TSharedPtr SequenceModel = ViewModel ? ViewModel->FindAncestorOfType() : nullptr; if (SequenceModel) { if (UMovieScene* MovieScene = SequenceModel->GetMovieScene()) { if (!MovieScene->IsConditionClassAllowed(UMovieSceneCondition::StaticClass())) { return; } } } TArray> AllTrackRows = GetSelectedTrackRows(); if (AllTrackRows.IsEmpty()) { return; } // Only show track row options for tracks that allow multiple rows if (Algo::AnyOf(AllTrackRows, [](const TPair TrackRow) { return TrackRow.Key && !TrackRow.Key->SupportsMultipleRows(); })) { return; } MenuBuilder.BeginSection(TEXT("TrackRowMetadata")); { // Empty here, will be implemented by extension. } MenuBuilder.EndSection(); } void FOutlinerItemModelMixin::BuildSidebarMenu(FMenuBuilder& MenuBuilder) { MenuBuilder.BeginSection(TEXT("Organize"), LOCTEXT("OrganizeContextMenuSectionName", "Organize")); BuildOrganizeContextMenu(MenuBuilder); MenuBuilder.EndSection(); BuildTrackOptionsMenu(MenuBuilder, GetSelectedTracks()); BuildTrackRowOptionsMenu(MenuBuilder); BuildDisplayOptionsMenu(MenuBuilder); } TArray FOutlinerItemModelMixin::GetSelectedSections() const { TArray Sections; const TSharedPtr EditorViewModel = GetEditor(); if (!EditorViewModel.IsValid()) { return Sections; } const TSharedPtr Sequencer = EditorViewModel->GetSequencer(); if (!Sequencer.IsValid()) { return Sections; } const TSharedPtr Selection = EditorViewModel->GetSelection(); if (!Selection.IsValid()) { return Sections; } for (const TViewModelPtr SectionModel : Selection->Outliner.Filter()) { if (UMovieSceneSection* const Section = SectionModel->GetSection()) { Sections.Add(Section); } } if (Sections.IsEmpty()) { for (const TViewModelPtr TrackExtension : Selection->Outliner.Filter()) { for (UMovieSceneSection* const Section : TrackExtension->GetSections()) { Sections.Add(Section); } } } return Sections; } TArray FOutlinerItemModelMixin::GetSelectedTracks() const { TArray AllTracks; const TSharedPtr EditorViewModel = GetEditor(); if (!EditorViewModel.IsValid()) { return AllTracks; } const TSharedPtr Sequencer = EditorViewModel->GetSequencer(); if (!Sequencer.IsValid()) { return AllTracks; } const TSharedPtr Selection = EditorViewModel->GetSelection(); if (!Selection.IsValid()) { return AllTracks; } return Selection->GetSelectedTracks().Array(); } TArray> FOutlinerItemModelMixin::GetSelectedTrackRows() const { TArray> AllTrackRows; const TSharedPtr EditorViewModel = GetEditor(); if (!EditorViewModel.IsValid()) { return AllTrackRows; } const TSharedPtr Selection = EditorViewModel->GetSelection(); if (!Selection.IsValid()) { return AllTrackRows; } for (const TViewModelPtr TrackExtension : Selection->Outliner.Filter()) { UMovieSceneTrack* const Track = TrackExtension->GetTrack(); if (IsValid(Track)) { AllTrackRows.Add(TPair(Track, TrackExtension->GetRowIndex())); } } return AllTrackRows; } void FOutlinerItemModelMixin::BuildSectionColorTintsMenu(FMenuBuilder& MenuBuilder) { const TSharedPtr EditorViewModel = GetEditor(); if (!EditorViewModel.IsValid()) { return; } const TSharedPtr Sequencer = EditorViewModel->GetSequencerImpl(); if (!Sequencer.IsValid()) { return; } const TArray Sections = GetSelectedSections(); if (Sections.IsEmpty()) { return; } const TWeakPtr WeakSequencer = Sequencer; FCanExecuteAction CanExecuteAction = FCanExecuteAction::CreateLambda([WeakSequencer] { return WeakSequencer.IsValid() ? !WeakSequencer.Pin()->IsReadOnly() : false; }); const TArray SectionColorTints = Sequencer->GetSequencerSettings()->GetSectionColorTints(); for (const FColor& SectionColorTint : SectionColorTints) { TSharedPtr ColorWidget = SNew(SBox) .WidthOverride(70.f) .HeightOverride(20.f) [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush(TEXT("WhiteBrush"))) .BorderBackgroundColor(FLinearColor::FromSRGBColor(SectionColorTint)) ]; MenuBuilder.AddMenuEntry( FUIAction( FExecuteAction::CreateLambda([this, WeakSequencer, SectionColorTint] { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer) { return; } const TArray Sections = GetSelectedSections(); if (Sections.IsEmpty()) { return; } Sequencer->SetSectionColorTint(Sections, SectionColorTint); }), CanExecuteAction), ColorWidget.ToSharedRef()); } MenuBuilder.AddSeparator(); // Clear any assigned color tints MenuBuilder.AddMenuEntry( LOCTEXT("ClearColorTintLabel", "Clear"), LOCTEXT("ClearColorTintTooltip", "Clear any assigned color tints"), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([this, WeakSequencer] { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer) { return; } const TArray Sections = GetSelectedSections(); if (Sections.IsEmpty()) { return; } Sequencer->SetSectionColorTint(Sections, FColor(0, 0, 0, 0)); }), CanExecuteAction)); // Pop up preferences to edit custom color tints MenuBuilder.AddMenuEntry( LOCTEXT("EditColorTintLabel", "Edit Color Tints..."), LOCTEXT("EditColorTintTooltip", "Edit the custom color tints"), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([WeakSequencer] { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer) { return; } const USequencerSettings* SequencerSettings = Sequencer->GetSequencerSettings(); if (!IsValid(SequencerSettings)) { return; } ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked(TEXT("Settings")); SettingsModule.ShowViewer("Editor", "ContentEditors", *SequencerSettings->GetName()); }) )); } bool FOutlinerItemModelMixin::HasCurves() const { return false; } TOptional FOutlinerItemModelMixin::GetUniquePathName() const { TStringBuilder<256> StringBuilder; IOutlinerExtension::GetPathName(*AsViewModel(), StringBuilder); FString PathName(StringBuilder.ToString()); TOptional FullPathName = PathName; return FullPathName; } TSharedPtr FOutlinerItemModelMixin::GetCurveEditorTreeItem() const { TSharedRef ThisShared(const_cast(AsViewModel())->AsShared()); return TSharedPtr(ThisShared, const_cast(this)); } TSharedPtr FOutlinerItemModelMixin::GenerateCurveEditorTreeWidget(const FName& InColumnName, TWeakPtr InCurveEditor, FCurveEditorTreeItemID InTreeItemID, const TSharedRef& TableRow) { using namespace UE::Sequencer; TSharedRef SharedThis(AsViewModel()->AsShared(), this); auto GetCurveEditorHighlightText = [](TWeakPtr InCurveEditor) -> FText { TSharedPtr PinnedCurveEditor = InCurveEditor.Pin(); if (!PinnedCurveEditor) { return FText::GetEmpty(); } const FCurveEditorTreeFilter* Filter = PinnedCurveEditor->GetTree()->FindFilterByType(ECurveEditorTreeFilterType::Text); if (Filter) { return static_cast(Filter)->InputText; } return FText::GetEmpty(); }; if (InColumnName == ColumnNames.Label) { return SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(FMargin(0.f, 0.f, 4.f, 0.f)) .VAlign(VAlign_Center) .AutoWidth() [ SNew(SOverlay) + SOverlay::Slot() [ SNew(SImage) .Image(SharedThis, &FOutlinerItemModelMixin::GetIconBrush) .ColorAndOpacity(SharedThis, &FOutlinerItemModelMixin::GetIconTint) ] + SOverlay::Slot() .VAlign(VAlign_Top) .HAlign(HAlign_Right) [ SNew(SImage) .Image(SharedThis, &FOutlinerItemModelMixin::GetIconOverlayBrush) ] + SOverlay::Slot() [ SNew(SSpacer) .Visibility(EVisibility::Visible) .ToolTipText(SharedThis, &FOutlinerItemModelMixin::GetIconToolTipText) ] ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(FMargin(0.f, 4.f, 0.f, 4.f)) [ SNew(STextBlock) .Text(SharedThis, &FOutlinerItemModelMixin::GetLabel) .Font(SharedThis, &FOutlinerItemModelMixin::GetLabelFont) .HighlightText_Static(GetCurveEditorHighlightText, InCurveEditor) .ToolTipText(SharedThis, &FOutlinerItemModelMixin::GetLabelToolTipText) ]; } else if (InColumnName == ColumnNames.SelectHeader) { return SNew(SCurveEditorTreeSelect, InCurveEditor, InTreeItemID, TableRow); } else if (InColumnName == ColumnNames.PinHeader) { return SNew(SCurveEditorTreePin, InCurveEditor, InTreeItemID, TableRow); } return nullptr; } void FOutlinerItemModelMixin::CreateCurveModels(TArray>& OutCurveModels) { } bool FOutlinerItemModelMixin::PassesFilter(const FCurveEditorTreeFilter* InFilter) const { if (InFilter->GetType() == ECurveEditorTreeFilterType::Text) { const FCurveEditorTreeTextFilter* Filter = static_cast(InFilter); TViewModelPtr This = AsViewModel()->CastThisShared(); // Must match all text tokens for (const FCurveEditorTreeTextFilterTerm& Term : Filter->GetTerms()) { if (!NodeMatchesTextFilterTerm(This, Term)) { return false; } } return true; } else if (InFilter->GetType() == ISequencerModule::GetSequencerSelectionFilterType()) { const FSequencerSelectionCurveFilter* Filter = static_cast(InFilter); return Filter->Match(AsViewModel()->AsShared()); } return false; } bool FEvaluableOutlinerItemModel::IsDeactivated() const { const TParentFirstChildIterator Descendants = GetDescendantsOfType(true); if (!Descendants) { return false; } bool bNoTrackAreaModels = true; for (const TViewModelPtr& TrackNode : Descendants) { const UMovieSceneTrack* const Track = TrackNode->GetTrack(); if (!IsValid(Track)) { continue; } const TViewModelPtr TrackAreaModel = TrackNode.ImplicitCast(); if (!TrackAreaModel->GetTrackAreaModelList()) { continue; } bNoTrackAreaModels = false; if (const TViewModelPtr TrackRowModel = TrackNode.ImplicitCast()) { if (!Track->IsRowEvalDisabled(TrackNode->GetRowIndex(), /*bInCheckLocal=*/false)) { return false; } } else { if (!Track->IsEvalDisabled(/*bInCheckLocal=*/false)) { return false; } } } return !bNoTrackAreaModels; } void FEvaluableOutlinerItemModel::SetIsDeactivated(const bool bInIsDeactivated) { bool bAnyChanged = false; for (const TViewModelPtr& TrackNode : GetDescendantsOfType(true)) { UMovieSceneTrack* const Track = TrackNode->GetTrack(); if (!IsValid(Track)) { continue; } // Deactive state (dirtying, saved with asset, evaluation) if (const TViewModelPtr TrackRowModel = TrackNode.ImplicitCast()) { if (bInIsDeactivated != Track->IsRowEvalDisabled(TrackNode->GetRowIndex(), /*bInCheckLocal=*/false)) { Track->Modify(); Track->SetRowEvalDisabled(bInIsDeactivated, TrackNode->GetRowIndex()); bAnyChanged = true; } } else { if (bInIsDeactivated != Track->IsEvalDisabled(/*bInCheckLocal=*/false)) { Track->Modify(); Track->SetEvalDisabled(bInIsDeactivated); bAnyChanged = true; } } } if (bAnyChanged) { GetEditor()->GetSequencer()->NotifyMovieSceneDataChanged(EMovieSceneDataChangeType::TrackValueChanged); } } } // namespace Sequencer } // namespace UE #undef LOCTEXT_NAMESPACE