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

3989 lines
125 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SCurveEditor.h"
#include "Fonts/SlateFontInfo.h"
#include "Rendering/DrawElements.h"
#include "Widgets/SBoxPanel.h"
#include "Styling/SlateTypes.h"
#include "Styling/CoreStyle.h"
#include "Layout/WidgetPath.h"
#include "Framework/Application/MenuStack.h"
#include "Fonts/FontMeasure.h"
#include "Framework/Application/SlateApplication.h"
#include "Textures/SlateIcon.h"
#include "Framework/Commands/UIAction.h"
#include "Framework/Commands/UICommandList.h"
#include "Widgets/Layout/SBorder.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Layout/SBox.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/SToolTip.h"
#include "Widgets/Notifications/SErrorText.h"
#include "Widgets/Input/SCheckBox.h"
#include "Styling/AppStyle.h"
#include "Factories/Factory.h"
#include "Factories/CurveFactory.h"
#include "Editor.h"
#include "Curves/SimpleCurve.h"
#include "CurveEditorCommands.h"
#include "CurveEditorSettings.h"
#include "ScopedTransaction.h"
#include "Framework/Commands/GenericCommands.h"
#include "Widgets/Input/SNumericEntryBox.h"
#include "Widgets/Input/STextEntryPopup.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "Framework/Notifications/NotificationManager.h"
#include "IPropertyUtilities.h"
#include "PropertyHandle.h"
#define LOCTEXT_NAMESPACE "SCurveEditor"
DEFINE_LOG_CATEGORY(LogCurveEditor);
const static FVector2D CONST_KeySize = FVector2D(11,11);
const static FVector2D CONST_TangentSize = FVector2D(7,7);
const static FVector2D CONST_CurveSize = FVector2D(12,12);
const static float CONST_FitMargin = 0.05f;
const static float CONST_MinViewRange = 0.01f;
const static float CONST_DefaultZoomRange = 1.0f;
const static float CONST_KeyTangentOffset = 60.0f;
//////////////////////////////////////////////////////////////////////////
// SCurveEditor
void SCurveEditor::Construct(const FArguments& InArgs)
{
CurveFactory = NULL;
Commands = TSharedPtr< FUICommandList >(new FUICommandList);
CurveOwner = NULL;
// view input
ViewMinInput = InArgs._ViewMinInput;
ViewMaxInput = InArgs._ViewMaxInput;
// data input - only used when it's set
DataMinInput = InArgs._DataMinInput;
DataMaxInput = InArgs._DataMaxInput;
ViewMinOutput = InArgs._ViewMinOutput;
ViewMaxOutput = InArgs._ViewMaxOutput;
InputSnap = InArgs._InputSnap;
OutputSnap = InArgs._OutputSnap;
bInputSnappingEnabled = InArgs._InputSnappingEnabled;
bOutputSnappingEnabled = InArgs._OutputSnappingEnabled;
bShowTimeInFrames = InArgs._ShowTimeInFrames;
bZoomToFitVertical = InArgs._ZoomToFitVertical;
bZoomToFitHorizontal = InArgs._ZoomToFitHorizontal;
DesiredSize = InArgs._DesiredSize;
GridColor = InArgs._GridColor;
bIsUsingSlider = false;
bAllowAutoFrame = true;
bRequireFocusToZoom = false;
bIsPendingRebuilt = false;
// if editor size is set, use it, otherwise, use default value
if (DesiredSize.Get().IsZero())
{
DesiredSize.Set(FVector2D(128, 64));
}
TimelineLength = InArgs._TimelineLength;
SetInputViewRangeHandler = InArgs._OnSetInputViewRange;
SetOutputViewRangeHandler = InArgs._OnSetOutputViewRange;
bDrawCurve = InArgs._DrawCurve;
bHideUI = InArgs._HideUI;
bAllowZoomOutput = InArgs._AllowZoomOutput;
bAlwaysDisplayColorCurves = InArgs._AlwaysDisplayColorCurves;
bAlwaysHideGradientEditor = InArgs._AlwaysHideGradientEditor;
bShowZoomButtons = InArgs._ShowZoomButtons;
bShowCurveSelector = InArgs._ShowCurveSelector;
bDrawInputGridNumbers = InArgs._ShowInputGridNumbers;
bDrawOutputGridNumbers = InArgs._ShowOutputGridNumbers;
bAreCurvesVisible = InArgs._AreCurvesVisible;
SetAreCurvesVisibleHandler = InArgs._OnSetAreCurvesVisible;
OnCreateAsset = InArgs._OnCreateAsset;
DragState = EDragState::None;
DragThreshold = 4;
MovementAxisLock = EMovementAxisLock::None;
TransactionIndex = -1;
ReduceTolerance = 0.001;
Settings = GetMutableDefault<UCurveEditorSettings>();
Commands->MapAction(FGenericCommands::Get().Undo,
FExecuteAction::CreateSP(this, &SCurveEditor::UndoAction));
Commands->MapAction(FGenericCommands::Get().Redo,
FExecuteAction::CreateSP(this, &SCurveEditor::RedoAction));
Commands->MapAction(FCurveEditorCommands::Get().ZoomToFitHorizontal,
FExecuteAction::CreateSP(this, &SCurveEditor::ZoomToFitHorizontal, false));
Commands->MapAction(FCurveEditorCommands::Get().ZoomToFitVertical,
FExecuteAction::CreateSP(this, &SCurveEditor::ZoomToFitVertical, false));
Commands->MapAction(FCurveEditorCommands::Get().ZoomToFit,
FExecuteAction::CreateSP(this, &SCurveEditor::ZoomToFit, false));
Commands->MapAction(FCurveEditorCommands::Get().ZoomToFitAll,
FExecuteAction::CreateSP(this, &SCurveEditor::ZoomToFit, true));
Commands->MapAction(FCurveEditorCommands::Get().ToggleInputSnapping,
FExecuteAction::CreateSP(this, &SCurveEditor::ToggleInputSnapping),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SCurveEditor::IsInputSnappingEnabled));
Commands->MapAction(FCurveEditorCommands::Get().ToggleOutputSnapping,
FExecuteAction::CreateSP(this, &SCurveEditor::ToggleOutputSnapping),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SCurveEditor::IsOutputSnappingEnabled));
// Interpolation
Commands->MapAction(FCurveEditorCommands::Get().InterpolationConstant,
FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectInterpolationMode, RCIM_Constant, RCTM_Auto),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SCurveEditor::IsInterpolationModeSelected, RCIM_Constant, RCTM_Auto));
Commands->MapAction(FCurveEditorCommands::Get().InterpolationLinear,
FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectInterpolationMode, RCIM_Linear, RCTM_Auto),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SCurveEditor::IsInterpolationModeSelected, RCIM_Linear, RCTM_Auto));
Commands->MapAction(FCurveEditorCommands::Get().InterpolationCubicAuto,
FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectInterpolationMode, RCIM_Cubic, RCTM_Auto),
FCanExecuteAction::CreateSP(this, &SCurveEditor::HasRichCurves),
FIsActionChecked::CreateSP(this, &SCurveEditor::IsInterpolationModeSelected, RCIM_Cubic, RCTM_Auto));
Commands->MapAction(FCurveEditorCommands::Get().InterpolationCubicSmartAuto,
FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectInterpolationMode, RCIM_Cubic, RCTM_SmartAuto),
FCanExecuteAction::CreateSP(this, &SCurveEditor::HasRichCurves),
FIsActionChecked::CreateSP(this, &SCurveEditor::IsInterpolationModeSelected, RCIM_Cubic, RCTM_SmartAuto));
Commands->MapAction(FCurveEditorCommands::Get().InterpolationCubicUser,
FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectInterpolationMode, RCIM_Cubic, RCTM_User),
FCanExecuteAction::CreateSP(this, &SCurveEditor::HasRichCurves),
FIsActionChecked::CreateSP(this, &SCurveEditor::IsInterpolationModeSelected, RCIM_Cubic, RCTM_User));
Commands->MapAction(FCurveEditorCommands::Get().InterpolationCubicBreak,
FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectInterpolationMode, RCIM_Cubic, RCTM_Break),
FCanExecuteAction::CreateSP(this, &SCurveEditor::HasRichCurves),
FIsActionChecked::CreateSP(this, &SCurveEditor::IsInterpolationModeSelected, RCIM_Cubic, RCTM_Break));
// Tangents
Commands->MapAction(FCurveEditorCommands::Get().FlattenTangents,
FExecuteAction::CreateSP(this, &SCurveEditor::OnFlattenOrStraightenTangents, true),
FCanExecuteAction::CreateSP(this, &SCurveEditor::HasRichCurves));
Commands->MapAction(FCurveEditorCommands::Get().StraightenTangents,
FExecuteAction::CreateSP(this, &SCurveEditor::OnFlattenOrStraightenTangents, false),
FCanExecuteAction::CreateSP(this, &SCurveEditor::HasRichCurves));
// Bake and reduce
Commands->MapAction(FCurveEditorCommands::Get().BakeCurve,
FExecuteAction::CreateSP(this, &SCurveEditor::OnBakeCurve));
Commands->MapAction(FCurveEditorCommands::Get().ReduceCurve,
FExecuteAction::CreateSP(this, &SCurveEditor::OnReduceCurve));
// Pre infinity extrapolation
Commands->MapAction(FCurveEditorCommands::Get().SetPreInfinityExtrapCycle,
FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPreInfinityExtrap, RCCE_Cycle),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SCurveEditor::IsPreInfinityExtrapSelected, RCCE_Cycle));
Commands->MapAction(FCurveEditorCommands::Get().SetPreInfinityExtrapCycleWithOffset,
FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPreInfinityExtrap, RCCE_CycleWithOffset),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SCurveEditor::IsPreInfinityExtrapSelected, RCCE_CycleWithOffset));
Commands->MapAction(FCurveEditorCommands::Get().SetPreInfinityExtrapOscillate,
FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPreInfinityExtrap, RCCE_Oscillate),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SCurveEditor::IsPreInfinityExtrapSelected, RCCE_Oscillate));
Commands->MapAction(FCurveEditorCommands::Get().SetPreInfinityExtrapLinear,
FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPreInfinityExtrap, RCCE_Linear),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SCurveEditor::IsPreInfinityExtrapSelected, RCCE_Linear));
Commands->MapAction(FCurveEditorCommands::Get().SetPreInfinityExtrapConstant,
FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPreInfinityExtrap, RCCE_Constant),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SCurveEditor::IsPreInfinityExtrapSelected, RCCE_Constant));
// Post infinity extrapolation
Commands->MapAction(FCurveEditorCommands::Get().SetPostInfinityExtrapCycle,
FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPostInfinityExtrap, RCCE_Cycle),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SCurveEditor::IsPostInfinityExtrapSelected, RCCE_Cycle));
Commands->MapAction(FCurveEditorCommands::Get().SetPostInfinityExtrapCycleWithOffset,
FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPostInfinityExtrap, RCCE_CycleWithOffset),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SCurveEditor::IsPostInfinityExtrapSelected, RCCE_CycleWithOffset));
Commands->MapAction(FCurveEditorCommands::Get().SetPostInfinityExtrapOscillate,
FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPostInfinityExtrap, RCCE_Oscillate),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SCurveEditor::IsPostInfinityExtrapSelected, RCCE_Oscillate));
Commands->MapAction(FCurveEditorCommands::Get().SetPostInfinityExtrapLinear,
FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPostInfinityExtrap, RCCE_Linear),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SCurveEditor::IsPostInfinityExtrapSelected, RCCE_Linear));
Commands->MapAction(FCurveEditorCommands::Get().SetPostInfinityExtrapConstant,
FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectPostInfinityExtrap, RCCE_Constant),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SCurveEditor::IsPostInfinityExtrapSelected, RCCE_Constant));
// Tangent Visibility
Commands->MapAction(FCurveEditorCommands::Get().SetAllTangentsVisibility,
FExecuteAction::CreateLambda( [this]{ Settings->SetTangentVisibility( ECurveEditorTangentVisibility::AllTangents ); } ),
FCanExecuteAction::CreateLambda( []{ return true; } ),
FIsActionChecked::CreateLambda( [this]{ return Settings->GetTangentVisibility() == ECurveEditorTangentVisibility::AllTangents; } ) );
Commands->MapAction(FCurveEditorCommands::Get().SetSelectedKeysTangentVisibility,
FExecuteAction::CreateLambda( [this]{ Settings->SetTangentVisibility( ECurveEditorTangentVisibility::SelectedKeys ); } ),
FCanExecuteAction::CreateLambda( []{ return true; } ),
FIsActionChecked::CreateLambda( [this]{ return Settings->GetTangentVisibility() == ECurveEditorTangentVisibility::SelectedKeys; } ) );
Commands->MapAction(FCurveEditorCommands::Get().SetNoTangentsVisibility,
FExecuteAction::CreateLambda( [this]{ Settings->SetTangentVisibility( ECurveEditorTangentVisibility::NoTangents ); } ),
FCanExecuteAction::CreateLambda( []{ return true; } ),
FIsActionChecked::CreateLambda( [this]{ return Settings->GetTangentVisibility() == ECurveEditorTangentVisibility::NoTangents; } ) );
Commands->MapAction(FCurveEditorCommands::Get().ToggleAutoFrameCurveEditor,
FExecuteAction::CreateLambda( [this]{ Settings->SetAutoFrameCurveEditor( !Settings->GetAutoFrameCurveEditor() ); } ),
FCanExecuteAction::CreateLambda( []{ return true; } ),
FIsActionChecked::CreateLambda( [this]{ return Settings->GetAutoFrameCurveEditor(); } ) );
Commands->MapAction(FCurveEditorCommands::Get().ToggleShowCurveEditorCurveToolTips,
FExecuteAction::CreateLambda( [this]{
Settings->SetShowCurveEditorCurveToolTips( !Settings->GetShowCurveEditorCurveToolTips() );
if (!Settings->GetShowCurveEditorCurveToolTips())
{
CurveToolTip.Reset();
SetToolTip(CurveToolTip);
} } ),
FCanExecuteAction::CreateLambda( []{ return true; } ),
FIsActionChecked::CreateLambda( [this]{ return Settings->GetShowCurveEditorCurveToolTips(); } ) );
FCoreUObjectDelegates::OnPackageReloaded.AddSP(this, &SCurveEditor::HandlePackageReloaded);
SAssignNew(WarningMessageText, SErrorText);
TSharedRef<SBox> CurveSelector = SNew(SBox)
.VAlign(VAlign_Top)
.Visibility(this, &SCurveEditor::GetCurveSelectorVisibility)
[
CreateCurveSelectionWidget()
];
CurveSelectionWidget = CurveSelector;
InputAxisName = InArgs._XAxisName.IsSet() ? FText::FromString(InArgs._XAxisName.GetValue()) : LOCTEXT("Time", "Time");
InputFrameAxisName = InArgs._XAxisName.IsSet() ? FText::FromString(InArgs._XAxisName.GetValue()) : LOCTEXT("Frame", "Frame");
OutputAxisName = InArgs._YAxisName.IsSet() ? FText::FromString(InArgs._YAxisName.GetValue()) : LOCTEXT("Value", "Value");
ChildSlot
[
SNew( SVerticalBox )
+ SVerticalBox::Slot()
.FillHeight(1.0f)
[
SNew(SHorizontalBox)
.Visibility( this, &SCurveEditor::GetCurveAreaVisibility )
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(FMargin(30, 12, 0, 0))
[
CurveSelector
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SBorder)
.VAlign(VAlign_Top)
.HAlign(HAlign_Left)
.BorderImage( FAppStyle::GetBrush("NoBorder") )
.DesiredSizeScale(FVector2D(256.0f,32.0f))
.Padding(FMargin(2, 12, 0, 0))
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SButton)
.ToolTipText(LOCTEXT("ZoomToFitHorizontal", "Zoom To Fit Horizontal"))
.Visibility(this, &SCurveEditor::GetZoomButtonVisibility)
.OnClicked(this, &SCurveEditor::ZoomToFitHorizontalClicked)
.ContentPadding(1)
[
SNew(SImage)
.Image( FAppStyle::GetBrush("CurveEd.FitHorizontal") )
.ColorAndOpacity( FSlateColor::UseForeground() )
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SButton)
.ToolTipText(LOCTEXT("ZoomToFitVertical", "Zoom To Fit Vertical"))
.Visibility(this, &SCurveEditor::GetZoomButtonVisibility)
.OnClicked(this, &SCurveEditor::ZoomToFitVerticalClicked)
.ContentPadding(1)
[
SNew(SImage)
.Image( FAppStyle::GetBrush("CurveEd.FitVertical") )
.ColorAndOpacity( FSlateColor::UseForeground() )
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.Padding(6.0f, 0.0, 3.0f, 0.0f)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Visibility(this, &SCurveEditor::GetEditVisibility)
.Text(this, &SCurveEditor::GetInputAxisName)
.ShadowOffset(FVector2D(1,1))
]
+ SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.AutoWidth()
[
SNew(SNumericEntryBox<float>)
.IsEnabled(this, &SCurveEditor::GetInputEditEnabled)
.Value(this, &SCurveEditor::OnGetTime)
.UndeterminedString(LOCTEXT("MultipleValues", "Multiple Values"))
.OnValueCommitted(this, &SCurveEditor::OnTimeComitted)
.OnValueChanged(this, &SCurveEditor::OnTimeChanged)
.OnBeginSliderMovement(this, &SCurveEditor::OnBeginSliderMovement, LOCTEXT("SetTime", "Set New Time"))
.OnEndSliderMovement(this, &SCurveEditor::OnEndSliderMovement)
.AllowSpin(true)
.MinValue(TOptional<float>())
.MaxValue(TOptional<float>())
.MaxSliderValue(TOptional<float>())
.MinSliderValue(TOptional<float>())
.Delta(this, &SCurveEditor::GetInputNumericEntryBoxDelta)
.MinDesiredValueWidth(60.0f)
.Visibility(this, &SCurveEditor::GetTimeEditVisibility)
]
+ SHorizontalBox::Slot()
.Padding(3.0f, 0.0f)
.VAlign(VAlign_Center)
.AutoWidth()
[
SNew(SNumericEntryBox<int32>)
.IsEnabled(this, &SCurveEditor::GetInputEditEnabled)
.Value(this, &SCurveEditor::OnGetTimeInFrames)
.UndeterminedString(LOCTEXT("MultipleValues", "Multiple Values"))
.OnValueCommitted(this, &SCurveEditor::OnTimeInFramesComitted)
.OnValueChanged(this, &SCurveEditor::OnTimeInFramesChanged)
.OnBeginSliderMovement(this, &SCurveEditor::OnBeginSliderMovement, LOCTEXT("SetFrame", "Set New Frame"))
.OnEndSliderMovement(this, &SCurveEditor::OnEndSliderMovement)
.LabelVAlign(VAlign_Center)
.AllowSpin(true)
.MinValue(TOptional<int32>())
.MaxValue(TOptional<int32>())
.MaxSliderValue(TOptional<int32>())
.MinSliderValue(TOptional<int32>())
.Delta(1)
.MinDesiredValueWidth(60.0f)
.Visibility(this, &SCurveEditor::GetFrameEditVisibility)
]
]
+ SHorizontalBox::Slot()
.Padding(3.0f, 0.0f)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Visibility(this, &SCurveEditor::GetEditVisibility)
.Text(OutputAxisName)
.ShadowOffset(FVector2D(1, 1))
]
+ SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.AutoWidth()
[
SNew(SNumericEntryBox<float>)
.Visibility(this, &SCurveEditor::GetEditVisibility)
.Value(this, &SCurveEditor::OnGetValue)
.UndeterminedString(LOCTEXT("MultipleValues", "Multiple Values"))
.OnValueCommitted(this, &SCurveEditor::OnValueComitted)
.OnValueChanged(this, &SCurveEditor::OnValueChanged)
.OnBeginSliderMovement(this, &SCurveEditor::OnBeginSliderMovement, LOCTEXT("SetValue", "Set New Value"))
.OnEndSliderMovement(this, &SCurveEditor::OnEndSliderMovement)
.AllowSpin(true)
.MinValue(TOptional<float>())
.MaxValue(TOptional<float>())
.MaxSliderValue(TOptional<float>())
.MinSliderValue(TOptional<float>())
.Delta(this, &SCurveEditor::GetOutputNumericEntryBoxDelta)
.MinDesiredValueWidth(60.0f)
]
]
]
]
+ SVerticalBox::Slot()
.VAlign(VAlign_Bottom)
.FillHeight(.75f)
[
SNew( SBorder )
.Visibility( this, &SCurveEditor::GetColorGradientVisibility )
.BorderImage( FAppStyle::GetBrush("ToolPanel.GroupBorder") )
.BorderBackgroundColor( FLinearColor( .8f, .8f, .8f, .60f ) )
.Padding(1.0f)
[
SAssignNew( GradientViewer, SColorGradientEditor )
.ViewMinInput( ViewMinInput )
.ViewMaxInput( ViewMaxInput )
.IsEditingEnabled( this, &SCurveEditor::IsEditingEnabled )
]
]
];
if (GEditor != NULL)
{
GEditor->RegisterForUndo(this);
}
FCoreUObjectDelegates::OnObjectPropertyChanged.AddSP(this, &SCurveEditor::OnObjectPropertyChanged);
}
FText SCurveEditor::GetIsCurveVisibleToolTip(TSharedPtr<FCurveViewModel> CurveViewModel) const
{
return CurveViewModel->bIsVisible ?
FText::Format(LOCTEXT("HideFormat", "Hide {0} curve"), FText::FromName(CurveViewModel->CurveInfo.CurveName)) :
FText::Format(LOCTEXT("ShowFormat", "Show {0} curve"), FText::FromName(CurveViewModel->CurveInfo.CurveName));
}
ECheckBoxState SCurveEditor::IsCurveVisible(TSharedPtr<FCurveViewModel> CurveViewModel) const
{
return CurveViewModel->bIsVisible ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
}
void SCurveEditor::OnCurveIsVisibleChanged(ECheckBoxState NewCheckboxState, TSharedPtr<FCurveViewModel> CurveViewModel)
{
if (NewCheckboxState == ECheckBoxState::Checked)
{
CurveViewModel->bIsVisible = true;
}
else
{
CurveViewModel->bIsVisible = false;
RemoveCurveKeysFromSelection(CurveViewModel);
}
}
FText SCurveEditor::GetIsCurveLockedToolTip(TSharedPtr<FCurveViewModel> CurveViewModel) const
{
return CurveViewModel->bIsLocked ?
FText::Format(LOCTEXT("UnlockFormat", "Unlock {0} curve for editing"), FText::FromName(CurveViewModel->CurveInfo.CurveName)) :
FText::Format(LOCTEXT("LockFormat", "Lock {0} curve for editing"), FText::FromName(CurveViewModel->CurveInfo.CurveName));
}
ECheckBoxState SCurveEditor::IsCurveLocked(TSharedPtr<FCurveViewModel> CurveViewModel) const
{
return CurveViewModel->bIsLocked ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
}
void SCurveEditor::OnCurveIsLockedChanged(ECheckBoxState NewCheckboxState, TSharedPtr<FCurveViewModel> CurveViewModel)
{
if (NewCheckboxState == ECheckBoxState::Checked)
{
CurveViewModel->bIsLocked = true;
RemoveCurveKeysFromSelection(CurveViewModel);
}
else
{
CurveViewModel->bIsLocked = false;
}
}
void SCurveEditor::RemoveCurveKeysFromSelection(TSharedPtr<FCurveViewModel> CurveViewModel)
{
TArray<FSelectedCurveKey> SelectedKeysForLockedCurve;
for (auto SelectedKey : SelectedKeys)
{
if (SelectedKey.Curve == CurveViewModel->CurveInfo.CurveToEdit)
{
SelectedKeysForLockedCurve.Add(SelectedKey);
}
}
for (auto KeyToDeselect : SelectedKeysForLockedCurve)
{
RemoveFromKeySelection(KeyToDeselect);
}
}
FText SCurveEditor::GetCurveToolTipNameText() const
{
return CurveToolTipNameText;
}
FText SCurveEditor::GetCurveToolTipInputText() const
{
return CurveToolTipInputText;
}
FText SCurveEditor::GetCurveToolTipOutputText() const
{
return CurveToolTipOutputText;
}
FText SCurveEditor::GetInputAxisName() const
{
return ShowTimeInFrames() ? InputFrameAxisName : InputAxisName;
}
SCurveEditor::~SCurveEditor()
{
if (GEditor != NULL)
{
GEditor->UnregisterForUndo(this);
}
FCoreUObjectDelegates::OnObjectPropertyChanged.RemoveAll(this);
}
TSharedRef<SWidget> SCurveEditor::CreateCurveSelectionWidget() const
{
TSharedRef<SVerticalBox> CurveBox = SNew(SVerticalBox);
if (CurveViewModels.Num() > 1)
{
// Only create curve controls if there are more than one.
for (auto CurveViewModel : CurveViewModels)
{
CurveBox->AddSlot()
.AutoHeight()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.Padding(0, 0, 5, 0)
.FillWidth(1.0f)
[
SNew(STextBlock)
.Font(FAppStyle::GetFontStyle("CurveEd.LabelFont"))
.ColorAndOpacity(CurveViewModel->Color)
.Text(FText::FromName(CurveViewModel->CurveInfo.CurveName))
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Fill)
[
SNew(SCheckBox)
.Style(&FAppStyle::Get().GetWidgetStyle<FCheckBoxStyle>("ToggleButtonCheckbox"))
.IsChecked(this, &SCurveEditor::IsCurveVisible, CurveViewModel)
.OnCheckStateChanged(const_cast<SCurveEditor*>(this), &SCurveEditor::OnCurveIsVisibleChanged, CurveViewModel)
.ToolTipText(this, &SCurveEditor::GetIsCurveVisibleToolTip, CurveViewModel)
.CheckedImage(FAppStyle::Get().GetBrush("Icons.Visible"))
.CheckedHoveredImage(FAppStyle::Get().GetBrush("Icons.Visible"))
.CheckedPressedImage(FAppStyle::Get().GetBrush("Icons.Visible"))
.UncheckedImage(FAppStyle::Get().GetBrush("Icons.Hidden"))
.UncheckedHoveredImage(FAppStyle::Get().GetBrush("Icons.Hidden"))
.UncheckedPressedImage(FAppStyle::Get().GetBrush("Icons.Hidden"))
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Fill)
.Padding(2, 0, 0, 0)
[
SNew(SCheckBox)
.Style(&FAppStyle::Get().GetWidgetStyle<FCheckBoxStyle>("ToggleButtonCheckbox"))
.IsChecked(this, &SCurveEditor::IsCurveLocked, CurveViewModel)
.OnCheckStateChanged(const_cast<SCurveEditor*>(this), &SCurveEditor::OnCurveIsLockedChanged, CurveViewModel)
.ToolTipText(this, &SCurveEditor::GetIsCurveLockedToolTip, CurveViewModel)
.CheckedImage(FAppStyle::Get().GetBrush("Icons.Lock"))
.CheckedHoveredImage(FAppStyle::Get().GetBrush("Icons.Lock"))
.CheckedPressedImage(FAppStyle::Get().GetBrush("Icons.Lock"))
.UncheckedImage(FAppStyle::Get().GetBrush("Icons.Unlock"))
.UncheckedHoveredImage(FAppStyle::Get().GetBrush("Icons.Unlock"))
.UncheckedPressedImage(FAppStyle::Get().GetBrush("Icons.Unlock"))
.Visibility(bCanEditTrack ? EVisibility::Visible : EVisibility::Collapsed)
]
];
}
}
TSharedRef<SBorder> Border = SNew(SBorder)
.Padding(FMargin(3, 2, 2, 2))
.BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder"))
.BorderBackgroundColor(FLinearColor(0.0f, 0.0f, 0.0f, 0.3f))
[
CurveBox
];
return Border;
}
void SCurveEditor::PushWarningMenu( FVector2D Position, const FText& Message )
{
WarningMessageText->SetError(Message);
FSlateApplication::Get().PushMenu(
SharedThis( this ),
FWidgetPath(),
WarningMessageText->AsWidget(),
Position,
FPopupTransitionEffect( FPopupTransitionEffect::ContextMenu));
}
void SCurveEditor::PushKeyMenu(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent)
{
FMenuBuilder MenuBuilder(true, Commands.ToSharedRef());
MenuBuilder.BeginSection("CurveEditorInterpolation", LOCTEXT("KeyInterpolationMode", "Key Interpolation"));
{
MenuBuilder.AddMenuEntry(FCurveEditorCommands::Get().InterpolationCubicAuto);
MenuBuilder.AddMenuEntry(FCurveEditorCommands::Get().InterpolationCubicUser);
MenuBuilder.AddMenuEntry(FCurveEditorCommands::Get().InterpolationCubicBreak);
MenuBuilder.AddMenuEntry(FCurveEditorCommands::Get().InterpolationLinear);
MenuBuilder.AddMenuEntry(FCurveEditorCommands::Get().InterpolationConstant);
}
MenuBuilder.EndSection(); //CurveEditorInterpolation
MenuBuilder.BeginSection("CurveEditorTangents", LOCTEXT("Tangents", "Tangents"));
{
MenuBuilder.AddMenuEntry(FCurveEditorCommands::Get().FlattenTangents);
MenuBuilder.AddMenuEntry(FCurveEditorCommands::Get().StraightenTangents);
}
MenuBuilder.EndSection(); //CurveEditorTangents
FWidgetPath WidgetPath = InMouseEvent.GetEventPath() != nullptr ? *InMouseEvent.GetEventPath() : FWidgetPath();
FVector2D Position = InMouseEvent.GetScreenSpacePosition();
FSlateApplication::Get().PushMenu(
SharedThis( this ),
WidgetPath,
MenuBuilder.MakeWidget(),
Position,
FPopupTransitionEffect( FPopupTransitionEffect::ContextMenu));
}
FVector2D SCurveEditor::ComputeDesiredSize( float ) const
{
return DesiredSize.Get();
}
EVisibility SCurveEditor::GetCurveAreaVisibility() const
{
return AreCurvesVisible() ? EVisibility::Visible : EVisibility::Collapsed;
}
EVisibility SCurveEditor::GetCurveSelectorVisibility() const
{
return (IsHovered() || (false == bHideUI)) && bShowCurveSelector ? EVisibility::Visible : EVisibility::Collapsed;
}
EVisibility SCurveEditor::GetEditVisibility() const
{
return (SelectedKeys.Num() > 0) && (IsHovered() || (false == bHideUI)) ? EVisibility::Visible : EVisibility::Collapsed;
}
EVisibility SCurveEditor::GetColorGradientVisibility() const
{
return IsGradientEditorVisible() && IsLinearColorCurve() ? EVisibility::Visible : EVisibility::Collapsed;
}
EVisibility SCurveEditor::GetZoomButtonVisibility() const
{
return (IsHovered() || (false == bHideUI)) && bShowZoomButtons ? EVisibility::Visible : EVisibility::Collapsed;
}
EVisibility SCurveEditor::GetTimeEditVisibility() const
{
if (GetEditVisibility().IsVisible())
{
return ShowTimeInFrames() ? EVisibility::Collapsed : EVisibility::Visible;
}
return EVisibility::Collapsed;
}
EVisibility SCurveEditor::GetFrameEditVisibility() const
{
if (GetEditVisibility().IsVisible())
{
return ShowTimeInFrames() ? EVisibility::Visible : EVisibility::Collapsed;
}
return EVisibility::Collapsed;
}
bool SCurveEditor::GetInputEditEnabled() const
{
bool bKeysOnSameCurves = false;
for (int32 SelectedIndex = 0; SelectedIndex < SelectedKeys.Num() - 1; SelectedIndex++)
{
for (int32 CompareIndex = SelectedIndex + 1; CompareIndex < SelectedKeys.Num(); CompareIndex++)
{
if (SelectedKeys[SelectedIndex].Curve == SelectedKeys[CompareIndex].Curve)
{
bKeysOnSameCurves = true;
break;
}
}
}
return (SelectedKeys.Num() == 1) || !bKeysOnSameCurves;
}
int32 SCurveEditor::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled ) const
{
// Rendering info
bool bEnabled = ShouldBeEnabled( bParentEnabled );
ESlateDrawEffect DrawEffects = bEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect;
const FSlateBrush* TimelineAreaBrush = FAppStyle::GetBrush("CurveEd.TimelineArea");
const FSlateBrush* WhiteBrush = FAppStyle::GetBrush("WhiteTexture");
FGeometry CurveAreaGeometry = AllottedGeometry;
// Positioning info
FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), CurveAreaGeometry.GetLocalSize());
if (FMath::IsNearlyEqual(ViewMinInput.Get(), ViewMaxInput.Get()) || FMath::IsNearlyEqual(ViewMinOutput.Get(), ViewMaxOutput.Get()))
{
return 0;
}
// Draw background to indicate valid timeline area
float ZeroInputX = ScaleInfo.InputToLocalX(0.f);
float ZeroOutputY = ScaleInfo.OutputToLocalY(0.f);
// timeline background
int32 BackgroundLayerId = LayerId;
float TimelineMaxX = ScaleInfo.InputToLocalX(TimelineLength.Get());
FSlateDrawElement::MakeBox
(
OutDrawElements,
BackgroundLayerId,
CurveAreaGeometry.ToPaintGeometry(FVector2D(TimelineMaxX - ZeroInputX, CurveAreaGeometry.GetLocalSize().Y), FSlateLayoutTransform(FVector2D(ZeroInputX, 0.f))),
TimelineAreaBrush,
DrawEffects,
TimelineAreaBrush->GetTint(InWidgetStyle) * InWidgetStyle.GetColorAndOpacityTint()
);
// grid lines.
int32 GridLineLayerId = BackgroundLayerId + 1;
PaintGridLines(CurveAreaGeometry, ScaleInfo, OutDrawElements, GridLineLayerId, MyCullingRect, DrawEffects);
// time=0 line
int32 ZeroLineLayerId = GridLineLayerId + 1;
TArray<FVector2D> ZeroLinePoints;
ZeroLinePoints.Add( FVector2D( ZeroInputX, 0 ) );
ZeroLinePoints.Add( FVector2D( ZeroInputX, CurveAreaGeometry.GetLocalSize().Y ) );
FSlateDrawElement::MakeLines(
OutDrawElements,
ZeroLineLayerId,
AllottedGeometry.ToPaintGeometry(),
ZeroLinePoints,
DrawEffects,
FLinearColor::White,
false );
// value=0 line
if( AreCurvesVisible() )
{
FSlateDrawElement::MakeBox
(
OutDrawElements,
ZeroLineLayerId,
CurveAreaGeometry.ToPaintGeometry( FVector2D(CurveAreaGeometry.Size.X, 1), FSlateLayoutTransform(FVector2D(0.f, ZeroOutputY)) ),
WhiteBrush,
DrawEffects,
WhiteBrush->GetTint( InWidgetStyle ) * InWidgetStyle.GetColorAndOpacityTint()
);
}
int32 LockedCurveLayerID = ZeroLineLayerId + 1;
int32 CurveLayerId = LockedCurveLayerID + 1;
int32 KeyLayerId = CurveLayerId + 1;
int32 SelectedKeyLayerId = KeyLayerId + 1;
bool bAnyCurveViewModelsSelected = AnyCurveViewModelsSelected();
if( AreCurvesVisible() )
{
//Paint the curves, unlocked curves will be on top
for ( auto CurveViewModel : CurveViewModels )
{
if (CurveViewModel->bIsVisible)
{
PaintCurve(CurveViewModel, CurveAreaGeometry, ScaleInfo, OutDrawElements, CurveViewModel->bIsLocked ? LockedCurveLayerID : CurveLayerId, MyCullingRect, DrawEffects, InWidgetStyle, bAnyCurveViewModelsSelected);
}
}
//Paint the keys on top of the curve
for (auto CurveViewModel : CurveViewModels)
{
if (CurveViewModel->bIsVisible)
{
PaintKeys(CurveViewModel, ScaleInfo, OutDrawElements, KeyLayerId, SelectedKeyLayerId, CurveAreaGeometry, MyCullingRect, DrawEffects, InWidgetStyle, bAnyCurveViewModelsSelected);
}
}
}
// Paint children
int32 ChildrenLayerId = SelectedKeyLayerId + 1;
int32 MarqueeLayerId = SCompoundWidget::OnPaint(Args, CurveAreaGeometry, MyCullingRect, OutDrawElements, ChildrenLayerId, InWidgetStyle, bParentEnabled);
// Paint marquee
if (DragState == EDragState::MarqueeSelect)
{
PaintMarquee(AllottedGeometry, MyCullingRect, OutDrawElements, MarqueeLayerId);
}
return MarqueeLayerId + 1;
}
void SCurveEditor::PaintCurve(TSharedPtr<FCurveViewModel> CurveViewModel, const FGeometry &AllottedGeometry, FTrackScaleInfo &ScaleInfo, FSlateWindowElementList &OutDrawElements,
int32 LayerId, const FSlateRect& MyCullingRect, ESlateDrawEffect DrawEffects, const FWidgetStyle &InWidgetStyle, bool bAnyCurveViewModelsSelected )const
{
if (CurveViewModel.IsValid())
{
if (bDrawCurve)
{
FLinearColor Color = InWidgetStyle.GetColorAndOpacityTint() * CurveViewModel->Color;
// Fade out curves that are not selected.
if (!CurveViewModel->bIsSelected && bAnyCurveViewModelsSelected)
{
Color *= FLinearColor(1.0f,1.0f,1.0f,0.2f);
}
// Fade out curves which are locked.
if(CurveViewModel->bIsLocked)
{
Color *= FLinearColor(1.0f,1.0f,1.0f,0.35f);
}
TArray<FVector2D> LinePoints;
int32 CurveDrawInterval = 1;
FRealCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit;
const float NumKeys = Curve->GetNumKeys();
if (NumKeys < 2)
{
//Not enough point, just draw flat line
float Value = Curve->Eval(0.0f);
float Y = ScaleInfo.OutputToLocalY(Value);
LinePoints.Add(FVector2D(0.0f, Y));
LinePoints.Add(FVector2D(AllottedGeometry.GetLocalSize().X, Y));
FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), LinePoints, DrawEffects, Color);
LinePoints.Empty();
}
else
{
TArray<FKeyHandle> KeyHandles;
TArray<TPair<float,float>> Key_TimeValuePairs;
KeyHandles.Reserve(NumKeys);
Key_TimeValuePairs.Reserve(NumKeys);
for (auto It = Curve->GetKeyHandleIterator(); It; ++It)
{
const FKeyHandle& KeyHandle = *It;
KeyHandles.Add(KeyHandle);
Key_TimeValuePairs.Emplace(Curve->GetKeyTimeValuePair(KeyHandle));
}
//Add arrive and exit lines
{
float ArriveX = ScaleInfo.InputToLocalX(Key_TimeValuePairs[0].Key);
float ArriveY = ScaleInfo.OutputToLocalY(Key_TimeValuePairs[0].Value);
float LeaveY = ScaleInfo.OutputToLocalY(Key_TimeValuePairs.Last().Value);
float LeaveX = ScaleInfo.InputToLocalX(Key_TimeValuePairs.Last().Key);
//Arrival line
LinePoints.Add(FVector2D(0.0f, ArriveY));
LinePoints.Add(FVector2D(ArriveX, ArriveY));
FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), LinePoints, DrawEffects, Color);
LinePoints.Empty();
//Leave line
LinePoints.Add(FVector2D(AllottedGeometry.GetLocalSize().X, LeaveY));
LinePoints.Add(FVector2D(LeaveX, LeaveY));
FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), LinePoints, DrawEffects, Color);
LinePoints.Empty();
}
//Add enclosed segments
for (int32 i = 0;i<NumKeys-1;++i)
{
CreateLinesForSegment(Curve, Curve->GetKeyInterpMode(KeyHandles[i]), Key_TimeValuePairs[i], Key_TimeValuePairs[i+1], LinePoints, ScaleInfo);
FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), LinePoints, DrawEffects, Color);
LinePoints.Empty();
}
}
}
}
}
void SCurveEditor::CreateLinesForSegment( FRealCurve* Curve, ERichCurveInterpMode InterpMode, const TPair<float,float>& Key1_TimeValue, const TPair<float,float>& Key2_TimeValue, TArray<FVector2D>& Points, FTrackScaleInfo &ScaleInfo ) const
{
switch(InterpMode)
{
case RCIM_Constant:
{
//@todo: should really only need 3 points here but something about the line rendering isn't quite behaving as I'd expect, so need extras
Points.Add(FVector2D(Key1_TimeValue.Key, Key1_TimeValue.Value));
Points.Add(FVector2D(Key2_TimeValue.Key, Key1_TimeValue.Value));
Points.Add(FVector2D(Key2_TimeValue.Key, Key1_TimeValue.Value));
Points.Add(FVector2D(Key2_TimeValue.Key, Key2_TimeValue.Value));
Points.Add(FVector2D(Key2_TimeValue.Key, Key1_TimeValue.Value));
}break;
case RCIM_Linear:
{
Points.Add(FVector2D(Key1_TimeValue.Key, Key1_TimeValue.Value));
Points.Add(FVector2D(Key2_TimeValue.Key, Key2_TimeValue.Value));
}break;
case RCIM_Cubic:
{
const float StepSize = 1.0f;
//clamp to screen to avoid massive slowdown when zoomed in
float StartX = FMath::Max(ScaleInfo.InputToLocalX(Key1_TimeValue.Key), 0.0f) ;
float EndX = FMath::Min(ScaleInfo.InputToLocalX(Key2_TimeValue.Key),ScaleInfo.WidgetSize.X);
for(;StartX<EndX; StartX += StepSize)
{
float CurveIn = ScaleInfo.LocalXToInput(FMath::Min(StartX,EndX));
float CurveOut = Curve->Eval(CurveIn);
Points.Add(FVector2D(CurveIn,CurveOut));
}
Points.Add(FVector2D(Key2_TimeValue.Key,Key2_TimeValue.Value));
}break;
}
//Transform to screen
for(auto It = Points.CreateIterator();It;++It)
{
FVector2D Vec2D = *It;
Vec2D.X = ScaleInfo.InputToLocalX(Vec2D.X);
Vec2D.Y = ScaleInfo.OutputToLocalY(Vec2D.Y);
*It = Vec2D;
}
}
void SCurveEditor::PaintKeys(TSharedPtr<FCurveViewModel> CurveViewModel, FTrackScaleInfo &ScaleInfo, FSlateWindowElementList &OutDrawElements, int32 LayerId, int32 SelectedLayerId, const FGeometry &AllottedGeometry, const FSlateRect& MyCullingRect, ESlateDrawEffect DrawEffects, const FWidgetStyle &InWidgetStyle, bool bAnyCurveViewModelsSelected ) const
{
FLinearColor KeyColor = CurveViewModel->bIsLocked ? FLinearColor(0.1f,0.1f,0.1f,1.f) : InWidgetStyle.GetColorAndOpacityTint();
const bool bHasRichCurves = CurveOwner->HasRichCurves();
// Iterate over each key
ERichCurveInterpMode LastInterpMode = RCIM_Linear;
FRealCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit;
for (auto It(Curve->GetKeyHandleIterator()); It; ++It)
{
FKeyHandle KeyHandle = *It;
// Work out where it is
FVector2D KeyLocation(
ScaleInfo.InputToLocalX(Curve->GetKeyTime(KeyHandle)),
ScaleInfo.OutputToLocalY(Curve->GetKeyValue(KeyHandle)));
FVector2D KeyIconLocation = KeyLocation - (CONST_KeySize / 2);
// Get brush
bool IsSelected = IsKeySelected(FSelectedCurveKey(Curve,KeyHandle));
const FSlateBrush* KeyBrush = IsSelected ? FAppStyle::GetBrush("CurveEd.CurveKeySelected") : FAppStyle::GetBrush("CurveEd.CurveKey");
int32 LayerToUse = IsSelected ? SelectedLayerId: LayerId;
// Fade out keys that are not selected and whose curve is not selected as well.
FLinearColor SelectionTint = !CurveViewModel->bIsSelected && !IsSelected && bAnyCurveViewModelsSelected ? FLinearColor(1.0f,1.0f,1.0f,0.2f) : FLinearColor(1.0f,1.0f,1.0f,1.0f);
FSlateDrawElement::MakeBox(
OutDrawElements,
LayerToUse,
AllottedGeometry.ToPaintGeometry( CONST_KeySize, FSlateLayoutTransform(KeyIconLocation) ),
KeyBrush,
DrawEffects,
KeyBrush->GetTint( InWidgetStyle ) * InWidgetStyle.GetColorAndOpacityTint() * KeyColor * SelectionTint
);
//Handle drawing the tangent controls for curve
bool bIsTangentSelected = false;
bool bIsArrivalSelected = false;
bool bIsLeaveSelected = false;
if (bHasRichCurves)
{
FRichCurve* RichCurve = (FRichCurve*)Curve;
if (IsTangentVisible(RichCurve, KeyHandle, bIsTangentSelected, bIsArrivalSelected, bIsLeaveSelected) && (RichCurve->GetKeyInterpMode(KeyHandle) == RCIM_Cubic || LastInterpMode == RCIM_Cubic))
{
PaintTangent(CurveViewModel, ScaleInfo, RichCurve, KeyHandle, KeyLocation, OutDrawElements, LayerId, AllottedGeometry, MyCullingRect, DrawEffects, LayerToUse, InWidgetStyle, bIsTangentSelected, bIsArrivalSelected, bIsLeaveSelected, bAnyCurveViewModelsSelected);
}
}
LastInterpMode = Curve->GetKeyInterpMode(KeyHandle);
}
}
void SCurveEditor::PaintTangent( TSharedPtr<FCurveViewModel> CurveViewModel, FTrackScaleInfo &ScaleInfo, FRichCurve* Curve, FKeyHandle KeyHandle, FVector2D KeyLocation, FSlateWindowElementList &OutDrawElements, int32 LayerId, const FGeometry &AllottedGeometry, const FSlateRect& MyCullingRect, ESlateDrawEffect DrawEffects, int32 LayerToUse, const FWidgetStyle &InWidgetStyle, bool bTangentSelected, bool bIsArrivalSelected, bool bIsLeaveSelected, bool bAnyCurveViewModelsSelected ) const
{
FVector2D ArriveTangentLocation, LeaveTangentLocation;
GetTangentPoints(ScaleInfo, FSelectedCurveKey(Curve,KeyHandle), ArriveTangentLocation, LeaveTangentLocation);
FVector2D ArriveTangentIconLocation = ArriveTangentLocation - (CONST_TangentSize / 2);
FVector2D LeaveTangentIconLocation = LeaveTangentLocation - (CONST_TangentSize / 2);
const FSlateBrush* TangentBrush = FAppStyle::GetBrush("CurveEd.Tangent");
const FSlateBrush* TangentBrushSelected = FAppStyle::GetBrush("CurveEd.TangentSelected");
const FLinearColor TangentColor = FAppStyle::GetColor("CurveEd.TangentColor");
const FLinearColor TangentColorSelected = FAppStyle::GetColor("CurveEd.TangentColorSelected");
bool LeaveTangentSelected = bTangentSelected && bIsLeaveSelected;
bool ArriveTangentSelected = bTangentSelected && bIsArrivalSelected;
FLinearColor LeaveSelectionTint = !CurveViewModel->bIsSelected && !LeaveTangentSelected && bAnyCurveViewModelsSelected ? FLinearColor(1.0f,1.0f,1.0f,0.2f) : FLinearColor(1.0f,1.0f,1.0f,1.0f);
FLinearColor ArriveSelectionTint = !CurveViewModel->bIsSelected && !ArriveTangentSelected && bAnyCurveViewModelsSelected ? FLinearColor(1.0f,1.0f,1.0f,0.2f) : FLinearColor(1.0f,1.0f,1.0f,1.0f);
//Add lines from tangent control point to 'key'
TArray<FVector2D> LinePoints;
LinePoints.Add(FVector2D(KeyLocation));
LinePoints.Add(FVector2D(ArriveTangentLocation));
FSlateDrawElement::MakeLines(
OutDrawElements,
LayerId,
AllottedGeometry.ToPaintGeometry(),
LinePoints,
DrawEffects,
ArriveTangentSelected ? TangentColorSelected * ArriveSelectionTint : TangentColor * ArriveSelectionTint
);
LinePoints.Empty();
LinePoints.Add(FVector2D(KeyLocation));
LinePoints.Add(FVector2D(LeaveTangentLocation));
FSlateDrawElement::MakeLines(
OutDrawElements,
LayerId,
AllottedGeometry.ToPaintGeometry(),
LinePoints,
DrawEffects,
LeaveTangentSelected ? TangentColorSelected * LeaveSelectionTint : TangentColor * LeaveSelectionTint
);
//Arrive tangent control
FSlateDrawElement::MakeBox(
OutDrawElements,
LayerToUse,
AllottedGeometry.ToPaintGeometry( CONST_TangentSize, FSlateLayoutTransform(ArriveTangentIconLocation) ),
ArriveTangentSelected ? TangentBrushSelected : TangentBrush,
DrawEffects,
ArriveTangentSelected ? TangentBrushSelected->GetTint( InWidgetStyle ) * ArriveSelectionTint : TangentBrush->GetTint( InWidgetStyle ) * InWidgetStyle.GetColorAndOpacityTint() * ArriveSelectionTint
);
//Leave tangent control
FSlateDrawElement::MakeBox(
OutDrawElements,
LayerToUse,
AllottedGeometry.ToPaintGeometry( CONST_TangentSize, FSlateLayoutTransform(LeaveTangentIconLocation) ),
LeaveTangentSelected ? TangentBrushSelected : TangentBrush,
DrawEffects,
LeaveTangentSelected ? TangentBrushSelected->GetTint( InWidgetStyle ) * LeaveSelectionTint : TangentBrush->GetTint( InWidgetStyle ) * InWidgetStyle.GetColorAndOpacityTint() * LeaveSelectionTint
);
}
float SCurveEditor::CalcGridLineStepDistancePow2(double RawValue)
{
return float(double(FMath::RoundUpToPowerOfTwo(uint32(RawValue*1024.0))>>1)/1024.0);
}
float SCurveEditor::GetTimeStep(FTrackScaleInfo &ScaleInfo) const
{
const float MaxGridPixelSpacing = 150.0f;
const float GridPixelSpacing = FMath::Min(ScaleInfo.WidgetSize.GetMin()/1.5f, MaxGridPixelSpacing);
double MaxTimeStep = ScaleInfo.LocalXToInput(ViewMinInput.Get() + GridPixelSpacing) - ScaleInfo.LocalXToInput(ViewMinInput.Get());
return CalcGridLineStepDistancePow2(MaxTimeStep);
}
void SCurveEditor::PaintGridLines(const FGeometry &AllottedGeometry, FTrackScaleInfo &ScaleInfo, FSlateWindowElementList &OutDrawElements,
int32 LayerId, const FSlateRect& MyCullingRect, ESlateDrawEffect DrawEffects )const
{
const float MaxGridPixelSpacing = 150.0f;
const float GridPixelSpacing = FMath::Min(ScaleInfo.WidgetSize.GetMin()/1.5f, MaxGridPixelSpacing);
const FLinearColor GridTextColor = FLinearColor(1.0f,1.0f,1.0f, 0.75f) ;
//Vertical grid(time)
{
float TimeStep = GetTimeStep(ScaleInfo);
float ScreenStepTime = ScaleInfo.InputToLocalX(TimeStep) - ScaleInfo.InputToLocalX(0.0f);
if(ScreenStepTime >= 1.0f)
{
float StartTime = ScaleInfo.LocalXToInput(0.0f);
TArray<FVector2D> LinePoints;
float ScaleX = (TimeStep)/(AllottedGeometry.GetLocalSize().X);
//draw vertical grid lines
float StartOffset = -FMath::Fractional(StartTime / TimeStep)*ScreenStepTime;
float Time = ScaleInfo.LocalXToInput(StartOffset);
for(float X = StartOffset;X< AllottedGeometry.GetLocalSize().X;X+= ScreenStepTime, Time += TimeStep)
{
if(SMALL_NUMBER < FMath::Abs(X)) //don't show at 0 to avoid overlapping with center axis
{
LinePoints.Add(FVector2D(X, 0.0));
LinePoints.Add(FVector2D(X, AllottedGeometry.GetLocalSize().Y));
FSlateDrawElement::MakeLines(
OutDrawElements,
LayerId,
AllottedGeometry.ToPaintGeometry(),
LinePoints,
DrawEffects,
GridColor,
false);
//Show grid time
if (bDrawInputGridNumbers)
{
FString TimeStr = FString::Printf(TEXT("%.2f"), Time);
FSlateDrawElement::MakeText(OutDrawElements,LayerId,AllottedGeometry.MakeChild(FVector2D(1.0f, ScaleX ), FSlateLayoutTransform(FVector2D(X, 0.0))).ToPaintGeometry(),TimeStr,
FAppStyle::GetFontStyle("CurveEd.InfoFont"), DrawEffects, GridTextColor );
}
LinePoints.Empty();
}
}
}
}
//Horizontal grid(values)
// This is only useful if the curves are visible
if( AreCurvesVisible() )
{
double MaxValueStep = ScaleInfo.LocalYToOutput(0) - ScaleInfo.LocalYToOutput(GridPixelSpacing) ;
float ValueStep = CalcGridLineStepDistancePow2(MaxValueStep);
float ScreenStepValue = ScaleInfo.OutputToLocalY(0.0f) - ScaleInfo.OutputToLocalY(ValueStep);
if(ScreenStepValue >= 1.0f)
{
float StartValue = ScaleInfo.LocalYToOutput(0.0f);
TArray<FVector2D> LinePoints;
float StartOffset = FMath::Fractional(StartValue / ValueStep)*ScreenStepValue;
float Value = ScaleInfo.LocalYToOutput(StartOffset);
float ScaleY = (ValueStep)/(AllottedGeometry.GetLocalSize().Y);
for(float Y = StartOffset;Y< AllottedGeometry.GetLocalSize().Y;Y+= ScreenStepValue, Value-=ValueStep)
{
if(SMALL_NUMBER < FMath::Abs(Y)) //don't show at 0 to avoid overlapping with center axis
{
LinePoints.Add(FVector2D(0.0f, Y));
LinePoints.Add(FVector2D(AllottedGeometry.GetLocalSize().X,Y));
FSlateDrawElement::MakeLines(
OutDrawElements,
LayerId,
AllottedGeometry.ToPaintGeometry(),
LinePoints,
DrawEffects,
GridColor,
false);
//Show grid value
if (bDrawOutputGridNumbers)
{
FString ValueStr = FString::Printf(TEXT("%.2f"), Value);
FSlateFontInfo Font = FAppStyle::GetFontStyle("CurveEd.InfoFont");
const TSharedRef< FSlateFontMeasure > FontMeasureService = FSlateApplication::Get().GetRenderer()->GetFontMeasureService();
FVector2D DrawSize = FontMeasureService->Measure(ValueStr, Font);
// draw at the start
FSlateDrawElement::MakeText(OutDrawElements,LayerId,AllottedGeometry.MakeChild(FVector2D(ScaleY, 1.0f ), FSlateLayoutTransform(FVector2D(0.0f, Y))).ToPaintGeometry(),
ValueStr, Font, DrawEffects, GridTextColor );
// draw at the last since sometimes start can be hidden
FSlateDrawElement::MakeText(OutDrawElements,LayerId,AllottedGeometry.MakeChild(FVector2D(ScaleY, 1.0f ), FSlateLayoutTransform(FVector2D(AllottedGeometry.GetLocalSize().X-DrawSize.X, Y))).ToPaintGeometry(),
ValueStr, Font, DrawEffects, GridTextColor );
}
LinePoints.Empty();
}
}
}
}
}
void SCurveEditor::PaintMarquee(const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId) const
{
FVector2D MarqueTopLeft(
FMath::Min(MouseDownLocation.X, MouseMoveLocation.X),
FMath::Min(MouseDownLocation.Y, MouseMoveLocation.Y)
);
FVector2D MarqueBottomRight(
FMath::Max(MouseDownLocation.X, MouseMoveLocation.X),
FMath::Max(MouseDownLocation.Y, MouseMoveLocation.Y)
);
FSlateDrawElement::MakeBox(
OutDrawElements,
LayerId,
AllottedGeometry.ToPaintGeometry(MarqueBottomRight - MarqueTopLeft, FSlateLayoutTransform(MarqueTopLeft)),
FAppStyle::GetBrush(TEXT("MarqueeSelection"))
);
}
float SCurveEditor::GetInputNumericEntryBoxDelta() const
{
return bInputSnappingEnabled.Get() ? InputSnap.Get() : 0;
}
float SCurveEditor::GetOutputNumericEntryBoxDelta() const
{
return bOutputSnappingEnabled.Get() ? OutputSnap.Get() : 0;
}
void SCurveEditor::SetCurveOwner(FCurveOwnerInterface* InCurveOwner, bool bCanEdit)
{
if(InCurveOwner != CurveOwner)
{
EmptyAllSelection();
}
GradientViewer->SetCurveOwner(InCurveOwner);
CurveOwner = InCurveOwner;
bCanEditTrack = bCanEdit;
if (bAreCurvesVisible.IsBound() == false || SetAreCurvesVisibleHandler.IsBound() == false)
{
bAreCurvesVisible = !IsLinearColorCurve();
}
bIsGradientEditorVisible = IsLinearColorCurve();
CurveViewModels.Empty();
if(CurveOwner != NULL)
{
for (const FRichCurveEditInfo& CurveInfo : CurveOwner->GetCurves())
{
CurveViewModels.Add(TSharedPtr<FCurveViewModel>(new FCurveViewModel(CurveInfo, CurveOwner->GetCurveColor(CurveInfo), !bCanEdit)));
}
if (bCanEdit)
{
CurveOwner->MakeTransactional();
}
}
ValidateSelection();
if (GetAutoFrame())
{
if( bZoomToFitVertical )
{
ZoomToFitVertical();
}
if ( bZoomToFitHorizontal )
{
ZoomToFitHorizontal();
}
}
CurveSelectionWidget.Pin()->SetContent(CreateCurveSelectionWidget());
}
void SCurveEditor::RegisterToPropertyChangedEvent(const TSharedPtr<IPropertyHandle>& InRootPropertyHandle)
{
RootPropertyHandleWeak = InRootPropertyHandle.ToWeakPtr();
const auto OnChildPropertyChanged = TDelegate<void(const FPropertyChangedEvent&)>::CreateSP(this, &SCurveEditor::OnRootPropertyChanged);
if (const TSharedPtr<IPropertyHandle> RootPropertyHandle = RootPropertyHandleWeak.Pin())
{
RootPropertyHandle->SetOnChildPropertyValueChangedWithData(OnChildPropertyChanged);
}
}
void SCurveEditor::SetZoomToFit(bool bNewZoomToFitVertical, bool bNewZoomToFitHorizontal)
{
bZoomToFitVertical = bNewZoomToFitVertical;
bZoomToFitHorizontal = bNewZoomToFitHorizontal;
}
void SCurveEditor::SetRequireFocusToZoom(bool bInRequireFocusToZoom)
{
bRequireFocusToZoom = bInRequireFocusToZoom;
}
TOptional<bool> SCurveEditor::OnQueryShowFocus(const EFocusCause InFocusCause) const
{
if (bRequireFocusToZoom)
{
// Enable showing a focus rectangle when the widget has keyboard focus
return TOptional<bool>(true);
}
return TOptional<bool>();
}
FCurveOwnerInterface* SCurveEditor::GetCurveOwner() const
{
return CurveOwner;
}
FRealCurve* SCurveEditor::GetCurve(int32 CurveIndex) const
{
if(CurveIndex < CurveViewModels.Num())
{
return CurveViewModels[CurveIndex]->CurveInfo.CurveToEdit;
}
return NULL;
}
void SCurveEditor::DeleteSelectedKeys()
{
const FScopedTransaction Transaction(LOCTEXT("CurveEditor_RemoveKeys", "Delete Key(s)"));
CurveOwner->ModifyOwner();
TSet<FRealCurve*> ChangedCurves;
// While there are still keys
while(SelectedKeys.Num() > 0)
{
// Pull one out of the selected set
FSelectedCurveKey Key = SelectedKeys.Pop();
if(IsValidCurve(Key.Curve))
{
// Remove from the curve
Key.Curve->DeleteKey(Key.KeyHandle);
ChangedCurves.Add(Key.Curve);
}
}
TArray<FRichCurveEditInfo> ChangedCurveEditInfos;
for (auto CurveViewModel : CurveViewModels)
{
if (ChangedCurves.Contains(CurveViewModel->CurveInfo.CurveToEdit))
{
ChangedCurveEditInfos.Add(CurveViewModel->CurveInfo);
}
}
CurveOwner->OnCurveChanged(ChangedCurveEditInfos);
}
FReply SCurveEditor::OnMouseButtonDown( const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent)
{
// End any transactions that weren't ended cleanly
EndDragTransaction();
const bool bLeftMouseButton = InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton;
const bool bMiddleMouseButton = InMouseEvent.GetEffectingButton() == EKeys::MiddleMouseButton;
const bool bRightMouseButton = InMouseEvent.GetEffectingButton() == EKeys::RightMouseButton;
DragState = EDragState::PreDrag;
MovementAxisLock = EMovementAxisLock::None;
if (bLeftMouseButton || bMiddleMouseButton || bRightMouseButton)
{
MouseDownLocation = InMyGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition());
// Set keyboard focus to this so that selected text box doesn't try to apply to newly selected keys
if(!HasKeyboardFocus())
{
FSlateApplication::Get().SetKeyboardFocus(SharedThis(this), EFocusCause::SetDirectly);
}
// Always capture mouse if we left or right click on the widget
return FReply::Handled().CaptureMouse(SharedThis(this));
}
return FReply::Unhandled();
}
void SCurveEditor::AddNewKey(FGeometry InMyGeometry, FVector2D ScreenPosition, TSharedPtr<TArray<TSharedPtr<FCurveViewModel>>> CurvesToAddKeysTo, bool bAddKeysInline)
{
const FScopedTransaction Transaction(LOCTEXT("CurveEditor_AddKey", "Add Key(s)"));
CurveOwner->ModifyOwner();
TArray<FRichCurveEditInfo> ChangedCurveEditInfos;
for (TSharedPtr<FCurveViewModel> CurveViewModel : *CurvesToAddKeysTo)
{
if (!CurveViewModel->bIsLocked)
{
FRealCurve* SelectedCurve = CurveViewModel->CurveInfo.CurveToEdit;
if (IsValidCurve(SelectedCurve))
{
FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), InMyGeometry.GetLocalSize());
FVector2D LocalClickPos = InMyGeometry.AbsoluteToLocal(ScreenPosition);
float Input = ScaleInfo.LocalXToInput(LocalClickPos.X);
float Output;
if (bAddKeysInline)
{
Output = SelectedCurve->Eval(Input);
}
else
{
Output = ScaleInfo.LocalYToOutput(LocalClickPos.Y);
}
FVector2D NewKeyLocation = SnapLocation(FVector2D(Input, Output));
FKeyHandle NewKeyHandle = SelectedCurve->AddKey(NewKeyLocation.X, NewKeyLocation.Y);
EmptyAllSelection();
AddToKeySelection(FSelectedCurveKey(SelectedCurve, NewKeyHandle));
ChangedCurveEditInfos.Add(CurveViewModel->CurveInfo);
}
}
}
if (ChangedCurveEditInfos.Num() > 0)
{
CurveOwner->OnCurveChanged(ChangedCurveEditInfos);
}
}
void SCurveEditor::OnMouseCaptureLost(const FCaptureLostEvent& CaptureLostEvent)
{
// if we began a drag transaction we need to finish it to make sure undo doesn't get out of sync
if (DragState == EDragState::DragKey || DragState == EDragState::FreeDrag || DragState == EDragState::DragTangent)
{
EndDragTransaction();
}
DragState = EDragState::None;
}
FReply SCurveEditor::OnMouseButtonUp( const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent )
{
if (this->HasMouseCapture())
{
if (DragState == EDragState::PreDrag)
{
// If the user didn't start dragging, handle the mouse operation as a click.
ProcessClick(InMyGeometry, InMouseEvent);
}
else
{
EndDrag(InMyGeometry, InMouseEvent);
}
return FReply::Handled().ReleaseMouseCapture();
}
return FReply::Unhandled();
}
void ClampViewRangeToDataIfBound( float& NewViewMin, float& NewViewMax, const TAttribute< TOptional<float> > & DataMin, const TAttribute< TOptional<float> > & DataMax, const float ViewRange)
{
// if we have data bound
const TOptional<float> & Min = DataMin.Get();
const TOptional<float> & Max = DataMax.Get();
if ( Min.IsSet() && NewViewMin < Min.GetValue())
{
// if we have min data set
NewViewMin = Min.GetValue();
NewViewMax = ViewRange;
}
else if ( Max.IsSet() && NewViewMax > Max.GetValue() )
{
// if we have min data set
NewViewMin = Max.GetValue() - ViewRange;
NewViewMax = Max.GetValue();
}
}
FReply SCurveEditor::OnMouseMove( const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent )
{
UpdateCurveToolTip(InMyGeometry, InMouseEvent);
FRealCurve* Curve = GetCurve(0);
if( Curve != NULL && this->HasMouseCapture())
{
if (DragState == EDragState::PreDrag)
{
TryStartDrag(InMyGeometry, InMouseEvent);
}
if (DragState != EDragState::None)
{
ProcessDrag(InMyGeometry, InMouseEvent);
}
MouseMoveLocation = InMyGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition());
return FReply::Handled();
}
return FReply::Unhandled();
}
void SCurveEditor::UpdateCurveToolTip(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent)
{
if (Settings->GetShowCurveEditorCurveToolTips())
{
TSharedPtr<FCurveViewModel> HoveredCurve = HitTestCurves(InMyGeometry, InMouseEvent);
//Display the tooltip only when the curve is visible
if (HoveredCurve.IsValid() && HoveredCurve->bIsVisible)
{
FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), InMyGeometry.GetLocalSize());
const FVector2D HitPosition = InMyGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition());
float Time = ScaleInfo.LocalXToInput(HitPosition.X);
float Value = HoveredCurve->CurveInfo.CurveToEdit->Eval(Time);
FNumberFormattingOptions FormattingOptions;
FormattingOptions.MaximumFractionalDigits = 2;
CurveToolTipNameText = FText::FromName(HoveredCurve->CurveInfo.CurveName);
CurveToolTipOutputText = FText::Format(LOCTEXT("CurveToolTipValueFormat", "{0}: {1}"), OutputAxisName, FText::AsNumber(Value, &FormattingOptions));
if (ShowTimeInFrames())
{
CurveToolTipInputText = FText::Format(LOCTEXT("CurveToolTipFrameFormat", "{0}: {1}"), GetInputAxisName(), FText::AsNumber(TimeToFrame(Time)));
}
else
{
CurveToolTipInputText = FText::Format(LOCTEXT("CurveToolTipTimeFormat", "{0}: {1}"), GetInputAxisName(), FText::AsNumber(Time, &FormattingOptions));
}
if (CurveToolTip.IsValid() == false)
{
SetToolTip(
SAssignNew(CurveToolTip, SToolTip)
.BorderImage( FCoreStyle::Get().GetBrush( "ToolTip.BrightBackground" ) )
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
[
SNew(STextBlock)
.Text(this, &SCurveEditor::GetCurveToolTipNameText)
.Font(FCoreStyle::Get().GetFontStyle("ToolTip.LargerFont"))
.ColorAndOpacity( FLinearColor::Black)
]
+ SVerticalBox::Slot()
[
SNew(STextBlock)
.Text(this, &SCurveEditor::GetCurveToolTipInputText)
.Font(FCoreStyle::Get().GetFontStyle("ToolTip.LargerFont"))
.ColorAndOpacity(FLinearColor::Black)
]
+ SVerticalBox::Slot()
[
SNew(STextBlock)
.Text(this, &SCurveEditor::GetCurveToolTipOutputText)
.Font(FCoreStyle::Get().GetFontStyle("ToolTip.LargerFont"))
.ColorAndOpacity(FLinearColor::Black)
]
]);
}
}
else
{
CurveToolTip.Reset();
SetToolTip(CurveToolTip);
}
}
}
FReply SCurveEditor::OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
if (!bRequireFocusToZoom || HasKeyboardFocus())
{
ZoomView(FVector2D(MouseEvent.GetWheelDelta(), MouseEvent.GetWheelDelta()));
return FReply::Handled();
}
return FReply::Unhandled();
}
void SCurveEditor::ZoomView(FVector2D Delta)
{
const FVector2D ZoomDelta = -0.1f * Delta;
if (bAllowZoomOutput)
{
const float OutputViewSize = ViewMaxOutput.Get() - ViewMinOutput.Get();
const float OutputChange = OutputViewSize * ZoomDelta.Y;
const float NewMinOutput = (ViewMinOutput.Get() - (OutputChange * 0.5f));
const float NewMaxOutput = (ViewMaxOutput.Get() + (OutputChange * 0.5f));
SetOutputMinMax(NewMinOutput, NewMaxOutput);
}
{
const float InputViewSize = ViewMaxInput.Get() - ViewMinInput.Get();
const float InputChange = InputViewSize * ZoomDelta.X;
const float NewMinInput = ViewMinInput.Get() - (InputChange * 0.5f);
const float NewMaxInput = ViewMaxInput.Get() + (InputChange * 0.5f);
SetInputMinMax(NewMinInput, NewMaxInput);
}
}
FReply SCurveEditor::OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent )
{
if (InKeyEvent.GetKey() == EKeys::Platform_Delete && SelectedKeys.Num() != 0)
{
DeleteSelectedKeys();
return FReply::Handled();
}
else
{
if( Commands->ProcessCommandBindings( InKeyEvent ) )
{
return FReply::Handled();
}
return FReply::Unhandled();
}
}
void SCurveEditor::TryStartDrag(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent)
{
const bool bLeftMouseButton = InMouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton);
const bool bMiddleMouseButton = InMouseEvent.IsMouseButtonDown(EKeys::MiddleMouseButton);
const bool bRightMouseButton = InMouseEvent.IsMouseButtonDown(EKeys::RightMouseButton);
const bool bControlDown = InMouseEvent.IsControlDown();
const bool bShiftDown = InMouseEvent.IsShiftDown();
const bool bAltDown = InMouseEvent.IsAltDown();
FVector2D MousePosition = InMyGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition());
FVector2D DragVector = MousePosition - MouseDownLocation;
if (DragVector.SizeSquared() >= FMath::Square(DragThreshold))
{
if (bShiftDown)
{
if(FMath::Abs(MousePosition.X - MouseDownLocation.X) > FMath::Abs(MousePosition.Y - MouseDownLocation.Y))
{
MovementAxisLock = EMovementAxisLock::AxisLock_Horizontal;
}
else
{
MovementAxisLock = EMovementAxisLock::AxisLock_Vertical;
}
}
if (bLeftMouseButton)
{
// Check if we should start dragging keys.
FSelectedCurveKey HitKey = HitTestKeys(InMyGeometry, InMyGeometry.LocalToAbsolute(MouseDownLocation));
if (HitKey.IsValid())
{
EmptyTangentSelection();
if (!IsKeySelected(HitKey))
{
if (!bControlDown)
{
EmptyKeySelection();
}
AddToKeySelection(HitKey);
}
BeginDragTransaction();
DragState = EDragState::DragKey;
DraggedKeyHandle = HitKey.KeyHandle;
PreDragKeyLocations.Empty();
for (auto SelectedKey : SelectedKeys)
{
PreDragKeyLocations.Add(SelectedKey.KeyHandle, FVector2D
(
SelectedKey.Curve->GetKeyTime(SelectedKey.KeyHandle),
SelectedKey.Curve->GetKeyValue(SelectedKey.KeyHandle)
));
}
}
else
{
// Check if we should start dragging a tangent.
FSelectedTangent Tangent = HitTestCubicTangents(InMyGeometry, InMyGeometry.LocalToAbsolute(MouseDownLocation));
if (Tangent.IsValid())
{
EmptyKeySelection();
if (!IsTangentSelected(Tangent))
{
if (!bControlDown)
{
EmptyTangentSelection();
}
AddToTangentSelection(Tangent);
}
BeginDragTransaction();
DragState = EDragState::DragTangent;
PreDragTangents.Empty();
for (auto SelectedTangent : SelectedTangents)
{
FRichCurve* Curve = (FRichCurve*)SelectedTangent.Key.Curve;
FKeyHandle KeyHandle = SelectedTangent.Key.KeyHandle;
float ArriveTangent = Curve->GetKey(KeyHandle).ArriveTangent;
float LeaveTangent = Curve->GetKey(KeyHandle).LeaveTangent;
PreDragTangents.Add(KeyHandle, FVector2D(ArriveTangent, LeaveTangent));
}
}
else
{
// Otherwise if the user left clicked on nothing and start a marquee select.
DragState = EDragState::MarqueeSelect;
}
}
}
else if (bMiddleMouseButton)
{
if (bAltDown)
{
DragState = EDragState::Pan;
}
else if (SelectedTangents.Num())
{
BeginDragTransaction();
DragState = EDragState::DragTangent;
PreDragTangents.Empty();
for (auto SelectedTangent : SelectedTangents)
{
FRichCurve* Curve = (FRichCurve*)SelectedTangent.Key.Curve;
FKeyHandle KeyHandle = SelectedTangent.Key.KeyHandle;
float ArriveTangent = Curve->GetKey(KeyHandle).ArriveTangent;
float LeaveTangent = Curve->GetKey(KeyHandle).LeaveTangent;
PreDragTangents.Add(KeyHandle, FVector2D(ArriveTangent, LeaveTangent));
}
}
else if (SelectedKeys.Num())
{
BeginDragTransaction();
DragState = EDragState::FreeDrag;
PreDragKeyLocations.Empty();
for (auto selectedKey : SelectedKeys)
{
PreDragKeyLocations.Add(selectedKey.KeyHandle, FVector2D
(
selectedKey.Curve->GetKeyTime(selectedKey.KeyHandle),
selectedKey.Curve->GetKeyValue(selectedKey.KeyHandle)
));
}
}
}
else if (bRightMouseButton)
{
if (bAltDown)
{
DragState = EDragState::Zoom;
}
else
{
DragState = EDragState::Pan;
}
}
else
{
DragState = EDragState::None;
}
}
}
/* Given a tangent value for a key, calculates the 2D delta vector from that key in curve space */
static inline FVector2D CalcTangentDir(float Tangent)
{
const float Angle = FMath::Atan(Tangent);
return FVector2D( FMath::Cos(Angle), -FMath::Sin(Angle) );
}
/*Given a 2d delta vector in curve space, calculates a tangent value */
static inline float CalcTangent(const FVector2D& HandleDelta)
{
// Ensure X is positive and non-zero.
// Tangent is gradient of handle.
return HandleDelta.Y / FMath::Max<double>(HandleDelta.X, KINDA_SMALL_NUMBER);
}
void SCurveEditor::ProcessDrag(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent)
{
FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), InMyGeometry.GetLocalSize());
FVector2D ScreenDelta = InMouseEvent.GetCursorDelta();
FVector2D InputDelta;
InputDelta.X = ScreenDelta.X / ScaleInfo.PixelsPerInput;
InputDelta.Y = -ScreenDelta.Y / ScaleInfo.PixelsPerOutput;
if (DragState == EDragState::DragKey)
{
FVector2D MousePosition = InMyGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition());
FVector2D NewLocation = FVector2D(ScaleInfo.LocalXToInput(MousePosition.X), ScaleInfo.LocalYToOutput(MousePosition.Y));
FVector2D SnappedNewLocation = SnapLocation(NewLocation);
FVector2D Delta = SnappedNewLocation - PreDragKeyLocations[DraggedKeyHandle];
MoveSelectedKeys(Delta);
}
else if (DragState == EDragState::FreeDrag)
{
FVector2D MousePosition = InMyGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition());
FVector2D NewLocation = FVector2D(ScaleInfo.LocalXToInput(MousePosition.X), ScaleInfo.LocalYToOutput(MousePosition.Y));
FVector2D Delta = NewLocation - FVector2D(ScaleInfo.LocalXToInput(MouseDownLocation.X), ScaleInfo.LocalYToOutput(MouseDownLocation.Y));
MoveSelectedKeys(Delta);
}
else if (DragState == EDragState::DragTangent)
{
FVector2D MousePositionScreen = InMyGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition());
FVector2D MouseDownPositionScreen = MouseDownLocation;
MoveTangents(ScaleInfo, MousePositionScreen - MouseDownPositionScreen);
}
else if (DragState == EDragState::Pan)
{
if (MovementAxisLock == EMovementAxisLock::AxisLock_Horizontal)
{
InputDelta.Y = 0;
}
else if (MovementAxisLock == EMovementAxisLock::AxisLock_Vertical)
{
InputDelta.X = 0;
}
// Output is not clamped.
const float NewMinOutput = (ViewMinOutput.Get() - InputDelta.Y);
const float NewMaxOutput = (ViewMaxOutput.Get() - InputDelta.Y);
SetOutputMinMax(NewMinOutput, NewMaxOutput);
// Input maybe clamped if DataMinInput or DataMaxOutput was set.
float NewMinInput = ViewMinInput.Get() - InputDelta.X;
float NewMaxInput = ViewMaxInput.Get() - InputDelta.X;
ClampViewRangeToDataIfBound(NewMinInput, NewMaxInput, DataMinInput, DataMaxInput, ScaleInfo.ViewInputRange);
SetInputMinMax(NewMinInput, NewMaxInput);
}
else if (DragState == EDragState::Zoom)
{
FVector2D Delta = FVector2D(ScreenDelta.X * 0.05f, ScreenDelta.X * 0.05f);
if (MovementAxisLock == EMovementAxisLock::AxisLock_Horizontal)
{
Delta.Y = 0;
}
else if (MovementAxisLock == EMovementAxisLock::AxisLock_Vertical)
{
Delta.X = 0;
Delta.Y = -ScreenDelta.Y * 0.1f;
}
ZoomView(Delta);
}
}
void SCurveEditor::EndDrag(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent)
{
const bool bControlDown = InMouseEvent.IsControlDown();
const bool bShiftDown = InMouseEvent.IsShiftDown();
if (DragState == EDragState::DragKey || DragState == EDragState::FreeDrag || DragState == EDragState::DragTangent)
{
EndDragTransaction();
}
else if (DragState == EDragState::MarqueeSelect)
{
FVector2D MarqueTopLeft
(
FMath::Min(MouseDownLocation.X, MouseMoveLocation.X),
FMath::Min(MouseDownLocation.Y, MouseMoveLocation.Y)
);
FVector2D MarqueBottomRight
(
FMath::Max(MouseDownLocation.X, MouseMoveLocation.X),
FMath::Max(MouseDownLocation.Y, MouseMoveLocation.Y)
);
TArray<FSelectedTangent> SelectedCurveTangents = GetEditableTangentsWithinMarquee(InMyGeometry, MarqueTopLeft, MarqueBottomRight);
TArray<FSelectedCurveKey> SelectedCurveKeys = GetEditableKeysWithinMarquee(InMyGeometry, MarqueTopLeft, MarqueBottomRight);
if (!bControlDown && !bShiftDown)
{
EmptyAllSelection();
}
if (SelectedCurveKeys.Num())
{
EmptyTangentSelection();
for (auto SelectedCurveKey : SelectedCurveKeys)
{
if (IsKeySelected(SelectedCurveKey))
{
RemoveFromKeySelection(SelectedCurveKey);
}
else
{
AddToKeySelection(SelectedCurveKey);
}
}
}
if (!SelectedCurveKeys.Num())
{
EmptyKeySelection();
for (auto SelectedCurveTangent : SelectedCurveTangents)
{
if (IsTangentSelected(SelectedCurveTangent))
{
RemoveFromTangentSelection(SelectedCurveTangent);
}
else
{
AddToTangentSelection(SelectedCurveTangent);
}
}
}
}
DragState = EDragState::None;
MovementAxisLock = EMovementAxisLock::None;
}
void SCurveEditor::ProcessClick(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent)
{
const bool bLeftMouseButton = InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton;
const bool bRightMouseButton = InMouseEvent.GetEffectingButton() == EKeys::RightMouseButton;
const bool bControlDown = InMouseEvent.IsControlDown();
const bool bShiftDown = InMouseEvent.IsShiftDown();
FSelectedCurveKey HitKey = HitTestKeys(InMyGeometry, InMouseEvent.GetScreenSpacePosition());
FSelectedTangent HitTangent = HitTestCubicTangents(InMyGeometry, InMouseEvent.GetScreenSpacePosition());
if (bLeftMouseButton)
{
// If the user left clicked a key, update selection based on modifier key state.
if (HitKey.IsValid())
{
EmptyTangentSelection();
if (!IsKeySelected(HitKey))
{
if (!bControlDown && !bShiftDown)
{
EmptyAllSelection();
}
AddToKeySelection(HitKey);
}
else if (bControlDown)
{
RemoveFromKeySelection(HitKey);
}
}
else if (HitTangent.IsValid())
{
EmptyKeySelection();
if (!IsTangentSelected(HitTangent))
{
if (!bControlDown && !bShiftDown)
{
EmptyAllSelection();
}
AddToTangentSelection(HitTangent);
}
else if (bControlDown)
{
RemoveFromTangentSelection(HitTangent);
}
}
else
{
// If the user didn't click a key, add a new one if shift is held down, or try to select a curve.
if (bShiftDown && IsEditingEnabled())
{
TSharedPtr<TArray<TSharedPtr<FCurveViewModel>>> CurvesToAddKeysTo = MakeShareable(new TArray<TSharedPtr<FCurveViewModel>>());
TSharedPtr<FCurveViewModel> HoveredCurve = HitTestCurves(InMyGeometry, InMouseEvent);
bool bAddKeysInline;
//To snap a point on the hovered curve the curve must be visible and unlock
if (HoveredCurve.IsValid() && !HoveredCurve->bIsLocked && HoveredCurve->bIsVisible)
{
CurvesToAddKeysTo->Add(HoveredCurve);
bAddKeysInline = true;
}
else
{
//Add all unlock curves in the editable array
for (auto CurveViewModel : CurveViewModels)
{
if (!CurveViewModel->bIsLocked)
{
CurvesToAddKeysTo->Add(CurveViewModel);
}
}
//If linear color curve and no show curve, always insert inline
//If the user is holding shift-ctrl we snap all curve to the mouse position. (false value)
//If the user is holding shift we snap to mouse only if there is only one editable curve. (false value)
//In all other case we add key directly on the curve. (true value)
bAddKeysInline = (IsLinearColorCurve() && !bAreCurvesVisible.Get()) || ((!bControlDown) && (CurvesToAddKeysTo->Num() != 1));
}
AddNewKey(InMyGeometry, InMouseEvent.GetScreenSpacePosition(), CurvesToAddKeysTo, bAddKeysInline);
}
else
{
// clicking on background clears all selection
EmptyAllSelection();
}
}
}
else if (bRightMouseButton)
{
// If the user right clicked, handle opening context menus.
if (HitKey.IsValid())
{
// Make sure key is selected in readiness for context menu
EmptyTangentSelection();
if (!IsKeySelected(HitKey))
{
EmptyAllSelection();
AddToKeySelection(HitKey);
}
PushKeyMenu(InMyGeometry, InMouseEvent);
}
else if (HitTangent.IsValid())
{
// Make sure key is selected in readiness for context menu
EmptyKeySelection();
if (!IsTangentSelected(HitTangent))
{
EmptyAllSelection();
AddToTangentSelection(HitTangent);
}
PushKeyMenu(InMyGeometry, InMouseEvent);
}
else
{
CreateContextMenu(InMyGeometry, InMouseEvent);
}
}
}
TOptional<float> SCurveEditor::OnGetTime() const
{
TOptional<float> Time;
// Return the time if all selected keys have the same time, otherwise return an unset value
if (SelectedKeys.Num() > 0)
{
Time = GetKeyTime(SelectedKeys[0]);
for (int32 i = 1; i < SelectedKeys.Num(); i++)
{
TOptional<float> NewTime = GetKeyTime(SelectedKeys[i]);
bool bAreEqual = ((!Time.IsSet() && !NewTime.IsSet()) || (Time.IsSet() && NewTime.IsSet() && Time.GetValue() == NewTime.GetValue()));
if (!bAreEqual)
{
return TOptional<float>();
}
}
}
return Time;
}
void SCurveEditor::OnTimeComitted(float NewTime, ETextCommit::Type CommitType)
{
// Don't digest the number if we just clicked away from the pop-up
if ( !bIsUsingSlider && ((CommitType == ETextCommit::OnEnter) || ( CommitType == ETextCommit::OnUserMovedFocus )) )
{
for(FSelectedCurveKey Key : SelectedKeys)
{
UpdateCurveTimeSingleKey(Key, NewTime);
}
FSlateApplication::Get().DismissAllMenus();
}
}
void SCurveEditor::OnTimeChanged(float NewTime)
{
if ( bIsUsingSlider )
{
for (FSelectedCurveKey Key : SelectedKeys)
{
UpdateCurveTimeSingleKey(Key, NewTime);
}
}
}
TOptional<int32> SCurveEditor::OnGetTimeInFrames() const
{
TOptional<float> Time;
// Return the time in frames if all selected keys have the same time, otherwise return an unset value
if (SelectedKeys.Num() > 0)
{
Time = GetKeyTime(SelectedKeys[0]);
for (int32 i = 1; i < SelectedKeys.Num(); i++)
{
TOptional<float> NewTime = GetKeyTime(SelectedKeys[i]);
bool bAreEqual = ((!Time.IsSet() && !NewTime.IsSet()) || (Time.IsSet() && NewTime.IsSet() && Time.GetValue() == NewTime.GetValue()));
if (!bAreEqual)
{
return TOptional<int32>();
}
}
}
if (Time.IsSet())
{
return TOptional<int32>(TimeToFrame(Time.GetValue()));
}
return TOptional<int32>();
}
void SCurveEditor::OnTimeInFramesComitted(int32 NewFrame, ETextCommit::Type CommitType)
{
// Don't digest the number if we just clicked away from the pop-up
if ( !bIsUsingSlider && ((CommitType == ETextCommit::OnEnter) || ( CommitType == ETextCommit::OnUserMovedFocus )) )
{
for (FSelectedCurveKey Key : SelectedKeys)
{
UpdateCurveTimeSingleKey(Key, NewFrame);
}
FSlateApplication::Get().DismissAllMenus();
}
}
void SCurveEditor::OnTimeInFramesChanged(int32 NewFrame)
{
if ( bIsUsingSlider )
{
for (FSelectedCurveKey Key : SelectedKeys)
{
UpdateCurveTimeSingleKey(Key, NewFrame);
}
}
}
void SCurveEditor::UpdateCurveTimeSingleKey(FSelectedCurveKey Key, float NewTime, bool bSetFromFrame)
{
// If the curve is valid and doesn't already have a key at that time
if (IsValidCurve(Key.Curve))
{
if (!Key.Curve->KeyExistsAtTime(NewTime))
{
FText TransactionText = bSetFromFrame ? LOCTEXT("CurveEditor_NewFrame", "New Frame Entered") : LOCTEXT("CurveEditor_NewTime", "New Time Entered");
const FScopedTransaction Transaction(TransactionText);
CurveOwner->ModifyOwner();
Key.Curve->SetKeyTime(Key.KeyHandle, NewTime);
TArray<FRichCurveEditInfo> ChangedCurveEditInfos;
ChangedCurveEditInfos.Add(GetViewModelForCurve(Key.Curve)->CurveInfo);
CurveOwner->OnCurveChanged(ChangedCurveEditInfos);
}
// if the existing key is not this key
else if (Key.Curve->FindKey(NewTime) != Key.KeyHandle)
{
LogAndToastCurveTimeWarning(Key.Curve);
}
}
}
void SCurveEditor::UpdateCurveTimeSingleKey(FSelectedCurveKey Key, int32 NewFrame)
{
UpdateCurveTimeSingleKey(Key, FrameToTime(NewFrame), true);
}
void SCurveEditor::LogAndToastCurveTimeWarning(FRealCurve* Curve)
{
FText Error = FText::Format(LOCTEXT("KeyTimeCollision","A key on {0} could not be moved because there was already a key at that time."), FText::FromName(GetViewModelForCurve(Curve)->CurveInfo.CurveName));
FNotificationInfo Info(Error);
Info.ExpireDuration = 5.0f;
FSlateNotificationManager::Get().AddNotification(Info);
UE_LOG(LogCurveEditor, Warning, TEXT("%s"), *Error.ToString());
}
TOptional<float> SCurveEditor::OnGetValue() const
{
TOptional<float> Value;
// Return the value string if all selected keys have the same output string, otherwise empty
if ( SelectedKeys.Num() > 0 )
{
Value = GetKeyValue(SelectedKeys[0]);
for ( int32 i=1; i < SelectedKeys.Num(); i++ )
{
TOptional<float> NewValue = GetKeyValue(SelectedKeys[i]);
bool bAreEqual = ( ( !Value.IsSet() && !NewValue.IsSet() ) || ( Value.IsSet() && NewValue.IsSet() && Value.GetValue() == NewValue.GetValue() ) );
if ( !bAreEqual )
{
return TOptional<float>();
}
}
}
return Value;
}
void SCurveEditor::OnValueComitted(float NewValue, ETextCommit::Type CommitType)
{
// Don't digest the number if we just clicked away from the popup
if ( !bIsUsingSlider && ((CommitType == ETextCommit::OnEnter) || ( CommitType == ETextCommit::OnUserMovedFocus )) )
{
const FScopedTransaction Transaction( LOCTEXT( "CurveEditor_NewValue", "New Value Entered" ) );
CurveOwner->ModifyOwner();
TSet<FRealCurve*> ChangedCurves;
// Iterate over selected set
for ( int32 i=0; i < SelectedKeys.Num(); i++ )
{
auto Key = SelectedKeys[i];
if ( IsValidCurve(Key.Curve) )
{
// Fill in each element of this key
Key.Curve->SetKeyValue(Key.KeyHandle, NewValue);
ChangedCurves.Add(Key.Curve);
}
}
TArray<FRichCurveEditInfo> ChangedCurveEditInfos;
for (auto CurveViewModel : CurveViewModels)
{
if (ChangedCurves.Contains(CurveViewModel->CurveInfo.CurveToEdit))
{
ChangedCurveEditInfos.Add(CurveViewModel->CurveInfo);
}
}
CurveOwner->OnCurveChanged(ChangedCurveEditInfos);
FSlateApplication::Get().DismissAllMenus();
}
}
void SCurveEditor::OnValueChanged(float NewValue)
{
if ( bIsUsingSlider )
{
const FScopedTransaction Transaction(LOCTEXT("CurveEditor_NewValue", "New Value Entered"));
TSet<FRealCurve*> ChangedCurves;
// Iterate over selected set
for ( int32 i=0; i < SelectedKeys.Num(); i++ )
{
auto Key = SelectedKeys[i];
if ( IsValidCurve(Key.Curve) )
{
CurveOwner->ModifyOwner();
// Fill in each element of this key
Key.Curve->SetKeyValue(Key.KeyHandle, NewValue);
ChangedCurves.Add(Key.Curve);
}
}
TArray<FRichCurveEditInfo> ChangedCurveEditInfos;
for (auto CurveViewModel : CurveViewModels)
{
if (ChangedCurves.Contains(CurveViewModel->CurveInfo.CurveToEdit))
{
ChangedCurveEditInfos.Add(CurveViewModel->CurveInfo);
}
}
CurveOwner->OnCurveChanged(ChangedCurveEditInfos);
}
}
void SCurveEditor::OnBeginSliderMovement(FText TransactionName)
{
bIsUsingSlider = true;
GEditor->BeginTransaction(TransactionName);
}
void SCurveEditor::OnEndSliderMovement(float NewValue)
{
bIsUsingSlider = false;
GEditor->EndTransaction();
}
void SCurveEditor::OnEndSliderMovement(int32 NewValue)
{
bIsUsingSlider = false;
GEditor->EndTransaction();
}
SCurveEditor::FSelectedCurveKey SCurveEditor::HitTestKeys(const FGeometry& InMyGeometry, const FVector2D& HitScreenPosition)
{
FSelectedCurveKey SelectedKey(NULL,FKeyHandle());
if( AreCurvesVisible() )
{
bool bAnyCurveViewModelsSelected = AnyCurveViewModelsSelected();
FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), InMyGeometry.GetLocalSize());
const FVector2D HitPosition = InMyGeometry.AbsoluteToLocal( HitScreenPosition );
for(auto CurveViewModel : CurveViewModels)
{
if (IsCurveSelectable(CurveViewModel))
{
FRealCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit;
if(Curve != NULL)
{
for (auto It(Curve->GetKeyHandleIterator()); It; ++It)
{
float KeyScreenX = ScaleInfo.InputToLocalX(Curve->GetKeyTime(*It));
float KeyScreenY = ScaleInfo.OutputToLocalY(Curve->GetKeyValue(*It));
if( HitPosition.X > (KeyScreenX - (0.5f * CONST_KeySize.X)) &&
HitPosition.X < (KeyScreenX + (0.5f * CONST_KeySize.X)) &&
HitPosition.Y > (KeyScreenY - (0.5f * CONST_KeySize.Y)) &&
HitPosition.Y < (KeyScreenY + (0.5f * CONST_KeySize.Y)) )
{
return FSelectedCurveKey(Curve, *It);
}
}
}
}
}
}
return SelectedKey;
}
void SCurveEditor::MoveSelectedKeys(FVector2D Delta)
{
TArray<FRichCurveEditInfo> ChangedCurveEditInfos;
const FScopedTransaction Transaction( LOCTEXT("CurveEditor_MoveKeys", "Move Keys") );
CurveOwner->ModifyOwnerChange();
// track all unique curves encountered so their tangents can be updated later
TSet<FRealCurve*> UniqueCurves;
// The total move distance for all keys is the difference between the current snapped location
// and the start location of the key which was actually dragged.
FVector2D TotalMoveDistance = Delta;
for (int32 i = 0; i < SelectedKeys.Num(); i++)
{
FSelectedCurveKey OldKey = SelectedKeys[i];
if (!IsValidCurve(OldKey.Curve))
{
continue;
}
FKeyHandle OldKeyHandle = OldKey.KeyHandle;
FRealCurve* Curve = OldKey.Curve;
FVector2D PreDragLocation = PreDragKeyLocations[OldKeyHandle];
FVector2D NewLocation = PreDragLocation + TotalMoveDistance;
// Update the key's value without updating the tangents.
if (MovementAxisLock != EMovementAxisLock::AxisLock_Horizontal)
{
Curve->SetKeyValue(OldKeyHandle, NewLocation.Y, false);
}
// Changing the time of a key returns a new handle, so make sure to update existing references.
if (MovementAxisLock != EMovementAxisLock::AxisLock_Vertical)
{
Curve->SetKeyTime(OldKeyHandle, NewLocation.X);
SelectedKeys[i] = FSelectedCurveKey(Curve, OldKeyHandle);
PreDragKeyLocations.Remove(OldKeyHandle);
PreDragKeyLocations.Add(OldKeyHandle, PreDragLocation);
}
UniqueCurves.Add(Curve);
ChangedCurveEditInfos.Add(GetViewModelForCurve(Curve)->CurveInfo);
}
if (CurveOwner->HasRichCurves())
{
// update auto tangents for all curves encountered, once each only
for(TSet<FRealCurve*>::TIterator SetIt(UniqueCurves);SetIt;++SetIt)
{
((FRichCurve*)(*SetIt))->AutoSetTangents();
}
}
if (ChangedCurveEditInfos.Num())
{
CurveOwner->OnCurveChanged(ChangedCurveEditInfos);
}
}
TOptional<float> SCurveEditor::GetKeyValue(FSelectedCurveKey Key) const
{
if(IsValidCurve(Key.Curve))
{
return Key.Curve->GetKeyValue(Key.KeyHandle);
}
return TOptional<float>();
}
TOptional<float> SCurveEditor::GetKeyTime(FSelectedCurveKey Key) const
{
if ( IsValidCurve(Key.Curve) )
{
return Key.Curve->GetKeyTime(Key.KeyHandle);
}
return TOptional<float>();
}
void SCurveEditor::EmptyKeySelection()
{
SelectedKeys.Empty();
}
void SCurveEditor::AddToKeySelection(FSelectedCurveKey Key)
{
SelectedKeys.AddUnique(Key);
}
void SCurveEditor::RemoveFromKeySelection(FSelectedCurveKey Key)
{
SelectedKeys.Remove(Key);
}
bool SCurveEditor::IsKeySelected(FSelectedCurveKey Key) const
{
return SelectedKeys.Contains(Key);
}
bool SCurveEditor::AreKeysSelected() const
{
return SelectedKeys.Num() > 0;
}
void SCurveEditor::EmptyTangentSelection()
{
SelectedTangents.Empty();
}
void SCurveEditor::AddToTangentSelection(FSelectedTangent Tangent)
{
SelectedTangents.Add(Tangent);
}
void SCurveEditor::RemoveFromTangentSelection(FSelectedTangent Tangent)
{
SelectedTangents.Remove(Tangent);
}
bool SCurveEditor::IsTangentSelected(FSelectedTangent Tangent) const
{
return SelectedTangents.Contains(Tangent);
}
bool SCurveEditor::AreTangentsSelected() const
{
return SelectedTangents.Num() > 0;
}
bool SCurveEditor::IsTangentVisible(FRichCurve* Curve, FKeyHandle KeyHandle, bool& bIsTangentSelected, bool& bIsArrivalSelected, bool& bIsLeaveSelected) const
{
bIsTangentSelected = false;
bIsArrivalSelected = false;
bIsLeaveSelected = false;
bool IsSelected = IsKeySelected(FSelectedCurveKey(Curve,KeyHandle));
for (auto SelectedTangent : SelectedTangents)
{
if (SelectedTangent.Key.KeyHandle == KeyHandle)
{
if (SelectedTangent.bIsArrival)
bIsArrivalSelected = true;
else
bIsLeaveSelected = true;
bIsTangentSelected = true;
}
}
return (IsSelected || bIsTangentSelected || Settings->GetTangentVisibility() == ECurveEditorTangentVisibility::AllTangents) && Settings->GetTangentVisibility() != ECurveEditorTangentVisibility::NoTangents;
}
void SCurveEditor::EmptyAllSelection()
{
EmptyKeySelection();
EmptyTangentSelection();
}
void SCurveEditor::ValidateSelection()
{
//remove all selection if we no longer have a curve interface (Curve Owner)
if (!CurveOwner)
{
EmptyAllSelection();
return;
}
//remove any invalid keys
for(int32 i = 0;i<SelectedKeys.Num();++i)
{
auto Key = SelectedKeys[i];
if(!IsValidCurve(Key.Curve) || !Key.IsValid())
{
SelectedKeys.RemoveAt(i);
i--;
}
}
// remove any invalid tangents
for (int32 i = 0;i<SelectedTangents.Num();++i)
{
auto Tangent = SelectedTangents[i];
if (!IsValidCurve(Tangent.Key.Curve) || !Tangent.Key.IsValid())
{
SelectedTangents.RemoveAt(i);
i--;
}
}
}
bool SCurveEditor::GetAutoFrame() const
{
return Settings->GetAutoFrameCurveEditor() && GetAllowAutoFrame();
}
TArray<FRealCurve*> SCurveEditor::GetCurvesToFit() const
{
TArray<FRealCurve*> FitCurves;
for(const TSharedPtr<FCurveViewModel>& CurveViewModel : CurveViewModels)
{
if (CurveViewModel->bIsVisible && CurveViewModel->CurveInfo.CurveToEdit != nullptr)
{
FitCurves.Add(CurveViewModel->CurveInfo.CurveToEdit);
}
}
return FitCurves;
}
void SCurveEditor::ZoomToFitHorizontal(const bool bZoomToFitAll)
{
TArray<FRealCurve*> CurvesToFit = GetCurvesToFit();
if(CurveViewModels.Num() > 0)
{
float InMin = FLT_MAX;
float InMax = -FLT_MAX;
int32 TotalKeys = 0;
if (SelectedKeys.Num() && !bZoomToFitAll)
{
for (auto SelectedKey : SelectedKeys)
{
TotalKeys++;
float KeyTime = SelectedKey.Curve->GetKeyTime(SelectedKey.KeyHandle);
InMin = FMath::Min(KeyTime, InMin);
InMax = FMath::Max(KeyTime, InMax);
FKeyHandle NextKeyHandle = SelectedKey.Curve->GetNextKey(SelectedKey.KeyHandle);
if (SelectedKey.Curve->IsKeyHandleValid(NextKeyHandle))
{
float NextKeyTime = SelectedKey.Curve->GetKeyTime(NextKeyHandle);
InMin = FMath::Min(NextKeyTime, InMin);
InMax = FMath::Max(NextKeyTime, InMax);
}
FKeyHandle PreviousKeyHandle = SelectedKey.Curve->GetPreviousKey(SelectedKey.KeyHandle);
if (SelectedKey.Curve->IsKeyHandleValid(PreviousKeyHandle))
{
float PreviousKeyTime = SelectedKey.Curve->GetKeyTime(PreviousKeyHandle);
InMin = FMath::Min(PreviousKeyTime, InMin);
InMax = FMath::Max(PreviousKeyTime, InMax);
}
}
}
else
{
for (FRealCurve* Curve : CurvesToFit)
{
float MinTime, MaxTime;
Curve->GetTimeRange(MinTime, MaxTime);
InMin = FMath::Min(MinTime, InMin);
InMax = FMath::Max(MaxTime, InMax);
TotalKeys += Curve->GetNumKeys();
}
}
if (TotalKeys > 0)
{
// Clamp the minimum size
float Size = InMax - InMin;
if (Size < CONST_MinViewRange)
{
InMin -= (0.5f*CONST_MinViewRange);
InMax += (0.5f*CONST_MinViewRange);
Size = InMax - InMin;
}
// add margin
InMin -= CONST_FitMargin*Size;
InMax += CONST_FitMargin*Size;
}
else
{
InMin = -CONST_FitMargin*2.0f;
InMax = (CONST_DefaultZoomRange + CONST_FitMargin)*2.0;
}
SetInputMinMax(InMin, InMax);
}
}
FReply SCurveEditor::ZoomToFitHorizontalClicked()
{
ZoomToFitHorizontal();
return FReply::Handled();
}
/** Set Default output values when range is too small **/
void SCurveEditor::SetDefaultOutput(const float MinZoomRange)
{
const float NewMinOutput = (ViewMinOutput.Get() - (0.5f*MinZoomRange));
const float NewMaxOutput = (ViewMaxOutput.Get() + (0.5f*MinZoomRange));
SetOutputMinMax(NewMinOutput, NewMaxOutput);
}
void SCurveEditor::ZoomToFitVertical(const bool bZoomToFitAll)
{
TArray<FRealCurve*> CurvesToFit = GetCurvesToFit();
if(CurvesToFit.Num() > 0)
{
float InMin = FLT_MAX;
float InMax = -FLT_MAX;
int32 TotalKeys = 0;
if (SelectedKeys.Num() != 0 && !bZoomToFitAll)
{
for (auto SelectedKey : SelectedKeys)
{
TotalKeys++;
float KeyValue = SelectedKey.Curve->GetKeyValue(SelectedKey.KeyHandle);
InMin = FMath::Min(KeyValue, InMin);
InMax = FMath::Max(KeyValue, InMax);
FKeyHandle NextKeyHandle = SelectedKey.Curve->GetNextKey(SelectedKey.KeyHandle);
if (SelectedKey.Curve->IsKeyHandleValid(NextKeyHandle))
{
float NextKeyValue = SelectedKey.Curve->GetKeyValue(NextKeyHandle);
InMin = FMath::Min(NextKeyValue, InMin);
InMax = FMath::Max(NextKeyValue, InMax);
}
FKeyHandle PreviousKeyHandle = SelectedKey.Curve->GetPreviousKey(SelectedKey.KeyHandle);
if (SelectedKey.Curve->IsKeyHandleValid(PreviousKeyHandle))
{
float PreviousKeyValue = SelectedKey.Curve->GetKeyValue(PreviousKeyHandle);
InMin = FMath::Min(PreviousKeyValue, InMin);
InMax = FMath::Max(PreviousKeyValue, InMax);
}
}
}
else
{
for (FRealCurve* Curve : CurvesToFit)
{
float MinVal, MaxVal;
Curve->GetValueRange(MinVal, MaxVal);
InMin = FMath::Min(MinVal, InMin);
InMax = FMath::Max(MaxVal, InMax);
TotalKeys += Curve->GetNumKeys();
}
}
const float MinZoomRange = (TotalKeys > 0 ) ? CONST_MinViewRange: CONST_DefaultZoomRange;
// if in max and in min is same, then include 0.f
if (InMax == InMin)
{
InMax = FMath::Max(InMax, 0.f);
InMin = FMath::Min(InMin, 0.f);
}
// Clamp the minimum size
float Size = InMax - InMin;
if( Size < MinZoomRange )
{
SetDefaultOutput(MinZoomRange);
InMin = ViewMinOutput.Get();
InMax = ViewMaxOutput.Get();
Size = InMax - InMin;
}
// add margin
const float NewMinOutput = (InMin - CONST_FitMargin*Size);
const float NewMaxOutput = (InMax + CONST_FitMargin*Size);
SetOutputMinMax(NewMinOutput, NewMaxOutput);
}
}
FReply SCurveEditor::ZoomToFitVerticalClicked()
{
ZoomToFitVertical();
return FReply::Handled();
}
void SCurveEditor::ZoomToFit(const bool bZoomToFitAll)
{
ZoomToFitHorizontal(bZoomToFitAll);
ZoomToFitVertical(bZoomToFitAll);
}
void SCurveEditor::ToggleInputSnapping()
{
if (bInputSnappingEnabled.IsBound() == false)
{
bInputSnappingEnabled = !bInputSnappingEnabled.Get();
}
}
void SCurveEditor::ToggleOutputSnapping()
{
if (bOutputSnappingEnabled.IsBound() == false)
{
bOutputSnappingEnabled = !bOutputSnappingEnabled.Get();
}
}
bool SCurveEditor::IsInputSnappingEnabled() const
{
return bInputSnappingEnabled.Get();
}
bool SCurveEditor::IsOutputSnappingEnabled() const
{
return bOutputSnappingEnabled.Get();
}
bool SCurveEditor::ShowTimeInFrames() const
{
return bShowTimeInFrames.Get();
}
void SCurveEditor::CreateContextMenu(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent)
{
TSharedPtr<TArray<TSharedPtr<FCurveViewModel>>> CurvesToAddKeysTo = MakeShareable(new TArray<TSharedPtr<FCurveViewModel>>());
bool bHoveredCurveValid = false;
TSharedPtr<FCurveViewModel> HoveredCurve = HitTestCurves(InMyGeometry, InMouseEvent);
//Curve must be visible and unlocked to show context menu
if (HoveredCurve.IsValid() && !HoveredCurve->bIsLocked && HoveredCurve->bIsVisible)
{
CurvesToAddKeysTo->Add(HoveredCurve);
bHoveredCurveValid = true;
}
else
{
// Get all editable curves
for (auto CurveViewModel : CurveViewModels)
{
if (!CurveViewModel->bIsLocked)
{
CurvesToAddKeysTo->Add(CurveViewModel);
}
}
}
const bool bCreateExternalCurve = OnCreateAsset.IsBound() && IsEditingEnabled();
const bool bShowLinearColorCurve = IsLinearColorCurve();
// Early out if there's no menu items to show
if (CurvesToAddKeysTo->Num() == 0 && !bCreateExternalCurve && !bShowLinearColorCurve)
{
return;
}
const FVector2D& ScreenPosition = InMouseEvent.GetScreenSpacePosition();
const bool CloseAfterSelection = true;
FMenuBuilder MenuBuilder( CloseAfterSelection, NULL );
MenuBuilder.BeginSection("EditCurveEditorActions", LOCTEXT("Actions", "Actions"));
{
FText MenuItemLabel;
FText MenuItemToolTip;
FText AddKeyToCurveLabelFormat = LOCTEXT("AddKeyToCurveLabelFormat", "Add key to {0}");
FText AddKeyToCurveToolTipFormat = LOCTEXT("AddKeyToCurveToolTipFormat", "Add a new key at the hovered time to the {0} curve. Keys can also be added with Shift + Click.");
FVector2D Position = InMouseEvent.GetScreenSpacePosition();
if (bHoveredCurveValid)
{
MenuItemLabel = FText::Format(AddKeyToCurveLabelFormat, FText::FromName(HoveredCurve->CurveInfo.CurveName));
MenuItemToolTip = FText::Format(AddKeyToCurveToolTipFormat, FText::FromName(HoveredCurve->CurveInfo.CurveName));
FUIAction Action = FUIAction(FExecuteAction::CreateSP(this, &SCurveEditor::AddNewKey, InMyGeometry, Position, CurvesToAddKeysTo, true));
MenuBuilder.AddMenuEntry(MenuItemLabel, MenuItemToolTip, FSlateIcon(), Action);
}
else
{
if (CurvesToAddKeysTo->Num() == 1)
{
MenuItemLabel = FText::Format(AddKeyToCurveLabelFormat, FText::FromName((*CurvesToAddKeysTo)[0]->CurveInfo.CurveName));
MenuItemToolTip = FText::Format(AddKeyToCurveToolTipFormat, FText::FromName((*CurvesToAddKeysTo)[0]->CurveInfo.CurveName));
FUIAction Action = FUIAction(FExecuteAction::CreateSP(this, &SCurveEditor::AddNewKey, InMyGeometry, Position, CurvesToAddKeysTo, false));
MenuBuilder.AddMenuEntry(MenuItemLabel, MenuItemToolTip, FSlateIcon(), Action);
}
else if (CurvesToAddKeysTo->Num() > 1) //Dont show the menu if we cannot edit any curve
{
//add key to all curve menu entry
MenuItemLabel = LOCTEXT("AddKeyToAllCurves", "Add key to all curves");
MenuItemToolTip = LOCTEXT("AddKeyToAllCurveToolTip", "Adds a key at the hovered time to all curves. Keys can also be added with Shift + Click.");
FUIAction Action = FUIAction(FExecuteAction::CreateSP(this, &SCurveEditor::AddNewKey, InMyGeometry, Position, CurvesToAddKeysTo, true));
MenuBuilder.AddMenuEntry(MenuItemLabel, MenuItemToolTip, FSlateIcon(), Action);
//This menu is not required when there is no curve display (color track can hide and show curves)
if (bAreCurvesVisible.Get())
{
//add key and value to all curve menu entry
MenuItemLabel = LOCTEXT("AddKeyValueToAllCurves", "Add key & value to all curves");
MenuItemToolTip = LOCTEXT("AddKeyValueToAllCurveToolTip", "Adds a key & value at the hovered time to all curves. Keys can also be added with Shift + ctrl + Click.");
Action = FUIAction(FExecuteAction::CreateSP(this, &SCurveEditor::AddNewKey, InMyGeometry, Position, CurvesToAddKeysTo, false));
MenuBuilder.AddMenuEntry(MenuItemLabel, MenuItemToolTip, FSlateIcon(), Action);
}
}
}
}
MenuBuilder.EndSection();
MenuBuilder.BeginSection("CurveEditorActions", LOCTEXT("CurveAction", "Curve Actions") );
{
if( bCreateExternalCurve )
{
FUIAction Action = FUIAction( FExecuteAction::CreateSP( this, &SCurveEditor::OnCreateExternalCurveClicked ) );
MenuBuilder.AddMenuEntry
(
LOCTEXT("CreateExternalCurve", "Create External Curve"),
LOCTEXT("CreateExternalCurve_ToolTip", "Create an external asset using this internal curve"),
FSlateIcon(),
Action
);
}
if( IsLinearColorCurve() && !bAlwaysDisplayColorCurves )
{
FUIAction ShowCurveAction( FExecuteAction::CreateSP( this, &SCurveEditor::OnShowCurveToggled ), FCanExecuteAction(), FIsActionChecked::CreateSP( this, &SCurveEditor::AreCurvesVisible ) );
MenuBuilder.AddMenuEntry
(
LOCTEXT("ShowCurves","Show Curves"),
LOCTEXT("ShowCurves_ToolTip", "Toggles displaying the curves for linear colors"),
FSlateIcon(),
ShowCurveAction,
NAME_None,
EUserInterfaceActionType::ToggleButton
);
}
if( IsLinearColorCurve() && !bAlwaysHideGradientEditor)
{
FUIAction ShowGradientAction( FExecuteAction::CreateSP( this, &SCurveEditor::OnShowGradientToggled ), FCanExecuteAction(), FIsActionChecked::CreateSP( this, &SCurveEditor::IsGradientEditorVisible ) );
MenuBuilder.AddMenuEntry
(
LOCTEXT("ShowGradient","Show Gradient"),
LOCTEXT("ShowGradient_ToolTip", "Toggles displaying the gradient for linear colors"),
FSlateIcon(),
ShowGradientAction,
NAME_None,
EUserInterfaceActionType::ToggleButton
);
}
}
MenuBuilder.EndSection();
FWidgetPath WidgetPath = InMouseEvent.GetEventPath() != nullptr ? *InMouseEvent.GetEventPath() : FWidgetPath();
FSlateApplication::Get().PushMenu(SharedThis(this), WidgetPath, MenuBuilder.MakeWidget(), FSlateApplication::Get().GetCursorPos(), FPopupTransitionEffect(FPopupTransitionEffect::ContextMenu));
}
void SCurveEditor::OnCreateExternalCurveClicked()
{
OnCreateAsset.ExecuteIfBound();
}
void SCurveEditor::OnShowCurveToggled()
{
if (bAreCurvesVisible.IsBound() && SetAreCurvesVisibleHandler.IsBound())
{
SetAreCurvesVisibleHandler.Execute(!bAreCurvesVisible.Get());
}
else
{
bAreCurvesVisible = !bAreCurvesVisible.Get();
}
}
UObject* SCurveEditor::CreateCurveObject( TSubclassOf<UCurveBase> CurveType, UObject* PackagePtr, FName& AssetName )
{
UObject* NewObj = NULL;
CurveFactory = Cast<UCurveFactory>(NewObject<UFactory>(GetTransientPackage(), UCurveFactory::StaticClass()));
if(CurveFactory)
{
CurveFactory->CurveClass = CurveType;
NewObj = CurveFactory->FactoryCreateNew( CurveFactory->GetSupportedClass(), PackagePtr, AssetName, RF_Public|RF_Standalone, NULL, GWarn );
}
CurveFactory = NULL;
return NewObj;
}
bool SCurveEditor::IsEditingEnabled() const
{
return bCanEditTrack;
}
void SCurveEditor::AddReferencedObjects( FReferenceCollector& Collector )
{
Collector.AddReferencedObject( Settings );
Collector.AddReferencedObject( CurveFactory );
}
TSharedPtr<FUICommandList> SCurveEditor::GetCommands()
{
return Commands;
}
bool SCurveEditor::IsValidCurve( FRealCurve* Curve ) const
{
bool bIsValid = false;
if(Curve && CurveOwner)
{
for(auto CurveViewModel : CurveViewModels)
{
if(CurveViewModel->CurveInfo.CurveToEdit == Curve && CurveOwner->IsValidCurve(CurveViewModel->CurveInfo))
{
bIsValid = true;
break;
}
}
}
return bIsValid;
}
void SCurveEditor::SetInputMinMax(float NewMin, float NewMax)
{
if (SetInputViewRangeHandler.IsBound())
{
SetInputViewRangeHandler.Execute(NewMin, NewMax);
}
else
{
//if no delegate and view min input isn't using a delegate just set value directly
if (ViewMinInput.IsBound() == false)
{
ViewMinInput.Set(NewMin);
}
if (ViewMaxInput.IsBound() == false)
{
ViewMaxInput.Set(NewMax);
}
}
}
void SCurveEditor::SetOutputMinMax(float NewMin, float NewMax)
{
if (SetOutputViewRangeHandler.IsBound())
{
SetOutputViewRangeHandler.Execute(NewMin, NewMax);
}
else
{
//if no delegate and view min output isn't using a delegate just set value directly
if (ViewMinOutput.IsBound() == false)
{
ViewMinOutput.Set(NewMin);
}
if (ViewMaxOutput.IsBound() == false)
{
ViewMaxOutput.Set(NewMax);
}
}
}
void SCurveEditor::EmptyAllCurveViewModels()
{
CurveViewModels.Empty();
}
void SCurveEditor::ClearSelectedCurveViewModels()
{
for(auto CurveViewModel : CurveViewModels)
{
CurveViewModel->bIsSelected = false;
}
}
void SCurveEditor::SetSelectedCurveViewModel(FRealCurve* CurveToSelect)
{
TSharedPtr<FCurveViewModel> ViewModel = GetViewModelForCurve(CurveToSelect);
if (ViewModel.IsValid())
{
ViewModel.Get()->bIsSelected = true;
}
}
bool SCurveEditor::AnyCurveViewModelsSelected() const
{
for (auto CurveViewModel : CurveViewModels)
{
if (CurveViewModel->bIsSelected)
{
return true;
}
}
return false;
}
TSharedPtr<FCurveViewModel> SCurveEditor::HitTestCurves( const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent )
{
if( AreCurvesVisible() )
{
FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), InMyGeometry.GetLocalSize());
const FVector2D HitPosition = InMyGeometry.AbsoluteToLocal( InMouseEvent.GetScreenSpacePosition() );
TArray<FRealCurve*> CurvesHit;
for(auto CurveViewModel : CurveViewModels)
{
FRealCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit;
if (IsValidCurve(Curve))
{
float Time = ScaleInfo.LocalXToInput(HitPosition.X);
float KeyScreenY = ScaleInfo.OutputToLocalY(Curve->Eval(Time));
if( HitPosition.Y > (KeyScreenY - (0.5f * CONST_CurveSize.Y)) &&
HitPosition.Y < (KeyScreenY + (0.5f * CONST_CurveSize.Y)))
{
return CurveViewModel;
}
}
}
}
return TSharedPtr<FCurveViewModel>();
}
bool SCurveEditor::IsCurveSelectable(TSharedPtr<FCurveViewModel> CurveViewModel) const
{
bool bAnyCurveViewModelsSelected = AnyCurveViewModelsSelected();
bool bDisabled = bAnyCurveViewModelsSelected && !CurveViewModel->bIsSelected;
return !CurveViewModel->bIsLocked && CurveViewModel->bIsVisible && !bDisabled;
}
SCurveEditor::FSelectedTangent SCurveEditor::HitTestCubicTangents( const FGeometry& InMyGeometry, const FVector2D& HitScreenPosition )
{
FSelectedTangent Tangent;
if( AreCurvesVisible() && CurveOwner && CurveOwner->HasRichCurves() )
{
FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), InMyGeometry.GetLocalSize());
const FVector2D HitPosition = InMyGeometry.AbsoluteToLocal( HitScreenPosition);
for (auto CurveViewModel : CurveViewModels)
{
if (IsCurveSelectable(CurveViewModel))
{
FRichCurve* Curve = (FRichCurve*)CurveViewModel->CurveInfo.CurveToEdit;
if (Curve != NULL)
{
for (auto It(Curve->GetKeyHandleIterator()); It; ++It)
{
FKeyHandle KeyHandle = *It;
FSelectedCurveKey SelectedCurveKey(Curve, KeyHandle);
if(SelectedCurveKey.IsValid())
{
bool bIsTangentSelected = false;
bool bIsArrivalSelected = false;
bool bIsLeaveSelected = false;
bool bIsTangentVisible = IsTangentVisible(Curve, KeyHandle, bIsTangentSelected, bIsArrivalSelected, bIsLeaveSelected);
if (bIsTangentVisible)
{
float Time = ScaleInfo.LocalXToInput(HitPosition.X);
float KeyScreenY = ScaleInfo.OutputToLocalY(Curve->Eval(Time));
FVector2D Arrive, Leave;
GetTangentPoints(ScaleInfo, SelectedCurveKey, Arrive, Leave);
if( HitPosition.Y > (Arrive.Y - (0.5f * CONST_CurveSize.Y)) &&
HitPosition.Y < (Arrive.Y + (0.5f * CONST_CurveSize.Y)) &&
HitPosition.X > (Arrive.X - (0.5f * CONST_TangentSize.X)) &&
HitPosition.X < (Arrive.X + (0.5f * CONST_TangentSize.X)))
{
Tangent.Key = SelectedCurveKey;
Tangent.bIsArrival = true;
break;
}
if( HitPosition.Y > (Leave.Y - (0.5f * CONST_CurveSize.Y)) &&
HitPosition.Y < (Leave.Y + (0.5f * CONST_CurveSize.Y)) &&
HitPosition.X > (Leave.X - (0.5f * CONST_TangentSize.X)) &&
HitPosition.X < (Leave.X + (0.5f * CONST_TangentSize.X)))
{
Tangent.Key = SelectedCurveKey;
Tangent.bIsArrival = false;
break;
}
}
}
}
}
}
}
}
return Tangent;
}
void SCurveEditor::OnSelectInterpolationMode(ERichCurveInterpMode InterpMode, ERichCurveTangentMode TangentMode)
{
const bool bHasRichCurves = CurveOwner->HasRichCurves();
if((SelectedKeys.Num() > 0 || SelectedTangents.Num() > 0) && (InterpMode != RCIM_Cubic || bHasRichCurves))
{
const FScopedTransaction Transaction(LOCTEXT("CurveEditor_SetInterpolationMode", "Select Interpolation Mode"));
CurveOwner->ModifyOwner();
if (bHasRichCurves)
{
for (auto It = SelectedKeys.CreateIterator(); It; ++It)
{
FSelectedCurveKey& Key = *It;
FRichCurve* RichCurve = (FRichCurve*)Key.Curve;
check(IsValidCurve(RichCurve));
RichCurve->SetKeyInterpMode(Key.KeyHandle, InterpMode);
RichCurve->SetKeyTangentMode(Key.KeyHandle, TangentMode);
}
for (auto It = SelectedTangents.CreateIterator(); It; ++It)
{
FSelectedTangent& Tangent = *It;
FRichCurve* RichCurve = (FRichCurve*)Tangent.Key.Curve;
check(IsValidCurve(RichCurve));
RichCurve->SetKeyInterpMode(Tangent.Key.KeyHandle, InterpMode);
RichCurve->SetKeyTangentMode(Tangent.Key.KeyHandle, TangentMode);
}
}
else
{
for (auto It = SelectedKeys.CreateIterator(); It; ++It)
{
FSelectedCurveKey& Key = *It;
FSimpleCurve* SimpleCurve = (FSimpleCurve*)Key.Curve;
check(IsValidCurve(SimpleCurve));
SimpleCurve->SetKeyInterpMode(InterpMode);
}
}
TArray<FRichCurveEditInfo> ChangedCurveEditInfos;
CurveOwner->OnCurveChanged(ChangedCurveEditInfos);
}
}
bool SCurveEditor::IsInterpolationModeSelected(ERichCurveInterpMode InterpMode, ERichCurveTangentMode TangentMode)
{
const bool bHasRichCurves = CurveOwner->HasRichCurves();
if (SelectedKeys.Num() > 0)
{
for (auto SelectedKey : SelectedKeys)
{
if (SelectedKey.Curve->GetKeyInterpMode(SelectedKey.KeyHandle) != InterpMode || (bHasRichCurves && ((FRichCurve*)SelectedKey.Curve)->GetKeyTangentMode(SelectedKey.KeyHandle) != TangentMode))
{
return false;
}
}
return true;
}
else if (SelectedTangents.Num() > 0)
{
for (auto SelectedTangent : SelectedTangents)
{
if (SelectedTangent.Key.Curve->GetKeyInterpMode(SelectedTangent.Key.KeyHandle) != InterpMode || ((FRichCurve*)SelectedTangent.Key.Curve)->GetKeyTangentMode(SelectedTangent.Key.KeyHandle) != TangentMode)
{
return false;
}
}
return true;
}
else
{
return false;
}
}
bool SCurveEditor::HasRichCurves() const
{
return CurveOwner->HasRichCurves();
}
void SCurveEditor::OnFlattenOrStraightenTangents(bool bFlattenTangents)
{
if ((SelectedKeys.Num() > 0 || SelectedTangents.Num() > 0) && CurveOwner->HasRichCurves())
{
const FScopedTransaction Transaction(LOCTEXT("CurveEditor_FlattenTangents", "Flatten Tangents"));
CurveOwner->ModifyOwner();
auto OnFlattenOrStraightenTangents_Internal = [this, bFlattenTangents](FSelectedCurveKey& Key)
{
FRichCurve* RichCurve = (FRichCurve*)Key.Curve;
check(IsValidCurve(RichCurve));
float LeaveTangent = RichCurve->GetKey(Key.KeyHandle).LeaveTangent;
float ArriveTangent = RichCurve->GetKey(Key.KeyHandle).ArriveTangent;
if (bFlattenTangents)
{
LeaveTangent = 0;
ArriveTangent = 0;
}
else
{
LeaveTangent = (LeaveTangent + ArriveTangent) * 0.5f;
ArriveTangent = LeaveTangent;
}
RichCurve->GetKey(Key.KeyHandle).LeaveTangent = LeaveTangent;
RichCurve->GetKey(Key.KeyHandle).ArriveTangent = ArriveTangent;
if (RichCurve->GetKey(Key.KeyHandle).InterpMode == RCIM_Cubic &&
RichCurve->GetKey(Key.KeyHandle).TangentMode == RCTM_Auto)
{
RichCurve->GetKey(Key.KeyHandle).TangentMode = RCTM_User;
}
};
for(auto It = SelectedKeys.CreateIterator();It;++It)
{
OnFlattenOrStraightenTangents_Internal(*It);
}
for(auto It = SelectedTangents.CreateIterator();It;++It)
{
OnFlattenOrStraightenTangents_Internal(It->Key);
}
TArray<FRichCurveEditInfo> ChangedCurveEditInfos;
CurveOwner->OnCurveChanged(ChangedCurveEditInfos);
}
}
void SCurveEditor::OnBakeCurve()
{
float BakeSampleRate = InputSnap.IsSet() ? InputSnap.Get() : 0.05f;
// Display dialog and let user enter sample rate.
GenericTextEntryModeless(
NSLOCTEXT("CurveEditor.Popups", "BakeSampleRate", "Sample Rate"),
FText::AsNumber( BakeSampleRate ),
FOnTextCommitted::CreateSP(this, &SCurveEditor::OnBakeCurveSampleRateCommitted)
);
}
void SCurveEditor::OnBakeCurveSampleRateCommitted(const FText& InText, ETextCommit::Type CommitInfo)
{
CloseEntryPopupMenu();
if (CommitInfo == ETextCommit::OnEnter)
{
double NewBakeSampleRate = FCString::Atod(*InText.ToString());
const bool bIsNumber = InText.IsNumeric();
if (!bIsNumber)
{
return;
}
if (NewBakeSampleRate <= 0.0)
{
UE_LOG(LogCurveEditor, Error, TEXT("Invalid Bake Sample Rate"));
return;
}
const float BakeSampleRate = (float)NewBakeSampleRate;
const FScopedTransaction Transaction(LOCTEXT("CurveEditor_BakeCurve", "Bake Curve"));
CurveOwner->ModifyOwner();
bool bAnyCurveViewModelsSelected = AnyCurveViewModelsSelected();
TArray<FRichCurveEditInfo> ChangedCurveEditInfos;
// If keys are selected, bake between them
TMap<FRealCurve*, TInterval<float> > CurveRangeMap;
for (auto SelectedKey : SelectedKeys)
{
float SelectedTime = SelectedKey.Curve->GetKeyTime(SelectedKey.KeyHandle);
if (CurveRangeMap.Find(SelectedKey.Curve) != nullptr)
{
CurveRangeMap[SelectedKey.Curve].Include(SelectedTime);
}
else
{
CurveRangeMap.Add(SelectedKey.Curve, TInterval<float>(SelectedTime, SelectedTime));
}
}
if (CurveRangeMap.Num())
{
for (auto CurveToBake : CurveRangeMap)
{
if (CurveToBake.Value.Min != CurveToBake.Value.Max)
{
CurveToBake.Key->BakeCurve(BakeSampleRate, CurveToBake.Value.Min, CurveToBake.Value.Max);
ChangedCurveEditInfos.Add(GetViewModelForCurve(CurveToBake.Key)->CurveInfo);
}
else
{
UE_LOG(LogCurveEditor, Warning, TEXT("Unable to bake single-point curve. Check if you don't have a single key selected before Baking."));
}
}
}
else
{
for (auto CurveViewModel : CurveViewModels)
{
if (!bAnyCurveViewModelsSelected || CurveViewModel->bIsSelected)
{
CurveViewModel->CurveInfo.CurveToEdit->BakeCurve(BakeSampleRate);
ChangedCurveEditInfos.Add(CurveViewModel->CurveInfo);
}
}
}
if (ChangedCurveEditInfos.Num())
{
CurveOwner->OnCurveChanged(ChangedCurveEditInfos);
}
}
}
void SCurveEditor::OnReduceCurve()
{
// Display dialog and let user enter tolerance.
GenericTextEntryModeless(
NSLOCTEXT("CurveEditor.Popups", "ReduceCurveTolerance", "Tolerance"),
FText::AsNumber( ReduceTolerance ),
FOnTextCommitted::CreateSP(this, &SCurveEditor::OnReduceCurveToleranceCommitted)
);
}
void SCurveEditor::OnReduceCurveToleranceCommitted(const FText& InText, ETextCommit::Type CommitInfo)
{
CloseEntryPopupMenu();
if (CommitInfo == ETextCommit::OnEnter)
{
double NewTolerance = FCString::Atod(*InText.ToString());
const bool bIsNumber = InText.IsNumeric();
if(!bIsNumber)
return;
ReduceTolerance = (float)NewTolerance;
const FScopedTransaction Transaction(LOCTEXT("CurveEditor_ReduceCurve", "Reduce Curve"));
CurveOwner->ModifyOwner();
bool bAnyCurveViewModelsSelected = AnyCurveViewModelsSelected();
TArray<FRichCurveEditInfo> ChangedCurveEditInfos;
// If keys are selected, bake between them
TMap<FRealCurve*, TInterval<float> > CurveRangeMap;
for (auto SelectedKey : SelectedKeys)
{
float SelectedTime = SelectedKey.Curve->GetKeyTime(SelectedKey.KeyHandle);
if (CurveRangeMap.Find(SelectedKey.Curve) != nullptr)
{
CurveRangeMap[SelectedKey.Curve].Include(SelectedTime);
}
else
{
CurveRangeMap.Add(SelectedKey.Curve, TInterval<float>(SelectedTime, SelectedTime));
}
}
if (CurveRangeMap.Num())
{
for (auto CurveToBake : CurveRangeMap)
{
CurveToBake.Key->RemoveRedundantKeys(ReduceTolerance, CurveToBake.Value.Min, CurveToBake.Value.Max);
ChangedCurveEditInfos.Add(GetViewModelForCurve(CurveToBake.Key)->CurveInfo);
}
}
else
{
for (auto CurveViewModel : CurveViewModels)
{
if (!bAnyCurveViewModelsSelected || CurveViewModel->bIsSelected)
{
CurveViewModel->CurveInfo.CurveToEdit->RemoveRedundantKeys(ReduceTolerance);
ChangedCurveEditInfos.Add(CurveViewModel->CurveInfo);
}
}
}
if (ChangedCurveEditInfos.Num())
{
CurveOwner->OnCurveChanged(ChangedCurveEditInfos);
}
}
}
void SCurveEditor::OnSelectPreInfinityExtrap(ERichCurveExtrapolation Extrapolation)
{
const FScopedTransaction Transaction(LOCTEXT("CurveEditor_SetPreInfinityExtrapolation", "Set Pre-Infinity Extrapolation"));
CurveOwner->ModifyOwner();
bool bAnyCurveViewModelsSelected = AnyCurveViewModelsSelected();
TArray<FRichCurveEditInfo> ChangedCurveEditInfos;
for (auto CurveViewModel : CurveViewModels)
{
if (!bAnyCurveViewModelsSelected || CurveViewModel->bIsSelected)
{
if (CurveViewModel->CurveInfo.CurveToEdit->PreInfinityExtrap != Extrapolation)
{
CurveViewModel->CurveInfo.CurveToEdit->PreInfinityExtrap = Extrapolation;
ChangedCurveEditInfos.Add(CurveViewModel->CurveInfo);
}
}
}
if (ChangedCurveEditInfos.Num())
{
CurveOwner->OnCurveChanged(ChangedCurveEditInfos);
}
}
bool SCurveEditor::IsPreInfinityExtrapSelected(ERichCurveExtrapolation Extrapolation)
{
bool bAnyCurveViewModelsSelected = AnyCurveViewModelsSelected();
for (auto CurveViewModel : CurveViewModels)
{
// If there are any curves selected, the setting must match all of the selected curves
if (bAnyCurveViewModelsSelected)
{
if (CurveViewModel->bIsSelected)
{
if (CurveViewModel->CurveInfo.CurveToEdit->PreInfinityExtrap != Extrapolation)
{
return false;
}
}
}
else
{
if (CurveViewModel->CurveInfo.CurveToEdit->PreInfinityExtrap != Extrapolation)
{
return false;
}
}
}
return CurveViewModels.Num() > 0;
}
void SCurveEditor::OnSelectPostInfinityExtrap(ERichCurveExtrapolation Extrapolation)
{
const FScopedTransaction Transaction(LOCTEXT("CurveEditor_SetPostInfinityExtrapolation", "Set Post-Infinity Extrapolation"));
CurveOwner->ModifyOwner();
bool bAnyCurveViewModelsSelected = AnyCurveViewModelsSelected();
TArray<FRichCurveEditInfo> ChangedCurveEditInfos;
for (auto CurveViewModel : CurveViewModels)
{
if (!bAnyCurveViewModelsSelected || CurveViewModel->bIsSelected)
{
if (CurveViewModel->CurveInfo.CurveToEdit->PostInfinityExtrap != Extrapolation)
{
CurveViewModel->CurveInfo.CurveToEdit->PostInfinityExtrap = Extrapolation;
ChangedCurveEditInfos.Add(CurveViewModel->CurveInfo);
}
}
}
if (ChangedCurveEditInfos.Num())
{
CurveOwner->OnCurveChanged(ChangedCurveEditInfos);
}
}
bool SCurveEditor::IsPostInfinityExtrapSelected(ERichCurveExtrapolation Extrapolation)
{
bool bAnyCurveViewModelsSelected = AnyCurveViewModelsSelected();
for (auto CurveViewModel : CurveViewModels)
{
// If there are any curves selected, the setting must match all of the selected curves
if (bAnyCurveViewModelsSelected)
{
if (CurveViewModel->bIsSelected)
{
if (CurveViewModel->CurveInfo.CurveToEdit->PostInfinityExtrap != Extrapolation)
{
return false;
}
}
}
else
{
if (CurveViewModel->CurveInfo.CurveToEdit->PostInfinityExtrap != Extrapolation)
{
return false;
}
}
}
return CurveViewModels.Num() > 0;
}
void SCurveEditor::MoveTangents(FTrackScaleInfo& ScaleInfo, FVector2D Delta)
{
TArray<FRichCurveEditInfo> ChangedCurveEditInfos;
CurveOwner->ModifyOwnerChange();
for (const FSelectedTangent& SelectedTangent : SelectedTangents)
{
FRichCurveKey& RichKey = ((FRichCurve*)SelectedTangent.Key.Curve)->GetKey(SelectedTangent.Key.KeyHandle);
const FSelectedCurveKey &Key = SelectedTangent.Key;
float PreDragArriveTangent = PreDragTangents[SelectedTangent.Key.KeyHandle][0];
float PreDragLeaveTangent = PreDragTangents[SelectedTangent.Key.KeyHandle][1];
// Get tangent points in screen space
FVector2D ArriveTangentDir = CalcTangentDir( PreDragArriveTangent );
FVector2D LeaveTangentDir = CalcTangentDir( PreDragLeaveTangent );
FVector2D KeyPosition( Key.Curve->GetKeyTime(Key.KeyHandle),Key.Curve->GetKeyValue(Key.KeyHandle) );
ArriveTangentDir.Y *= -1.0f;
LeaveTangentDir.Y *= -1.0f;
FVector2D ArrivePosition = -ArriveTangentDir + KeyPosition;
FVector2D LeavePosition = LeaveTangentDir + KeyPosition;
FVector2D Arrive = FVector2D(ScaleInfo.InputToLocalX(ArrivePosition.X), ScaleInfo.OutputToLocalY(ArrivePosition.Y));
FVector2D Leave = FVector2D(ScaleInfo.InputToLocalX(LeavePosition.X), ScaleInfo.OutputToLocalY(LeavePosition.Y));
FVector2D KeyScreenPosition = FVector2D(ScaleInfo.InputToLocalX(KeyPosition.X), ScaleInfo.OutputToLocalY(KeyPosition.Y));
FVector2D ToArrive = Arrive - KeyScreenPosition;
ToArrive.Normalize();
Arrive = KeyScreenPosition + ToArrive*CONST_KeyTangentOffset;
FVector2D ToLeave = Leave - KeyScreenPosition;
ToLeave.Normalize();
Leave = KeyScreenPosition + ToLeave*CONST_KeyTangentOffset;
// New arrive and leave directions in screen space
if (SelectedTangent.bIsArrival)
{
Arrive += Delta;
Leave -= Delta;
}
else
{
Arrive -= Delta;
Leave += Delta;
}
// Convert back to input/output space
FVector2D NewArriveDir(ScaleInfo.LocalXToInput(Arrive.X), ScaleInfo.LocalYToOutput(Arrive.Y));
FVector2D NewLeaveDir(ScaleInfo.LocalXToInput(Leave.X), ScaleInfo.LocalYToOutput(Leave.Y));
// Compute tangents
float NewArriveTangent = CalcTangent(-1.f*(NewArriveDir - KeyPosition));
float NewLeaveTangent = CalcTangent(NewLeaveDir - KeyPosition);
if(RichKey.TangentMode != RCTM_Break)
{
RichKey.ArriveTangent = NewArriveTangent;
RichKey.LeaveTangent = NewLeaveTangent;
RichKey.TangentMode = RCTM_User;
}
else
{
if(SelectedTangent.bIsArrival)
{
RichKey.ArriveTangent = NewArriveTangent;
}
else
{
RichKey.LeaveTangent = NewLeaveTangent;
}
}
RichKey.InterpMode = RCIM_Cubic;
ChangedCurveEditInfos.Add(GetViewModelForCurve(SelectedTangent.Key.Curve)->CurveInfo);
}
if (ChangedCurveEditInfos.Num())
{
CurveOwner->OnCurveChanged(ChangedCurveEditInfos);
}
}
void SCurveEditor::GetTangentPoints( FTrackScaleInfo &ScaleInfo, const FSelectedCurveKey &Key, FVector2D& Arrive, FVector2D& Leave ) const
{
FVector2D ArriveTangentDir = CalcTangentDir( ((FRichCurve*)Key.Curve)->GetKey(Key.KeyHandle).ArriveTangent);
FVector2D LeaveTangentDir = CalcTangentDir( ((FRichCurve*)Key.Curve)->GetKey(Key.KeyHandle).LeaveTangent);
FVector2D KeyPosition( Key.Curve->GetKeyTime(Key.KeyHandle),Key.Curve->GetKeyValue(Key.KeyHandle) );
ArriveTangentDir.Y *= -1.0f;
LeaveTangentDir.Y *= -1.0f;
FVector2D ArrivePosition = -ArriveTangentDir + KeyPosition;
FVector2D LeavePosition = LeaveTangentDir + KeyPosition;
Arrive = FVector2D(ScaleInfo.InputToLocalX(ArrivePosition.X), ScaleInfo.OutputToLocalY(ArrivePosition.Y));
Leave = FVector2D(ScaleInfo.InputToLocalX(LeavePosition.X), ScaleInfo.OutputToLocalY(LeavePosition.Y));
FVector2D KeyScreenPosition = FVector2D(ScaleInfo.InputToLocalX(KeyPosition.X), ScaleInfo.OutputToLocalY(KeyPosition.Y));
FVector2D ToArrive = Arrive - KeyScreenPosition;
ToArrive.Normalize();
Arrive = KeyScreenPosition + ToArrive*CONST_KeyTangentOffset;
FVector2D ToLeave = Leave - KeyScreenPosition;
ToLeave.Normalize();
Leave = KeyScreenPosition + ToLeave*CONST_KeyTangentOffset;
}
TArray<SCurveEditor::FSelectedCurveKey> SCurveEditor::GetEditableKeysWithinMarquee(const FGeometry& InMyGeometry, FVector2D MarqueeTopLeft, FVector2D MarqueeBottomRight) const
{
TArray<FSelectedCurveKey> KeysWithinMarquee;
if (AreCurvesVisible())
{
FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), InMyGeometry.GetLocalSize());
for (auto CurveViewModel : CurveViewModels)
{
if (IsCurveSelectable(CurveViewModel))
{
FRealCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit;
if (Curve != NULL)
{
for (auto It(Curve->GetKeyHandleIterator()); It; ++It)
{
float KeyScreenX = ScaleInfo.InputToLocalX(Curve->GetKeyTime(*It));
float KeyScreenY = ScaleInfo.OutputToLocalY(Curve->GetKeyValue(*It));
if (KeyScreenX >= (MarqueeTopLeft.X - (0.5f * CONST_KeySize.X)) &&
KeyScreenX <= (MarqueeBottomRight.X + (0.5f * CONST_KeySize.X)) &&
KeyScreenY >= (MarqueeTopLeft.Y - (0.5f * CONST_KeySize.Y)) &&
KeyScreenY <= (MarqueeBottomRight.Y + (0.5f * CONST_KeySize.Y)))
{
KeysWithinMarquee.Add(FSelectedCurveKey(Curve, *It));
}
}
}
}
}
}
return KeysWithinMarquee;
}
TArray<SCurveEditor::FSelectedTangent> SCurveEditor::GetEditableTangentsWithinMarquee(const FGeometry& InMyGeometry, FVector2D MarqueeTopLeft, FVector2D MarqueeBottomRight) const
{
FBox MarqueeBox;
MarqueeBox.Min = FVector(MarqueeTopLeft.X, MarqueeTopLeft.Y, 0);
MarqueeBox.Max = FVector(MarqueeBottomRight.X, MarqueeBottomRight.Y, 0);
TArray<FSelectedTangent> TangentsWithinMarquee;
if (AreCurvesVisible() && CurveOwner && CurveOwner->HasRichCurves())
{
FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), InMyGeometry.GetLocalSize());
for (auto CurveViewModel : CurveViewModels)
{
if (IsCurveSelectable(CurveViewModel))
{
FRichCurve* Curve = (FRichCurve*)CurveViewModel->CurveInfo.CurveToEdit;
if (Curve != NULL)
{
for (auto It(Curve->GetKeyHandleIterator()); It; ++It)
{
FKeyHandle KeyHandle = *It;
FSelectedCurveKey SelectedCurveKey(Curve, KeyHandle);
if(SelectedCurveKey.IsValid())
{
bool bIsTangentSelected = false;
bool bIsArrivalSelected = false;
bool bIsLeaveSelected = false;
bool bIsTangentVisible = IsTangentVisible(Curve, KeyHandle, bIsTangentSelected, bIsArrivalSelected, bIsLeaveSelected);
if (bIsTangentVisible)
{
FVector2D Arrive, Leave;
GetTangentPoints(ScaleInfo, SelectedCurveKey, Arrive, Leave);
bool bArriveInside = MarqueeBox.IsInsideOrOn(FVector(Arrive.X, Arrive.Y, 0));
bool bLeaveInside = MarqueeBox.IsInsideOrOn(FVector(Leave.X, Leave.Y, 0));
if (bArriveInside || bLeaveInside)
{
FSelectedTangent SelectedTangent(SelectedCurveKey);
SelectedTangent.bIsArrival = bArriveInside;
TangentsWithinMarquee.Add(SelectedTangent);
}
}
}
}
}
}
}
}
return TangentsWithinMarquee;
}
void SCurveEditor::BeginDragTransaction()
{
TransactionIndex = GEditor->BeginTransaction( LOCTEXT("CurveEditor_Drag", "Mouse Drag") );
CurveOwner->ModifyOwner();
}
void SCurveEditor::EndDragTransaction()
{
if ( TransactionIndex >= 0 )
{
TArray<FRichCurveEditInfo> ChangedCurveEditInfos;
if (DragState == EDragState::DragKey || DragState == EDragState::FreeDrag)
{
for (auto SelectedKey : SelectedKeys)
{
ChangedCurveEditInfos.Add(GetViewModelForCurve(SelectedKey.Curve)->CurveInfo);
}
}
else if (DragState == EDragState::DragTangent)
{
for (auto SelectedTangent : SelectedTangents)
{
ChangedCurveEditInfos.Add(GetViewModelForCurve(SelectedTangent.Key.Curve)->CurveInfo);
}
}
if (ChangedCurveEditInfos.Num())
{
CurveOwner->OnCurveChanged(ChangedCurveEditInfos);
}
GEditor->EndTransaction();
TransactionIndex = -1;
}
}
bool SCurveEditor::FSelectedTangent::IsValid() const
{
return Key.IsValid();
}
void SCurveEditor::UndoAction()
{
GEditor->UndoTransaction();
}
void SCurveEditor::RedoAction()
{
GEditor->RedoTransaction();
}
void SCurveEditor::RefreshDetailsView()
{
check(!bIsPendingRebuilt);
bIsPendingRebuilt = true;
//To clean up dangling references
EmptyAllSelection();
EmptyAllCurveViewModels();
RegisterActiveTimer(0.f, FWidgetActiveTimerDelegate::CreateSPLambda(
this,
[this](double, float)
{
if(const TSharedPtr<IPropertyUtilities> PropertyUtilities = PropertyUtilitiesWeak.Pin())
{
//Rebuild the widget so it will reference to the new correct address of Curves, and show things correctly
PropertyUtilities->ForceRefresh();
}
return EActiveTimerReturnType::Stop;
}
));
ValidateSelection();
}
void SCurveEditor::OnObjectPropertyChanged(UObject* Object, FPropertyChangedEvent& PropertyChangedEvent)
{
if (CurveOwner && CurveOwner->GetOwners().Contains(Object))
{
if (!bIsPendingRebuilt)
{
if (GIsTransacting)
{
RefreshDetailsView();
}
}
}
}
void SCurveEditor::OnRootPropertyChanged(const FPropertyChangedEvent& PropertyChangedEvent)
{
if (!bIsPendingRebuilt)
{
// CurveEditor will hold dangling references to CurveData and access them in some cases :
// - Curve Data is inline allocated, or wrapped by InstancedStruct, in a container. And we do Array Remove/Clear op, or Add that causes resize
// - Curve Data is wrapped by InstancedStruct in a Container, and we do undo/redo(will trigger emptying and refilling the container, causing address changed)
// - Curve Data is wrapped by InstancedStruct and InstancedStruct is being reset/replaced, causing actual memory address changed
// todo: should only rebuild when it's the InstanceStruct with ValueSet. But currently there is no way to detect if there is an InstanceStruct in the PropertyNode Hierarchy.
if (PropertyChangedEvent.ChangeType
& (EPropertyChangeType::ArrayAdd
| EPropertyChangeType::ArrayRemove
| EPropertyChangeType::ArrayClear
| EPropertyChangeType::ValueSet
| EPropertyChangeType::ResetToDefault))
{
RefreshDetailsView();
}
}
ValidateSelection();
}
void SCurveEditor::HandlePackageReloaded(const EPackageReloadPhase InPackageReloadPhase, FPackageReloadedEvent* InPackageReloadedEvent)
{
if (InPackageReloadPhase == EPackageReloadPhase::OnPackageFixup && CurveOwner)
{
// Our curve owner may be an object that has been reloaded, so we need to check that and update the curve editor appropriately
// We have to do this via the interface as the object addresses stored in the remap table will be offset from the interface pointer due to multiple inheritance
FCurveOwnerInterface* NewCurveOwner = nullptr;
if (CurveOwner->RepointCurveOwner(*InPackageReloadedEvent, NewCurveOwner))
{
SetCurveOwner(NewCurveOwner, bCanEditTrack);
}
}
}
void SCurveEditor::PostUndo(bool bSuccess)
{
ValidateSelection();
}
bool SCurveEditor::IsLinearColorCurve() const
{
return CurveOwner && CurveOwner->IsLinearColorCurve();
}
FVector2D SCurveEditor::SnapLocation(FVector2D InLocation)
{
if (bInputSnappingEnabled.Get())
{
const float InputSnapNow = InputSnap.Get();
InLocation.X = InputSnapNow != 0 ? FMath::RoundToInt(InLocation.X / InputSnapNow) * InputSnapNow : InLocation.X;
}
if (bOutputSnappingEnabled.Get())
{
const float OutputSnapNow = OutputSnap.Get();
InLocation.Y = OutputSnapNow != 0 ? FMath::RoundToInt(InLocation.Y / OutputSnapNow) * OutputSnapNow : InLocation.Y;
}
return InLocation;
}
TSharedPtr<FCurveViewModel> SCurveEditor::GetViewModelForCurve(FRealCurve* InCurve)
{
for (auto CurveViewModel : CurveViewModels)
{
if (InCurve == CurveViewModel->CurveInfo.CurveToEdit)
{
return CurveViewModel;
}
}
return TSharedPtr<FCurveViewModel>();
}
void SCurveEditor::GenericTextEntryModeless(const FText& DialogText, const FText& DefaultText, FOnTextCommitted OnTextComitted)
{
TSharedRef<STextEntryPopup> TextEntryPopup =
SNew(STextEntryPopup)
.Label(DialogText)
.DefaultText(DefaultText)
.OnTextCommitted(OnTextComitted)
.ClearKeyboardFocusOnCommit(false)
.SelectAllTextWhenFocused(true)
.MaxWidth(1024.0f);
EntryPopupMenu = FSlateApplication::Get().PushMenu(
SharedThis(this),
FWidgetPath(),
TextEntryPopup,
FSlateApplication::Get().GetCursorPos(),
FPopupTransitionEffect(FPopupTransitionEffect::TypeInPopup)
);
}
void SCurveEditor::CloseEntryPopupMenu()
{
if (EntryPopupMenu.IsValid())
{
EntryPopupMenu.Pin()->Dismiss();
}
}
int32 SCurveEditor::TimeToFrame(float InTime) const
{
const float FrameRate = InputSnap.IsSet() ? 1.0f / InputSnap.Get() : 1.f;
float Frame = InTime * FrameRate;
return FMath::RoundToInt(Frame);
}
float SCurveEditor::FrameToTime(int32 InFrame) const
{
const float FrameRate = InputSnap.IsSet() ? 1.0f / InputSnap.Get() : 1.f;
return InFrame / FrameRate;
}
#undef LOCTEXT_NAMESPACE