Files
UnrealEngine/Engine/Source/Editor/Sequencer/Private/SequencerContextMenus.cpp
2025-05-18 13:04:45 +08:00

2491 lines
75 KiB
C++

// 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<ISequencer> InWeakSequencer, TSharedPtr<FStructOnScope>& OutKeyStruct, TWeakObjectPtr<UMovieSceneSection>& OutKeyStructSection)
{
using namespace UE::Sequencer;
const TSharedPtr<ISequencer> Sequencer = InWeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return;
}
const FKeySelection& SelectedKeys = Sequencer->GetViewModel()->GetSelection()->KeySelection;
if (SelectedKeys.Num() == 1)
{
for (FKeyHandle Key : SelectedKeys)
{
TSharedPtr<FChannelModel> Channel = SelectedKeys.GetModelForKey(Key);
if (Channel)
{
OutKeyStruct = Channel->GetKeyArea()->GetKeyStruct(Key);
OutKeyStructSection = Channel->GetSection();
return;
}
}
}
else
{
TArray<FKeyHandle> KeyHandles;
UMovieSceneSection* CommonSection = nullptr;
for (FKeyHandle Key : SelectedKeys)
{
TSharedPtr<FChannelModel> 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<TSharedPtr<FChannelModel>> GetChannelModels(const TWeakPtr<FSequencer>& InWeakSequencer)
{
TSet<TSharedPtr<FChannelModel>> Channels;
const TSharedPtr<FSequencer> Sequencer = InWeakSequencer.IsValid() ? InWeakSequencer.Pin() : nullptr;
if (Sequencer.IsValid())
{
const FSequencerSelection& Selection = *Sequencer->GetViewModel()->GetSelection();
for (const TViewModelPtr<IOutlinerExtension>& Item : Selection.Outliner)
{
SequencerHelpers::GetAllChannels(Item, Channels);
}
if (Channels.IsEmpty())
{
for (const TWeakViewModelPtr<IOutlinerExtension>& DisplayNode : Selection.GetNodesWithSelectedKeysOrSections())
{
SequencerHelpers::GetAllChannels(DisplayNode.Pin(), Channels);
}
}
}
return MoveTemp(Channels);
}
}
void FKeyContextMenu::BuildMenu(FMenuBuilder& MenuBuilder, TSharedPtr<FExtender> MenuExtender, TWeakPtr<FSequencer> InWeakSequencer)
{
TSharedRef<FKeyContextMenu> Menu = MakeShareable(new FKeyContextMenu(InWeakSequencer));
Menu->PopulateMenu(MenuBuilder, MenuExtender);
}
void FKeyContextMenu::PopulateMenu(FMenuBuilder& MenuBuilder, TSharedPtr<FExtender> MenuExtender)
{
using namespace UE::Sequencer;
const TSharedPtr<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return;
}
TSharedRef<FKeyContextMenu> Shared = AsShared();
CreateKeyStructForSelection(WeakSequencer, KeyStruct, KeyStructSection);
{
ISequencerModule& SequencerModule = FModuleManager::LoadModuleChecked<ISequencerModule>("Sequencer");
FSelectedKeysByChannel SelectedKeysByChannel(Sequencer->GetViewModel()->GetSelection()->KeySelection);
TMap<FName, TArray<FExtendKeyMenuParams>> 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<USequencerToolMenuContext>();
TSharedPtr<ISequencer> Sequencer = ContextObject ? ContextObject->WeakSequencer.Pin() : nullptr;
if (Sequencer && HotspotCast<FKeyHotspot>(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<USequencerToolMenuContext>();
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<FSequencer> 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<FSequencer> InWeakSequencer, FFrameTime InMouseDownTime)
: WeakSequencer(InWeakSequencer)
, MouseDownTime(InMouseDownTime)
{
const TSharedPtr<FSequencer> 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<UMovieSceneSection*>& SectionArray = SectionsByType.FindOrAdd(ChannelTypeName);
SectionArray.Add(Section);
TArray<FMovieSceneChannelHandle>& 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<FExtender> MenuExtender, TWeakPtr<FSequencer> InWeakSequencer, FFrameTime InMouseDownTime)
{
TSharedRef<FSectionContextMenu> Menu = MakeShareable(new FSectionContextMenu(InWeakSequencer, InMouseDownTime));
Menu->PopulateMenu(MenuBuilder, MenuExtender);
}
void FSectionContextMenu::BuildKeyEditMenu(FMenuBuilder& MenuBuilder, TWeakPtr<FSequencer> InWeakSequencer, FFrameTime InMouseDownTime)
{
TSharedRef<FSectionContextMenu> Menu = MakeShareable(new FSectionContextMenu(InWeakSequencer, InMouseDownTime));
Menu->AddKeyInterpolationMenu(MenuBuilder);
Menu->AddKeyEditMenu(MenuBuilder);
}
void FSectionContextMenu::PopulateMenu(FMenuBuilder& MenuBuilder, TSharedPtr<FExtender> MenuExtender)
{
using namespace UE::Sequencer;
const TSharedPtr<FSequencer> 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<FSectionContextMenu> 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<ISequencerModule>("Sequencer");
for (auto& Pair : ChannelsByType)
{
const TArray<UMovieSceneSection*>& Sections = SectionsByType.FindChecked(Pair.Key);
ISequencerChannelInterface* ChannelInterface = SequencerModule.FindChannelEditorInterface(Pair.Key);
if (ChannelInterface)
{
TArray<TWeakObjectPtr<UMovieSceneSection>> 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<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return;
}
TArray<TWeakObjectPtr<UObject>> Sections;
for (TViewModelPtr<FSectionModel> SectionModel : Sequencer->GetViewModel()->GetSelection()->TrackArea.Filter<FSectionModel>())
{
if (UMovieSceneSection* Section = SectionModel->GetSection())
{
Sections.Add(Section);
}
}
SequencerHelpers::BuildEditSectionMenu(Sequencer, Sections, SubMenuBuilder, false);
})
);
MenuBuilder.BeginSection("SequencerKeyEdit", LOCTEXT("EditMenu", "Edit"));
{
TSharedPtr<FPasteFromHistoryContextMenu> PasteFromHistoryMenu;
TSharedPtr<FPasteContextMenu> 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<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer)
{
return false;
}
TSet<UClass*> CompatibleDecorations;
for (UMovieSceneSection* Section : Sequencer->GetViewModel()->GetSelection()->GetSelectedSections())
{
if (Section)
{
if (Cast<IMovieSceneScalingDriver>(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<FSectionContextMenu> Shared = AsShared();
auto GetScalingDriverCheckState = [Shared]
{
TOptional<ECheckBoxState> CheckState;
TSharedPtr<FSequencer> Sequencer = Shared->WeakSequencer.Pin();
if (Sequencer)
{
for (UMovieSceneSection* Section : Sequencer->GetViewModel()->GetSelection()->GetSelectedSections())
{
ECheckBoxState ThisCheckState = (Section && Section->FindDecoration<UMovieSceneSectionAnchorsDecoration>() != 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<FSequencer> 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<UMovieSceneSectionAnchorsDecoration>();
}
else
{
Section->GetOrCreateDecoration<UMovieSceneSectionAnchorsDecoration>();
}
}
}
}
}),
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<FSectionContextMenu> 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<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return;
}
Sequencer->GetSequencerSettings()->SetDeleteKeysWhenTrimming(!Sequencer->GetSequencerSettings()->GetDeleteKeysWhenTrimming());
}),
FCanExecuteAction(),
FIsActionChecked::CreateLambda([this]
{
const TSharedPtr<FSequencer> 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<FSectionContextMenu> 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<FSectionContextMenu> 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<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return;
}
Sequencer->GetSequencerSettings()->SetReduceKeysTolerance(InNewValue);
};
MenuBuilder.AddWidget(
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
[
SNew(SSpacer)
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SSpinBox<float>)
.Style(&FAppStyle::GetWidgetStyle<FSpinBoxStyle>(TEXT("Sequencer.HyperlinkSpinBox")))
.OnValueCommitted_Lambda([OnReduceKeysToleranceChanged](float Value, ETextCommit::Type)
{
OnReduceKeysToleranceChanged(Value);
})
.OnValueChanged_Lambda(OnReduceKeysToleranceChanged)
.MinValue(0)
.MaxValue(TOptional<float>())
.Value_Lambda([this]() -> float
{
const TSharedPtr<FSequencer> 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<FSequencer> 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<FSectionContextMenu> 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<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return;
}
TArray<TWeakObjectPtr<UMovieSceneSection>> Sections;
for (TViewModelPtr<FSectionModel> SectionModel : Sequencer->GetViewModel()->GetSelection()->TrackArea.Filter<FSectionModel>())
{
if (UMovieSceneSection* Section = SectionModel->GetSection())
{
Sections.Add(Section);
}
}
FSequencerUtilities::PopulateMenu_SetBlendType(MenuBuilder, Sections, WeakSequencer);
}
void FSectionContextMenu::SelectAllKeys()
{
using namespace UE::Sequencer;
const TSharedPtr<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return;
}
const TSet<TSharedPtr<FChannelModel>> Channels = Private::GetChannelModels(WeakSequencer);
if (!Channels.IsEmpty())
{
FSequencerSelection& Selection = *Sequencer->GetViewModel()->GetSelection();
FSelectionEventSuppressor EventSuppressor = Selection.SuppressEvents();
TArray<FKeyHandle> HandlesScratch;
for (const TSharedPtr<FChannelModel>& Channel: Channels)
{
if (Channel->GetLinkedOutlinerItem() && !Channel->GetLinkedOutlinerItem()->IsFilteredOut())
{
HandlesScratch.Reset();
Channel->GetKeyArea()->GetKeyHandles(HandlesScratch);
Selection.KeySelection.SelectRange(Channel, HandlesScratch);
}
}
}
}
void FSectionContextMenu::CopyAllKeys()
{
const TSharedPtr<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return;
}
SelectAllKeys();
Sequencer->CopySelectedKeys();
}
void FSectionContextMenu::SetSectionToKey()
{
const TSharedPtr<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return;
}
TSet<UMovieSceneSection*> SelectedSections = Sequencer->GetViewModel()->GetSelection()->GetSelectedSections();
if (SelectedSections.Num() != 1)
{
return;
}
const bool bToggle = IsSectionToKey();
UMovieSceneSection* Section = *SelectedSections.CreateConstIterator();
UMovieSceneTrack* Track = Section->GetTypedOuter<UMovieSceneTrack>();
if (Track)
{
FScopedTransaction Transaction(LOCTEXT("SetSectionToKey", "Set Section To Key"));
Track->Modify();
Track->SetSectionToKey(bToggle ? nullptr : Section);
}
}
bool FSectionContextMenu::IsSectionToKey() const
{
const TSharedPtr<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return false;
}
for (UMovieSceneSection* Section : Sequencer->GetViewModel()->GetSelection()->GetSelectedSections())
{
UMovieSceneTrack* Track = Section->GetTypedOuter<UMovieSceneTrack>();
if (Track && Track->GetSectionToKey() != Section)
{
return false;
}
}
return true;
}
bool FSectionContextMenu::CanSetSectionToKey() const
{
const TSharedPtr<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return false;
}
TSet<UMovieSceneSection*> SelectedSections = Sequencer->GetViewModel()->GetSelection()->GetSelectedSections();
if (SelectedSections.Num() != 1)
{
return false;
}
UMovieSceneSection* Section = *SelectedSections.CreateConstIterator();
UMovieSceneTrack* Track = Section->GetTypedOuter<UMovieSceneTrack>();
if (Track && Section->GetBlendType().IsValid() && Section->GetBlendType().Get() != EMovieSceneBlendType::Invalid)
{
return true;
}
return false;
}
bool FSectionContextMenu::CanSelectAllKeys() const
{
for (const TTuple<FName, TArray<FMovieSceneChannelHandle>>& 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<FSequencer> 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<TRange<FFrameNumber> > DefaultSectionLength = Section->GetAutoSizeRange();
if (DefaultSectionLength.IsSet())
{
Section->SetRange(DefaultSectionLength.GetValue());
}
}
}
Sequencer->NotifyMovieSceneDataChanged( EMovieSceneDataChangeType::MovieSceneStructureItemAdded );
}
void FSectionContextMenu::ReduceKeys()
{
using namespace UE::Sequencer;
const TSharedPtr<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return;
}
const TSet<TSharedPtr<FChannelModel>> ChannelModels = Private::GetChannelModels(WeakSequencer);
if (ChannelModels.IsEmpty())
{
return;
}
TSet<UMovieSceneSection*> Sections;
TSet<FMovieSceneChannel*> Channels;
Channels.Reserve(ChannelModels.Num());
for (const TSharedPtr<FChannelModel>& ChannelModel: ChannelModels)
{
if (ChannelModel->GetLinkedOutlinerItem() && !ChannelModel->GetLinkedOutlinerItem()->IsFilteredOut())
{
const TSharedPtr<IKeyArea> 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<FSequencer> 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<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return false;
}
TSet<TSharedPtr<IKeyArea> > KeyAreas;
for (const TWeakPtr<FViewModel>& WeakItem : Sequencer->GetViewModel()->GetSelection()->Outliner)
{
SequencerHelpers::GetAllKeyAreas(WeakItem.Pin(), KeyAreas);
}
if (KeyAreas.Num() == 0)
{
for (const TWeakViewModelPtr<IOutlinerExtension>& 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<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return;
}
FScopedTransaction SetInterpTangentModeTransaction(LOCTEXT("SetInterpTangentMode_Transaction", "Set Interpolation and Tangent Mode"));
TSet<TSharedPtr<IKeyArea> > KeyAreas;
for (const TWeakPtr<FViewModel>& WeakItem : Sequencer->GetViewModel()->GetSelection()->Outliner)
{
SequencerHelpers::GetAllKeyAreas(WeakItem.Pin(), KeyAreas);
}
if (KeyAreas.Num() == 0)
{
for (const TWeakViewModelPtr<IOutlinerExtension>& DisplayNode : Sequencer->GetViewModel()->GetSelection()->GetNodesWithSelectedKeysOrSections())
{
SequencerHelpers::GetAllKeyAreas(DisplayNode.Pin(), KeyAreas);
}
}
bool bAnythingChanged = false;
for (TSharedPtr<IKeyArea> KeyArea : KeyAreas)
{
if (KeyArea.IsValid())
{
if (UMovieSceneSignedObject* OwningObject = Cast<UMovieSceneSignedObject>(KeyArea->GetOwningObject()))
{
OwningObject->Modify();
}
FMovieSceneChannelHandle Handle = KeyArea->GetChannel();
if (Handle.GetChannelTypeName() == FMovieSceneFloatChannel::StaticStruct()->GetFName())
{
FMovieSceneFloatChannel* FloatChannel = static_cast<FMovieSceneFloatChannel*>(Handle.Get());
TMovieSceneChannelData<FMovieSceneFloatValue> ChannelData = FloatChannel->GetData();
TArrayView<FMovieSceneFloatValue> 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<FMovieSceneDoubleChannel*>(Handle.Get());
TMovieSceneChannelData<FMovieSceneDoubleValue> ChannelData = DoubleChannel->GetData();
TArrayView<FMovieSceneDoubleValue> 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<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return false;
}
TSet<TSharedPtr<IKeyArea> > KeyAreas;
for (const TWeakPtr<FViewModel>& WeakItem : Sequencer->GetViewModel()->GetSelection()->Outliner)
{
SequencerHelpers::GetAllKeyAreas(WeakItem.Pin(), KeyAreas);
}
if (KeyAreas.Num() == 0)
{
for (const TWeakViewModelPtr<IOutlinerExtension>& DisplayNode : Sequencer->GetViewModel()->GetSelection()->GetNodesWithSelectedKeysOrSections())
{
SequencerHelpers::GetAllKeyAreas(DisplayNode.Pin(), KeyAreas);
}
}
for (TSharedPtr<IKeyArea> 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<FSequencer> 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<FSequencer> 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<FSequencer> 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<FSequencer> 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<FSequencer> 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<UMovieSceneSection*> Sections;
/** A set of sections that are to be operated on */
TSet<UMovieSceneSection*> 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<UMovieSceneTrack*, TMap<int32, FTrackSectionRow>> GenerateTrackRowsFromSelection(FSequencer& Sequencer)
{
TMap<UMovieSceneTrack*, TMap<int32, FTrackSectionRow>> TrackRows;
for (UMovieSceneSection* Section : Sequencer.GetViewModel()->GetSelection()->GetSelectedSections())
{
UMovieSceneTrack* Track = Section->GetTypedOuter<UMovieSceneTrack>();
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<UMovieSceneTrack*, TMap<int32, FTrackSectionRow>>& 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<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return;
}
TMap<UMovieSceneTrack*, TMap<int32, FTrackSectionRow>> 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<int32, FTrackSectionRow>& 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<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return;
}
TMap<UMovieSceneTrack*, TMap<int32, FTrackSectionRow>> 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<int32, FTrackSectionRow>& 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<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return;
}
TMap<UMovieSceneTrack*, TMap<int32, FTrackSectionRow>> 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<int32, FTrackSectionRow>& 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<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return;
}
TMap<UMovieSceneTrack*, TMap<int32, FTrackSectionRow>> 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<int32, FTrackSectionRow>& 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<FExtender> MenuExtender, TWeakPtr<FSequencer> InWeakSequencer, const FPasteContextMenuArgs& Args)
{
TSharedRef<FPasteContextMenu> Menu = MakeShareable(new FPasteContextMenu(InWeakSequencer, Args));
Menu->Setup();
if (!Menu->IsValidPaste())
{
return false;
}
Menu->PopulateMenu(MenuBuilder, MenuExtender);
return true;
}
TSharedRef<FPasteContextMenu> FPasteContextMenu::CreateMenu(TWeakPtr<FSequencer> InWeakSequencer, const FPasteContextMenuArgs& Args)
{
TSharedRef<FPasteContextMenu> Menu = MakeShareable(new FPasteContextMenu(InWeakSequencer, Args));
Menu->Setup();
return Menu;
}
TArray<TSharedPtr<UE::Sequencer::FChannelGroupModel>> KeyAreaNodesBuffer;
void FPasteContextMenu::GatherPasteDestinationsForNode(const UE::Sequencer::TViewModelPtr<UE::Sequencer::IOutlinerExtension>& InNode, UMovieSceneSection* InSection, const FName& CurrentScope, TMap<FName, FSequencerClipboardReconciler>& Map)
{
using namespace UE::Sequencer;
KeyAreaNodesBuffer.Reset();
for (const TViewModelPtr<FChannelGroupModel>& ChannelNode : InNode.AsModel()->GetDescendantsOfType<FChannelGroupModel>(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<FChannelGroupModel>& KeyAreaNode : KeyAreaNodesBuffer)
{
TSharedPtr<FChannelModel> Channel = KeyAreaNode->GetChannel(InSection);
if (Channel)
{
Group.Add(Channel);
}
}
// Add children
for (const TViewModelPtr<IOutlinerExtension>& Child : InNode.AsModel()->GetChildrenOfType<IOutlinerExtension>())
{
GatherPasteDestinationsForNode(Child, InSection, ThisScope, Map);
}
}
void FPasteContextMenu::Setup()
{
using namespace UE::Sequencer;
const TSharedPtr<FSequencer> 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<TSharedPtr<FSectionModel>> 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<IOutlinerExtension>& Node : Args.DestinationNodes)
{
TViewModelPtr<ITrackExtension> TrackNode = Node.AsModel()->FindAncestorOfType<ITrackExtension>(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<IOutlinerExtension>& Node : Args.DestinationNodes)
{
TViewModelPtr<ITrackExtension> TrackNode = Node.AsModel()->FindAncestorOfType<ITrackExtension>(true);
if (!TrackNode)
{
continue;
}
UMovieSceneSection* Section = MovieSceneHelpers::FindNearestSectionAtTime(TrackNode->GetSections(), Args.PasteAtTime);
TSharedPtr<FSectionModel> SectionModel = Sequencer->GetNodeTree()->GetSectionModel(Section);
if (SectionModel)
{
SectionModels.Add(SectionModel);
}
}
}
else
{
// Use the selected sections
for (UMovieSceneSection* WeakSection : Sequencer->GetViewModel()->GetSelection()->GetSelectedSections())
{
if (TSharedPtr<FSectionModel> SectionHandle = Sequencer->GetNodeTree()->GetSectionModel(WeakSection))
{
SectionModels.Add(SectionHandle);
}
}
}
TMap<FName, TArray<TSharedPtr<FSectionModel>>> SectionsByType;
for (TSharedPtr<FSectionModel> SectionModel : SectionModels)
{
UMovieSceneTrack* Track = SectionModel->GetParentTrackExtension()->GetTrack();
if (Track)
{
SectionsByType.FindOrAdd(Track->GetClass()->GetFName()).Add(SectionModel);
}
}
for (const TTuple<FName, TArray<TSharedPtr<FSectionModel>>>& Pair : SectionsByType)
{
FPasteDestination& Destination = PasteDestinations[PasteDestinations.AddDefaulted()];
if (Pair.Value.Num() == 1)
{
TSharedPtr<FViewModel> 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<FSectionModel> 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<FExtender> 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<FPasteContextMenu> 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<FPasteContextMenu> Shared = AsShared();
for (auto& Pair : PasteDestinations[DestinationIndex].Reconcilers)
{
MenuBuilder.AddMenuEntry(
FText::FromName(Pair.Key),
FText(),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateLambda([=](){
TSet<FSequencerSelectedKey> NewSelection;
Shared->BeginPasteInto();
const bool bAnythingPasted = Shared->PasteInto(DestinationIndex, Pair.Key, NewSelection);
Shared->EndPasteInto(bAnythingPasted, NewSelection);
})
)
);
}
}
bool FPasteContextMenu::AutoPaste()
{
TSet<FSequencerSelectedKey> 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<FSequencerSelectedKey>& NewSelection)
{
using namespace UE::Sequencer;
if (!bAnythingPasted)
{
GEditor->CancelTransaction(0);
return;
}
GEditor->EndTransaction();
UE::Sequencer::SSequencerSection::ThrobKeySelection();
const TSharedPtr<FSequencer> 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<FChannelModel> 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<FSequencerSelectedKey>& NewSelection)
{
const TSharedPtr<FSequencer> 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<UE::Sequencer::FChannelModel> Channel){
NewSelection.Add(FSequencerSelectedKey(*Channel->GetSection(), Channel, Handle));
};
return Reconciler.Paste(PasteEnvironment);
}
bool FPasteFromHistoryContextMenu::BuildMenu(FMenuBuilder& MenuBuilder, TSharedPtr<FExtender> MenuExtender, TWeakPtr<FSequencer> InWeakSequencer, const FPasteContextMenuArgs& Args)
{
const TSharedPtr<FSequencer> Sequencer = InWeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return false;
}
if (Sequencer->GetClipboardStack().Num() == 0)
{
return false;
}
TSharedRef<FPasteFromHistoryContextMenu> Menu = MakeShareable(new FPasteFromHistoryContextMenu(InWeakSequencer, Args));
Menu->PopulateMenu(MenuBuilder, MenuExtender);
return true;
}
TSharedPtr<FPasteFromHistoryContextMenu> FPasteFromHistoryContextMenu::CreateMenu(TWeakPtr<FSequencer> InWeakSequencer, const FPasteContextMenuArgs& Args)
{
const TSharedPtr<FSequencer> 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<FExtender> MenuExtender)
{
const TSharedPtr<FSequencer> 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<FPasteFromHistoryContextMenu> 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<FPasteContextMenu> 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<FExtender> MenuExtender, const TArray<UE::Sequencer::FEasingAreaHandle>& InEasings, TWeakPtr<FSequencer> InWeakSequencer, FFrameTime InMouseDownTime)
{
TSharedRef<FEasingContextMenu> EasingMenu = MakeShareable(new FEasingContextMenu(InEasings, InWeakSequencer));
EasingMenu->PopulateMenu(MenuBuilder, MenuExtender);
MenuBuilder.AddMenuSeparator();
FSectionContextMenu::BuildMenu(MenuBuilder, MenuExtender, InWeakSequencer, InMouseDownTime);
}
void FEasingContextMenu::PopulateMenu(FMenuBuilder& MenuBuilder, TSharedPtr<FExtender> MenuExtender)
{
using namespace UE::Sequencer;
const TSharedPtr<FSequencer> 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<UMovieSceneTrack>();
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<FEasingContextMenu> 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<SWidget> SpinBox = SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.Padding(FMargin(5.f,0.f))
[
SNew(SBox)
.HAlign(HAlign_Right)
[
SNew(SNumericEntryBox<double>)
.SpinBoxStyle(&FAppStyle::GetWidgetStyle<FSpinBoxStyle>("Sequencer.HyperlinkSpinBox"))
.EditableTextBoxStyle(&FAppStyle::GetWidgetStyle<FEditableTextBoxStyle>("Sequencer.HyperlinkTextBox"))
// Don't update the value when undetermined text changes
.OnUndeterminedValueChanged_Lambda([](FText){})
.AllowSpin(true)
.IsEnabled(!bReadOnly)
.MinValue(0.f)
.MaxValue(TOptional<double>())
.MaxSliderValue(TOptional<double>())
.MinSliderValue(0.f)
.Delta_Lambda([this]() -> double
{
const TSharedPtr<FSequencer> Sequencer = WeakSequencer.Pin();
if (!Sequencer.IsValid())
{
return 0;
}
return Sequencer->GetDisplayRateDeltaFrameCount();
})
.Value_Lambda([=]
{
TOptional<int32> Current = Shared->GetCurrentLength();
if (Current.IsSet())
{
return TOptional<double>(Current.GetValue());
}
return TOptional<double>();
})
.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<FText>::Create(TAttribute<FText>::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<int32> FEasingContextMenu::GetCurrentLength() const
{
using namespace UE::Sequencer;
TOptional<int32> 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<int32>();
}
}
}
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<bool> 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<UMovieSceneTrack*> AllTracks;
for (const FEasingAreaHandle& Handle : Easings)
{
if (UMovieSceneSection* Section = Handle.WeakSectionModel.Pin()->GetSection())
{
AllTracks.AddUnique(Section->GetTypedOuter<UMovieSceneTrack>());
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", "<Multiple>");
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<FClassViewerFilterFuncs> 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<const IUnloadedBlueprintData> InUnloadedClassData, TSharedRef<FClassViewerFilterFuncs> 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<FClassViewerModule>("ClassViewer");
FClassViewerInitializationOptions InitOptions;
InitOptions.NameTypeToDisplay = EClassViewerNameTypeToDisplay::DisplayName;
InitOptions.ClassFilters.Add(MakeShared<FFilter>());
// 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<FEasingContextMenu> Shared = AsShared();
TSharedRef<SWidget> 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<IMovieSceneEasingFunction>& EaseObject = Handle.EasingType == ESequencerEasingType::In ? Section->Easing.EaseIn : Section->Easing.EaseOut;
if (!EaseObject.GetObject() || EaseObject.GetObject()->GetClass() != NewClass)
{
UObject* NewEasingFunction = NewObject<UObject>(Section, NewClass);
EaseObject.SetObject(NewEasingFunction);
EaseObject.SetInterface(Cast<IMovieSceneEasingFunction>(NewEasingFunction));
}
}
}
void FEasingContextMenu::EasingOptionsMenu(FMenuBuilder& MenuBuilder)
{
using namespace UE::Sequencer;
FPropertyEditorModule& EditModule = FModuleManager::Get().GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
FDetailsViewArgs DetailsViewArgs;
DetailsViewArgs.bAllowSearch = false;
DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea;
DetailsViewArgs.bHideSelectionTip = true;
DetailsViewArgs.bShowOptions = false;
DetailsViewArgs.bShowScrollBar = false;
TSharedRef<IDetailsView> DetailsView = EditModule.CreateDetailView(DetailsViewArgs);
TArray<UObject*> 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