Files
UnrealEngineUWP/Engine/Source/Editor/CurveEditor/Private/CurveEditor.cpp
Max Chen f137d29b86 Curve Editor: Add select all keys forward and backwards shortcuts: Ctrl-] and Ctrl-[
#jira UE-152631
#preflight 6288345c95170b5599ffc30e
#rb matt.hoffman

[CL 20335578 by Max Chen in ue5-main branch]
2022-05-23 15:33:28 -04:00

2145 lines
69 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "CurveEditor.h"
#include "Layout/Geometry.h"
#include "CurveEditorSnapMetrics.h"
#include "CurveEditorCommands.h"
#include "CurveEditorSettings.h"
#include "CurveDrawInfo.h"
#include "CurveEditorCopyBuffer.h"
#include "Framework/Commands/GenericCommands.h"
#include "Framework/Commands/UICommandList.h"
#include "Editor.h"
#include "ScopedTransaction.h"
#include "SCurveEditorView.h"
#include "SCurveEditorPanel.h"
#include "ICurveEditorExtension.h"
#include "ICurveEditorToolExtension.h"
#include "Tree/ICurveEditorTreeItem.h"
#include "Filters/CurveEditorFilterBase.h"
#include "Filters/SCurveEditorFilterPanel.h"
#include "Editor/EditorEngine.h"
#include "ITimeSlider.h"
#include "Framework/Notifications/NotificationManager.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "Runtime/Core/Public/Algo/Transform.h"
#include "UnrealExporter.h"
#include "Exporters/Exporter.h"
#include "Factories.h"
#include "HAL/PlatformApplicationMisc.h"
#include "SCurveEditor.h" // for access to LogCurveEditor
#include "Widgets/Colors/SColorPicker.h"
#define LOCTEXT_NAMESPACE "CurveEditor"
FCurveModelID FCurveModelID::Unique()
{
static uint32 CurrentID = 1;
FCurveModelID ID;
ID.ID = CurrentID++;
return ID;
}
FCurveEditor::FCurveEditor()
: Bounds(new FStaticCurveEditorBounds)
, bBoundTransformUpdatesSuppressed(false)
, ActiveCurvesSerialNumber(0)
, SuspendBroadcastCount(0)
{
Settings = GetMutableDefault<UCurveEditorSettings>();
CommandList = MakeShared<FUICommandList>();
OutputSnapEnabledAttribute = true;
InputSnapEnabledAttribute = true;
InputSnapRateAttribute = FFrameRate(10, 1);
GridLineLabelFormatXAttribute = LOCTEXT("GridXLabelFormat", "{0}s");
GridLineLabelFormatYAttribute = LOCTEXT("GridYLabelFormat", "{0}");
}
void FCurveEditor::InitCurveEditor(const FCurveEditorInitParams& InInitParams)
{
ICurveEditorModule& CurveEditorModule = FModuleManager::LoadModuleChecked<ICurveEditorModule>("CurveEditor");
Selection = FCurveEditorSelection(SharedThis(this));
// Editor Extensions can be registered in the Curve Editor module. To allow users to derive from FCurveEditor
// we have to manually reach out to the module and get a list of extensions to create an instance of them.
// If none of your extensions are showing up, it's because you forgot to call this function after construction
// We're not allowed to use SharedThis(...) in a Constructor so it must exist as a separate function call.
TArrayView<const FOnCreateCurveEditorExtension> Extensions = CurveEditorModule.GetEditorExtensions();
for (int32 DelegateIndex = 0; DelegateIndex < Extensions.Num(); ++DelegateIndex)
{
check(Extensions[DelegateIndex].IsBound());
// We call a delegate and have the delegate create the instance to cover cross-module
TSharedRef<ICurveEditorExtension> NewExtension = Extensions[DelegateIndex].Execute(SharedThis(this));
EditorExtensions.Add(NewExtension);
}
TArrayView<const FOnCreateCurveEditorToolExtension> Tools = CurveEditorModule.GetToolExtensions();
for (int32 DelegateIndex = 0; DelegateIndex < Tools.Num(); ++DelegateIndex)
{
check(Tools[DelegateIndex].IsBound());
// We call a delegate and have the delegate create the instance to cover cross-module
AddTool(Tools[DelegateIndex].Execute(SharedThis(this)));
}
SuspendBroadcastCount = 0;
// Listen to global undo so we can fix up our selection state for keys that no longer exist.
GEditor->RegisterForUndo(this);
}
void FCurveEditor::SetPanel(TSharedPtr<SCurveEditorPanel> InPanel)
{
WeakPanel = InPanel;
}
TSharedPtr<SCurveEditorPanel> FCurveEditor::GetPanel() const
{
return WeakPanel.Pin();
}
void FCurveEditor::SetView(TSharedPtr<SCurveEditorView> InView)
{
WeakView = InView;
}
TSharedPtr<SCurveEditorView> FCurveEditor::GetView() const
{
return WeakView.Pin();
}
FCurveModel* FCurveEditor::FindCurve(FCurveModelID CurveID) const
{
const TUniquePtr<FCurveModel>* Ptr = CurveData.Find(CurveID);
return Ptr ? Ptr->Get() : nullptr;
}
const TMap<FCurveModelID, TUniquePtr<FCurveModel>>& FCurveEditor::GetCurves() const
{
return CurveData;
}
FCurveEditorToolID FCurveEditor::AddTool(TUniquePtr<ICurveEditorToolExtension>&& InTool)
{
FCurveEditorToolID NewID = FCurveEditorToolID::Unique();
ToolExtensions.Add(NewID, MoveTemp(InTool));
ToolExtensions[NewID]->SetToolID(NewID);
return NewID;
}
FCurveModelID FCurveEditor::AddCurve(TUniquePtr<FCurveModel>&& InCurve)
{
FCurveModelID NewID = FCurveModelID::Unique();
FCurveModel *Curve = InCurve.Get();
CurveData.Add(NewID, MoveTemp(InCurve));
++ActiveCurvesSerialNumber;
if (IsBroadcasting())
{
OnCurveArrayChanged.Broadcast(Curve, true, this);
}
return NewID;
}
void FCurveEditor::BroadcastCurveChanged(FCurveModel* InCurve)
{
if (IsBroadcasting())
{
OnCurveArrayChanged.Broadcast(InCurve, true, this);
}
}
FCurveModelID FCurveEditor::AddCurveForTreeItem(TUniquePtr<FCurveModel>&& InCurve, FCurveEditorTreeItemID TreeItemID)
{
FCurveModelID NewID = FCurveModelID::Unique();
FCurveModel *Curve = InCurve.Get();
if(IsBroadcasting())
{
OnCurveArrayChanged.Broadcast(InCurve.Get(), true, this);
}
CurveData.Add(NewID, MoveTemp(InCurve));
TreeIDByCurveID.Add(NewID, TreeItemID);
++ActiveCurvesSerialNumber;
return NewID;
}
void FCurveEditor::RemoveCurve(FCurveModelID InCurveID)
{
TSharedPtr<SCurveEditorPanel> Panel = WeakPanel.Pin();
if (Panel.IsValid())
{
Panel->RemoveCurveFromViews(InCurveID);
}
if(IsBroadcasting())
{
OnCurveArrayChanged.Broadcast(FindCurve(InCurveID), false,this);
}
CurveData.Remove(InCurveID);
Selection.Remove(InCurveID);
PinnedCurves.Remove(InCurveID);
++ActiveCurvesSerialNumber;
}
void FCurveEditor::RemoveAllCurves()
{
TSharedPtr<SCurveEditorPanel> Panel = WeakPanel.Pin();
if (Panel.IsValid())
{
for (TPair<FCurveModelID, TUniquePtr<FCurveModel>>& CurvePair : CurveData)
{
Panel->RemoveCurveFromViews(CurvePair.Key);
}
}
CurveData.Empty();
Selection.Clear();
PinnedCurves.Empty();
++ActiveCurvesSerialNumber;
}
bool FCurveEditor::IsCurvePinned(FCurveModelID InCurveID) const
{
return PinnedCurves.Contains(InCurveID);
}
void FCurveEditor::PinCurve(FCurveModelID InCurveID)
{
PinnedCurves.Add(InCurveID);
++ActiveCurvesSerialNumber;
}
void FCurveEditor::UnpinCurve(FCurveModelID InCurveID)
{
PinnedCurves.Remove(InCurveID);
++ActiveCurvesSerialNumber;
}
const SCurveEditorView* FCurveEditor::FindFirstInteractiveView(FCurveModelID InCurveID) const
{
TSharedPtr<SCurveEditorPanel> Panel = WeakPanel.Pin();
if (Panel.IsValid())
{
for (auto ViewIt = Panel->FindViews(InCurveID); ViewIt; ++ViewIt)
{
if (ViewIt.Value()->IsInteractive())
{
return &ViewIt.Value().Get();
}
}
}
return nullptr;
}
FCurveEditorTreeItem& FCurveEditor::GetTreeItem(FCurveEditorTreeItemID ItemID)
{
return Tree.GetItem(ItemID);
}
const FCurveEditorTreeItem& FCurveEditor::GetTreeItem(FCurveEditorTreeItemID ItemID) const
{
return Tree.GetItem(ItemID);
}
FCurveEditorTreeItem* FCurveEditor::FindTreeItem(FCurveEditorTreeItemID ItemID)
{
return Tree.FindItem(ItemID);
}
const FCurveEditorTreeItem* FCurveEditor::FindTreeItem(FCurveEditorTreeItemID ItemID) const
{
return Tree.FindItem(ItemID);
}
const TArray<FCurveEditorTreeItemID>& FCurveEditor::GetRootTreeItems() const
{
return Tree.GetRootItems();
}
FCurveEditorTreeItemID FCurveEditor::GetTreeIDFromCurveID(FCurveModelID CurveID) const
{
if (TreeIDByCurveID.Contains(CurveID))
{
return TreeIDByCurveID[CurveID];
}
return FCurveEditorTreeItemID();
}
FCurveEditorTreeItem* FCurveEditor::AddTreeItem(FCurveEditorTreeItemID ParentID)
{
return Tree.AddItem(ParentID);
}
void FCurveEditor::RemoveTreeItem(FCurveEditorTreeItemID ItemID)
{
FCurveEditorTreeItem* Item = Tree.FindItem(ItemID);
if (!Item)
{
return;
}
Tree.RemoveItem(ItemID, this);
++ActiveCurvesSerialNumber;
}
void FCurveEditor::RemoveAllTreeItems()
{
TArray<FCurveEditorTreeItemID> RootItems = Tree.GetRootItems();
for(FCurveEditorTreeItemID ItemID : RootItems)
{
Tree.RemoveItem(ItemID, this);
}
++ActiveCurvesSerialNumber;
}
void FCurveEditor::SetTreeSelection(TArray<FCurveEditorTreeItemID>&& TreeItems)
{
Tree.SetDirectSelection(MoveTemp(TreeItems), this);
}
void FCurveEditor::RemoveFromTreeSelection(TArrayView<const FCurveEditorTreeItemID> TreeItems)
{
Tree.RemoveFromSelection(TreeItems, this);
}
ECurveEditorTreeSelectionState FCurveEditor::GetTreeSelectionState(FCurveEditorTreeItemID InTreeItemID) const
{
return Tree.GetSelectionState(InTreeItemID);
}
const TMap<FCurveEditorTreeItemID, ECurveEditorTreeSelectionState>& FCurveEditor::GetTreeSelection() const
{
return Tree.GetSelection();
}
void FCurveEditor::SetBounds(TUniquePtr<ICurveEditorBounds>&& InBounds)
{
check(InBounds.IsValid());
Bounds = MoveTemp(InBounds);
}
bool FCurveEditor::ShouldAutoFrame() const
{
return Settings->GetAutoFrameCurveEditor();
}
void FCurveEditor::BindCommands()
{
UCurveEditorSettings* CurveSettings = Settings;
CommandList->MapAction(FGenericCommands::Get().Undo, FExecuteAction::CreateLambda([]{ GEditor->UndoTransaction(); }));
CommandList->MapAction(FGenericCommands::Get().Redo, FExecuteAction::CreateLambda([]{ GEditor->RedoTransaction(); }));
CommandList->MapAction(FGenericCommands::Get().Delete, FExecuteAction::CreateSP(this, &FCurveEditor::DeleteSelection));
CommandList->MapAction(FGenericCommands::Get().Cut, FExecuteAction::CreateSP(this, &FCurveEditor::CutSelection));
CommandList->MapAction(FGenericCommands::Get().Copy, FExecuteAction::CreateSP(this, &FCurveEditor::CopySelection));
CommandList->MapAction(FGenericCommands::Get().Paste, FExecuteAction::CreateSP(this, &FCurveEditor::PasteKeys, TSet<FCurveModelID>()));
CommandList->MapAction(FCurveEditorCommands::Get().ZoomToFit, FExecuteAction::CreateSP(this, &FCurveEditor::ZoomToFit, EAxisList::All));
CommandList->MapAction(FCurveEditorCommands::Get().ToggleExpandCollapseNodes, FExecuteAction::CreateSP(this, &FCurveEditor::ToggleExpandCollapseNodes, false));
CommandList->MapAction(FCurveEditorCommands::Get().ToggleExpandCollapseNodesAndDescendants, FExecuteAction::CreateSP(this, &FCurveEditor::ToggleExpandCollapseNodes, true));
CommandList->MapAction(FCurveEditorCommands::Get().TranslateSelectedKeysLeft, FExecuteAction::CreateSP(this, &FCurveEditor::TranslateSelectedKeysLeft));
CommandList->MapAction(FCurveEditorCommands::Get().TranslateSelectedKeysRight, FExecuteAction::CreateSP(this, &FCurveEditor::TranslateSelectedKeysRight));
CommandList->MapAction(FCurveEditorCommands::Get().StepToNextKey, FExecuteAction::CreateSP(this, &FCurveEditor::StepToNextKey));
CommandList->MapAction(FCurveEditorCommands::Get().StepToPreviousKey, FExecuteAction::CreateSP(this, &FCurveEditor::StepToPreviousKey));
CommandList->MapAction(FCurveEditorCommands::Get().StepForward, FExecuteAction::CreateSP(this, &FCurveEditor::StepForward), EUIActionRepeatMode::RepeatEnabled);
CommandList->MapAction(FCurveEditorCommands::Get().StepBackward, FExecuteAction::CreateSP(this, &FCurveEditor::StepBackward), EUIActionRepeatMode::RepeatEnabled);
CommandList->MapAction(FCurveEditorCommands::Get().JumpToStart, FExecuteAction::CreateSP(this, &FCurveEditor::JumpToStart));
CommandList->MapAction(FCurveEditorCommands::Get().JumpToEnd, FExecuteAction::CreateSP(this, &FCurveEditor::JumpToEnd));
CommandList->MapAction(FCurveEditorCommands::Get().SetSelectionRangeStart, FExecuteAction::CreateSP(this, &FCurveEditor::SetSelectionRangeStart));
CommandList->MapAction(FCurveEditorCommands::Get().SetSelectionRangeEnd, FExecuteAction::CreateSP(this, &FCurveEditor::SetSelectionRangeEnd));
CommandList->MapAction(FCurveEditorCommands::Get().ClearSelectionRange, FExecuteAction::CreateSP(this, &FCurveEditor::ClearSelectionRange));
CommandList->MapAction(FCurveEditorCommands::Get().SelectAllKeys, FExecuteAction::CreateSP(this, &FCurveEditor::SelectAllKeys));
CommandList->MapAction(FCurveEditorCommands::Get().SelectForward, FExecuteAction::CreateSP(this, &FCurveEditor::SelectForward));
CommandList->MapAction(FCurveEditorCommands::Get().SelectBackward, FExecuteAction::CreateSP(this, &FCurveEditor::SelectBackward));
{
FExecuteAction ToggleInputSnapping = FExecuteAction::CreateSP(this, &FCurveEditor::ToggleInputSnapping);
FIsActionChecked IsInputSnappingEnabled = FIsActionChecked::CreateSP(this, &FCurveEditor::IsInputSnappingEnabled);
FExecuteAction ToggleOutputSnapping = FExecuteAction::CreateSP(this, &FCurveEditor::ToggleOutputSnapping);
FIsActionChecked IsOutputSnappingEnabled = FIsActionChecked::CreateSP(this, &FCurveEditor::IsOutputSnappingEnabled);
CommandList->MapAction(FCurveEditorCommands::Get().ToggleInputSnapping, ToggleInputSnapping, FCanExecuteAction(), IsInputSnappingEnabled);
CommandList->MapAction(FCurveEditorCommands::Get().ToggleOutputSnapping, ToggleOutputSnapping, FCanExecuteAction(), IsOutputSnappingEnabled);
}
// Flatten and Straighten Tangents
{
CommandList->MapAction(FCurveEditorCommands::Get().FlattenTangents, FExecuteAction::CreateSP(this, &FCurveEditor::FlattenSelection), FCanExecuteAction::CreateSP(this, &FCurveEditor::CanFlattenOrStraightenSelection) );
CommandList->MapAction(FCurveEditorCommands::Get().StraightenTangents, FExecuteAction::CreateSP(this, &FCurveEditor::StraightenSelection), FCanExecuteAction::CreateSP(this, &FCurveEditor::CanFlattenOrStraightenSelection) );
}
// Curve Colors
{
CommandList->MapAction(FCurveEditorCommands::Get().SetRandomCurveColorsForSelected, FExecuteAction::CreateSP(this, &FCurveEditor::SetRandomCurveColorsForSelected), FCanExecuteAction());
CommandList->MapAction(FCurveEditorCommands::Get().SetCurveColorsForSelected, FExecuteAction::CreateSP(this, &FCurveEditor::SetCurveColorsForSelected), FCanExecuteAction());
}
// Tangent Visibility
{
FExecuteAction SetAllTangents = FExecuteAction::CreateUObject(Settings, &UCurveEditorSettings::SetTangentVisibility, ECurveEditorTangentVisibility::AllTangents);
FExecuteAction SetSelectedKeyTangents = FExecuteAction::CreateUObject(Settings, &UCurveEditorSettings::SetTangentVisibility, ECurveEditorTangentVisibility::SelectedKeys);
FExecuteAction SetNoTangents = FExecuteAction::CreateUObject(Settings, &UCurveEditorSettings::SetTangentVisibility, ECurveEditorTangentVisibility::NoTangents);
FIsActionChecked IsAllTangents = FIsActionChecked::CreateLambda( [CurveSettings]{ return CurveSettings->GetTangentVisibility() == ECurveEditorTangentVisibility::AllTangents; } );
FIsActionChecked IsSelectedKeyTangents = FIsActionChecked::CreateLambda( [CurveSettings]{ return CurveSettings->GetTangentVisibility() == ECurveEditorTangentVisibility::SelectedKeys; } );
FIsActionChecked IsNoTangents = FIsActionChecked::CreateLambda( [CurveSettings]{ return CurveSettings->GetTangentVisibility() == ECurveEditorTangentVisibility::NoTangents; } );
CommandList->MapAction(FCurveEditorCommands::Get().SetAllTangentsVisibility, SetAllTangents, FCanExecuteAction(), IsAllTangents);
CommandList->MapAction(FCurveEditorCommands::Get().SetSelectedKeysTangentVisibility, SetSelectedKeyTangents, FCanExecuteAction(), IsSelectedKeyTangents);
CommandList->MapAction(FCurveEditorCommands::Get().SetNoTangentsVisibility, SetNoTangents, FCanExecuteAction(), IsNoTangents);
}
CommandList->MapAction(FCurveEditorCommands::Get().ToggleAutoFrameCurveEditor,
FExecuteAction::CreateLambda( [CurveSettings]{ CurveSettings->SetAutoFrameCurveEditor( !CurveSettings->GetAutoFrameCurveEditor() ); } ),
FCanExecuteAction(),
FIsActionChecked::CreateLambda( [CurveSettings]{ return CurveSettings->GetAutoFrameCurveEditor(); } )
);
CommandList->MapAction(FCurveEditorCommands::Get().ToggleSnapTimeToSelection,
FExecuteAction::CreateLambda( [CurveSettings]{ CurveSettings->SetSnapTimeToSelection( !CurveSettings->GetSnapTimeToSelection() ); } ),
FCanExecuteAction(),
FIsActionChecked::CreateLambda( [CurveSettings]{ return CurveSettings->GetSnapTimeToSelection(); } )
);
CommandList->MapAction(FCurveEditorCommands::Get().ToggleShowBufferedCurves,
FExecuteAction::CreateLambda( [CurveSettings]{ CurveSettings->SetShowBufferedCurves( !CurveSettings->GetShowBufferedCurves() ); } ),
FCanExecuteAction(),
FIsActionChecked::CreateLambda( [CurveSettings]{ return CurveSettings->GetShowBufferedCurves(); } ) );
CommandList->MapAction(FCurveEditorCommands::Get().ToggleShowCurveEditorCurveToolTips,
FExecuteAction::CreateLambda( [CurveSettings]{ CurveSettings->SetShowCurveEditorCurveToolTips( !CurveSettings->GetShowCurveEditorCurveToolTips() ); } ),
FCanExecuteAction(),
FIsActionChecked::CreateLambda( [CurveSettings]{ return CurveSettings->GetShowCurveEditorCurveToolTips(); } ) );
// Deactivate Current Tool
CommandList->MapAction(FCurveEditorCommands::Get().DeactivateCurrentTool,
FExecuteAction::CreateSP(this, &FCurveEditor::MakeToolActive, FCurveEditorToolID::Unset()),
FCanExecuteAction(),
FIsActionChecked::CreateLambda( [this]{ return ActiveTool.IsSet() == false; } ) );
// Bind commands for Editor Extensions
for (TSharedRef<ICurveEditorExtension> Extension : EditorExtensions)
{
Extension->BindCommands(CommandList.ToSharedRef());
}
// Bind Commands for Tool Extensions
for (TPair<FCurveEditorToolID, TUniquePtr<ICurveEditorToolExtension>>& Pair : ToolExtensions)
{
Pair.Value->BindCommands(CommandList.ToSharedRef());
}
}
FCurveSnapMetrics FCurveEditor::GetCurveSnapMetrics(FCurveModelID CurveModel) const
{
FCurveSnapMetrics CurveMetrics;
const SCurveEditorView* View = FindFirstInteractiveView(CurveModel);
if (!View)
{
return CurveMetrics;
}
// get the grid lines in view space
TArray<float> ViewSpaceGridLines;
View->GetGridLinesY(SharedThis(this), ViewSpaceGridLines, ViewSpaceGridLines);
// convert the grid lines from view space
TArray<double> CurveSpaceGridLines;
ViewSpaceGridLines.Reserve(ViewSpaceGridLines.Num());
FCurveEditorScreenSpace CurveSpace = View->GetCurveSpace(CurveModel);
Algo::Transform(ViewSpaceGridLines, CurveSpaceGridLines, [&CurveSpace](float VSVal) { return CurveSpace.ScreenToValue(VSVal); });
// create metrics struct;
CurveMetrics.bSnapOutputValues = OutputSnapEnabledAttribute.Get();
CurveMetrics.bSnapInputValues = InputSnapEnabledAttribute.Get();
CurveMetrics.AllGridLines = CurveSpaceGridLines;
CurveMetrics.InputSnapRate = InputSnapRateAttribute.Get();
return CurveMetrics;
}
void FCurveEditor::ZoomToFit(EAxisList::Type Axes)
{
// If they have keys selected, we fit the specific keys.
if (Selection.Count() > 0)
{
ZoomToFitSelection(Axes);
}
else
{
TMap<FCurveModelID, FKeyHandleSet> AllCurves;
for (FCurveModelID ID : GetEditedCurves())
{
AllCurves.Add(ID);
}
ZoomToFitInternal(Axes, AllCurves);
}
}
void FCurveEditor::ZoomToFitCurves(TArrayView<const FCurveModelID> CurveModelIDs, EAxisList::Type Axes)
{
TMap<FCurveModelID, FKeyHandleSet> AllCurves;
for (FCurveModelID ID : CurveModelIDs)
{
AllCurves.Add(ID);
}
ZoomToFitInternal(Axes, AllCurves);
}
void FCurveEditor::ZoomToFitSelection(EAxisList::Type Axes)
{
ZoomToFitInternal(Axes, Selection.GetAll());
}
void FCurveEditor::ZoomToFitInternal(EAxisList::Type Axes, const TMap<FCurveModelID, FKeyHandleSet>& CurveKeySet)
{
TArray<FKeyPosition> KeyPositionsScratch;
double InputMin = TNumericLimits<double>::Max(), InputMax = TNumericLimits<double>::Lowest();
TMap<TSharedRef<SCurveEditorView>, TTuple<double, double>> ViewToOutputBounds;
for (const TTuple<FCurveModelID, FKeyHandleSet>& Pair : CurveKeySet)
{
FCurveModelID CurveID = Pair.Key;
const FCurveModel* Curve = FindCurve(CurveID);
if (!Curve)
{
continue;
}
double OutputMin = TNumericLimits<double>::Max(), OutputMax = TNumericLimits<double>::Lowest();
int32 NumKeys = Pair.Value.AsArray().Num();
if (NumKeys == 0)
{
double LocalMin = 0.0, LocalMax = 1.0;
// Zoom to the entire curve range if no specific keys are specified
if (Curve->GetNumKeys())
{
// Only zoom time range if there are keys on the curve (otherwise where do we zoom *to* on an infinite timeline?)
Curve->GetTimeRange(LocalMin, LocalMax);
InputMin = FMath::Min(InputMin, LocalMin);
InputMax = FMath::Max(InputMax, LocalMax);
}
// Most curve types we know about support default values, so we can zoom to that even if there are no keys
Curve->GetValueRange(LocalMin, LocalMax);
OutputMin = FMath::Min(OutputMin, LocalMin);
OutputMax = FMath::Max(OutputMax, LocalMax);
}
else
{
// Zoom to the min/max of the specified key set
KeyPositionsScratch.SetNum(NumKeys, false);
Curve->GetKeyPositions(Pair.Value.AsArray(), KeyPositionsScratch);
for (const FKeyPosition& Key : KeyPositionsScratch)
{
InputMin = FMath::Min(InputMin, Key.InputValue);
InputMax = FMath::Max(InputMax, Key.InputValue);
OutputMin = FMath::Min(OutputMin, Key.OutputValue);
OutputMax = FMath::Max(OutputMax, Key.OutputValue);
}
}
if (Axes & EAxisList::Y)
{
TSharedPtr<SCurveEditorPanel> Panel = WeakPanel.Pin();
TSharedPtr<SCurveEditorView> View = WeakView.Pin();
if (Panel.IsValid())
{
// Store the min max for each view
for (auto ViewIt = Panel->FindViews(CurveID); ViewIt; ++ViewIt)
{
TTuple<double, double>* ViewBounds = ViewToOutputBounds.Find(ViewIt.Value());
if (ViewBounds)
{
ViewBounds->Get<0>() = FMath::Min(ViewBounds->Get<0>(), OutputMin);
ViewBounds->Get<1>() = FMath::Max(ViewBounds->Get<1>(), OutputMax);
}
else
{
ViewToOutputBounds.Add(ViewIt.Value(), MakeTuple(OutputMin, OutputMax));
}
}
}
else if(View.IsValid())
{
TTuple<double, double>* ViewBounds = ViewToOutputBounds.Find(View.ToSharedRef());
if (ViewBounds)
{
ViewBounds->Get<0>() = FMath::Min(ViewBounds->Get<0>(), OutputMin);
ViewBounds->Get<1>() = FMath::Max(ViewBounds->Get<1>(), OutputMax);
}
else
{
ViewToOutputBounds.Add(View.ToSharedRef(), MakeTuple(OutputMin, OutputMax));
}
}
}
}
if (Axes & EAxisList::X && InputMin != TNumericLimits<double>::Max() && InputMax != TNumericLimits<double>::Lowest())
{
// If zooming to the same (or invalid) min/max, keep the same zoom scale and center within the timeline
if (InputMin >= InputMax)
{
double CurrentInputMin = 0.0, CurrentInputMax = 1.0;
Bounds->GetInputBounds(CurrentInputMin, CurrentInputMax);
const double HalfInputScale = (CurrentInputMax - CurrentInputMin)*0.5;
InputMin -= HalfInputScale;
InputMax += HalfInputScale;
}
else
{
TSharedPtr<SCurveEditorPanel> Panel = WeakPanel.Pin();
TSharedPtr<SCurveEditorView> View = WeakView.Pin();
int32 PanelWidth = 0;
if (Panel.IsValid())
{
PanelWidth = WeakPanel.Pin()->GetViewContainerGeometry().GetLocalSize().X;
}
else if (View.IsValid())
{
PanelWidth = View->GetViewSpace().GetPhysicalWidth();
}
double InputPercentage = PanelWidth != 0 ? FMath::Min(Settings->GetFrameInputPadding() / (float)PanelWidth, 0.5) : 0.1; // Cannot pad more than half the width
const double MinInputZoom = InputSnapEnabledAttribute.Get() ? InputSnapRateAttribute.Get().AsInterval() : 0.00001;
const double InputPadding = FMath::Max((InputMax - InputMin) * InputPercentage, MinInputZoom);
InputMax = FMath::Max(InputMin + MinInputZoom, InputMax);
InputMin -= InputPadding;
InputMax += InputPadding;
}
Bounds->SetInputBounds(InputMin, InputMax);
}
// Perform per-view output zoom for any computed ranges
for (const TTuple<TSharedRef<SCurveEditorView>, TTuple<double, double>>& ViewAndBounds : ViewToOutputBounds)
{
TSharedRef<SCurveEditorView> View = ViewAndBounds.Key;
double OutputMin = ViewAndBounds.Value.Get<0>();
double OutputMax = ViewAndBounds.Value.Get<1>();
// If zooming to the same (or invalid) min/max, keep the same zoom scale and center within the timeline
if (OutputMin >= OutputMax)
{
const double HalfOutputScale = (View->GetOutputMax() - View->GetOutputMin())*0.5;
OutputMin -= HalfOutputScale;
OutputMax += HalfOutputScale;
}
else
{
TSharedPtr<SCurveEditorPanel> Panel = WeakPanel.Pin();
int32 PanelHeight = 0;
if (Panel.IsValid())
{
PanelHeight = WeakPanel.Pin()->GetViewContainerGeometry().GetLocalSize().Y;
}
else
{
PanelHeight = View->GetViewSpace().GetPhysicalHeight();
}
double OutputPercentage = PanelHeight != 0 ? FMath::Min(Settings->GetFrameOutputPadding() / (float)PanelHeight, 0.5) : 0.1; // Cannot pad more than half the height
constexpr double MinOutputZoom = 0.00001;
const double OutputPadding = FMath::Max((OutputMax - OutputMin) * OutputPercentage, MinOutputZoom);
OutputMin -= OutputPadding;
OutputMax = FMath::Max(OutputMin + MinOutputZoom, OutputMax) + OutputPadding;
}
View->SetOutputBounds(OutputMin, OutputMax);
}
}
void FCurveEditor::TranslateSelectedKeys(double SecondsToAdd)
{
if (Selection.Count() > 0)
{
for (const TTuple<FCurveModelID, FKeyHandleSet>& Pair : Selection.GetAll())
{
if (FCurveModel* Curve = FindCurve(Pair.Key))
{
int32 NumKeys = Pair.Value.Num();
if (NumKeys > 0)
{
TArrayView<const FKeyHandle> KeyHandles = Pair.Value.AsArray();
TArray<FKeyPosition> KeyPositions;
KeyPositions.SetNum(KeyHandles.Num());
Curve->GetKeyPositions(KeyHandles, KeyPositions);
for (int KeyIndex = 0; KeyIndex < KeyPositions.Num(); ++KeyIndex)
{
KeyPositions[KeyIndex].InputValue += SecondsToAdd;
}
Curve->SetKeyPositions(KeyHandles, KeyPositions);
}
}
}
}
}
void FCurveEditor::TranslateSelectedKeysLeft()
{
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
if (!TimeSliderController.IsValid())
{
return;
}
FScopedTransaction Transaction(LOCTEXT("TranslateKeysLeft", "Translate Keys Left"));
FFrameRate FrameRate = TimeSliderController->GetDisplayRate();
double SecondsToAdd = -FrameRate.AsInterval();
TranslateSelectedKeys(SecondsToAdd);
}
void FCurveEditor::TranslateSelectedKeysRight()
{
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
if (!TimeSliderController.IsValid())
{
return;
}
FScopedTransaction Transaction(LOCTEXT("TranslateKeyRight", "Translate Keys Right"));
FFrameRate FrameRate = TimeSliderController->GetDisplayRate();
double SecondsToAdd = FrameRate.AsInterval();
TranslateSelectedKeys(SecondsToAdd);
}
void FCurveEditor::SnapToSelectedKey()
{
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
if (!TimeSliderController.IsValid())
{
return;
}
FFrameRate TickResolution = TimeSliderController->GetTickResolution();
TOptional<double> MinTime;
for (const TTuple<FCurveModelID, FKeyHandleSet>& Pair : Selection.GetAll())
{
if (FCurveModel* Curve = FindCurve(Pair.Key))
{
int32 NumKeys = Pair.Value.Num();
if (NumKeys > 0)
{
TArrayView<const FKeyHandle> KeyHandles = Pair.Value.AsArray();
TArray<FKeyPosition> KeyPositions;
KeyPositions.SetNum(KeyHandles.Num());
Curve->GetKeyPositions(KeyHandles, KeyPositions);
for (const FKeyPosition& KeyPosition : KeyPositions)
{
if (MinTime.IsSet())
{
MinTime = FMath::Min(KeyPosition.InputValue, MinTime.GetValue());
}
else
{
MinTime = KeyPosition.InputValue;
}
}
}
}
}
if (MinTime.IsSet())
{
TimeSliderController->SetScrubPosition(MinTime.GetValue() * TickResolution,/*bEvaluate*/ true);
}
}
void FCurveEditor::StepToNextKey()
{
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
if (!TimeSliderController.IsValid())
{
return;
}
FFrameRate TickResolution = TimeSliderController->GetTickResolution();
double CurrentTime = TickResolution.AsSeconds(TimeSliderController->GetScrubPosition());
TOptional<double> NextTime;
TOptional<double> MinTime;
for (const TTuple<FCurveModelID, TUniquePtr<FCurveModel>>& Pair : CurveData)
{
FCurveModel* CurveModel = Pair.Value.Get();
if (CurveModel)
{
TArray<FKeyHandle> KeyHandles;
double MaxTime = NextTime.IsSet() ? NextTime.GetValue() : TNumericLimits<double>::Max();
CurveModel->GetKeys(*this, CurrentTime, MaxTime, TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), KeyHandles);
TArray<FKeyPosition> KeyPositions;
KeyPositions.SetNum(KeyHandles.Num());
CurveModel->GetKeyPositions(TArrayView<FKeyHandle>(KeyHandles), KeyPositions);
for (const FKeyPosition& KeyPosition : KeyPositions)
{
if (KeyPosition.InputValue > CurrentTime)
{
if (!NextTime.IsSet() || KeyPosition.InputValue < NextTime.GetValue())
{
NextTime = KeyPosition.InputValue;
}
}
}
double CurveMinTime, CurveMaxTime;
CurveModel->GetTimeRange(CurveMinTime, CurveMaxTime);
if (!MinTime.IsSet() || CurveMinTime < MinTime.GetValue())
{
MinTime = CurveMinTime;
}
}
}
if (NextTime.IsSet())
{
TimeSliderController->SetScrubPosition(NextTime.GetValue() * TickResolution,/*bEvaluate*/ true);
}
else if (MinTime.IsSet())
{
TimeSliderController->SetScrubPosition(MinTime.GetValue() * TickResolution, /*bEvaluate*/ true);
}
}
void FCurveEditor::StepToPreviousKey()
{
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
if (!TimeSliderController.IsValid())
{
return;
}
FFrameRate TickResolution = TimeSliderController->GetTickResolution();
double CurrentTime = TickResolution.AsSeconds(TimeSliderController->GetScrubPosition());
TOptional<double> PreviousTime;
TOptional<double> MaxTime;
for (const TTuple<FCurveModelID, TUniquePtr<FCurveModel>>& Pair : CurveData)
{
FCurveModel* CurveModel = Pair.Value.Get();
if (CurveModel)
{
TArray<FKeyHandle> KeyHandles;
double MinTime = PreviousTime.IsSet() ? PreviousTime.GetValue() : TNumericLimits<double>::Lowest();
CurveModel->GetKeys(*this, MinTime, CurrentTime, TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), KeyHandles);
TArray<FKeyPosition> KeyPositions;
KeyPositions.SetNum(KeyHandles.Num());
CurveModel->GetKeyPositions(TArrayView<FKeyHandle>(KeyHandles), KeyPositions);
for (const FKeyPosition& KeyPosition : KeyPositions)
{
if (KeyPosition.InputValue < CurrentTime)
{
if (!PreviousTime.IsSet() || KeyPosition.InputValue > PreviousTime.GetValue())
{
PreviousTime = KeyPosition.InputValue;
}
}
}
double CurveMinTime, CurveMaxTime;
CurveModel->GetTimeRange(CurveMinTime, CurveMaxTime);
if (!MaxTime.IsSet() || CurveMaxTime > MaxTime.GetValue())
{
MaxTime = CurveMaxTime;
}
}
}
if (PreviousTime.IsSet())
{
TimeSliderController->SetScrubPosition(PreviousTime.GetValue() * TickResolution,/*bEvaluate*/ true);
}
else if (MaxTime.IsSet())
{
TimeSliderController->SetScrubPosition(MaxTime.GetValue() * TickResolution, /*bEvaluate*/ true);
}
}
void FCurveEditor::StepForward()
{
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
if (!TimeSliderController.IsValid())
{
return;
}
FFrameRate TickResolution = TimeSliderController->GetTickResolution();
FFrameRate DisplayRate = TimeSliderController->GetDisplayRate();
FFrameTime OneFrame = FFrameRate::TransformTime(FFrameTime(1), DisplayRate, TickResolution);
TimeSliderController->SetScrubPosition(TimeSliderController->GetScrubPosition() + OneFrame, /*bEvaluate*/ true);
}
void FCurveEditor::StepBackward()
{
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
if (!TimeSliderController.IsValid())
{
return;
}
FFrameRate TickResolution = TimeSliderController->GetTickResolution();
FFrameRate DisplayRate = TimeSliderController->GetDisplayRate();
FFrameTime OneFrame = FFrameRate::TransformTime(FFrameTime(1), DisplayRate, TickResolution);
TimeSliderController->SetScrubPosition(TimeSliderController->GetScrubPosition() - OneFrame, /*bEvaluate*/ true);
}
void FCurveEditor::JumpToStart()
{
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
if (!TimeSliderController.IsValid())
{
return;
}
TimeSliderController->SetScrubPosition(TimeSliderController->GetPlayRange().GetLowerBoundValue(), /*bEvaluate*/ true);
}
void FCurveEditor::JumpToEnd()
{
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
if (!TimeSliderController.IsValid())
{
return;
}
const bool bInsetDisplayFrame = IsInputSnappingEnabled();
FFrameRate TickResolution = TimeSliderController->GetTickResolution();
FFrameRate DisplayRate = TimeSliderController->GetDisplayRate();
// Calculate an offset from the end to go to. If they have snapping on (and the scrub style is a block) the last valid frame is represented as one
// whole display rate frame before the end, otherwise we just subtract a single frame which matches the behavior of hitting play and letting it run to the end.
FFrameTime OneFrame = bInsetDisplayFrame ? FFrameRate::TransformTime(FFrameTime(1), DisplayRate, TickResolution) : FFrameTime(1);
FFrameTime NewTime = TimeSliderController->GetPlayRange().GetUpperBoundValue() - OneFrame;
TimeSliderController->SetScrubPosition(NewTime, /*bEvaluate*/ true);
}
void FCurveEditor::SetSelectionRangeStart()
{
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
if (!TimeSliderController.IsValid())
{
return;
}
FFrameNumber LocalTime = TimeSliderController->GetScrubPosition().FrameNumber;
FFrameNumber UpperBound = TimeSliderController->GetSelectionRange().GetUpperBoundValue();
if (UpperBound <= LocalTime)
{
TimeSliderController->SetSelectionRange(TRange<FFrameNumber>(LocalTime, LocalTime + 1));
}
else
{
TimeSliderController->SetSelectionRange(TRange<FFrameNumber>(LocalTime, UpperBound));
}
}
void FCurveEditor::SetSelectionRangeEnd()
{
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
if (!TimeSliderController.IsValid())
{
return;
}
FFrameNumber LocalTime = TimeSliderController->GetScrubPosition().FrameNumber;
FFrameNumber LowerBound = TimeSliderController->GetSelectionRange().GetLowerBoundValue();
if (LowerBound >= LocalTime)
{
TimeSliderController->SetSelectionRange(TRange<FFrameNumber>(LocalTime - 1, LocalTime));
}
else
{
TimeSliderController->SetSelectionRange(TRange<FFrameNumber>(LowerBound, LocalTime));
}
}
void FCurveEditor::ClearSelectionRange()
{
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
if (!TimeSliderController.IsValid())
{
return;
}
TimeSliderController->SetSelectionRange(TRange<FFrameNumber>::Empty());
}
void FCurveEditor::SelectAllKeys()
{
for (FCurveModelID ID : GetEditedCurves())
{
if (FCurveModel* Curve = FindCurve(ID))
{
TArray<FKeyHandle> KeyHandles;
Curve->GetKeys(*this, TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), KeyHandles);
Selection.Add(ID, ECurvePointType::Key, KeyHandles);
}
}
}
void FCurveEditor::SelectForward()
{
Selection.Clear();
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
if (!TimeSliderController.IsValid())
{
return;
}
FFrameRate TickResolution = TimeSliderController->GetTickResolution();
double CurrentTime = TickResolution.AsSeconds(TimeSliderController->GetScrubPosition());
for (FCurveModelID ID : GetEditedCurves())
{
if (FCurveModel* Curve = FindCurve(ID))
{
TArray<FKeyHandle> KeyHandles;
Curve->GetKeys(*this, CurrentTime, TNumericLimits<double>::Max(), TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), KeyHandles);
Selection.Add(ID, ECurvePointType::Key, KeyHandles);
}
}
}
void FCurveEditor::SelectBackward()
{
Selection.Clear();
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
if (!TimeSliderController.IsValid())
{
return;
}
FFrameRate TickResolution = TimeSliderController->GetTickResolution();
double CurrentTime = TickResolution.AsSeconds(TimeSliderController->GetScrubPosition());
for (FCurveModelID ID : GetEditedCurves())
{
if (FCurveModel* Curve = FindCurve(ID))
{
TArray<FKeyHandle> KeyHandles;
Curve->GetKeys(*this, TNumericLimits<double>::Min(), CurrentTime, TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), KeyHandles);
Selection.Add(ID, ECurvePointType::Key, KeyHandles);
}
}
}
bool FCurveEditor::IsInputSnappingEnabled() const
{
return InputSnapEnabledAttribute.Get();
}
void FCurveEditor::ToggleInputSnapping()
{
bool NewValue = !InputSnapEnabledAttribute.Get();
if (!InputSnapEnabledAttribute.IsBound())
{
InputSnapEnabledAttribute = NewValue;
}
else
{
OnInputSnapEnabledChanged.ExecuteIfBound(NewValue);
}
}
bool FCurveEditor::IsOutputSnappingEnabled() const
{
return OutputSnapEnabledAttribute.Get();
}
void FCurveEditor::ToggleOutputSnapping()
{
bool NewValue = !OutputSnapEnabledAttribute.Get();
if (!OutputSnapEnabledAttribute.IsBound())
{
OutputSnapEnabledAttribute = NewValue;
}
else
{
OnOutputSnapEnabledChanged.ExecuteIfBound(NewValue);
}
}
void
FCurveEditor::ToggleExpandCollapseNodes(bool bRecursive)
{
Tree.ToggleExpansionState(bRecursive);
}
FCurveEditorScreenSpaceH FCurveEditor::GetPanelInputSpace() const
{
const float PanelWidth = FMath::Max(1.f, WeakPanel.Pin()->GetViewContainerGeometry().GetLocalSize().X);
double InputMin = 0.0, InputMax = 1.0;
Bounds->GetInputBounds(InputMin, InputMax);
InputMax = FMath::Max(InputMax, InputMin + 1e-10);
return FCurveEditorScreenSpaceH(PanelWidth, InputMin, InputMax);
}
void FCurveEditor::ConstructXGridLines(TArray<float>& MajorGridLines, TArray<float>& MinorGridLines, TArray<FText>* MajorGridLabels) const
{
FCurveEditorScreenSpaceH InputSpace = GetPanelInputSpace();
double MajorGridStep = 0.0;
int32 MinorDivisions = 0;
if (InputSnapRateAttribute.Get().ComputeGridSpacing(InputSpace.PixelsPerInput(), MajorGridStep, MinorDivisions))
{
FText GridLineLabelFormatX = GridLineLabelFormatXAttribute.Get();
const double FirstMajorLine = FMath::FloorToDouble(InputSpace.GetInputMin() / MajorGridStep) * MajorGridStep;
const double LastMajorLine = FMath::CeilToDouble(InputSpace.GetInputMax() / MajorGridStep) * MajorGridStep;
for (double CurrentMajorLine = FirstMajorLine; CurrentMajorLine < LastMajorLine; CurrentMajorLine += MajorGridStep)
{
MajorGridLines.Add( (CurrentMajorLine - InputSpace.GetInputMin()) * InputSpace.PixelsPerInput() );
if (MajorGridLabels)
{
MajorGridLabels->Add(FText::Format(GridLineLabelFormatX, FText::AsNumber(CurrentMajorLine)));
}
for (int32 Step = 1; Step < MinorDivisions; ++Step)
{
float MinorLine = CurrentMajorLine + Step*MajorGridStep/MinorDivisions;
MinorGridLines.Add( (MinorLine - InputSpace.GetInputMin()) * InputSpace.PixelsPerInput() );
}
}
}
}
void FCurveEditor::CutSelection()
{
FScopedTransaction Transaction(LOCTEXT("CutKeys", "Cut Keys"));
CopySelection();
DeleteSelection();
}
void FCurveEditor::GetChildCurveModelIDs(const FCurveEditorTreeItemID TreeItemID, TSet<FCurveModelID>& OutCurveModelIDs) const
{
const FCurveEditorTreeItem& TreeItem = GetTreeItem(TreeItemID);
for (const FCurveModelID& CurveModelID : TreeItem.GetCurves())
{
OutCurveModelIDs.Add(CurveModelID);
}
for (const FCurveEditorTreeItemID& ChildTreeItem : TreeItem.GetChildren())
{
GetChildCurveModelIDs(ChildTreeItem, OutCurveModelIDs);
}
}
void FCurveEditor::CopySelection() const
{
FStringOutputDevice Archive;
const FExportObjectInnerContext Context;
TOptional<double> KeyOffset;
UCurveEditorCopyBuffer* CopyableBuffer = NewObject<UCurveEditorCopyBuffer>(GetTransientPackage(), UCurveEditorCopyBuffer::StaticClass(), NAME_None, RF_Transient);
if (Selection.Count() > 0)
{
for (const TTuple<FCurveModelID, FKeyHandleSet>& Pair : Selection.GetAll())
{
if (FCurveModel* Curve = FindCurve(Pair.Key))
{
int32 NumKeys = Pair.Value.Num();
if (NumKeys > 0)
{
UCurveEditorCopyableCurveKeys *CopyableCurveKeys = NewObject<UCurveEditorCopyableCurveKeys>(CopyableBuffer, UCurveEditorCopyableCurveKeys::StaticClass(), NAME_None, RF_Transient);
CopyableCurveKeys->ShortDisplayName = Curve->GetShortDisplayName().ToString();
CopyableCurveKeys->LongDisplayName = Curve->GetLongDisplayName().ToString();
CopyableCurveKeys->IntentionName = Curve->GetIntentionName();
CopyableCurveKeys->KeyPositions.SetNum(NumKeys, false);
CopyableCurveKeys->KeyAttributes.SetNum(NumKeys, false);
TArrayView<const FKeyHandle> KeyHandles = Pair.Value.AsArray();
Curve->GetKeyPositions(KeyHandles, CopyableCurveKeys->KeyPositions);
Curve->GetKeyAttributes(KeyHandles, CopyableCurveKeys->KeyAttributes);
for (int KeyIndex = 0; KeyIndex < CopyableCurveKeys->KeyPositions.Num(); ++KeyIndex)
{
if (!KeyOffset.IsSet() || CopyableCurveKeys->KeyPositions[KeyIndex].InputValue < KeyOffset.GetValue())
{
KeyOffset = CopyableCurveKeys->KeyPositions[KeyIndex].InputValue;
}
}
CopyableBuffer->Curves.Add(CopyableCurveKeys);
}
}
}
}
else
{
TSet<FCurveModelID> CurveModelIDs;
for (const TTuple<FCurveEditorTreeItemID, ECurveEditorTreeSelectionState>& Pair : GetTreeSelection())
{
if (Pair.Value == ECurveEditorTreeSelectionState::Explicit)
{
GetChildCurveModelIDs(Pair.Key, CurveModelIDs);
}
}
for(const FCurveModelID& CurveModelID : CurveModelIDs)
{
if (FCurveModel* Curve = FindCurve(CurveModelID))
{
TUniquePtr<IBufferedCurveModel> CurveModelCopy = Curve->CreateBufferedCurveCopy();
if (CurveModelCopy)
{
TArray<FKeyPosition> KeyPositions;
CurveModelCopy->GetKeyPositions(KeyPositions);
if (KeyPositions.Num() > 0)
{
UCurveEditorCopyableCurveKeys *CopyableCurveKeys = NewObject<UCurveEditorCopyableCurveKeys>(CopyableBuffer, UCurveEditorCopyableCurveKeys::StaticClass(), NAME_None, RF_Transient);
CopyableCurveKeys->ShortDisplayName = Curve->GetShortDisplayName().ToString();
CopyableCurveKeys->LongDisplayName = Curve->GetLongDisplayName().ToString();
CopyableCurveKeys->IntentionName = Curve->GetIntentionName();
CopyableCurveKeys->KeyPositions = KeyPositions;
CurveModelCopy->GetKeyAttributes(CopyableCurveKeys->KeyAttributes);
CopyableBuffer->Curves.Add(CopyableCurveKeys);
}
}
}
}
// When copying entire curve objects we want absolute positions, so reset the detected offset
KeyOffset.Reset();
}
if (KeyOffset.IsSet())
{
for (UCurveEditorCopyableCurveKeys* Curve : CopyableBuffer->Curves)
{
for (int Index = 0; Index < Curve->KeyPositions.Num(); ++Index)
{
Curve->KeyPositions[Index].InputValue -= KeyOffset.GetValue();
}
}
CopyableBuffer->TimeOffset = KeyOffset.GetValue();
}
else
{
CopyableBuffer->bAbsolutePosition = true;
}
UExporter::ExportToOutputDevice(&Context, CopyableBuffer, nullptr, Archive, TEXT("copy"), 0, PPF_ExportsNotFullyQualified | PPF_Copy | PPF_Delimited, false, CopyableBuffer);
FPlatformApplicationMisc::ClipboardCopy(*Archive);
}
class FCurveEditorCopyableCurveKeysObjectTextFactory : public FCustomizableTextObjectFactory
{
public:
FCurveEditorCopyableCurveKeysObjectTextFactory()
: FCustomizableTextObjectFactory(GWarn)
{
}
// FCustomizableTextObjectFactory implementation
virtual bool CanCreateClass(UClass* InObjectClass, bool& bOmitSubObjs) const override
{
if (InObjectClass->IsChildOf(UCurveEditorCopyBuffer::StaticClass()))
{
return true;
}
return false;
}
virtual void ProcessConstructedObject(UObject* NewObject) override
{
check(NewObject);
NewCopyBuffers.Add(Cast<UCurveEditorCopyBuffer>(NewObject));
}
public:
TArray<UCurveEditorCopyBuffer*> NewCopyBuffers;
};
bool FCurveEditor::CanPaste(const FString& TextToImport) const
{
FCurveEditorCopyableCurveKeysObjectTextFactory CopyableCurveKeysFactory;
if (CopyableCurveKeysFactory.CanCreateObjectsFromText(TextToImport))
{
return true;
}
return false;
}
void FCurveEditor::ImportCopyBufferFromText(const FString& TextToImport, /*out*/ TArray<UCurveEditorCopyBuffer*>& ImportedCopyBuffers) const
{
UPackage* TempPackage = NewObject<UPackage>(nullptr, TEXT("/Engine/Editor/CurveEditor/Transient"), RF_Transient);
TempPackage->AddToRoot();
// Turn the text buffer into objects
FCurveEditorCopyableCurveKeysObjectTextFactory Factory;
Factory.ProcessBuffer(TempPackage, RF_Transactional, TextToImport);
ImportedCopyBuffers = Factory.NewCopyBuffers;
// Remove the temp package from the root now that it has served its purpose
TempPackage->RemoveFromRoot();
}
void FCurveEditor::PasteKeys(TSet<FCurveModelID> CurveModelIDs)
{
// Grab the text to paste from the clipboard
FString TextToImport;
FPlatformApplicationMisc::ClipboardPaste(TextToImport);
TArray<UCurveEditorCopyBuffer*> ImportedCopyBuffers;
ImportCopyBufferFromText(TextToImport, ImportedCopyBuffers);
if (ImportedCopyBuffers.Num() == 0)
{
return;
}
// Determine whether all the copied keys are from the same curve, if yes, they can all be pasted to the target curves without name matching
bool bAllCopiedCurvesLongNameEqual = true;
FString AllCopiedCurvesLongName;
for (UCurveEditorCopyBuffer* CopyBuffer : ImportedCopyBuffers)
{
for (UCurveEditorCopyableCurveKeys* CopyableCurveKeys : CopyBuffer->Curves)
{
if (AllCopiedCurvesLongName.IsEmpty())
{
AllCopiedCurvesLongName = CopyableCurveKeys->LongDisplayName;
}
else if (CopyableCurveKeys->LongDisplayName != AllCopiedCurvesLongName)
{
bAllCopiedCurvesLongNameEqual = false;
break;
}
}
}
bool bSelectionNeedsLongNames = false;
if (CurveModelIDs.Num() == 0)
{
TOptional<FCurveModelID> HoveredID;
if (WeakPanel.IsValid())
{
for (TSharedPtr<SCurveEditorView> View : WeakPanel.Pin()->GetViews())
{
if (View.IsValid() && View->GetHoveredCurve().IsSet())
{
HoveredID = View->GetHoveredCurve().GetValue();
break;
}
}
}
if (HoveredID.IsSet())
{
CurveModelIDs.Add(HoveredID.GetValue());
}
else
{
TArray<FCurveEditorTreeItemID> NodesToSearch;
// Try nodes with selected keys
for (const TTuple<FCurveModelID, FKeyHandleSet>& Pair : Selection.GetAll())
{
CurveModelIDs.Add(Pair.Key);
}
// Try selected nodes
if (CurveModelIDs.Num() == 0)
{
for (const TTuple<FCurveEditorTreeItemID, ECurveEditorTreeSelectionState>& Pair : GetTreeSelection())
{
NodesToSearch.Add(Pair.Key);
}
// If no curves are selected, paste to the entire tree using fully qualified long names
if (NodesToSearch.Num() == 0)
{
bSelectionNeedsLongNames = true;
bAllCopiedCurvesLongNameEqual = false;
if (Tree.GetAllItems().GetKeys(NodesToSearch) == 0)
{
// If we don't have any curves to paste in to, exit now
return;
}
}
}
for (const FCurveEditorTreeItemID& TreeItemID: NodesToSearch)
{
FCurveEditorTreeItem& TreeItem = GetTreeItem(TreeItemID);
for (const FCurveModelID& CurveModelID : TreeItem.GetCurves())
{
CurveModelIDs.Add(CurveModelID);
}
}
}
}
if (CurveModelIDs.Num() == 0)
{
return;
}
FScopedTransaction Transaction(LOCTEXT("PasteKeys", "Paste Keys"));
Selection.Clear();
// We don't expect/want multiple copy buffers, but the way serialization works it's a possibile edge case,
// so we'll try to handle it sanely and treat each one as an individual block to paste.
for (UCurveEditorCopyBuffer* CopyBuffer : ImportedCopyBuffers)
{
bool bUseLongDisplayName = bSelectionNeedsLongNames;
double TimeOffset = 0.0f;
bool bApplyOffset = !CopyBuffer->bAbsolutePosition;
if (bApplyOffset)
{
TSharedPtr<ITimeSliderController> TimeSliderController = WeakTimeSliderController.Pin();
if (TimeSliderController.IsValid())
{
FFrameRate TickResolution = TimeSliderController->GetTickResolution();
TimeOffset = TimeSliderController->GetScrubPosition() / TickResolution;
}
else
{
TimeOffset = CopyBuffer->TimeOffset;
}
}
TArray<UCurveEditorCopyableCurveKeys*> UsedCurves;
for (FCurveModelID CurveID : CurveModelIDs)
{
FCurveModel* Curve = FindCurve(CurveID);
if (Curve)
{
const FString CurveLongDisplayName = Curve->GetLongDisplayName().ToString();
const FString CurveIntentionName = Curve->GetIntentionName();
bool bFoundMatch = false;
for (UCurveEditorCopyableCurveKeys* CopyableCurveKeys : CopyBuffer->Curves)
{
// Use up all curves in the copied buffer before reusing. For example, if the following 6 curves have been copied,
// Cube1.Location.X, Cube1.Location.Y, Cube1.Location.Z
// Cube2.Location.X, Cube2.Location.Y, Cube2.Location.Z
// and the user attempts to paste onto multiple objects, Cube1's curves will match before Cube2's curves if this restriction is not in place.
//
if (UsedCurves.Contains(CopyableCurveKeys))
{
continue;
}
if (bAllCopiedCurvesLongNameEqual ||
(!bUseLongDisplayName && CurveIntentionName.Equals(CopyableCurveKeys->IntentionName))
|| (bUseLongDisplayName && CurveLongDisplayName.Equals(CopyableCurveKeys->LongDisplayName)))
{
bFoundMatch = true;
for (int32 Index = 0; Index < CopyableCurveKeys->KeyPositions.Num(); ++Index)
{
FKeyPosition KeyPosition = CopyableCurveKeys->KeyPositions[Index];
if (bApplyOffset)
{
KeyPosition.InputValue += TimeOffset;
}
TOptional<FKeyHandle> KeyHandle = Curve->AddKey(KeyPosition, CopyableCurveKeys->KeyAttributes[Index]);
if (KeyHandle.IsSet())
{
Selection.Add(FCurvePointHandle(CurveID, ECurvePointType::Key, KeyHandle.GetValue()));
}
}
UsedCurves.Add(CopyableCurveKeys);
if (UsedCurves.Num() == CopyBuffer->Curves.Num())
{
UsedCurves.Empty();
}
break; // Just paste one of the matching curves
}
}
if (!bFoundMatch)
{
UE_LOG(LogCurveEditor, Warning, TEXT("Failed to find matching curve to copy onto: %s"), *CurveLongDisplayName);
}
}
}
}
}
void FCurveEditor::DeleteSelection()
{
FScopedTransaction Transaction(LOCTEXT("DeleteKeys", "Delete Keys"));
for (const TTuple<FCurveModelID, FKeyHandleSet>& Pair : Selection.GetAll())
{
if (FCurveModel* Curve = FindCurve(Pair.Key))
{
Curve->Modify();
Curve->RemoveKeys(Pair.Value.AsArray());
}
}
Selection.Clear();
}
void FCurveEditor::FlattenSelection()
{
FScopedTransaction Transaction(LOCTEXT("FlattenTangents", "Flatten Tangents"));
bool bFoundAnyTangents = false;
TArray<FKeyHandle> KeyHandles;
TArray<FKeyAttributes> AllKeyPositions;
//Since we don't have access here to the Section to get Tick Resolution if we flatten a weighted tangent we
//do so by converting it to non-weighted and then back again.
TArray<FKeyHandle> KeyHandlesWeighted;
TArray<FKeyAttributes> KeyAttributesWeighted;
for (const TTuple<FCurveModelID, FKeyHandleSet>& Pair : Selection.GetAll())
{
if (FCurveModel* Curve = FindCurve(Pair.Key))
{
KeyHandles.Reset(Pair.Value.Num());
KeyHandles.Append(Pair.Value.AsArray().GetData(), Pair.Value.Num());
AllKeyPositions.SetNum(KeyHandles.Num());
Curve->GetKeyAttributes(KeyHandles, AllKeyPositions);
KeyHandlesWeighted.Reset(Pair.Value.Num());
KeyHandlesWeighted.Append(Pair.Value.AsArray().GetData(), Pair.Value.Num());
KeyAttributesWeighted.SetNum(KeyHandlesWeighted.Num());
Curve->GetKeyAttributes(KeyHandlesWeighted, KeyAttributesWeighted);
// Straighten tangents, ignoring any keys that we can't set tangents on
for (int32 Index = AllKeyPositions.Num()-1 ; Index >= 0; --Index)
{
FKeyAttributes& Attributes = AllKeyPositions[Index];
if (Attributes.HasTangentMode() && (Attributes.HasArriveTangent() || Attributes.HasLeaveTangent()))
{
Attributes.SetArriveTangent(0.f).SetLeaveTangent(0.f);
if (Attributes.GetTangentMode() == RCTM_Auto)
{
Attributes.SetTangentMode(RCTM_User);
}
//if any weighted convert and convert back to both (which is what only support other modes are not really used).,
if (Attributes.GetTangentWeightMode() == RCTWM_WeightedBoth || Attributes.GetTangentWeightMode() == RCTWM_WeightedArrive
|| Attributes.GetTangentWeightMode() == RCTWM_WeightedLeave)
{
Attributes.SetTangentWeightMode(RCTWM_WeightedNone);
FKeyAttributes& WeightedAttributes = KeyAttributesWeighted[Index];
WeightedAttributes.UnsetArriveTangent();
WeightedAttributes.UnsetLeaveTangent();
WeightedAttributes.UnsetArriveTangentWeight();
WeightedAttributes.UnsetLeaveTangentWeight();
WeightedAttributes.SetTangentWeightMode(RCTWM_WeightedBoth);
}
else
{
KeyAttributesWeighted.RemoveAtSwap(Index, 1, false);
KeyHandlesWeighted.RemoveAtSwap(Index, 1, false);
}
}
else
{
AllKeyPositions.RemoveAtSwap(Index, 1, false);
KeyHandles.RemoveAtSwap(Index, 1, false);
KeyAttributesWeighted.RemoveAtSwap(Index, 1, false);
KeyHandlesWeighted.RemoveAtSwap(Index, 1, false);
}
}
if (AllKeyPositions.Num() > 0)
{
Curve->Modify();
Curve->SetKeyAttributes(KeyHandles, AllKeyPositions);
if (KeyAttributesWeighted.Num() > 0)
{
Curve->SetKeyAttributes(KeyHandlesWeighted, KeyAttributesWeighted);
}
bFoundAnyTangents = true;
}
}
}
if (!bFoundAnyTangents)
{
Transaction.Cancel();
}
}
void FCurveEditor::StraightenSelection()
{
FScopedTransaction Transaction(LOCTEXT("StraightenTangents", "Straighten Tangents"));
bool bFoundAnyTangents = false;
TArray<FKeyHandle> KeyHandles;
TArray<FKeyAttributes> AllKeyPositions;
for (const TTuple<FCurveModelID, FKeyHandleSet>& Pair : Selection.GetAll())
{
if (FCurveModel* Curve = FindCurve(Pair.Key))
{
KeyHandles.Reset(Pair.Value.Num());
KeyHandles.Append(Pair.Value.AsArray().GetData(), Pair.Value.Num());
AllKeyPositions.SetNum(KeyHandles.Num());
Curve->GetKeyAttributes(KeyHandles, AllKeyPositions);
// Straighten tangents, ignoring any keys that we can't set tangents on
for (int32 Index = AllKeyPositions.Num()-1 ; Index >= 0; --Index)
{
FKeyAttributes& Attributes = AllKeyPositions[Index];
if (Attributes.HasTangentMode() && Attributes.HasArriveTangent() && Attributes.HasLeaveTangent())
{
float NewTangent = (Attributes.GetLeaveTangent() + Attributes.GetArriveTangent()) * 0.5f;
Attributes.SetArriveTangent(NewTangent).SetLeaveTangent(NewTangent);
if (Attributes.GetTangentMode() == RCTM_Auto)
{
Attributes.SetTangentMode(RCTM_User);
}
}
else
{
AllKeyPositions.RemoveAtSwap(Index, 1, false);
KeyHandles.RemoveAtSwap(Index, 1, false);
}
}
if (AllKeyPositions.Num() > 0)
{
Curve->Modify();
Curve->SetKeyAttributes(KeyHandles, AllKeyPositions);
bFoundAnyTangents = true;
}
}
}
if (!bFoundAnyTangents)
{
Transaction.Cancel();
}
}
bool FCurveEditor::CanFlattenOrStraightenSelection() const
{
return Selection.Count() > 0;
}
void FCurveEditor::SetRandomCurveColorsForSelected()
{
for (TPair<FCurveModelID, TUniquePtr<FCurveModel>>& CurvePair : CurveData)
{
if (FCurveModel* Curve = CurvePair.Value.Get())
{
UObject* Object = nullptr;
FString Name;
Curve->GetCurveColorObjectAndName(&Object, Name);
if (Object)
{
FLinearColor Color = UCurveEditorSettings::GetNextRandomColor();
Settings->SetCustomColor(Object->GetClass(), Name, Color);
Curve->SetColor(Color);
}
}
}
}
void FCurveEditor::SetCurveColorsForSelected()
{
if (CurveData.Num() > 0)
{
TMap<FCurveModelID, TUniquePtr<FCurveModel>>::TIterator It = CurveData.CreateIterator();
FColorPickerArgs PickerArgs;
PickerArgs.bUseAlpha = false;
PickerArgs.InitialColorOverride = It->Value->GetColor();
PickerArgs.OnColorCommitted.BindLambda([this](FLinearColor NewColor) {
for (TPair<FCurveModelID, TUniquePtr<FCurveModel>>& CurvePair : CurveData)
{
if (FCurveModel* Curve = CurvePair.Value.Get())
{
UObject* Object = nullptr;
FString Name;
Curve->GetCurveColorObjectAndName(&Object, Name);
if (Object)
{
Settings->SetCustomColor(Object->GetClass(), Name, NewColor);
Curve->SetColor(NewColor);
}
}
}
});
OpenColorPicker(PickerArgs);
}
}
bool FCurveEditor::IsToolActive(const FCurveEditorToolID InToolID) const
{
if (ActiveTool.IsSet())
{
return ActiveTool == InToolID;
}
return false;
}
void FCurveEditor::MakeToolActive(const FCurveEditorToolID InToolID)
{
if (ActiveTool.IsSet())
{
// Early out in the event that they're trying to switch to the same tool. This avoids
// unwanted activation/deactivation calls.
if (ActiveTool == InToolID)
{
return;
}
// Deactivate the current tool before we activate the new one.
ToolExtensions[ActiveTool.GetValue()]->OnToolDeactivated();
}
ActiveTool.Reset();
// Notify anyone listening that we've switched tools (possibly to an inactive one)
OnActiveToolChangedDelegate.Broadcast(InToolID);
if (InToolID != FCurveEditorToolID::Unset())
{
ActiveTool = InToolID;
ToolExtensions[ActiveTool.GetValue()]->OnToolActivated();
}
}
ICurveEditorToolExtension* FCurveEditor::GetCurrentTool() const
{
if (ActiveTool.IsSet())
{
return ToolExtensions[ActiveTool.GetValue()].Get();
}
// If there is no active tool we return nullptr.
return nullptr;
}
TSet<FCurveModelID> FCurveEditor::GetEditedCurves() const
{
TArray<FCurveModelID> AllCurves;
GetCurves().GenerateKeyArray(AllCurves);
return TSet<FCurveModelID>(AllCurves);
}
void FCurveEditor::AddBufferedCurves(const TSet<FCurveModelID>& InCurves)
{
// We make a copy of the curve data and store it.
for (FCurveModelID CurveID : InCurves)
{
FCurveModel* CurveModel = FindCurve(CurveID);
check(CurveModel);
// Add a buffered curve copy if the curve model supports buffered curves
TUniquePtr<IBufferedCurveModel> CurveModelCopy = CurveModel->CreateBufferedCurveCopy();
if (CurveModelCopy)
{
// Remove any existing buffered curves
for (int32 BufferedCurveIndex = 0; BufferedCurveIndex < BufferedCurves.Num(); )
{
if (BufferedCurves[BufferedCurveIndex]->GetLongDisplayName() == CurveModel->GetLongDisplayName().ToString())
{
BufferedCurves.RemoveAt(BufferedCurveIndex);
}
else
{
++BufferedCurveIndex;
}
}
BufferedCurves.Add(MoveTemp(CurveModelCopy));
}
else
{
UE_LOG(LogCurveEditor, Warning, TEXT("Failed to buffer curve, curve model did not provide a copy."))
}
}
}
void FCurveEditor::ApplyBufferedCurveToTarget(const IBufferedCurveModel* BufferedCurve, FCurveModel* TargetCurve)
{
check(TargetCurve);
check(BufferedCurve);
TArray<FKeyPosition> KeyPositions;
TArray<FKeyAttributes> KeyAttributes;
BufferedCurve->GetKeyPositions(KeyPositions);
BufferedCurve->GetKeyAttributes(KeyAttributes);
// Copy the data from the Buffered curve into the target curve. This just does wholesale replacement.
TArray<FKeyHandle> TargetKeyHandles;
TargetCurve->GetKeys(*this, TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), TargetKeyHandles);
// Clear our current keys from the target curve
TargetCurve->RemoveKeys(TargetKeyHandles);
// Now put our buffered keys into the target curve
TargetCurve->AddKeys(KeyPositions, KeyAttributes);
}
bool FCurveEditor::ApplyBufferedCurves(const TSet<FCurveModelID>& InCurvesToApplyTo, const bool bSwapBufferCurves)
{
FScopedTransaction Transaction(bSwapBufferCurves ? LOCTEXT("SwapBufferedCurves", "Swap Buffered Curves") : LOCTEXT("ApplyBufferedCurves", "Apply Buffered Curves"));
// Each curve can specify an "Intention" name. This gives a little bit of context about how the curve is intended to be used,
// without locking anyone into a specific set of intentions. When you go to apply the buffered curves, for each curve that you
// want to apply it to, we can look in our stored curves to see if someone has the same intention. If there isn't a matching intention
// then we skip and consider a fallback method (such as 1:1 copy). There is a lot of guessing still involved as there are complex
// situations that users may try to use it in (such as buffering two sets of transform curves and applying it to two destination transform curves)
// or trying to copy something with a name like "Focal Length" and pasting it onto a different track. We don't handle these cases for now,
// but attempt to communicate it to the user via toast notification when pasting fails for whatever reason.
int32 NumCurvesMatchedByIntent = 0;
int32 NumCurvesNoMatchedIntent = 0;
bool bFoundAnyMatchedIntent = false;
TMap<FString, int32> IntentMatchIndexes;
for (const FCurveModelID& CurveModelID : InCurvesToApplyTo)
{
FCurveModel* TargetCurve = FindCurve(CurveModelID);
check(TargetCurve);
// Figure out what our destination thinks it's supposed to be used for, ie "Location.X"
FString TargetIntent = TargetCurve->GetLongDisplayName().ToString();
if (TargetIntent.IsEmpty())
{
// We don't try to match curves with no intent as that's just chaos.
NumCurvesNoMatchedIntent++;
continue;
}
TargetCurve->Modify();
// In an attempt to support buffering multiple curves with the same intention, we'll try to match them up in pairs. This means
// for the first curve that we're trying to apply to, if the intention is "Location.X" we will search the buffered curves for a
// "Location.X". Upon finding one, we store the index that it was found at, so the next time we try to find a curve with the same
// intention, we look for the second "Location.X" and so forth. If we don't find a second "Location.X" in our buffered curves we'll
// fall back to the first buffered one so you can 1:Many copy a curve.
int32 BufferedCurveSearchIndexStart = 0;
const int32* PreviouslyFoundIntent = IntentMatchIndexes.Find(TargetIntent);
if (PreviouslyFoundIntent)
{
// Start our search on the next item in the array. If we don't find one, we'll fall back to the last one.
BufferedCurveSearchIndexStart = IntentMatchIndexes[TargetIntent] + 1;
}
int32 MatchedBufferedCurveIndex = -1;
for (int32 BufferedCurveIndex = BufferedCurveSearchIndexStart; BufferedCurveIndex < BufferedCurves.Num(); BufferedCurveIndex++)
{
if (BufferedCurves[BufferedCurveIndex]->GetLongDisplayName() == TargetIntent)
{
MatchedBufferedCurveIndex = BufferedCurveIndex;
// Update our previously found intent to the latest one.
IntentMatchIndexes.FindOrAdd(TargetIntent) = MatchedBufferedCurveIndex;
break;
}
}
// The Intent Match Indexes stores the latest index to find a valid curve, or the last one if no new valid one was found.
// If there is an entry in the match indexes now, we can use that to figure out which buffered curve we'll pull from.
// If we didn't find any more with the same intention, we fall back to the existing one (if it exists!)
if (IntentMatchIndexes.Find(TargetIntent))
{
MatchedBufferedCurveIndex = IntentMatchIndexes[TargetIntent];
}
// Finally, we can try to use the matched curve if one was found.
if (MatchedBufferedCurveIndex >= 0)
{
// We successfully matched, so count that one up!
NumCurvesMatchedByIntent++;
bFoundAnyMatchedIntent = true;
const IBufferedCurveModel* BufferedCurve = BufferedCurves[MatchedBufferedCurveIndex].Get();
TUniquePtr<IBufferedCurveModel> CurveModelCopy;
if (bSwapBufferCurves)
{
CurveModelCopy = TargetCurve->CreateBufferedCurveCopy();
}
ApplyBufferedCurveToTarget(BufferedCurve, TargetCurve);
if (bSwapBufferCurves)
{
BufferedCurves[MatchedBufferedCurveIndex] = MoveTemp(CurveModelCopy);
}
}
else
{
// We couldn't find a match despite our best efforts
NumCurvesNoMatchedIntent++;
}
}
// If we managed to match any by intent, we're going to early out and assume that's what their intent was.
if (bFoundAnyMatchedIntent)
{
const FText NotificationText = FText::Format(LOCTEXT("MatchedBufferedCurvesByIntent", "Applied {0}/{1} buffered curves to {2}/{3} target curves."),
FText::AsNumber(IntentMatchIndexes.Num()), FText::AsNumber(BufferedCurves.Num()), // We used X of Y total buffered curves
FText::AsNumber(NumCurvesMatchedByIntent), FText::AsNumber(InCurvesToApplyTo.Num())); // To apply to Z of W target curves,
FNotificationInfo Info(NotificationText);
Info.ExpireDuration = 6.f;
Info.bUseLargeFont = false;
Info.bUseSuccessFailIcons = false;
FSlateNotificationManager::Get().AddNotification(Info);
if (NumCurvesNoMatchedIntent > 0)
{
const FText FailedNotificationText = FText::Format(LOCTEXT("NumCurvesNotMatchedByIntent", "Failed to find a buffered curve with the same intent for {0} target curves, skipping..."),
FText::AsNumber(NumCurvesNoMatchedIntent)); // Leaving V many target curves unaffected due to no intent match.
FNotificationInfo FailInfo(FailedNotificationText);
FailInfo.ExpireDuration = 6.f;
FailInfo.bUseLargeFont = false;
FailInfo.bUseSuccessFailIcons = true;
FSlateNotificationManager::Get().AddNotification(FailInfo);
}
// Early out
return true;
}
// If we got this far, it means that the buffered curves have no recognizable relation to the target curves.
// If the number of curves match, we'll just do a 1:1 mapping. This works for most cases where you're trying
// to paste an unrelated curve onto another as it's likely that there's only one curve. We don't limit it to
// one curve though, we'll just warn...
if (InCurvesToApplyTo.Num() == BufferedCurves.Num())
{
// This will work great in the case there's only one curve. It'll guess if there's more than one, relying on
// sets with no guaranteed order.
TArray<FCurveModelID> CurvesToApplyTo = InCurvesToApplyTo.Array();
for (int32 CurveIndex = 0; CurveIndex < InCurvesToApplyTo.Num(); CurveIndex++)
{
FCurveModel* TargetCurve = FindCurve(CurvesToApplyTo[CurveIndex]);
TUniquePtr<IBufferedCurveModel> CurveModelCopy;
if (bSwapBufferCurves)
{
CurveModelCopy = TargetCurve->CreateBufferedCurveCopy();
}
ApplyBufferedCurveToTarget(BufferedCurves[CurveIndex].Get(), TargetCurve);
if (bSwapBufferCurves)
{
BufferedCurves[CurveIndex] = MoveTemp(CurveModelCopy);
}
}
FText NotificationText;
if (InCurvesToApplyTo.Num() == 1)
{
NotificationText = LOCTEXT("MatchedBufferedCurvesBySolo", "Applied buffered curve to target curve with no intention matching.");
}
else
{
NotificationText = LOCTEXT("MatchedBufferedCurvesByIndex", "Applied buffered curves with no intention matching. Order not guranteed.");
}
FNotificationInfo Info(NotificationText);
Info.ExpireDuration = 6.f;
Info.bUseLargeFont = false;
Info.bUseSuccessFailIcons = false;
FSlateNotificationManager::Get().AddNotification(Info);
// Early out
return true;
}
// If we got this far, we have no idea what to do. They're trying to match a bunch of curves with no intention and different amounts.
// Warn of failure and give up.
{
const FText FailedNotificationText = LOCTEXT("NoBufferedCurvesMatched", "Failed to apply buffered curves, apply them one at a time instead.");
FNotificationInfo FailInfo(FailedNotificationText);
FailInfo.ExpireDuration = 6.f;
FailInfo.bUseLargeFont = false;
FailInfo.bUseSuccessFailIcons = true;
FSlateNotificationManager::Get().AddNotification(FailInfo);
}
// No need to make a entry in the Undo/Redo buffer if it didn't apply anything.
Transaction.Cancel();
return false;
}
TSet<FCurveModelID> FCurveEditor::GetCurvesForBufferedCurves() const
{
TSet<FCurveModelID> CurveModelIDs;
// Buffer curves operates on the selected curves (tree selection or key selection)
for (const TTuple<FCurveEditorTreeItemID, ECurveEditorTreeSelectionState>& Pair : GetTreeSelection())
{
if (Pair.Value == ECurveEditorTreeSelectionState::Explicit)
{
const FCurveEditorTreeItem& TreeItem = GetTreeItem(Pair.Key);
for (const FCurveModelID& CurveModelID : TreeItem.GetCurves())
{
CurveModelIDs.Add(CurveModelID);
}
}
}
for (const TTuple<FCurveModelID, FKeyHandleSet>& Pair : Selection.GetAll())
{
CurveModelIDs.Add(Pair.Key);
}
return CurveModelIDs;
}
bool FCurveEditor::IsActiveBufferedCurve(const TUniquePtr<IBufferedCurveModel>& BufferedCurve) const
{
TSet<FCurveModelID> CurveModelIDs = GetCurvesForBufferedCurves();
for (const FCurveModelID& CurveModelID : CurveModelIDs)
{
if (FCurveModel* Curve = FindCurve(CurveModelID))
{
if (Curve->GetLongDisplayName().ToString() == BufferedCurve.Get()->GetLongDisplayName())
{
return true;
}
}
}
return false;
}
void FCurveEditor::PostUndo(bool bSuccess)
{
if (WeakPanel.IsValid())
{
WeakPanel.Pin()->PostUndo();
}
// If you create keys and then undo them the selection set still thinks there's keys selected.
// This presents issues with context menus and other things that are activated when there is a selection set.
// To fix this, we have to loop through all of our curve models, and re-select only the key handles that were
// previously selected that still exist. Ugly, but reasonably functional.
TMap<FCurveModelID, FKeyHandleSet> SelectionSet = Selection.GetAll();
for (const TPair<FCurveModelID, FKeyHandleSet>& Set : SelectionSet)
{
FCurveModel* CurveModel = FindCurve(Set.Key);
// If the entire curve was removed, just dump that out of the selection set.
if (!CurveModel)
{
Selection.Remove(Set.Key);
continue;
}
// Get all of the key handles from this curve.
TArray<FKeyHandle> KeyHandles;
CurveModel->GetKeys(*this, TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), TNumericLimits<double>::Lowest(), TNumericLimits<double>::Max(), KeyHandles);
// The set handles will be mutated as we remove things so we need a copy that we can iterate through.
TArrayView<const FKeyHandle> SelectedHandles = Set.Value.AsArray();
TArray<FKeyHandle> NonMutableArray = TArray<FKeyHandle>(SelectedHandles.GetData(), SelectedHandles.Num());
for (const FKeyHandle& Handle : NonMutableArray)
{
// Check to see if our curve model contains this handle still.
if (!KeyHandles.Contains(Handle))
{
Selection.Remove(Set.Key, ECurvePointType::Key, Handle);
}
}
}
}
#undef LOCTEXT_NAMESPACE