// Copyright Epic Games, Inc. All Rights Reserved. #include "SequencerContextMenus.h" #include "Modules/ModuleManager.h" #include "Styling/AppStyle.h" #include "Decorations/MovieSceneSectionAnchorsDecoration.h" #include "SequencerCommonHelpers.h" #include "SequencerCommands.h" #include "SSequencer.h" #include "IKeyArea.h" #include "SSequencerSection.h" #include "SequencerSettings.h" #include "MVVM/Views/ITrackAreaHotspot.h" #include "SequencerHotspots.h" #include "ScopedTransaction.h" #include "MovieSceneToolHelpers.h" #include "MovieSceneCommonHelpers.h" #include "MovieSceneKeyStruct.h" #include "Framework/Commands/GenericCommands.h" #include "IDetailsView.h" #include "IStructureDetailsView.h" #include "PropertyEditorModule.h" #include "Sections/MovieSceneSubSection.h" #include "Curves/IntegralCurve.h" #include "Editor.h" #include "SequencerUtilities.h" #include "ClassViewerModule.h" #include "Generators/MovieSceneEasingFunction.h" #include "ClassViewerFilter.h" #include "Widgets/Input/SNumericEntryBox.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SSpacer.h" #include "ISequencerChannelInterface.h" #include "Channels/MovieSceneChannelProxy.h" #include "SKeyEditInterface.h" #include "MovieSceneTimeHelpers.h" #include "FrameNumberDetailsCustomization.h" #include "MovieSceneSectionDetailsCustomization.h" #include "MovieSceneSequence.h" #include "MovieScene.h" #include "Channels/MovieSceneChannel.h" #include "MVVM/Extensions/ITrackExtension.h" #include "MVVM/ViewModels/ViewModelIterators.h" #include "MVVM/ViewModels/SectionModel.h" #include "MVVM/ViewModels/TrackModel.h" #include "MVVM/ViewModels/ViewModel.h" #include "MVVM/ViewModels/SequencerEditorViewModel.h" #include "MVVM/Selection/Selection.h" #include "Tracks/MovieScenePropertyTrack.h" #include "Algo/AnyOf.h" #include "IKeyArea.h" #include "SequencerToolMenuContext.h" #include "ToolMenus.h" #define LOCTEXT_NAMESPACE "SequencerContextMenus" static void CreateKeyStructForSelection(TWeakPtr InWeakSequencer, TSharedPtr& OutKeyStruct, TWeakObjectPtr& OutKeyStructSection) { using namespace UE::Sequencer; const TSharedPtr Sequencer = InWeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } const FKeySelection& SelectedKeys = Sequencer->GetViewModel()->GetSelection()->KeySelection; if (SelectedKeys.Num() == 1) { for (FKeyHandle Key : SelectedKeys) { TSharedPtr Channel = SelectedKeys.GetModelForKey(Key); if (Channel) { OutKeyStruct = Channel->GetKeyArea()->GetKeyStruct(Key); OutKeyStructSection = Channel->GetSection(); return; } } } else { TArray KeyHandles; UMovieSceneSection* CommonSection = nullptr; for (FKeyHandle Key : SelectedKeys) { TSharedPtr Channel = SelectedKeys.GetModelForKey(Key); if (Channel) { KeyHandles.Add(Key); if (!CommonSection) { CommonSection = Channel->GetSection(); } else if (CommonSection != Channel->GetSection()) { CommonSection = nullptr; return; } } } if (CommonSection) { OutKeyStruct = CommonSection->GetKeyStruct(KeyHandles); OutKeyStructSection = CommonSection; } } } namespace UE::Sequencer::Private { TSet> GetChannelModels(const TWeakPtr& InWeakSequencer) { TSet> Channels; const TSharedPtr Sequencer = InWeakSequencer.IsValid() ? InWeakSequencer.Pin() : nullptr; if (Sequencer.IsValid()) { const FSequencerSelection& Selection = *Sequencer->GetViewModel()->GetSelection(); for (const TViewModelPtr& Item : Selection.Outliner) { SequencerHelpers::GetAllChannels(Item, Channels); } if (Channels.IsEmpty()) { for (const TWeakViewModelPtr& DisplayNode : Selection.GetNodesWithSelectedKeysOrSections()) { SequencerHelpers::GetAllChannels(DisplayNode.Pin(), Channels); } } } return MoveTemp(Channels); } } void FKeyContextMenu::BuildMenu(FMenuBuilder& MenuBuilder, TSharedPtr MenuExtender, TWeakPtr InWeakSequencer) { TSharedRef Menu = MakeShareable(new FKeyContextMenu(InWeakSequencer)); Menu->PopulateMenu(MenuBuilder, MenuExtender); } void FKeyContextMenu::PopulateMenu(FMenuBuilder& MenuBuilder, TSharedPtr MenuExtender) { using namespace UE::Sequencer; const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } TSharedRef Shared = AsShared(); CreateKeyStructForSelection(WeakSequencer, KeyStruct, KeyStructSection); { ISequencerModule& SequencerModule = FModuleManager::LoadModuleChecked("Sequencer"); FSelectedKeysByChannel SelectedKeysByChannel(Sequencer->GetViewModel()->GetSelection()->KeySelection); TMap> ChannelAndHandlesByType; for (FSelectedChannelInfo& ChannelInfo : SelectedKeysByChannel.SelectedChannels) { FExtendKeyMenuParams ExtendKeyMenuParams; ExtendKeyMenuParams.Section = ChannelInfo.OwningSection; ExtendKeyMenuParams.WeakOwner = ChannelInfo.OwningObject; ExtendKeyMenuParams.Channel = ChannelInfo.Channel; ExtendKeyMenuParams.Handles = MoveTemp(ChannelInfo.KeyHandles); ChannelAndHandlesByType.FindOrAdd(ChannelInfo.Channel.GetChannelTypeName()).Add(MoveTemp(ExtendKeyMenuParams)); } for (auto& Pair : ChannelAndHandlesByType) { ISequencerChannelInterface* ChannelInterface = SequencerModule.FindChannelEditorInterface(Pair.Key); if (ChannelInterface) { ChannelInterface->ExtendKeyMenu_Raw(MenuBuilder, MenuExtender, MoveTemp(Pair.Value), Sequencer); } } } if (KeyStruct.IsValid()) { MenuBuilder.AddSubMenu( LOCTEXT("KeyProperties", "Properties"), LOCTEXT("KeyPropertiesTooltip", "Modify the key properties"), FNewMenuDelegate::CreateLambda([=](FMenuBuilder& SubMenuBuilder){ Shared->AddPropertiesMenu(SubMenuBuilder); }), FUIAction ( FExecuteAction(), // @todo sequencer: only one struct per structure view supported right now :/ FCanExecuteAction::CreateLambda([this]{ return KeyStruct.IsValid(); }) ), NAME_None, EUserInterfaceActionType::Button ); } if (!UToolMenus::Get()->IsMenuRegistered("Sequencer.KeyContextMenu")) { UToolMenu* KeyContextMenu = UToolMenus::Get()->RegisterMenu("Sequencer.KeyContextMenu", NAME_None, EMultiBoxType::Menu); KeyContextMenu->bSearchable = false; KeyContextMenu->AddDynamicSection("Edit", FNewToolMenuDelegate::CreateStatic([](UToolMenu* InMenu){ USequencerToolMenuContext* ContextObject = InMenu->FindContext(); TSharedPtr Sequencer = ContextObject ? ContextObject->WeakSequencer.Pin() : nullptr; if (Sequencer && HotspotCast(Sequencer->GetViewModel()->GetHotspot())) { FToolMenuSection& Section = InMenu->AddSection("SequencerKeyEdit", LOCTEXT("EditMenu", "Edit")); Section.AddMenuEntry(FGenericCommands::Get().Cut); Section.AddMenuEntry(FGenericCommands::Get().Copy); Section.AddMenuEntry(FGenericCommands::Get().Duplicate); } })); FToolMenuSection& KeysSection = KeyContextMenu->AddSection("SequencerKeys", LOCTEXT("KeysMenu", "Keys")); KeysSection.AddMenuEntry(FSequencerCommands::Get().SetKeyTime); KeysSection.AddMenuEntry(FSequencerCommands::Get().Rekey); KeysSection.AddMenuEntry(FSequencerCommands::Get().SnapToFrame); FToolMenuSection& KeysRemovalSection = KeyContextMenu->AddSection("KeyRemoval"); KeysRemovalSection.AddSeparator(NAME_None); // This used to be a command in FSequencerCommands; removed because people were rebinding it and confused that delete, bound to FGenericCommands::Delete, still deleted keys. KeysRemovalSection.AddMenuEntry( NAME_None, LOCTEXT("DeleteKeys.Label", "Delete Keys"), LOCTEXT("DeleteKeys.Description", "Deletes the selected keys"), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(WeakSequencer.Pin().Get(), &FSequencer::DeleteSelectedKeys)) ); } USequencerToolMenuContext* ContextObject = NewObject(); ContextObject->WeakSequencer = Sequencer; FToolMenuContext MenuContext(ContextObject); MenuContext.AppendCommandList(Sequencer->GetCommandBindings(ESequencerCommandBindings::Sequencer)); MenuContext.AddExtender(MenuExtender); MenuBuilder.AddWidget(UToolMenus::Get()->GenerateWidget("Sequencer.KeyContextMenu", MenuContext), FText(), true, false); } void FKeyContextMenu::AddPropertiesMenu(FMenuBuilder& MenuBuilder) { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } auto UpdateAndRetrieveEditData = [this] { FKeyEditData EditData; CreateKeyStructForSelection(WeakSequencer, EditData.KeyStruct, EditData.OwningSection); return EditData; }; MenuBuilder.AddWidget( SNew(SKeyEditInterface, Sequencer.ToSharedRef()) .EditData_Lambda(UpdateAndRetrieveEditData) , FText::GetEmpty(), true); } FSectionContextMenu::FSectionContextMenu(TWeakPtr InWeakSequencer, FFrameTime InMouseDownTime) : WeakSequencer(InWeakSequencer) , MouseDownTime(InMouseDownTime) { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } for (UMovieSceneSection* Section : Sequencer->GetViewModel()->GetSelection()->GetSelectedSections()) { FMovieSceneChannelProxy& ChannelProxy = Section->GetChannelProxy(); for (const FMovieSceneChannelEntry& Entry : ChannelProxy.GetAllEntries()) { FName ChannelTypeName = Entry.GetChannelTypeName(); TArray& SectionArray = SectionsByType.FindOrAdd(ChannelTypeName); SectionArray.Add(Section); TArray& ChannelHandles = ChannelsByType.FindOrAdd(ChannelTypeName); const int32 NumChannels = Entry.GetChannels().Num(); for (int32 Index = 0; Index < NumChannels; ++Index) { ChannelHandles.Add(ChannelProxy.MakeHandle(ChannelTypeName, Index)); } } } } void FSectionContextMenu::BuildMenu(FMenuBuilder& MenuBuilder, TSharedPtr MenuExtender, TWeakPtr InWeakSequencer, FFrameTime InMouseDownTime) { TSharedRef Menu = MakeShareable(new FSectionContextMenu(InWeakSequencer, InMouseDownTime)); Menu->PopulateMenu(MenuBuilder, MenuExtender); } void FSectionContextMenu::BuildKeyEditMenu(FMenuBuilder& MenuBuilder, TWeakPtr InWeakSequencer, FFrameTime InMouseDownTime) { TSharedRef Menu = MakeShareable(new FSectionContextMenu(InWeakSequencer, InMouseDownTime)); Menu->AddKeyInterpolationMenu(MenuBuilder); Menu->AddKeyEditMenu(MenuBuilder); } void FSectionContextMenu::PopulateMenu(FMenuBuilder& MenuBuilder, TSharedPtr MenuExtender) { using namespace UE::Sequencer; const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } // Copy a reference to the context menu by value into each lambda handler to ensure the type stays alive until the menu is closed TSharedRef Shared = AsShared(); // Clean SectionGroups to prevent any potential stale references from affecting the context menu entries Sequencer->GetFocusedMovieSceneSequence()->GetMovieScene()->CleanSectionGroups(); // These are potentially expensive checks in large sequences, and won't change while context menu is open const bool bCanGroup = Sequencer->CanGroupSelectedSections(); const bool bCanUngroup = Sequencer->CanUngroupSelectedSections(); ISequencerModule& SequencerModule = FModuleManager::LoadModuleChecked("Sequencer"); for (auto& Pair : ChannelsByType) { const TArray& Sections = SectionsByType.FindChecked(Pair.Key); ISequencerChannelInterface* ChannelInterface = SequencerModule.FindChannelEditorInterface(Pair.Key); if (ChannelInterface) { TArray> WeakSections; Algo::Transform(Sections, WeakSections, [](UMovieSceneSection* const InSection) { return InSection; }); ChannelInterface->ExtendSectionMenu_Raw(MenuBuilder, MenuExtender, Pair.Value, WeakSections, WeakSequencer); } } MenuBuilder.AddSubMenu( LOCTEXT("SectionProperties", "Properties"), LOCTEXT("SectionPropertiesTooltip", "Modify the section properties"), FNewMenuDelegate::CreateLambda([this](FMenuBuilder& SubMenuBuilder) { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } TArray> Sections; for (TViewModelPtr SectionModel : Sequencer->GetViewModel()->GetSelection()->TrackArea.Filter()) { if (UMovieSceneSection* Section = SectionModel->GetSection()) { Sections.Add(Section); } } SequencerHelpers::BuildEditSectionMenu(Sequencer, Sections, SubMenuBuilder, false); }) ); MenuBuilder.BeginSection("SequencerKeyEdit", LOCTEXT("EditMenu", "Edit")); { TSharedPtr PasteFromHistoryMenu; TSharedPtr PasteMenu; if (Sequencer->GetClipboardStack().Num() != 0) { FPasteContextMenuArgs PasteArgs = FPasteContextMenuArgs::PasteAt(MouseDownTime.FrameNumber); PasteMenu = FPasteContextMenu::CreateMenu(Sequencer, PasteArgs); PasteFromHistoryMenu = FPasteFromHistoryContextMenu::CreateMenu(Sequencer, PasteArgs); } MenuBuilder.AddSubMenu( LOCTEXT("Paste", "Paste"), FText(), FNewMenuDelegate::CreateLambda([=](FMenuBuilder& SubMenuBuilder){ if (PasteMenu.IsValid()) { PasteMenu->PopulateMenu(SubMenuBuilder, MenuExtender); } }), FUIAction ( FExecuteAction(), FCanExecuteAction::CreateLambda([=]{ return PasteMenu.IsValid() && PasteMenu->IsValidPaste(); }) ), NAME_None, EUserInterfaceActionType::Button ); MenuBuilder.AddSubMenu( LOCTEXT("PasteFromHistory", "Paste From History"), FText(), FNewMenuDelegate::CreateLambda([=](FMenuBuilder& SubMenuBuilder){ if (PasteFromHistoryMenu.IsValid()) { PasteFromHistoryMenu->PopulateMenu(SubMenuBuilder, MenuExtender); } }), FUIAction ( FExecuteAction(), FCanExecuteAction::CreateLambda([=]{ return PasteFromHistoryMenu.IsValid(); }) ), NAME_None, EUserInterfaceActionType::Button ); } MenuBuilder.EndSection(); // SequencerKeyEdit MenuBuilder.BeginSection("SequencerChannels", LOCTEXT("ChannelsMenu", "Channels")); { } MenuBuilder.EndSection(); // SequencerChannels MenuBuilder.BeginSection("SequencerSections", LOCTEXT("SectionsMenu", "Sections")); { if (CanSelectAllKeys()) { MenuBuilder.AddMenuEntry( LOCTEXT("SelectAllKeys", "Select All Keys"), LOCTEXT("SelectAllKeysTooltip", "Select all keys in section"), FSlateIcon(), FUIAction(FExecuteAction::CreateLambda([=] { return Shared->SelectAllKeys(); })) ); MenuBuilder.AddMenuEntry( LOCTEXT("CopyAllKeys", "Copy All Keys"), LOCTEXT("CopyAllKeysTooltip", "Copy all keys in section"), FSlateIcon(), FUIAction(FExecuteAction::CreateLambda([=] { return Shared->CopyAllKeys(); })) ); } if (SelectionSupportsScaling()) { MenuBuilder.AddSubMenu( LOCTEXT("ScalingSection", "Scaling"), LOCTEXT("ScalingSectionTooltip", "Options for scaling this section"), FNewMenuDelegate::CreateLambda([=](FMenuBuilder& InMenuBuilder) { Shared->AddScalingMenu(InMenuBuilder); })); } MenuBuilder.AddSubMenu( LOCTEXT("EditSection", "Edit"), LOCTEXT("EditSectionTooltip", "Edit section"), FNewMenuDelegate::CreateLambda([=](FMenuBuilder& InMenuBuilder) { Shared->AddEditMenu(InMenuBuilder); })); MenuBuilder.AddSubMenu( LOCTEXT("OrderSection", "Order"), LOCTEXT("OrderSectionTooltip", "Order section"), FNewMenuDelegate::CreateLambda([=](FMenuBuilder& SubMenuBuilder) { Shared->AddOrderMenu(SubMenuBuilder); })); if (GetSupportedBlendTypes().Num() > 1) { MenuBuilder.AddSubMenu( LOCTEXT("BlendTypeSection", "Blend Type"), LOCTEXT("BlendTypeSectionTooltip", "Change the way in which this section blends with other sections of the same type"), FNewMenuDelegate::CreateLambda([=](FMenuBuilder& SubMenuBuilder) { Shared->AddBlendTypeMenu(SubMenuBuilder); })); } MenuBuilder.AddMenuEntry( LOCTEXT("ToggleSectionActive", "Active"), LOCTEXT("ToggleSectionActiveTooltip", "Toggle section active/inactive"), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([=] { Shared->ToggleSectionActive(); }), FCanExecuteAction(), FIsActionChecked::CreateLambda([=] { return Shared->IsSectionActive(); })), NAME_None, EUserInterfaceActionType::ToggleButton ); MenuBuilder.AddMenuEntry( NSLOCTEXT("Sequencer", "ToggleSectionLocked", "Locked"), NSLOCTEXT("Sequencer", "ToggleSectionLockedTooltip", "Toggle section locked/unlocked"), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([=] { Shared->ToggleSectionLocked(); }), FCanExecuteAction(), FIsActionChecked::CreateLambda([=] { return Shared->IsSectionLocked(); })), NAME_None, EUserInterfaceActionType::ToggleButton ); MenuBuilder.AddMenuEntry( LOCTEXT("GroupSections", "Group"), LOCTEXT("GroupSectionsTooltip", "Group selected sections together so that when any section is moved, all sections in that group move together."), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(Sequencer.ToSharedRef(), &FSequencer::GroupSelectedSections), FCanExecuteAction::CreateLambda([bCanGroup] { return bCanGroup; }) ), NAME_None, EUserInterfaceActionType::Button ); MenuBuilder.AddMenuEntry( LOCTEXT("UngroupSections", "Ungroup"), LOCTEXT("UngroupSectionsTooltip", "Ungroup selected sections"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(Sequencer.ToSharedRef(), &FSequencer::UngroupSelectedSections), FCanExecuteAction::CreateLambda([bCanUngroup] { return bCanUngroup; }) ), NAME_None, EUserInterfaceActionType::Button ); // @todo Sequencer this should delete all selected sections // delete/selection needs to be rethought in general MenuBuilder.AddMenuEntry( LOCTEXT("DeleteSection", "Delete"), LOCTEXT("DeleteSectionToolTip", "Deletes this section"), FSlateIcon(), FUIAction(FExecuteAction::CreateLambda([=] { return Shared->DeleteSection(); })) ); if (CanSetSectionToKey()) { MenuBuilder.AddMenuEntry( LOCTEXT("KeySection", "Key This Section"), LOCTEXT("KeySection_ToolTip", "This section will get changed when we modify the property externally"), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([=] { return Shared->SetSectionToKey(); }), FCanExecuteAction(), FIsActionChecked::CreateLambda([=] { return Shared->IsSectionToKey(); }) ), NAME_None, EUserInterfaceActionType::Check ); } } MenuBuilder.EndSection(); // SequencerSections } bool FSectionContextMenu::SelectionSupportsScaling() const { TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer) { return false; } TSet CompatibleDecorations; for (UMovieSceneSection* Section : Sequencer->GetViewModel()->GetSelection()->GetSelectedSections()) { if (Section) { if (Cast(Section)) { return true; } Section->GetCompatibleUserDecorations(CompatibleDecorations); } } return CompatibleDecorations.Contains(UMovieSceneSectionAnchorsDecoration::StaticClass()); } void FSectionContextMenu::AddScalingMenu(FMenuBuilder& MenuBuilder) { // Copy a reference to the context menu by value into each lambda handler to ensure the type stays alive until the menu is closed TSharedRef Shared = AsShared(); auto GetScalingDriverCheckState = [Shared] { TOptional CheckState; TSharedPtr Sequencer = Shared->WeakSequencer.Pin(); if (Sequencer) { for (UMovieSceneSection* Section : Sequencer->GetViewModel()->GetSelection()->GetSelectedSections()) { ECheckBoxState ThisCheckState = (Section && Section->FindDecoration() != nullptr) ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; if (!CheckState.IsSet()) { CheckState = ThisCheckState; } else if (CheckState.GetValue() != ThisCheckState) { return ECheckBoxState::Undetermined; } } } return CheckState.Get(ECheckBoxState::Unchecked); }; MenuBuilder.AddMenuEntry( LOCTEXT("ScalingDriver", "Scaling Driver"), LOCTEXT("ScalingDriverTooltip", "Defines whether this section will rescale the sequence based on its start/end times when being played back."), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([Shared, GetScalingDriverCheckState] { TSharedPtr Sequencer = Shared->WeakSequencer.Pin(); if (Sequencer) { ECheckBoxState State = GetScalingDriverCheckState(); FScopedTransaction Transaction(LOCTEXT("ToggleScaling", "Toggle Scaling")); for (UMovieSceneSection* Section : Sequencer->GetViewModel()->GetSelection()->GetSelectedSections()) { if (Section) { Section->Modify(); if (State == ECheckBoxState::Checked) { Section->RemoveDecoration(); } else { Section->GetOrCreateDecoration(); } } } } }), FCanExecuteAction(), FGetActionCheckState::CreateLambda(GetScalingDriverCheckState) ), NAME_None, EUserInterfaceActionType::ToggleButton ); } void FSectionContextMenu::AddEditMenu(FMenuBuilder& MenuBuilder) { // Copy a reference to the context menu by value into each lambda handler to ensure the type stays alive until the menu is closed TSharedRef Shared = AsShared(); MenuBuilder.BeginSection("Trimming", LOCTEXT("TrimmingSectionMenu", "Trimming")); MenuBuilder.AddMenuEntry(FSequencerCommands::Get().TrimSectionLeft); MenuBuilder.AddMenuEntry(FSequencerCommands::Get().TrimSectionRight); MenuBuilder.AddMenuEntry(FSequencerCommands::Get().SplitSection); MenuBuilder.AddMenuEntry( LOCTEXT("DeleteKeysWhenTrimming", "Delete Keys"), LOCTEXT("DeleteKeysWhenTrimmingTooltip", "Delete keys outside of the trimmed range"), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([this] { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } Sequencer->GetSequencerSettings()->SetDeleteKeysWhenTrimming(!Sequencer->GetSequencerSettings()->GetDeleteKeysWhenTrimming()); }), FCanExecuteAction(), FIsActionChecked::CreateLambda([this] { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return false; } return Sequencer->GetSequencerSettings()->GetDeleteKeysWhenTrimming(); })), NAME_None, EUserInterfaceActionType::ToggleButton ); MenuBuilder.EndSection(); MenuBuilder.AddMenuSeparator(); MenuBuilder.AddMenuEntry( LOCTEXT("AutoSizeSection", "Auto Size"), LOCTEXT("AutoSizeSectionTooltip", "Auto size the section length to the duration of the source of this section (ie. audio, animation or shot length)"), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([=]{ Shared->AutoSizeSection(); }), FCanExecuteAction::CreateLambda([=]{ return Shared->CanAutoSize(); })) ); AddKeyInterpolationMenu(MenuBuilder); AddKeyEditMenu(MenuBuilder); } void FSectionContextMenu::AddKeyInterpolationMenu(FMenuBuilder& MenuBuilder) { // Copy a reference to the context menu by value into each lambda handler to ensure the type stays alive until the menu is closed TSharedRef Shared = AsShared(); MenuBuilder.BeginSection(TEXT("SequencerInterpolation"), LOCTEXT("KeyInterpolationMenu", "Key Interpolation")); MenuBuilder.AddMenuEntry( LOCTEXT("SetKeyInterpolationSmartAuto", "Cubic (Smart Auto)"), LOCTEXT("SetKeyInterpolationSmartAutoTooltip", "Set key interpolation to smart auto"), FSlateIcon(FAppStyle::GetAppStyleSetName(), TEXT("Sequencer.IconKeySmartAuto")), FUIAction( FExecuteAction::CreateLambda([=] { Shared->SetInterpTangentMode(RCIM_Cubic, RCTM_SmartAuto); }), FCanExecuteAction::CreateLambda([=] { return Shared->CanSetInterpTangentMode(); })) ); MenuBuilder.AddMenuEntry( LOCTEXT("SetKeyInterpolationAuto", "Cubic (Auto)"), LOCTEXT("SetKeyInterpolationAutoTooltip", "Set key interpolation to auto"), FSlateIcon(FAppStyle::GetAppStyleSetName(), TEXT("Sequencer.IconKeyAuto")), FUIAction( FExecuteAction::CreateLambda([=]{ Shared->SetInterpTangentMode(RCIM_Cubic, RCTM_Auto); }), FCanExecuteAction::CreateLambda([=]{ return Shared->CanSetInterpTangentMode(); }) ) ); MenuBuilder.AddMenuEntry( LOCTEXT("SetKeyInterpolationUser", "Cubic (User)"), LOCTEXT("SetKeyInterpolationUserTooltip", "Set key interpolation to user"), FSlateIcon(FAppStyle::GetAppStyleSetName(), TEXT("Sequencer.IconKeyUser")), FUIAction( FExecuteAction::CreateLambda([=]{ Shared->SetInterpTangentMode(RCIM_Cubic, RCTM_User); }), FCanExecuteAction::CreateLambda([=]{ return Shared->CanSetInterpTangentMode(); }) ) ); MenuBuilder.AddMenuEntry( LOCTEXT("SetKeyInterpolationBreak", "Cubic (Break)"), LOCTEXT("SetKeyInterpolationBreakTooltip", "Set key interpolation to break"), FSlateIcon(FAppStyle::GetAppStyleSetName(), TEXT("Sequencer.IconKeyBreak")), FUIAction( FExecuteAction::CreateLambda([=]{ Shared->SetInterpTangentMode(RCIM_Cubic, RCTM_Break); }), FCanExecuteAction::CreateLambda([=]{ return Shared->CanSetInterpTangentMode(); }) ) ); MenuBuilder.AddMenuEntry( LOCTEXT("SetKeyInterpolationLinear", "Linear"), LOCTEXT("SetKeyInterpolationLinearTooltip", "Set key interpolation to linear"), FSlateIcon(FAppStyle::GetAppStyleSetName(), TEXT("Sequencer.IconKeyLinear")), FUIAction( FExecuteAction::CreateLambda([=]{ Shared->SetInterpTangentMode(RCIM_Linear, RCTM_Auto); }), FCanExecuteAction::CreateLambda([=]{ return Shared->CanSetInterpTangentMode(); }) ) ); MenuBuilder.AddMenuEntry( LOCTEXT("SetKeyInterpolationConstant", "Constant"), LOCTEXT("SetKeyInterpolationConstantTooltip", "Set key interpolation to constant"), FSlateIcon(FAppStyle::GetAppStyleSetName(), TEXT("Sequencer.IconKeyConstant")), FUIAction( FExecuteAction::CreateLambda([=]{ Shared->SetInterpTangentMode(RCIM_Constant, RCTM_Auto); }), FCanExecuteAction::CreateLambda([=]{ return Shared->CanSetInterpTangentMode(); }) ) ); MenuBuilder.EndSection(); } void FSectionContextMenu::AddKeyEditMenu(FMenuBuilder& MenuBuilder) { // Copy a reference to the context menu by value into each lambda handler to ensure the type stays alive until the menu is closed TSharedRef Shared = AsShared(); MenuBuilder.BeginSection(TEXT("Key Editing"), LOCTEXT("KeyEditingSectionMenus", "Key Editing")); MenuBuilder.AddMenuEntry( LOCTEXT("ReduceKeysSection", "Reduce Keys"), LOCTEXT("ReduceKeysTooltip", "Reduce keys in this section"), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([=]{ Shared->ReduceKeys(); }), FCanExecuteAction::CreateLambda([=]{ return Shared->CanReduceKeys(); })) ); auto OnReduceKeysToleranceChanged = [this](float InNewValue) { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } Sequencer->GetSequencerSettings()->SetReduceKeysTolerance(InNewValue); }; MenuBuilder.AddWidget( SNew(SHorizontalBox) + SHorizontalBox::Slot() [ SNew(SSpacer) ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(SSpinBox) .Style(&FAppStyle::GetWidgetStyle(TEXT("Sequencer.HyperlinkSpinBox"))) .OnValueCommitted_Lambda([OnReduceKeysToleranceChanged](float Value, ETextCommit::Type) { OnReduceKeysToleranceChanged(Value); }) .OnValueChanged_Lambda(OnReduceKeysToleranceChanged) .MinValue(0) .MaxValue(TOptional()) .Value_Lambda([this]() -> float { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return 0; } return Sequencer->GetSequencerSettings()->GetReduceKeysTolerance(); }) ], LOCTEXT("ReduceKeysTolerance", "Tolerance"), true); MenuBuilder.EndSection(); } FMovieSceneBlendTypeField FSectionContextMenu::GetSupportedBlendTypes() const { FMovieSceneBlendTypeField BlendTypes = FMovieSceneBlendTypeField::All(); if (const TSharedPtr Sequencer = WeakSequencer.Pin()) { for (UMovieSceneSection* Section : Sequencer->GetViewModel()->GetSelection()->GetSelectedSections()) { // Remove unsupported blend types BlendTypes.Remove(Section->GetSupportedBlendTypes().Invert()); } } return BlendTypes; } void FSectionContextMenu::AddOrderMenu(FMenuBuilder& MenuBuilder) { // Copy a reference to the context menu by value into each lambda handler to ensure the type stays alive until the menu is closed TSharedRef Shared = AsShared(); MenuBuilder.AddMenuEntry(LOCTEXT("BringToFront", "Bring To Front"), FText(), FSlateIcon(), FUIAction(FExecuteAction::CreateLambda([=]{ return Shared->BringToFront(); }))); MenuBuilder.AddMenuEntry(LOCTEXT("SendToBack", "Send To Back"), FText(), FSlateIcon(), FUIAction(FExecuteAction::CreateLambda([=]{ return Shared->SendToBack(); }))); MenuBuilder.AddMenuEntry(LOCTEXT("BringForward", "Bring Forward"), FText(), FSlateIcon(), FUIAction(FExecuteAction::CreateLambda([=]{ return Shared->BringForward(); }))); MenuBuilder.AddMenuEntry(LOCTEXT("SendBackward", "Send Backward"), FText(), FSlateIcon(), FUIAction(FExecuteAction::CreateLambda([=]{ return Shared->SendBackward(); }))); } void FSectionContextMenu::AddBlendTypeMenu(FMenuBuilder& MenuBuilder) { using namespace UE::Sequencer; const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } TArray> Sections; for (TViewModelPtr SectionModel : Sequencer->GetViewModel()->GetSelection()->TrackArea.Filter()) { if (UMovieSceneSection* Section = SectionModel->GetSection()) { Sections.Add(Section); } } FSequencerUtilities::PopulateMenu_SetBlendType(MenuBuilder, Sections, WeakSequencer); } void FSectionContextMenu::SelectAllKeys() { using namespace UE::Sequencer; const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } const TSet> Channels = Private::GetChannelModels(WeakSequencer); if (!Channels.IsEmpty()) { FSequencerSelection& Selection = *Sequencer->GetViewModel()->GetSelection(); FSelectionEventSuppressor EventSuppressor = Selection.SuppressEvents(); TArray HandlesScratch; for (const TSharedPtr& Channel: Channels) { if (Channel->GetLinkedOutlinerItem() && !Channel->GetLinkedOutlinerItem()->IsFilteredOut()) { HandlesScratch.Reset(); Channel->GetKeyArea()->GetKeyHandles(HandlesScratch); Selection.KeySelection.SelectRange(Channel, HandlesScratch); } } } } void FSectionContextMenu::CopyAllKeys() { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } SelectAllKeys(); Sequencer->CopySelectedKeys(); } void FSectionContextMenu::SetSectionToKey() { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } TSet SelectedSections = Sequencer->GetViewModel()->GetSelection()->GetSelectedSections(); if (SelectedSections.Num() != 1) { return; } const bool bToggle = IsSectionToKey(); UMovieSceneSection* Section = *SelectedSections.CreateConstIterator(); UMovieSceneTrack* Track = Section->GetTypedOuter(); if (Track) { FScopedTransaction Transaction(LOCTEXT("SetSectionToKey", "Set Section To Key")); Track->Modify(); Track->SetSectionToKey(bToggle ? nullptr : Section); } } bool FSectionContextMenu::IsSectionToKey() const { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return false; } for (UMovieSceneSection* Section : Sequencer->GetViewModel()->GetSelection()->GetSelectedSections()) { UMovieSceneTrack* Track = Section->GetTypedOuter(); if (Track && Track->GetSectionToKey() != Section) { return false; } } return true; } bool FSectionContextMenu::CanSetSectionToKey() const { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return false; } TSet SelectedSections = Sequencer->GetViewModel()->GetSelection()->GetSelectedSections(); if (SelectedSections.Num() != 1) { return false; } UMovieSceneSection* Section = *SelectedSections.CreateConstIterator(); UMovieSceneTrack* Track = Section->GetTypedOuter(); if (Track && Section->GetBlendType().IsValid() && Section->GetBlendType().Get() != EMovieSceneBlendType::Invalid) { return true; } return false; } bool FSectionContextMenu::CanSelectAllKeys() const { for (const TTuple>& Pair : ChannelsByType) { for (const FMovieSceneChannelHandle& Handle : Pair.Value) { const FMovieSceneChannel* Channel = Handle.Get(); if (Channel && Channel->GetNumKeys() != 0) { return true; } } } return false; } void FSectionContextMenu::AutoSizeSection() { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } FScopedTransaction AutoSizeSectionTransaction(LOCTEXT("AutoSizeSection_Transaction", "Auto Size Section")); for (UMovieSceneSection* Section : Sequencer->GetViewModel()->GetSelection()->GetSelectedSections()) { if (Section && Section->GetAutoSizeRange().IsSet()) { TOptional > DefaultSectionLength = Section->GetAutoSizeRange(); if (DefaultSectionLength.IsSet()) { Section->SetRange(DefaultSectionLength.GetValue()); } } } Sequencer->NotifyMovieSceneDataChanged( EMovieSceneDataChangeType::MovieSceneStructureItemAdded ); } void FSectionContextMenu::ReduceKeys() { using namespace UE::Sequencer; const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } const TSet> ChannelModels = Private::GetChannelModels(WeakSequencer); if (ChannelModels.IsEmpty()) { return; } TSet Sections; TSet Channels; Channels.Reserve(ChannelModels.Num()); for (const TSharedPtr& ChannelModel: ChannelModels) { if (ChannelModel->GetLinkedOutlinerItem() && !ChannelModel->GetLinkedOutlinerItem()->IsFilteredOut()) { const TSharedPtr KeyArea = ChannelModel->GetKeyArea(); if (KeyArea.IsValid()) { FMovieSceneChannel* Channel = KeyArea->GetChannel().Get(); UMovieSceneSection* Section = KeyArea->GetOwningSection(); if (Channel && Section) { Channels.Add(Channel); Sections.Add(Section); } } } } if (!Sections.IsEmpty()) { FScopedTransaction ReduceKeysTransaction(LOCTEXT("ReduceKeys_Transaction", "Reduce Keys")); for (UMovieSceneSection* Section: Sections) { Section->Modify(); } FKeyDataOptimizationParams Params; Params.bAutoSetInterpolation = true; Params.Tolerance = Sequencer->GetSequencerSettings()->GetReduceKeysTolerance(); for (FMovieSceneChannel* Channel: Channels) { Channel->Optimize(Params); } Sequencer->NotifyMovieSceneDataChanged( EMovieSceneDataChangeType::TrackValueChanged ); } } bool FSectionContextMenu::CanAutoSize() const { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return false; } for (UMovieSceneSection* Section : Sequencer->GetViewModel()->GetSelection()->GetSelectedSections()) { if (Section && Section->GetAutoSizeRange().IsSet()) { return true; } } return false; } bool FSectionContextMenu::CanReduceKeys() const { using namespace UE::Sequencer; const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return false; } TSet > KeyAreas; for (const TWeakPtr& WeakItem : Sequencer->GetViewModel()->GetSelection()->Outliner) { SequencerHelpers::GetAllKeyAreas(WeakItem.Pin(), KeyAreas); } if (KeyAreas.Num() == 0) { for (const TWeakViewModelPtr& DisplayNode : Sequencer->GetViewModel()->GetSelection()->GetNodesWithSelectedKeysOrSections()) { SequencerHelpers::GetAllKeyAreas(DisplayNode.Pin(), KeyAreas); } } return KeyAreas.Num() != 0; } void FSectionContextMenu::SetInterpTangentMode(ERichCurveInterpMode InterpMode, ERichCurveTangentMode TangentMode) { using namespace UE::Sequencer; const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } FScopedTransaction SetInterpTangentModeTransaction(LOCTEXT("SetInterpTangentMode_Transaction", "Set Interpolation and Tangent Mode")); TSet > KeyAreas; for (const TWeakPtr& WeakItem : Sequencer->GetViewModel()->GetSelection()->Outliner) { SequencerHelpers::GetAllKeyAreas(WeakItem.Pin(), KeyAreas); } if (KeyAreas.Num() == 0) { for (const TWeakViewModelPtr& DisplayNode : Sequencer->GetViewModel()->GetSelection()->GetNodesWithSelectedKeysOrSections()) { SequencerHelpers::GetAllKeyAreas(DisplayNode.Pin(), KeyAreas); } } bool bAnythingChanged = false; for (TSharedPtr KeyArea : KeyAreas) { if (KeyArea.IsValid()) { if (UMovieSceneSignedObject* OwningObject = Cast(KeyArea->GetOwningObject())) { OwningObject->Modify(); } FMovieSceneChannelHandle Handle = KeyArea->GetChannel(); if (Handle.GetChannelTypeName() == FMovieSceneFloatChannel::StaticStruct()->GetFName()) { FMovieSceneFloatChannel* FloatChannel = static_cast(Handle.Get()); TMovieSceneChannelData ChannelData = FloatChannel->GetData(); TArrayView Values = ChannelData.GetValues(); for (int32 KeyIndex = 0; KeyIndex < FloatChannel->GetNumKeys(); ++KeyIndex) { Values[KeyIndex].InterpMode = InterpMode; Values[KeyIndex].TangentMode = TangentMode; bAnythingChanged = true; } FloatChannel->AutoSetTangents(); } else if (Handle.GetChannelTypeName() == FMovieSceneDoubleChannel::StaticStruct()->GetFName()) { FMovieSceneDoubleChannel* DoubleChannel = static_cast(Handle.Get()); TMovieSceneChannelData ChannelData = DoubleChannel->GetData(); TArrayView Values = ChannelData.GetValues(); for (int32 KeyIndex = 0; KeyIndex < DoubleChannel->GetNumKeys(); ++KeyIndex) { Values[KeyIndex].InterpMode = InterpMode; Values[KeyIndex].TangentMode = TangentMode; bAnythingChanged = true; } DoubleChannel->AutoSetTangents(); } } } if (bAnythingChanged) { Sequencer->NotifyMovieSceneDataChanged( EMovieSceneDataChangeType::TrackValueChanged ); } } bool FSectionContextMenu::CanSetInterpTangentMode() const { using namespace UE::Sequencer; const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return false; } TSet > KeyAreas; for (const TWeakPtr& WeakItem : Sequencer->GetViewModel()->GetSelection()->Outliner) { SequencerHelpers::GetAllKeyAreas(WeakItem.Pin(), KeyAreas); } if (KeyAreas.Num() == 0) { for (const TWeakViewModelPtr& DisplayNode : Sequencer->GetViewModel()->GetSelection()->GetNodesWithSelectedKeysOrSections()) { SequencerHelpers::GetAllKeyAreas(DisplayNode.Pin(), KeyAreas); } } for (TSharedPtr KeyArea : KeyAreas) { if (KeyArea.IsValid()) { FMovieSceneChannelHandle Handle = KeyArea->GetChannel(); return (Handle.GetChannelTypeName() == FMovieSceneFloatChannel::StaticStruct()->GetFName() || Handle.GetChannelTypeName() == FMovieSceneDoubleChannel::StaticStruct()->GetFName()); } } return false; } void FSectionContextMenu::ToggleSectionActive() { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } FScopedTransaction ToggleSectionActiveTransaction( LOCTEXT("ToggleSectionActive_Transaction", "Toggle Section Active") ); bool bIsActive = !IsSectionActive(); bool bAnythingChanged = false; for (UMovieSceneSection* Section : Sequencer->GetViewModel()->GetSelection()->GetSelectedSections()) { bAnythingChanged = true; Section->Modify(); Section->SetIsActive(bIsActive); } if (bAnythingChanged) { Sequencer->NotifyMovieSceneDataChanged( EMovieSceneDataChangeType::TrackValueChanged ); } else { ToggleSectionActiveTransaction.Cancel(); } } bool FSectionContextMenu::IsSectionActive() const { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return false; } // Active only if all are active for (UMovieSceneSection* Section : Sequencer->GetViewModel()->GetSelection()->GetSelectedSections()) { if (Section && !Section->IsActive()) { return false; } } return true; } void FSectionContextMenu::ToggleSectionLocked() { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } FScopedTransaction ToggleSectionLockedTransaction( NSLOCTEXT("Sequencer", "ToggleSectionLocked_Transaction", "Toggle Section Locked") ); bool bIsLocked = !IsSectionLocked(); bool bAnythingChanged = false; for (UMovieSceneSection* Section : Sequencer->GetViewModel()->GetSelection()->GetSelectedSections()) { if (Section) { bAnythingChanged = true; Section->Modify(); Section->SetIsLocked(bIsLocked); } } if (bAnythingChanged) { Sequencer->NotifyMovieSceneDataChanged( EMovieSceneDataChangeType::TrackValueChanged ); } else { ToggleSectionLockedTransaction.Cancel(); } } bool FSectionContextMenu::IsSectionLocked() const { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return false; } // Locked only if all are locked for (UMovieSceneSection* Section : Sequencer->GetViewModel()->GetSelection()->GetSelectedSections()) { if (Section && !Section->IsLocked()) { return false; } } return true; } void FSectionContextMenu::DeleteSection() { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } Sequencer->DeleteSections(Sequencer->GetViewModel()->GetSelection()->GetSelectedSections()); } /** Information pertaining to a specific row in a track, required for z-ordering operations */ struct FTrackSectionRow { /** The minimum z-order value for all the sections in this row */ int32 MinOrderValue; /** The maximum z-order value for all the sections in this row */ int32 MaxOrderValue; /** All the sections contained in this row */ TArray Sections; /** A set of sections that are to be operated on */ TSet SectionToReOrder; void AddSection(UMovieSceneSection* InSection) { Sections.Add(InSection); MinOrderValue = FMath::Min(MinOrderValue, InSection->GetOverlapPriority()); MaxOrderValue = FMath::Max(MaxOrderValue, InSection->GetOverlapPriority()); } }; /** Generate the data required for re-ordering rows based on the current sequencer selection */ /** @note: Produces a map of track -> rows, keyed on row index. Only returns rows that contain selected sections */ TMap> GenerateTrackRowsFromSelection(FSequencer& Sequencer) { TMap> TrackRows; for (UMovieSceneSection* Section : Sequencer.GetViewModel()->GetSelection()->GetSelectedSections()) { UMovieSceneTrack* Track = Section->GetTypedOuter(); if (!Track) { continue; } FTrackSectionRow& Row = TrackRows.FindOrAdd(Track).FindOrAdd(Section->GetRowIndex()); Row.SectionToReOrder.Add(Section); } // Now ensure all rows that we're operating on are fully populated for (auto& Pair : TrackRows) { UMovieSceneTrack* Track = Pair.Key; for (auto& RowPair : Pair.Value) { const int32 RowIndex = RowPair.Key; for (UMovieSceneSection* Section : Track->GetAllSections()) { if (Section->GetRowIndex() == RowIndex) { RowPair.Value.AddSection(Section); } } } } return TrackRows; } /** Modify all the sections contained within the specified data structure */ void ModifySections(TMap>& TrackRows) { for (auto& Pair : TrackRows) { UMovieSceneTrack* Track = Pair.Key; for (auto& RowPair : Pair.Value) { for (UMovieSceneSection* Section : RowPair.Value.Sections) { Section->Modify(); } } } } void FSectionContextMenu::BringToFront() { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } TMap> TrackRows = GenerateTrackRowsFromSelection(*Sequencer.Get()); if (TrackRows.Num() == 0) { return; } FScopedTransaction Transaction(LOCTEXT("BringToFrontTransaction", "Bring to Front")); ModifySections(TrackRows); for (auto& Pair : TrackRows) { UMovieSceneTrack* Track = Pair.Key; TMap& Rows = Pair.Value; for (auto& RowPair : Rows) { FTrackSectionRow& Row = RowPair.Value; Row.Sections.StableSort([&](UMovieSceneSection& A, UMovieSceneSection& B){ bool bIsActiveA = Row.SectionToReOrder.Contains(&A); bool bIsActiveB = Row.SectionToReOrder.Contains(&B); // Sort secondarily on overlap priority if (bIsActiveA == bIsActiveB) { return A.GetOverlapPriority() < B.GetOverlapPriority(); } // Sort and primarily on whether we're sending to the back or not (bIsActive) else { return !bIsActiveA; } }); int32 CurrentPriority = Row.MinOrderValue; for (UMovieSceneSection* Section : Row.Sections) { Section->SetOverlapPriority(CurrentPriority++); } } } Sequencer->SetLocalTimeDirectly(Sequencer->GetLocalTime().Time); } void FSectionContextMenu::SendToBack() { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } TMap> TrackRows = GenerateTrackRowsFromSelection(*Sequencer.Get()); if (TrackRows.Num() == 0) { return; } FScopedTransaction Transaction(LOCTEXT("SendToBackTransaction", "Send to Back")); ModifySections(TrackRows); for (auto& Pair : TrackRows) { UMovieSceneTrack* Track = Pair.Key; TMap& Rows = Pair.Value; for (auto& RowPair : Rows) { FTrackSectionRow& Row = RowPair.Value; Row.Sections.StableSort([&](UMovieSceneSection& A, UMovieSceneSection& B){ bool bIsActiveA = Row.SectionToReOrder.Contains(&A); bool bIsActiveB = Row.SectionToReOrder.Contains(&B); // Sort secondarily on overlap priority if (bIsActiveA == bIsActiveB) { return A.GetOverlapPriority() < B.GetOverlapPriority(); } // Sort and primarily on whether we're bringing to the front or not (bIsActive) else { return bIsActiveA; } }); int32 CurrentPriority = Row.MinOrderValue; for (UMovieSceneSection* Section : Row.Sections) { Section->SetOverlapPriority(CurrentPriority++); } } } Sequencer->SetLocalTimeDirectly(Sequencer->GetLocalTime().Time); } void FSectionContextMenu::BringForward() { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } TMap> TrackRows = GenerateTrackRowsFromSelection(*Sequencer.Get()); if (TrackRows.Num() == 0) { return; } FScopedTransaction Transaction(LOCTEXT("BringForwardTransaction", "Bring Forward")); ModifySections(TrackRows); for (auto& Pair : TrackRows) { UMovieSceneTrack* Track = Pair.Key; TMap& Rows = Pair.Value; for (auto& RowPair : Rows) { FTrackSectionRow& Row = RowPair.Value; Row.Sections.Sort([&](UMovieSceneSection& A, UMovieSceneSection& B){ return A.GetOverlapPriority() < B.GetOverlapPriority(); }); for (int32 SectionIndex = Row.Sections.Num() - 2; SectionIndex > 0; --SectionIndex) { UMovieSceneSection* ThisSection = Row.Sections[SectionIndex]; if (Row.SectionToReOrder.Contains(ThisSection)) { UMovieSceneSection* OtherSection = Row.Sections[SectionIndex + 1]; Row.Sections.Swap(SectionIndex, SectionIndex+1); const int32 SwappedPriority = OtherSection->GetOverlapPriority(); OtherSection->SetOverlapPriority(ThisSection->GetOverlapPriority()); ThisSection->SetOverlapPriority(SwappedPriority); } } } } Sequencer->SetLocalTimeDirectly(Sequencer->GetLocalTime().Time); } void FSectionContextMenu::SendBackward() { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } TMap> TrackRows = GenerateTrackRowsFromSelection(*Sequencer.Get()); if (TrackRows.Num() == 0) { return; } FScopedTransaction Transaction(LOCTEXT("SendBackwardTransaction", "Send Backward")); ModifySections(TrackRows); for (auto& Pair : TrackRows) { UMovieSceneTrack* Track = Pair.Key; TMap& Rows = Pair.Value; for (auto& RowPair : Rows) { FTrackSectionRow& Row = RowPair.Value; Row.Sections.Sort([&](UMovieSceneSection& A, UMovieSceneSection& B){ return A.GetOverlapPriority() < B.GetOverlapPriority(); }); for (int32 SectionIndex = 1; SectionIndex < Row.Sections.Num(); ++SectionIndex) { UMovieSceneSection* ThisSection = Row.Sections[SectionIndex]; if (Row.SectionToReOrder.Contains(ThisSection)) { UMovieSceneSection* OtherSection = Row.Sections[SectionIndex - 1]; Row.Sections.Swap(SectionIndex, SectionIndex - 1); const int32 SwappedPriority = OtherSection->GetOverlapPriority(); OtherSection->SetOverlapPriority(ThisSection->GetOverlapPriority()); ThisSection->SetOverlapPriority(SwappedPriority); } } } } Sequencer->SetLocalTimeDirectly(Sequencer->GetLocalTime().Time); } bool FPasteContextMenu::BuildMenu(FMenuBuilder& MenuBuilder, TSharedPtr MenuExtender, TWeakPtr InWeakSequencer, const FPasteContextMenuArgs& Args) { TSharedRef Menu = MakeShareable(new FPasteContextMenu(InWeakSequencer, Args)); Menu->Setup(); if (!Menu->IsValidPaste()) { return false; } Menu->PopulateMenu(MenuBuilder, MenuExtender); return true; } TSharedRef FPasteContextMenu::CreateMenu(TWeakPtr InWeakSequencer, const FPasteContextMenuArgs& Args) { TSharedRef Menu = MakeShareable(new FPasteContextMenu(InWeakSequencer, Args)); Menu->Setup(); return Menu; } TArray> KeyAreaNodesBuffer; void FPasteContextMenu::GatherPasteDestinationsForNode(const UE::Sequencer::TViewModelPtr& InNode, UMovieSceneSection* InSection, const FName& CurrentScope, TMap& Map) { using namespace UE::Sequencer; KeyAreaNodesBuffer.Reset(); for (const TViewModelPtr& ChannelNode : InNode.AsModel()->GetDescendantsOfType(true)) { KeyAreaNodesBuffer.Add(ChannelNode); } if (!KeyAreaNodesBuffer.Num()) { return; } FName ThisScope; { FString ThisScopeString; if (!CurrentScope.IsNone()) { ThisScopeString.Append(CurrentScope.ToString()); ThisScopeString.AppendChar('.'); } ThisScopeString.Append(InNode->GetIdentifier().ToString()); ThisScope = *ThisScopeString; } FSequencerClipboardReconciler* Reconciler = Map.Find(ThisScope); if (!Reconciler) { Reconciler = &Map.Add(ThisScope, FSequencerClipboardReconciler(Args.Clipboard.ToSharedRef())); } FSequencerClipboardPasteGroup Group = Reconciler->AddDestinationGroup(); for (const TSharedPtr& KeyAreaNode : KeyAreaNodesBuffer) { TSharedPtr Channel = KeyAreaNode->GetChannel(InSection); if (Channel) { Group.Add(Channel); } } // Add children for (const TViewModelPtr& Child : InNode.AsModel()->GetChildrenOfType()) { GatherPasteDestinationsForNode(Child, InSection, ThisScope, Map); } } void FPasteContextMenu::Setup() { using namespace UE::Sequencer; const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } if (!Args.Clipboard.IsValid()) { if (Sequencer->GetClipboardStack().Num() != 0) { Args.Clipboard = Sequencer->GetClipboardStack().Last(); } else { return; } } // Gather a list of sections we want to paste into TArray> SectionModels; if (Args.DestinationNodes.Num()) { // If we have exactly one channel to paste, first check if we have exactly one valid target channel selected to support copying between channels e.g. from Tranform.x to Transform.y if (Args.Clipboard->GetKeyTrackGroups().Num() == 1) { for (const TViewModelPtr& Node : Args.DestinationNodes) { TViewModelPtr TrackNode = Node.AsModel()->FindAncestorOfType(true); if (!TrackNode) { continue; } FPasteDestination& Destination = PasteDestinations[PasteDestinations.AddDefaulted()]; for (UMovieSceneSection* Section : TrackNode->GetSections()) { if (Section) { GatherPasteDestinationsForNode(Node, Section, NAME_None, Destination.Reconcilers); } } // Reconcile and remove invalid pastes for (auto It = Destination.Reconcilers.CreateIterator(); It; ++It) { if (!It.Value().Reconcile() || !It.Value().CanAutoPaste()) { It.RemoveCurrent(); } } if (!Destination.Reconcilers.Num()) { PasteDestinations.RemoveAt(PasteDestinations.Num() - 1, EAllowShrinking::No); } } int32 ExactMatchCount = 0; for (int32 PasteDestinationIndex = 0; PasteDestinationIndex < PasteDestinations.Num(); ++PasteDestinationIndex) { if (PasteDestinations[PasteDestinationIndex].Reconcilers.Num() == 1) { ++ExactMatchCount; } } if (ExactMatchCount > 0 && ExactMatchCount == PasteDestinations.Num()) { bPasteFirstOnly = false; return; } // Otherwise reset our list and move on PasteDestinations.Reset(); } // Build a list of sections based on selected tracks for (const TViewModelPtr& Node : Args.DestinationNodes) { TViewModelPtr TrackNode = Node.AsModel()->FindAncestorOfType(true); if (!TrackNode) { continue; } UMovieSceneSection* Section = MovieSceneHelpers::FindNearestSectionAtTime(TrackNode->GetSections(), Args.PasteAtTime); TSharedPtr SectionModel = Sequencer->GetNodeTree()->GetSectionModel(Section); if (SectionModel) { SectionModels.Add(SectionModel); } } } else { // Use the selected sections for (UMovieSceneSection* WeakSection : Sequencer->GetViewModel()->GetSelection()->GetSelectedSections()) { if (TSharedPtr SectionHandle = Sequencer->GetNodeTree()->GetSectionModel(WeakSection)) { SectionModels.Add(SectionHandle); } } } TMap>> SectionsByType; for (TSharedPtr SectionModel : SectionModels) { UMovieSceneTrack* Track = SectionModel->GetParentTrackExtension()->GetTrack(); if (Track) { SectionsByType.FindOrAdd(Track->GetClass()->GetFName()).Add(SectionModel); } } for (const TTuple>>& Pair : SectionsByType) { FPasteDestination& Destination = PasteDestinations[PasteDestinations.AddDefaulted()]; if (Pair.Value.Num() == 1) { TSharedPtr Model = Pair.Value[0]->FindAncestorOfTypes({ITrackExtension::ID, IOutlinerExtension::ID}); if (ensure(Model)) { FString Path = IOutlinerExtension::GetPathName(Model); Destination.Name = FText::FromString(Path); } } else { Destination.Name = FText::Format(LOCTEXT("PasteMenuHeaderFormat", "{0} ({1} tracks)"), FText::FromName(Pair.Key), FText::AsNumber(Pair.Value.Num())); } for (TSharedPtr Section : Pair.Value) { FViewModelPtr Model = Section->FindAncestorOfTypes({ITrackExtension::ID, IOutlinerExtension::ID}); GatherPasteDestinationsForNode(Model.ImplicitCast(), Section->GetSection(), NAME_None, Destination.Reconcilers); } // Reconcile and remove invalid pastes for (auto It = Destination.Reconcilers.CreateIterator(); It; ++It) { if (!It.Value().Reconcile()) { It.RemoveCurrent(); } } if (!Destination.Reconcilers.Num()) { PasteDestinations.RemoveAt(PasteDestinations.Num() - 1, EAllowShrinking::No); } } } bool FPasteContextMenu::IsValidPaste() const { return Args.Clipboard.IsValid() && PasteDestinations.Num() != 0; } void FPasteContextMenu::PopulateMenu(FMenuBuilder& MenuBuilder, TSharedPtr MenuExtender) { // Copy a reference to the context menu by value into each lambda handler to ensure the type stays alive until the menu is closed TSharedRef Shared = AsShared(); bool bElevateMenu = PasteDestinations.Num() == 1; for (int32 Index = 0; Index < PasteDestinations.Num(); ++Index) { if (bElevateMenu) { MenuBuilder.BeginSection("PasteInto", FText::Format(LOCTEXT("PasteIntoTitle", "Paste Into {0}"), PasteDestinations[Index].Name)); AddPasteMenuForTrackType(MenuBuilder, Index); MenuBuilder.EndSection(); break; } MenuBuilder.AddSubMenu( PasteDestinations[Index].Name, FText(), FNewMenuDelegate::CreateLambda([=](FMenuBuilder& SubMenuBuilder){ Shared->AddPasteMenuForTrackType(SubMenuBuilder, Index); }) ); } } void FPasteContextMenu::AddPasteMenuForTrackType(FMenuBuilder& MenuBuilder, int32 DestinationIndex) { // Copy a reference to the context menu by value into each lambda handler to ensure the type stays alive until the menu is closed TSharedRef Shared = AsShared(); for (auto& Pair : PasteDestinations[DestinationIndex].Reconcilers) { MenuBuilder.AddMenuEntry( FText::FromName(Pair.Key), FText(), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([=](){ TSet NewSelection; Shared->BeginPasteInto(); const bool bAnythingPasted = Shared->PasteInto(DestinationIndex, Pair.Key, NewSelection); Shared->EndPasteInto(bAnythingPasted, NewSelection); }) ) ); } } bool FPasteContextMenu::AutoPaste() { TSet NewSelection; BeginPasteInto(); bool bAnythingPasted = false; for (int32 PasteDestinationIndex = 0; PasteDestinationIndex < PasteDestinations.Num(); ++PasteDestinationIndex) { for (auto& Pair : PasteDestinations[PasteDestinationIndex].Reconcilers) { if (Pair.Value.CanAutoPaste()) { if (PasteInto(PasteDestinationIndex, Pair.Key, NewSelection)) { bAnythingPasted = true; if (bPasteFirstOnly) { break; } } } } } EndPasteInto(bAnythingPasted, NewSelection); return bAnythingPasted; } void FPasteContextMenu::BeginPasteInto() { GEditor->BeginTransaction(LOCTEXT("PasteKeysTransaction", "Paste Keys")); } void FPasteContextMenu::EndPasteInto(bool bAnythingPasted, const TSet& NewSelection) { using namespace UE::Sequencer; if (!bAnythingPasted) { GEditor->CancelTransaction(0); return; } GEditor->EndTransaction(); UE::Sequencer::SSequencerSection::ThrobKeySelection(); const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } FSequencerSelection& Selection = *Sequencer->GetViewModel()->GetSelection(); { FSelectionEventSuppressor EventSuppressor = Selection.SuppressEvents(); Selection.TrackArea.Empty(); Selection.KeySelection.Empty(); for (const FSequencerSelectedKey& NewKey : NewSelection) { if (TSharedPtr Channel = NewKey.WeakChannel.Pin()) { Selection.KeySelection.Select(Channel, NewKey.KeyHandle); } } } Sequencer->OnClipboardUsed(Args.Clipboard); Sequencer->NotifyMovieSceneDataChanged(EMovieSceneDataChangeType::TrackValueChanged); } bool FPasteContextMenu::PasteInto(int32 DestinationIndex, FName KeyAreaName, TSet& NewSelection) { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return false; } FSequencerClipboardReconciler& Reconciler = PasteDestinations[DestinationIndex].Reconcilers[KeyAreaName]; FSequencerPasteEnvironment PasteEnvironment; PasteEnvironment.TickResolution = Sequencer->GetFocusedTickResolution(); PasteEnvironment.CardinalTime = Args.PasteAtTime; PasteEnvironment.TimeTransform = Sequencer->GetFocusedMovieSceneSequenceTransform().LinearTransform; PasteEnvironment.OnKeyPasted = [&](FKeyHandle Handle, TSharedPtr Channel){ NewSelection.Add(FSequencerSelectedKey(*Channel->GetSection(), Channel, Handle)); }; return Reconciler.Paste(PasteEnvironment); } bool FPasteFromHistoryContextMenu::BuildMenu(FMenuBuilder& MenuBuilder, TSharedPtr MenuExtender, TWeakPtr InWeakSequencer, const FPasteContextMenuArgs& Args) { const TSharedPtr Sequencer = InWeakSequencer.Pin(); if (!Sequencer.IsValid()) { return false; } if (Sequencer->GetClipboardStack().Num() == 0) { return false; } TSharedRef Menu = MakeShareable(new FPasteFromHistoryContextMenu(InWeakSequencer, Args)); Menu->PopulateMenu(MenuBuilder, MenuExtender); return true; } TSharedPtr FPasteFromHistoryContextMenu::CreateMenu(TWeakPtr InWeakSequencer, const FPasteContextMenuArgs& Args) { const TSharedPtr Sequencer = InWeakSequencer.Pin(); if (!Sequencer.IsValid()) { return nullptr; } if (Sequencer->GetClipboardStack().Num() == 0) { return nullptr; } return MakeShareable(new FPasteFromHistoryContextMenu(InWeakSequencer, Args)); } void FPasteFromHistoryContextMenu::PopulateMenu(FMenuBuilder& MenuBuilder, TSharedPtr MenuExtender) { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } // Copy a reference to the context menu by value into each lambda handler to ensure the type stays alive until the menu is closed TSharedRef Shared = AsShared(); MenuBuilder.BeginSection("SequencerPasteHistory", LOCTEXT("PasteFromHistory", "Paste From History")); for (int32 Index = Sequencer->GetClipboardStack().Num() - 1; Index >= 0; --Index) { FPasteContextMenuArgs ThisPasteArgs = Args; ThisPasteArgs.Clipboard = Sequencer->GetClipboardStack()[Index]; TSharedRef PasteMenu = FPasteContextMenu::CreateMenu(Sequencer, ThisPasteArgs); MenuBuilder.AddSubMenu( ThisPasteArgs.Clipboard->GetDisplayText(), FText(), FNewMenuDelegate::CreateLambda([=](FMenuBuilder& SubMenuBuilder){ PasteMenu->PopulateMenu(SubMenuBuilder, MenuExtender); }), FUIAction ( FExecuteAction(), FCanExecuteAction::CreateLambda([=]{ return PasteMenu->IsValidPaste(); }) ), NAME_None, EUserInterfaceActionType::Button ); } MenuBuilder.EndSection(); } void FEasingContextMenu::BuildMenu(FMenuBuilder& MenuBuilder, TSharedPtr MenuExtender, const TArray& InEasings, TWeakPtr InWeakSequencer, FFrameTime InMouseDownTime) { TSharedRef EasingMenu = MakeShareable(new FEasingContextMenu(InEasings, InWeakSequencer)); EasingMenu->PopulateMenu(MenuBuilder, MenuExtender); MenuBuilder.AddMenuSeparator(); FSectionContextMenu::BuildMenu(MenuBuilder, MenuExtender, InWeakSequencer, InMouseDownTime); } void FEasingContextMenu::PopulateMenu(FMenuBuilder& MenuBuilder, TSharedPtr MenuExtender) { using namespace UE::Sequencer; const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return; } FText SectionText = Easings.Num() == 1 ? LOCTEXT("EasingCurve", "Easing Curve") : FText::Format(LOCTEXT("EasingCurvesFormat", "Easing Curves ({0} curves)"), FText::AsNumber(Easings.Num())); const bool bReadOnly = Algo::AnyOf(Easings, [](const FEasingAreaHandle& Handle) -> bool { const UMovieSceneSection* Section = Handle.WeakSectionModel.Pin()->GetSection(); const UMovieSceneTrack* SectionTrack = Section->GetTypedOuter(); FMovieSceneSupportsEasingParams Params(Section); return !EnumHasAllFlags(SectionTrack->SupportsEasing(Params), EMovieSceneTrackEasingSupportFlags::ManualEasing); }); MenuBuilder.BeginSection("SequencerEasingEdit", SectionText); { // Copy a reference to the context menu by value into each lambda handler to ensure the type stays alive until the menu is closed TSharedRef Shared = AsShared(); auto OnBeginSliderMovement = [=] { GEditor->BeginTransaction(LOCTEXT("SetEasingTimeText", "Set Easing Length")); }; auto OnEndSliderMovement = [=](double NewLength) { if (GEditor->IsTransactionActive()) { GEditor->EndTransaction(); } }; auto OnValueCommitted = [=](double NewLength, ETextCommit::Type CommitInfo) { if (CommitInfo == ETextCommit::OnEnter || CommitInfo == ETextCommit::OnUserMovedFocus) { FScopedTransaction Transaction(LOCTEXT("SetEasingTimeText", "Set Easing Length")); Shared->OnUpdateLength((int32)NewLength); } }; TSharedRef SpinBox = SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(FMargin(5.f,0.f)) [ SNew(SBox) .HAlign(HAlign_Right) [ SNew(SNumericEntryBox) .SpinBoxStyle(&FAppStyle::GetWidgetStyle("Sequencer.HyperlinkSpinBox")) .EditableTextBoxStyle(&FAppStyle::GetWidgetStyle("Sequencer.HyperlinkTextBox")) // Don't update the value when undetermined text changes .OnUndeterminedValueChanged_Lambda([](FText){}) .AllowSpin(true) .IsEnabled(!bReadOnly) .MinValue(0.f) .MaxValue(TOptional()) .MaxSliderValue(TOptional()) .MinSliderValue(0.f) .Delta_Lambda([this]() -> double { const TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer.IsValid()) { return 0; } return Sequencer->GetDisplayRateDeltaFrameCount(); }) .Value_Lambda([=] { TOptional Current = Shared->GetCurrentLength(); if (Current.IsSet()) { return TOptional(Current.GetValue()); } return TOptional(); }) .OnValueChanged_Lambda([=](double NewLength){ Shared->OnUpdateLength(NewLength); }) .OnValueCommitted_Lambda(OnValueCommitted) .OnBeginSliderMovement_Lambda(OnBeginSliderMovement) .OnEndSliderMovement_Lambda(OnEndSliderMovement) .BorderForegroundColor(FAppStyle::GetSlateColor("DefaultForeground")) .TypeInterface(Sequencer->GetNumericTypeInterface()) ] ] + SHorizontalBox::Slot() .HAlign(HAlign_Right) .AutoWidth() [ SNew(SCheckBox) .IsEnabled(!bReadOnly) .IsChecked_Lambda([=]{ return Shared->GetAutoEasingCheckState(); }) .OnCheckStateChanged_Lambda([=](ECheckBoxState CheckState){ return Shared->SetAutoEasing(CheckState == ECheckBoxState::Checked); }) [ SNew(STextBlock) .Text(LOCTEXT("AutomaticEasingText", "Auto?")) ] ]; MenuBuilder.AddWidget(SpinBox, LOCTEXT("EasingAmountLabel", "Easing Length")); MenuBuilder.AddSubMenu( TAttribute::Create(TAttribute::FGetter::CreateLambda([=]{ return Shared->GetEasingTypeText(); })), LOCTEXT("EasingTypeToolTip", "Change the type of curve used for the easing"), FNewMenuDelegate::CreateLambda([=](FMenuBuilder& SubMenuBuilder){ Shared->EasingTypeMenu(SubMenuBuilder); }) ); MenuBuilder.AddSubMenu( LOCTEXT("EasingOptions", "Options"), LOCTEXT("EasingOptionsToolTip", "Edit easing settings for this curve"), FNewMenuDelegate::CreateLambda([=](FMenuBuilder& SubMenuBuilder){ Shared->EasingOptionsMenu(SubMenuBuilder); }) ); } MenuBuilder.EndSection(); } TOptional FEasingContextMenu::GetCurrentLength() const { using namespace UE::Sequencer; TOptional Value; for (const FEasingAreaHandle& Handle : Easings) { UMovieSceneSection* Section = Handle.WeakSectionModel.Pin()->GetSection(); if (Section) { if (Handle.EasingType == ESequencerEasingType::In && Section->Easing.GetEaseInDuration() == Value.Get(Section->Easing.GetEaseInDuration())) { Value = Section->Easing.GetEaseInDuration(); } else if (Handle.EasingType == ESequencerEasingType::Out && Section->Easing.GetEaseOutDuration() == Value.Get(Section->Easing.GetEaseOutDuration())) { Value = Section->Easing.GetEaseOutDuration(); } else { return TOptional(); } } } return Value; } void FEasingContextMenu::OnUpdateLength(int32 NewLength) { using namespace UE::Sequencer; for (const FEasingAreaHandle& Handle : Easings) { if (UMovieSceneSection* Section = Handle.WeakSectionModel.Pin()->GetSection()) { Section->Modify(); if (Handle.EasingType == ESequencerEasingType::In) { Section->Easing.bManualEaseIn = true; Section->Easing.ManualEaseInDuration = FMath::Min(UE::MovieScene::DiscreteSize(Section->GetRange()), NewLength); } else { Section->Easing.bManualEaseOut = true; Section->Easing.ManualEaseOutDuration = FMath::Min(UE::MovieScene::DiscreteSize(Section->GetRange()), NewLength); } } } } ECheckBoxState FEasingContextMenu::GetAutoEasingCheckState() const { using namespace UE::Sequencer; TOptional IsChecked; for (const FEasingAreaHandle& Handle : Easings) { if (UMovieSceneSection* Section = Handle.WeakSectionModel.Pin()->GetSection()) { if (Handle.EasingType == ESequencerEasingType::In) { if (IsChecked.IsSet() && IsChecked.GetValue() != !Section->Easing.bManualEaseIn) { return ECheckBoxState::Undetermined; } IsChecked = !Section->Easing.bManualEaseIn; } else { if (IsChecked.IsSet() && IsChecked.GetValue() != !Section->Easing.bManualEaseOut) { return ECheckBoxState::Undetermined; } IsChecked = !Section->Easing.bManualEaseOut; } } } return IsChecked.IsSet() ? IsChecked.GetValue() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked : ECheckBoxState::Undetermined; } void FEasingContextMenu::SetAutoEasing(bool bAutoEasing) { using namespace UE::Sequencer; FScopedTransaction Transaction(LOCTEXT("SetAutoEasingText", "Set Automatic Easing")); TArray AllTracks; for (const FEasingAreaHandle& Handle : Easings) { if (UMovieSceneSection* Section = Handle.WeakSectionModel.Pin()->GetSection()) { AllTracks.AddUnique(Section->GetTypedOuter()); Section->Modify(); if (Handle.EasingType == ESequencerEasingType::In) { Section->Easing.bManualEaseIn = !bAutoEasing; } else { Section->Easing.bManualEaseOut = !bAutoEasing; } } } for (UMovieSceneTrack* Track : AllTracks) { Track->UpdateEasing(); } } FText FEasingContextMenu::GetEasingTypeText() const { using namespace UE::Sequencer; FText CurrentText; UClass* ClassType = nullptr; for (const FEasingAreaHandle& Handle : Easings) { if (UMovieSceneSection* Section = Handle.WeakSectionModel.Pin()->GetSection()) { UObject* Object = Handle.EasingType == ESequencerEasingType::In ? Section->Easing.EaseIn.GetObject() : Section->Easing.EaseOut.GetObject(); if (Object) { if (!ClassType) { ClassType = Object->GetClass(); } else if (Object->GetClass() != ClassType) { CurrentText = LOCTEXT("MultipleEasingTypesText", ""); break; } } } } if (CurrentText.IsEmpty()) { CurrentText = ClassType ? ClassType->GetDisplayNameText() : LOCTEXT("NoneEasingText", "None"); } return FText::Format(LOCTEXT("EasingTypeTextFormat", "Method ({0})"), CurrentText); } void FEasingContextMenu::EasingTypeMenu(FMenuBuilder& MenuBuilder) { struct FFilter : IClassViewerFilter { virtual bool IsClassAllowed(const FClassViewerInitializationOptions& InInitOptions, const UClass* InClass, TSharedRef InFilterFuncs) override { bool bIsCorrectInterface = InClass->ImplementsInterface(UMovieSceneEasingFunction::StaticClass()); bool bMatchesFlags = !InClass->HasAnyClassFlags(CLASS_Hidden | CLASS_HideDropDown | CLASS_Deprecated | CLASS_Abstract); return bIsCorrectInterface && bMatchesFlags; } virtual bool IsUnloadedClassAllowed(const FClassViewerInitializationOptions& InInitOptions, const TSharedRef InUnloadedClassData, TSharedRef InFilterFuncs) override { bool bIsCorrectInterface = InUnloadedClassData->ImplementsInterface(UMovieSceneEasingFunction::StaticClass()); bool bMatchesFlags = !InUnloadedClassData->HasAnyClassFlags(CLASS_Hidden | CLASS_HideDropDown | CLASS_Deprecated | CLASS_Abstract); return bIsCorrectInterface && bMatchesFlags; } }; FClassViewerModule& ClassViewer = FModuleManager::LoadModuleChecked("ClassViewer"); FClassViewerInitializationOptions InitOptions; InitOptions.NameTypeToDisplay = EClassViewerNameTypeToDisplay::DisplayName; InitOptions.ClassFilters.Add(MakeShared()); // Copy a reference to the context menu by value into each lambda handler to ensure the type stays alive until the menu is closed TSharedRef Shared = AsShared(); TSharedRef ClassViewerWidget = ClassViewer.CreateClassViewer(InitOptions, FOnClassPicked::CreateLambda([=](UClass* NewClass) { Shared->OnEasingTypeChanged(NewClass); })); MenuBuilder.AddWidget(ClassViewerWidget, FText(), true, false); } void FEasingContextMenu::OnEasingTypeChanged(UClass* NewClass) { using namespace UE::Sequencer; FScopedTransaction Transaction(LOCTEXT("SetEasingType", "Set Easing Method")); for (const FEasingAreaHandle& Handle : Easings) { UMovieSceneSection* Section = Handle.WeakSectionModel.Pin()->GetSection(); if (!Section) { continue; } Section->Modify(); TScriptInterface& EaseObject = Handle.EasingType == ESequencerEasingType::In ? Section->Easing.EaseIn : Section->Easing.EaseOut; if (!EaseObject.GetObject() || EaseObject.GetObject()->GetClass() != NewClass) { UObject* NewEasingFunction = NewObject(Section, NewClass); EaseObject.SetObject(NewEasingFunction); EaseObject.SetInterface(Cast(NewEasingFunction)); } } } void FEasingContextMenu::EasingOptionsMenu(FMenuBuilder& MenuBuilder) { using namespace UE::Sequencer; FPropertyEditorModule& EditModule = FModuleManager::Get().GetModuleChecked("PropertyEditor"); FDetailsViewArgs DetailsViewArgs; DetailsViewArgs.bAllowSearch = false; DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea; DetailsViewArgs.bHideSelectionTip = true; DetailsViewArgs.bShowOptions = false; DetailsViewArgs.bShowScrollBar = false; TSharedRef DetailsView = EditModule.CreateDetailView(DetailsViewArgs); TArray Objects; for (const FEasingAreaHandle& Handle : Easings) { if (UMovieSceneSection* Section = Handle.WeakSectionModel.Pin()->GetSection()) { if (Handle.EasingType == ESequencerEasingType::In) { UObject* EaseInObject = Section->Easing.EaseIn.GetObject(); EaseInObject->SetFlags(RF_Transactional); Objects.AddUnique(EaseInObject); } else { UObject* EaseOutObject = Section->Easing.EaseOut.GetObject(); EaseOutObject->SetFlags(RF_Transactional); Objects.AddUnique(EaseOutObject); } } } DetailsView->SetObjects(Objects, true); MenuBuilder.AddWidget(DetailsView, FText(), true, false); } #undef LOCTEXT_NAMESPACE