3989 lines
125 KiB
C++
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
|