Files
UnrealEngineUWP/Engine/Source/Editor/Sequencer/Private/SequencerContextMenus.cpp
Max Chen 0167d4ea0f Sequencer: MVVM2 branch and Layer Bars
Copying //Tasks/UE5/Dev-SequencerMVVM2 to Main (//UE5/Main) @20364093

#preflight 628866dfb94f739b152c1e29
#preflight 628866e4585e8f793ee80943
#rb ludovic.chabant, andrew.rodham
#fyi ludovic.chabant, andrew.rodham, andrew.porter
#jira UE-105322

[CL 20364493 by Max Chen in ue5-main branch]
2022-05-25 10:39:33 -04:00

2126 lines
66 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SequencerContextMenus.h"
#include "MVVM/ViewModels/ViewModel.h"
#include "Modules/ModuleManager.h"
#include "Styling/AppStyle.h"
#include "SequencerCommonHelpers.h"
#include "SequencerCommands.h"
#include "SSequencer.h"
#include "SectionLayout.h"
#include "SSequencerSection.h"
#include "SequencerSettings.h"
#include "MVVM/Views/ITrackAreaHotspot.h"
#include "MVVM/ViewModels/ViewModel.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/ViewModels/ViewModelIterators.h"
#include "MVVM/ViewModels/SectionModel.h"
#include "MVVM/ViewModels/TrackModel.h"
#include "MVVM/ViewModels/SequencerEditorViewModel.h"
#include "Tracks/MovieScenePropertyTrack.h"
#include "Algo/AnyOf.h"
#define LOCTEXT_NAMESPACE "SequencerContextMenus"
static void CreateKeyStructForSelection(TSharedPtr<ISequencer> InSequencer, TSharedPtr<FStructOnScope>& OutKeyStruct, TWeakObjectPtr<UMovieSceneSection>& OutKeyStructSection)
{
const TSet<FSequencerSelectedKey>& SelectedKeys = InSequencer->GetSelection().GetSelectedKeys();
if (SelectedKeys.Num() == 1)
{
for (const FSequencerSelectedKey& Key : SelectedKeys)
{
if (Key.IsValid())
{
OutKeyStruct = Key.WeakChannel.Pin()->GetKeyArea()->GetKeyStruct(Key.KeyHandle);
OutKeyStructSection = Key.WeakChannel.Pin()->GetSection();
return;
}
}
}
else
{
TArray<FKeyHandle> KeyHandles;
UMovieSceneSection* CommonSection = nullptr;
for (const FSequencerSelectedKey& Key : SelectedKeys)
{
if (Key.IsValid())
{
KeyHandles.Add(Key.KeyHandle);
if (!CommonSection)
{
CommonSection = Key.WeakChannel.Pin()->GetSection();
}
else if (CommonSection != Key.WeakChannel.Pin()->GetSection())
{
CommonSection = nullptr;
return;
}
}
}
if (CommonSection)
{
OutKeyStruct = CommonSection->GetKeyStruct(KeyHandles);
OutKeyStructSection = CommonSection;
}
}
}
void FKeyContextMenu::BuildMenu(FMenuBuilder& MenuBuilder, FSequencer& InSequencer)
{
TSharedRef<FKeyContextMenu> Menu = MakeShareable(new FKeyContextMenu(InSequencer));
Menu->PopulateMenu(MenuBuilder);
}
void FKeyContextMenu::PopulateMenu(FMenuBuilder& MenuBuilder)
{
using namespace UE::Sequencer;
FSequencer* SequencerPtr = &Sequencer.Get();
TSharedRef<FKeyContextMenu> Shared = AsShared();
CreateKeyStructForSelection(Sequencer, KeyStruct, KeyStructSection);
{
ISequencerModule& SequencerModule = FModuleManager::LoadModuleChecked<ISequencerModule>("Sequencer");
FSelectedKeysByChannel SelectedKeysByChannel(SequencerPtr->GetSelection().GetSelectedKeys().Array());
TMap<FName, TArray<FExtendKeyMenuParams>> ChannelAndHandlesByType;
for (FSelectedChannelInfo& ChannelInfo : SelectedKeysByChannel.SelectedChannels)
{
FExtendKeyMenuParams ExtendKeyMenuParams;
ExtendKeyMenuParams.Section = ChannelInfo.OwningSection;
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, 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([=]{ return KeyStruct.IsValid(); })
),
NAME_None,
EUserInterfaceActionType::Button
);
}
MenuBuilder.BeginSection("SequencerKeyEdit", LOCTEXT("EditMenu", "Edit"));
{
if (HotspotCast<FKeyHotspot>(SequencerPtr->GetViewModel()->GetTrackArea()->GetHotspot()))
{
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Cut);
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Copy);
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Duplicate);
}
}
MenuBuilder.EndSection(); // SequencerKeyEdit
MenuBuilder.BeginSection("SequencerKeys", LOCTEXT("KeysMenu", "Keys"));
{
MenuBuilder.AddMenuEntry(LOCTEXT("SetKeyTime", "Set Key Time"), LOCTEXT("SetKeyTimeTooltip", "Set the key to a specified time"),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateSP(SequencerPtr, &FSequencer::SetKeyTime),
FCanExecuteAction::CreateSP(SequencerPtr, &FSequencer::CanSetKeyTime))
);
MenuBuilder.AddMenuEntry(LOCTEXT("Rekey", "Rekey"), LOCTEXT("RekeyTooltip", "Set the selected key's time to the current time"),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateSP(SequencerPtr, &FSequencer::Rekey),
FCanExecuteAction::CreateSP(SequencerPtr, &FSequencer::CanRekey))
);
MenuBuilder.AddMenuEntry(
LOCTEXT("SnapToFrame", "Snap to Frame"),
LOCTEXT("SnapToFrameToolTip", "Snap selected keys to frame"),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateSP(SequencerPtr, &FSequencer::SnapToFrame),
FCanExecuteAction::CreateSP(SequencerPtr, &FSequencer::CanSnapToFrame))
);
MenuBuilder.AddMenuEntry(
LOCTEXT("DeleteKey", "Delete"),
LOCTEXT("DeleteKeyToolTip", "Deletes the selected keys"),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(SequencerPtr, &FSequencer::DeleteSelectedKeys))
);
}
MenuBuilder.EndSection(); // SequencerKeys
}
void FKeyContextMenu::AddPropertiesMenu(FMenuBuilder& MenuBuilder)
{
auto UpdateAndRetrieveEditData = [this]
{
FKeyEditData EditData;
CreateKeyStructForSelection(Sequencer, EditData.KeyStruct, EditData.OwningSection);
return EditData;
};
MenuBuilder.AddWidget(
SNew(SKeyEditInterface, Sequencer)
.EditData_Lambda(UpdateAndRetrieveEditData)
, FText::GetEmpty(), true);
}
FSectionContextMenu::FSectionContextMenu(FSequencer& InSeqeuncer, FFrameTime InMouseDownTime)
: Sequencer(StaticCastSharedRef<FSequencer>(InSeqeuncer.AsShared()))
, MouseDownTime(InMouseDownTime)
{
for (TWeakObjectPtr<UMovieSceneSection> WeakSection : Sequencer->GetSelection().GetSelectedSections())
{
if (UMovieSceneSection* Section = WeakSection.Get())
{
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, FSequencer& InSequencer, FFrameTime InMouseDownTime)
{
TSharedRef<FSectionContextMenu> Menu = MakeShareable(new FSectionContextMenu(InSequencer, InMouseDownTime));
Menu->PopulateMenu(MenuBuilder);
}
void FSectionContextMenu::PopulateMenu(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();
// 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)
{
ChannelInterface->ExtendSectionMenu_Raw(MenuBuilder, Pair.Value, Sections, Sequencer);
}
}
MenuBuilder.AddSubMenu(
LOCTEXT("SectionProperties", "Properties"),
LOCTEXT("SectionPropertiesTooltip", "Modify the section properties"),
FNewMenuDelegate::CreateLambda([=](FMenuBuilder& SubMenuBuilder)
{
TArray<TWeakObjectPtr<UObject>> Sections;
{
for (TWeakObjectPtr<UMovieSceneSection> Section : Sequencer->GetSelection().GetSelectedSections())
{
if (Section.IsValid())
{
Sections.Add(Section);
}
}
}
SequencerHelpers::AddPropertiesMenu(*Sequencer, SubMenuBuilder, Sections);
})
);
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); } }),
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); } }),
FUIAction (
FExecuteAction(),
FCanExecuteAction::CreateLambda([=]{ return PasteFromHistoryMenu.IsValid(); })
),
NAME_None,
EUserInterfaceActionType::Button
);
}
MenuBuilder.EndSection(); // SequencerKeyEdit
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(); }))
);
}
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, &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, &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(); }))
);
}
}
MenuBuilder.EndSection(); // SequencerSections
}
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([=] { Sequencer->GetSequencerSettings()->SetDeleteKeysWhenTrimming(!Sequencer->GetSequencerSettings()->GetDeleteKeysWhenTrimming()); }),
FCanExecuteAction(),
FIsActionChecked::CreateLambda([=] { 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(); }))
);
MenuBuilder.BeginSection("SequencerInterpolation", LOCTEXT("KeyInterpolationMenu", "Key Interpolation"));
MenuBuilder.AddMenuEntry(
LOCTEXT("SetKeyInterpolationAuto", "Cubic (Auto)"),
LOCTEXT("SetKeyInterpolationAutoTooltip", "Set key interpolation to auto"),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "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(), "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(), "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(), "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(), "Sequencer.IconKeyConstant"),
FUIAction(
FExecuteAction::CreateLambda([=]{ Shared->SetInterpTangentMode(RCIM_Constant, RCTM_Auto); }),
FCanExecuteAction::CreateLambda([=]{ return Shared->CanSetInterpTangentMode(); }) )
);
MenuBuilder.EndSection(); // SequencerInterpolation
MenuBuilder.BeginSection("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 = [=](float NewValue) {
Sequencer->GetSequencerSettings()->SetReduceKeysTolerance(NewValue);
};
MenuBuilder.AddWidget(
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
[
SNew(SSpacer)
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SSpinBox<float>)
.Style(&FAppStyle::GetWidgetStyle<FSpinBoxStyle>("Sequencer.HyperlinkSpinBox"))
.OnValueCommitted_Lambda([=](float Value, ETextCommit::Type) { OnReduceKeysToleranceChanged(Value); })
.OnValueChanged_Lambda(OnReduceKeysToleranceChanged)
.MinValue(0)
.MaxValue(TOptional<float>())
.Value_Lambda([=]() -> float {
return Sequencer->GetSequencerSettings()->GetReduceKeysTolerance();
})
],
LOCTEXT("ReduceKeysTolerance", "Tolerance"));
MenuBuilder.EndSection();
}
FMovieSceneBlendTypeField FSectionContextMenu::GetSupportedBlendTypes() const
{
FMovieSceneBlendTypeField BlendTypes = FMovieSceneBlendTypeField::All();
for (TWeakObjectPtr<UMovieSceneSection> WeakSection : Sequencer->GetSelection().GetSelectedSections())
{
if (UMovieSceneSection* Section = WeakSection.Get())
{
// 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)
{
TArray<TWeakObjectPtr<UMovieSceneSection>> Sections;
for (TWeakObjectPtr<UMovieSceneSection> WeakSection : Sequencer->GetSelection().GetSelectedSections())
{
if (WeakSection.IsValid())
{
Sections.Add(WeakSection);
}
}
TWeakPtr<FSequencer> WeakSequencer = Sequencer;
FSequencerUtilities::PopulateMenu_SetBlendType(MenuBuilder, Sections, WeakSequencer);
}
void FSectionContextMenu::SelectAllKeys()
{
using namespace UE::Sequencer;
for (const TWeakObjectPtr<UMovieSceneSection>& WeakSection : Sequencer->GetSelection().GetSelectedSections())
{
UMovieSceneSection* Section = WeakSection.Get();
TSharedPtr<FSectionModel> SectionHandle = Sequencer->GetNodeTree()->GetSectionModel(Section);
if (!SectionHandle)
{
continue;
}
FSectionLayout Layout(SectionHandle);
for (const FSectionLayoutElement& Element : Layout.GetElements())
{
for (const TWeakPtr<FChannelModel>& WeakChannel : Element.GetChannels())
{
if (TSharedPtr<FChannelModel> Channel = WeakChannel.Pin())
{
TArray<FKeyHandle> Handles;
Channel->GetKeyArea()->GetKeyHandles(Handles);
for (FKeyHandle KeyHandle : Handles)
{
FSequencerSelectedKey SelectKey(*Section, Channel, KeyHandle);
Sequencer->GetSelection().AddToSelection(SelectKey);
}
}
}
}
}
}
void FSectionContextMenu::CopyAllKeys()
{
SelectAllKeys();
Sequencer->CopySelectedKeys();
}
void FSectionContextMenu::SetSectionToKey()
{
if (Sequencer->GetSelection().GetSelectedSections().Num() != 1)
{
return;
}
for (TWeakObjectPtr<UMovieSceneSection> WeakSection : Sequencer->GetSelection().GetSelectedSections())
{
if (UMovieSceneSection* Section = WeakSection.Get())
{
UMovieSceneTrack* Track = Section->GetTypedOuter<UMovieSceneTrack>();
if (Track)
{
FScopedTransaction Transaction(LOCTEXT("SetSectionToKey", "Set Section To Key"));
Track->Modify();
Track->SetSectionToKey(Section);
}
}
break;
}
}
bool FSectionContextMenu::CanSetSectionToKey() const
{
if (Sequencer->GetSelection().GetSelectedSections().Num() != 1)
{
return false;
}
for (TWeakObjectPtr<UMovieSceneSection> WeakSection : Sequencer->GetSelection().GetSelectedSections())
{
if (UMovieSceneSection* Section = WeakSection.Get())
{
UMovieSceneTrack* Track = Section->GetTypedOuter<UMovieSceneTrack>();
if (Track && Section->GetBlendType().IsValid() && (Section->GetBlendType().Get() == EMovieSceneBlendType::Absolute || Section->GetBlendType().Get() == EMovieSceneBlendType::Additive))
{
return true;
}
}
break;
}
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()
{
FScopedTransaction AutoSizeSectionTransaction(LOCTEXT("AutoSizeSection_Transaction", "Auto Size Section"));
for (auto Section : Sequencer->GetSelection().GetSelectedSections())
{
if (Section.IsValid() && 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;
FScopedTransaction ReduceKeysTransaction(LOCTEXT("ReduceKeys_Transaction", "Reduce Keys"));
TSet<TSharedPtr<IKeyArea> > KeyAreas;
for (const TWeakPtr<FViewModel>& WeakItem : Sequencer->GetSelection().GetSelectedOutlinerItems())
{
SequencerHelpers::GetAllKeyAreas(WeakItem.Pin(), KeyAreas);
}
if (KeyAreas.Num() == 0)
{
for (const TWeakPtr<FViewModel>& DisplayNode : Sequencer->GetSelection().GetNodesWithSelectedKeysOrSections())
{
SequencerHelpers::GetAllKeyAreas(DisplayNode.Pin(), KeyAreas);
}
}
FKeyDataOptimizationParams Params;
Params.bAutoSetInterpolation = true;
Params.Tolerance = Sequencer->GetSequencerSettings()->GetReduceKeysTolerance();
for (TSharedPtr<IKeyArea> KeyArea : KeyAreas)
{
if (KeyArea.IsValid())
{
UMovieSceneSection* Section = KeyArea->GetOwningSection();
if (Section)
{
Section->Modify();
for (const FMovieSceneChannelEntry& Entry : Section->GetChannelProxy().GetAllEntries())
{
for (FMovieSceneChannel* Channel : Entry.GetChannels())
{
Channel->Optimize(Params);
}
}
}
}
}
Sequencer->NotifyMovieSceneDataChanged( EMovieSceneDataChangeType::TrackValueChanged );
}
bool FSectionContextMenu::CanAutoSize() const
{
for (auto Section : Sequencer->GetSelection().GetSelectedSections())
{
if (Section.IsValid() && Section->GetAutoSizeRange().IsSet())
{
return true;
}
}
return false;
}
bool FSectionContextMenu::CanReduceKeys() const
{
using namespace UE::Sequencer;
TSet<TSharedPtr<IKeyArea> > KeyAreas;
for (const TWeakPtr<FViewModel>& WeakItem : Sequencer->GetSelection().GetSelectedOutlinerItems())
{
SequencerHelpers::GetAllKeyAreas(WeakItem.Pin(), KeyAreas);
}
if (KeyAreas.Num() == 0)
{
for (const TWeakPtr<FViewModel>& DisplayNode : Sequencer->GetSelection().GetNodesWithSelectedKeysOrSections())
{
SequencerHelpers::GetAllKeyAreas(DisplayNode.Pin(), KeyAreas);
}
}
return KeyAreas.Num() != 0;
}
void FSectionContextMenu::SetInterpTangentMode(ERichCurveInterpMode InterpMode, ERichCurveTangentMode TangentMode)
{
using namespace UE::Sequencer;
FScopedTransaction SetInterpTangentModeTransaction(LOCTEXT("SetInterpTangentMode_Transaction", "Set Interpolation and Tangent Mode"));
TSet<TSharedPtr<IKeyArea> > KeyAreas;
for (const TWeakPtr<FViewModel>& WeakItem : Sequencer->GetSelection().GetSelectedOutlinerItems())
{
SequencerHelpers::GetAllKeyAreas(WeakItem.Pin(), KeyAreas);
}
if (KeyAreas.Num() == 0)
{
for (const TWeakPtr<FViewModel>& DisplayNode : Sequencer->GetSelection().GetNodesWithSelectedKeysOrSections())
{
SequencerHelpers::GetAllKeyAreas(DisplayNode.Pin(), KeyAreas);
}
}
bool bAnythingChanged = false;
for (TSharedPtr<IKeyArea> KeyArea : KeyAreas)
{
if (KeyArea.IsValid())
{
UMovieSceneSection* Section = KeyArea->GetOwningSection();
if (Section)
{
Section->Modify();
for (FMovieSceneFloatChannel* FloatChannel : Section->GetChannelProxy().GetChannels<FMovieSceneFloatChannel>())
{
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();
}
for (FMovieSceneDoubleChannel* DoubleChannel : Section->GetChannelProxy().GetChannels<FMovieSceneDoubleChannel>())
{
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;
TSet<TSharedPtr<IKeyArea> > KeyAreas;
for (const TWeakPtr<FViewModel>& WeakItem : Sequencer->GetSelection().GetSelectedOutlinerItems())
{
SequencerHelpers::GetAllKeyAreas(WeakItem.Pin(), KeyAreas);
}
if (KeyAreas.Num() == 0)
{
for (const TWeakPtr<FViewModel>& DisplayNode : Sequencer->GetSelection().GetNodesWithSelectedKeysOrSections())
{
SequencerHelpers::GetAllKeyAreas(DisplayNode.Pin(), KeyAreas);
}
}
for (TSharedPtr<IKeyArea> KeyArea : KeyAreas)
{
if (KeyArea.IsValid())
{
UMovieSceneSection* Section = KeyArea->GetOwningSection();
if (Section)
{
return (Section->GetChannelProxy().GetChannels<FMovieSceneFloatChannel>().Num() != 0 ||
Section->GetChannelProxy().GetChannels<FMovieSceneDoubleChannel>().Num() != 0);
}
}
}
return false;
}
void FSectionContextMenu::ToggleSectionActive()
{
FScopedTransaction ToggleSectionActiveTransaction( LOCTEXT("ToggleSectionActive_Transaction", "Toggle Section Active") );
bool bIsActive = !IsSectionActive();
bool bAnythingChanged = false;
for (auto Section : Sequencer->GetSelection().GetSelectedSections())
{
if (Section.IsValid())
{
bAnythingChanged = true;
Section->Modify();
Section->SetIsActive(bIsActive);
}
}
if (bAnythingChanged)
{
Sequencer->NotifyMovieSceneDataChanged( EMovieSceneDataChangeType::TrackValueChanged );
}
else
{
ToggleSectionActiveTransaction.Cancel();
}
}
bool FSectionContextMenu::IsSectionActive() const
{
// Active only if all are active
for (auto Section : Sequencer->GetSelection().GetSelectedSections())
{
if (Section.IsValid() && !Section->IsActive())
{
return false;
}
}
return true;
}
void FSectionContextMenu::ToggleSectionLocked()
{
FScopedTransaction ToggleSectionLockedTransaction( NSLOCTEXT("Sequencer", "ToggleSectionLocked_Transaction", "Toggle Section Locked") );
bool bIsLocked = !IsSectionLocked();
bool bAnythingChanged = false;
for (auto Section : Sequencer->GetSelection().GetSelectedSections())
{
if (Section.IsValid())
{
bAnythingChanged = true;
Section->Modify();
Section->SetIsLocked(bIsLocked);
}
}
if (bAnythingChanged)
{
Sequencer->NotifyMovieSceneDataChanged( EMovieSceneDataChangeType::TrackValueChanged );
}
else
{
ToggleSectionLockedTransaction.Cancel();
}
}
bool FSectionContextMenu::IsSectionLocked() const
{
// Locked only if all are locked
for (auto Section : Sequencer->GetSelection().GetSelectedSections())
{
if (Section.IsValid() && !Section->IsLocked())
{
return false;
}
}
return true;
}
void FSectionContextMenu::DeleteSection()
{
Sequencer->DeleteSections(Sequencer->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 (const TWeakObjectPtr<UMovieSceneSection>& SectionPtr : Sequencer.GetSelection().GetSelectedSections())
{
UMovieSceneSection* Section = SectionPtr.Get();
if (!Section)
{
continue;
}
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()
{
TMap<UMovieSceneTrack*, TMap<int32, FTrackSectionRow>> TrackRows = GenerateTrackRowsFromSelection(*Sequencer);
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()
{
TMap<UMovieSceneTrack*, TMap<int32, FTrackSectionRow>> TrackRows = GenerateTrackRowsFromSelection(*Sequencer);
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()
{
TMap<UMovieSceneTrack*, TMap<int32, FTrackSectionRow>> TrackRows = GenerateTrackRowsFromSelection(*Sequencer);
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() - 1; 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()
{
TMap<UMovieSceneTrack*, TMap<int32, FTrackSectionRow>> TrackRows = GenerateTrackRowsFromSelection(*Sequencer);
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, FSequencer& InSequencer, const FPasteContextMenuArgs& Args)
{
TSharedRef<FPasteContextMenu> Menu = MakeShareable(new FPasteContextMenu(InSequencer, Args));
Menu->Setup();
if (!Menu->IsValidPaste())
{
return false;
}
Menu->PopulateMenu(MenuBuilder);
return true;
}
TSharedRef<FPasteContextMenu> FPasteContextMenu::CreateMenu(FSequencer& InSequencer, const FPasteContextMenuArgs& Args)
{
TSharedRef<FPasteContextMenu> Menu = MakeShareable(new FPasteContextMenu(InSequencer, 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);
}
}
TSharedPtr<UE::Sequencer::FTrackModel> GetTrackFromNode(const UE::Sequencer::TViewModelPtr<UE::Sequencer::IOutlinerExtension>& InNode, FString& Scope)
{
using namespace UE::Sequencer;
if (TSharedPtr<FTrackModel> TrackModel = InNode.ImplicitCast())
{
return TrackModel;
}
if (TSharedPtr<FObjectBindingModel> ObjectBindingModel = InNode.ImplicitCast())
{
return nullptr;
}
TViewModelPtr<IOutlinerExtension> Parent = InNode.AsModel()->FindAncestorOfType<IOutlinerExtension>();
if (Parent)
{
TSharedPtr<FTrackModel> Track = GetTrackFromNode(Parent, Scope);
if (Track.IsValid())
{
FString ThisScope = Track->GetLabel().ToString();
if (!Scope.IsEmpty())
{
ThisScope.AppendChar('.');
ThisScope.Append(Scope);
Scope = MoveTemp(ThisScope);
}
return Track;
}
}
return nullptr;
}
void FPasteContextMenu::Setup()
{
using namespace UE::Sequencer;
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)
{
if (!Node.AsModel()->IsA<FChannelModel>() && !Node.AsModel()->IsA<FChannelGroupModel>())
{
continue;
}
FString Scope;
TSharedPtr<FTrackModel> TrackNode = GetTrackFromNode(Node, Scope);
if (!TrackNode.IsValid())
{
continue;
}
FPasteDestination& Destination = PasteDestinations[PasteDestinations.AddDefaulted()];
TArray<UMovieSceneSection*> Sections;
for (const TViewModelPtr<FSectionModel>& Section : TrackNode->GetSections().IterateSubList<FSectionModel>())
{
if (Section->GetSection())
{
GatherPasteDestinationsForNode(Node, Section->GetSection(), 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, 1, false);
}
}
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)
{
FString Scope;
TSharedPtr<FTrackModel> TrackNode = GetTrackFromNode(Node, Scope);
if (!TrackNode.IsValid())
{
continue;
}
TArray<UMovieSceneSection*> Sections;
for (const TViewModelPtr<FSectionModel>& Section : TrackNode->GetSections().IterateSubList<FSectionModel>())
{
if (Section->GetSection())
{
Sections.Add(Section->GetSection());
}
}
UMovieSceneSection* Section = MovieSceneHelpers::FindNearestSectionAtTime(Sections, Args.PasteAtTime);
TSharedPtr<FSectionModel> SectionModel = Sequencer->GetNodeTree()->GetSectionModel(Section);
if (SectionModel)
{
SectionModels.Add(SectionModel);
}
}
}
else
{
// Use the selected sections
for (TWeakObjectPtr<UMovieSceneSection> WeakSection : Sequencer->GetSelection().GetSelectedSections())
{
if (TSharedPtr<FSectionModel> SectionHandle = Sequencer->GetNodeTree()->GetSectionModel(WeakSection.Get()))
{
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, 1, false);
}
}
}
bool FPasteContextMenu::IsValidPaste() const
{
return Args.Clipboard.IsValid() && PasteDestinations.Num() != 0;
}
void FPasteContextMenu::PopulateMenu(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<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)
{
if (!bAnythingPasted)
{
GEditor->CancelTransaction(0);
return;
}
GEditor->EndTransaction();
UE::Sequencer::SSequencerSection::ThrobKeySelection();
FSequencerSelection& Selection = Sequencer->GetSelection();
Selection.SuspendBroadcast();
Selection.EmptySelectedTrackAreaItems();
Selection.EmptySelectedKeys();
for (const FSequencerSelectedKey& Key : NewSelection)
{
Selection.AddToSelection(Key);
}
Selection.ResumeBroadcast();
Selection.GetOnKeySelectionChanged().Broadcast();
Sequencer->OnClipboardUsed(Args.Clipboard);
Sequencer->NotifyMovieSceneDataChanged(EMovieSceneDataChangeType::TrackValueChanged);
}
bool FPasteContextMenu::PasteInto(int32 DestinationIndex, FName KeyAreaName, TSet<FSequencerSelectedKey>& NewSelection)
{
FSequencerClipboardReconciler& Reconciler = PasteDestinations[DestinationIndex].Reconcilers[KeyAreaName];
FSequencerPasteEnvironment PasteEnvironment;
PasteEnvironment.TickResolution = Sequencer->GetFocusedTickResolution();
PasteEnvironment.CardinalTime = Args.PasteAtTime;
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, FSequencer& InSequencer, const FPasteContextMenuArgs& Args)
{
if (InSequencer.GetClipboardStack().Num() == 0)
{
return false;
}
TSharedRef<FPasteFromHistoryContextMenu> Menu = MakeShareable(new FPasteFromHistoryContextMenu(InSequencer, Args));
Menu->PopulateMenu(MenuBuilder);
return true;
}
TSharedPtr<FPasteFromHistoryContextMenu> FPasteFromHistoryContextMenu::CreateMenu(FSequencer& InSequencer, const FPasteContextMenuArgs& Args)
{
if (InSequencer.GetClipboardStack().Num() == 0)
{
return nullptr;
}
return MakeShareable(new FPasteFromHistoryContextMenu(InSequencer, Args));
}
void FPasteFromHistoryContextMenu::PopulateMenu(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<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); }),
FUIAction (
FExecuteAction(),
FCanExecuteAction::CreateLambda([=]{ return PasteMenu->IsValidPaste(); })
),
NAME_None,
EUserInterfaceActionType::Button
);
}
MenuBuilder.EndSection();
}
void FEasingContextMenu::BuildMenu(FMenuBuilder& MenuBuilder, const TArray<UE::Sequencer::FEasingAreaHandle>& InEasings, FSequencer& Sequencer, FFrameTime InMouseDownTime)
{
TSharedRef<FEasingContextMenu> EasingMenu = MakeShareable(new FEasingContextMenu(InEasings, Sequencer));
EasingMenu->PopulateMenu(MenuBuilder);
MenuBuilder.AddMenuSeparator();
FSectionContextMenu::BuildMenu(MenuBuilder, Sequencer, InMouseDownTime);
}
void FEasingContextMenu::PopulateMenu(FMenuBuilder& MenuBuilder)
{
using namespace UE::Sequencer;
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([=]() -> double { 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