Files
UnrealEngine/Engine/Source/Runtime/Slate/Private/Widgets/Input/SSpinBox.cpp
2025-05-18 13:04:45 +08:00

1167 lines
40 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Widgets/Input/SSpinBox.h"
namespace SpinBoxPrivate
{
bool bUseSpinBoxMouseMoveOptimization = true;
FAutoConsoleVariableRef CVar
(
TEXT("Slate.Spinbox.MouseMoveOptimization"),
bUseSpinBoxMouseMoveOptimization,
TEXT("")
);
}
float SpinBoxComputeExponentSliderFraction(float FractionFilled, float StartFractionFilled, float SliderExponent)
{
if (FractionFilled <= StartFractionFilled)
{
float DeltaFraction = (StartFractionFilled - FractionFilled)/StartFractionFilled;
float LeftFractionFilled = 1.0f - FMath::Pow(1.0f - DeltaFraction, SliderExponent);
FractionFilled = StartFractionFilled - (StartFractionFilled*LeftFractionFilled);
}
else
{
float DeltaFraction = (FractionFilled - StartFractionFilled)/(1.0f - StartFractionFilled);
float RightFractionFilled = 1.0f - FMath::Pow(1.0f - DeltaFraction, SliderExponent);
FractionFilled = StartFractionFilled + (1.0f - StartFractionFilled) * RightFractionFilled;
}
return FractionFilled;
}
template <typename NumericType>
SSpinBox<NumericType>::SSpinBox()
{
}
template <typename NumericType>
SSpinBox<NumericType>::~SSpinBox()
{
if (bDragging || PointerDraggingSliderIndex != INDEX_NONE)
{
CancelMouseCapture();
}
}
template <typename NumericType>
void SSpinBox<NumericType>::Construct(const FArguments& InArgs)
{
check(InArgs._Style);
Style = InArgs._Style;
SetForegroundColor(InArgs._Style->ForegroundColor);
InterfaceAttr = InArgs._TypeInterface;
if (!InterfaceAttr.IsBound() && InterfaceAttr.Get() == nullptr)
{
InterfaceAttr = MakeShared<TDefaultNumericTypeInterface<NumericType>>();
}
TSharedPtr<INumericTypeInterface<NumericType>> Interface = InterfaceAttr.Get();
if (Interface->GetOnSettingChanged())
{
Interface->GetOnSettingChanged()->AddSP(this, &SSpinBox::ResetCachedValueString);
}
ValueAttribute = InArgs._Value;
OnValueChanged = InArgs._OnValueChanged;
OnValueCommitted = InArgs._OnValueCommitted;
OnBeginSliderMovement = InArgs._OnBeginSliderMovement;
OnEndSliderMovement = InArgs._OnEndSliderMovement;
MinDesiredWidth = InArgs._MinDesiredWidth;
MinValue = InArgs._MinValue;
MaxValue = InArgs._MaxValue;
MinSliderValue = (InArgs._MinSliderValue.Get().IsSet()) ? InArgs._MinSliderValue : MinValue;
MaxSliderValue = (InArgs._MaxSliderValue.Get().IsSet()) ? InArgs._MaxSliderValue : MaxValue;
MinFractionalDigits = (InArgs._MinFractionalDigits.Get().IsSet()) ? InArgs._MinFractionalDigits : DefaultMinFractionalDigits;
MaxFractionalDigits = (InArgs._MaxFractionalDigits.Get().IsSet()) ? InArgs._MaxFractionalDigits : DefaultMaxFractionalDigits;
SetMaxFractionalDigits(MaxFractionalDigits);
SetMinFractionalDigits(MinFractionalDigits);
AlwaysUsesDeltaSnap = InArgs._AlwaysUsesDeltaSnap;
EnableSlider = InArgs._EnableSlider;
SupportDynamicSliderMaxValue = InArgs._SupportDynamicSliderMaxValue;
SupportDynamicSliderMinValue = InArgs._SupportDynamicSliderMinValue;
OnDynamicSliderMaxValueChanged = InArgs._OnDynamicSliderMaxValueChanged;
OnDynamicSliderMinValueChanged = InArgs._OnDynamicSliderMinValueChanged;
OnGetDisplayValue = InArgs._OnGetDisplayValue;
bEnableWheel = InArgs._EnableWheel;
bBroadcastValueChangesPerKey = InArgs._BroadcastValueChangesPerKey;
WheelStep = InArgs._WheelStep;
bPreventThrottling = InArgs._PreventThrottling;
CachedExternalValue = ValueAttribute.Get();
CachedValueString = Interface->ToString(CachedExternalValue);
bCachedValueStringDirty = false;
InternalValue = (double)CachedExternalValue;
if (SupportDynamicSliderMaxValue.Get() && CachedExternalValue > GetMaxSliderValue())
{
ApplySliderMaxValueChanged(float(CachedExternalValue - GetMaxSliderValue()), true);
}
else if (SupportDynamicSliderMinValue.Get() && CachedExternalValue < GetMinSliderValue())
{
ApplySliderMinValueChanged(float(CachedExternalValue - GetMinSliderValue()), true);
}
UpdateIsSpinRangeUnlimited();
SliderExponent = InArgs._SliderExponent;
SliderExponentNeutralValue = InArgs._SliderExponentNeutralValue;
DistanceDragged = 0.0f;
PreDragValue = NumericType();
Delta = InArgs._Delta;
ShiftMultiplier = InArgs._ShiftMultiplier;
CtrlMultiplier = InArgs._CtrlMultiplier;
LinearDeltaSensitivity = InArgs._LinearDeltaSensitivity;
BackgroundHoveredBrush = &InArgs._Style->HoveredBackgroundBrush;
BackgroundBrush = &InArgs._Style->BackgroundBrush;
BackgroundActiveBrush = InArgs._Style->ActiveBackgroundBrush.IsSet() ? &InArgs._Style->ActiveBackgroundBrush : BackgroundHoveredBrush;
ActiveFillBrush = &InArgs._Style->ActiveFillBrush;
HoveredFillBrush = InArgs._Style->HoveredFillBrush.IsSet() ? &InArgs._Style->HoveredFillBrush : ActiveFillBrush;
InactiveFillBrush = &InArgs._Style->InactiveFillBrush;
const FMargin& TextMargin = InArgs._Style->TextPadding;
bDragging = false;
PointerDraggingSliderIndex = INDEX_NONE;
bIsTextChanging = false;
this->ChildSlot
.Padding(InArgs._ContentPadding)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
.Padding(TextMargin)
.HAlign(HAlign_Fill)
.VAlign(VAlign_Center)
[
SAssignNew(TextBlock, STextBlock)
.Font(InArgs._Font)
.Text(this, &SSpinBox<NumericType>::GetDisplayValue)
.MinDesiredWidth(this, &SSpinBox<NumericType>::GetTextMinDesiredWidth)
.Justification(InArgs._Justification)
]
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
.Padding(TextMargin)
.HAlign(HAlign_Fill)
.VAlign(VAlign_Center)
[
SAssignNew(EditableText, SEditableText)
.Visibility(EVisibility::Collapsed)
.Font(InArgs._Font)
.SelectAllTextWhenFocused(true)
.Text(this, &SSpinBox<NumericType>::GetValueAsText)
.RevertTextOnEscape(InArgs._RevertTextOnEscape)
.OnIsTypedCharValid(this, &SSpinBox<NumericType>::IsCharacterValid)
.OnTextChanged(this, &SSpinBox<NumericType>::TextField_OnTextChanged)
.OnTextCommitted(this, &SSpinBox<NumericType>::TextField_OnTextCommitted)
.ClearKeyboardFocusOnCommit(InArgs._ClearKeyboardFocusOnCommit)
.SelectAllTextOnCommit(InArgs._SelectAllTextOnCommit)
.MinDesiredWidth(this, &SSpinBox<NumericType>::GetTextMinDesiredWidth)
.VirtualKeyboardType(InArgs._KeyboardType)
.Justification(InArgs._Justification)
.VirtualKeyboardTrigger(EVirtualKeyboardTrigger::OnAllFocusEvents)
.ContextMenuExtender(InArgs._ContextMenuExtender)
]
+ SHorizontalBox::Slot()
.AutoWidth()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Center)
[
SNew(SImage)
.Image(&InArgs._Style->ArrowsImage)
.ColorAndOpacity(FSlateColor::UseForeground())
]
];
}
template <typename NumericType>
int32 SSpinBox<NumericType>::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect,
FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
const bool bActiveFeedback = bDragging || IsInTextMode();
const FSlateBrush* BackgroundImage = bActiveFeedback ? BackgroundActiveBrush : IsHovered() ?
BackgroundHoveredBrush :
BackgroundBrush;
const FSlateBrush* FillImage = bActiveFeedback ?
ActiveFillBrush : IsHovered() ? HoveredFillBrush :
InactiveFillBrush;
const int32 BackgroundLayer = LayerId;
const bool bEnabled = ShouldBeEnabled(bParentEnabled);
const ESlateDrawEffect DrawEffects = bEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect;
FSlateDrawElement::MakeBox(
OutDrawElements,
BackgroundLayer,
AllottedGeometry.ToPaintGeometry(),
BackgroundImage,
DrawEffects,
BackgroundImage->GetTint(InWidgetStyle) * InWidgetStyle.GetColorAndOpacityTint()
);
const int32 FilledLayer = BackgroundLayer + 1;
//if there is a spin range limit, draw the filler bar
if (!bUnlimitedSpinRange)
{
NumericType Value = ValueAttribute.Get();
NumericType CurrentDelta = Delta.Get();
if (CurrentDelta != NumericType())
{
Value = FMath::GridSnap(Value, CurrentDelta); // snap value to nearest Delta
}
float FractionFilled = Fraction((double)Value, (double)GetMinSliderValue(), (double)GetMaxSliderValue());
const float CachedSliderExponent = SliderExponent.Get();
if (!FMath::IsNearlyEqual(CachedSliderExponent, 1.f))
{
if (SliderExponentNeutralValue.IsSet() && SliderExponentNeutralValue.Get() > GetMinSliderValue() && SliderExponentNeutralValue.Get() < GetMaxSliderValue())
{
//Compute a log curve on both side of the neutral value
float StartFractionFilled = Fraction((double)SliderExponentNeutralValue.Get(), (double)GetMinSliderValue(), (double)GetMaxSliderValue());
FractionFilled = SpinBoxComputeExponentSliderFraction(FractionFilled, StartFractionFilled, CachedSliderExponent);
}
else
{
FractionFilled = 1.0f - FMath::Pow(1.0f - FractionFilled, CachedSliderExponent);
}
}
const FVector2D FillSize(AllottedGeometry.GetLocalSize().X * FractionFilled, AllottedGeometry.GetLocalSize().Y);
if (!IsInTextMode())
{
FSlateDrawElement::MakeBox(
OutDrawElements,
FilledLayer,
AllottedGeometry.ToPaintGeometry(FillSize - FVector2D(Style->InsetPadding.GetTotalSpaceAlong<Orient_Horizontal>(), Style->InsetPadding.GetTotalSpaceAlong<Orient_Vertical>()), FSlateLayoutTransform(Style->InsetPadding.GetTopLeft())),
FillImage,
DrawEffects,
FillImage->GetTint(InWidgetStyle) * InWidgetStyle.GetColorAndOpacityTint()
);
}
}
return FMath::Max(FilledLayer, SCompoundWidget::OnPaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, FilledLayer, InWidgetStyle, bEnabled));
}
template <typename NumericType>
void SSpinBox<NumericType>::Tick(const FGeometry& AlottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
if (PendingCommitValue)
{
const NumericType RoundedNewValue = RoundIfIntegerValue(PendingCommitValue->NewValue);
CommitValue(RoundedNewValue, PendingCommitValue->NewValue, PendingCommitValue->CommitMethod, ETextCommit::OnEnter);
PendingCommitValue.Reset();
}
}
template <typename NumericType>
const bool SSpinBox<NumericType>::CommitWithMultiplier(const FPointerEvent& MouseEvent)
{
return MouseEvent.IsShiftDown() || MouseEvent.IsControlDown();
}
template <typename NumericType>
FReply SSpinBox<NumericType>::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && PointerDraggingSliderIndex == INDEX_NONE)
{
DistanceDragged = 0.f;
PreDragValue = ValueAttribute.Get();
InternalValue = (double)PreDragValue;
PointerDraggingSliderIndex = MouseEvent.GetPointerIndex();
CachedMousePosition = MouseEvent.GetScreenSpacePosition().IntPoint();
FReply ReturnReply = FReply::Handled().CaptureMouse(SharedThis(this)).UseHighPrecisionMouseMovement(SharedThis(this)).SetUserFocus(SharedThis(this), EFocusCause::Mouse);
if (bPreventThrottling)
{
ReturnReply.PreventThrottling();
}
return ReturnReply;
}
else
{
return FReply::Unhandled();
}
}
template <typename NumericType>
FReply SSpinBox<NumericType>::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && PointerDraggingSliderIndex == MouseEvent.GetPointerIndex())
{
if (!this->HasMouseCapture())
{
// Lost Capture - ensure reset
bDragging = false;
PointerDraggingSliderIndex = INDEX_NONE;
return FReply::Unhandled();
}
if (bDragging)
{
NumericType CurrentDelta = Delta.Get();
if (CurrentDelta != NumericType() && !CommitWithMultiplier(MouseEvent))
{
InternalValue = FMath::GridSnap(InternalValue, (double)CurrentDelta);
}
const NumericType CurrentValue = RoundIfIntegerValue(InternalValue);
NotifyValueCommitted(CurrentValue);
}
bDragging = false;
PointerDraggingSliderIndex = INDEX_NONE;
FReply Reply = FReply::Handled().ReleaseMouseCapture();
if (!MouseEvent.IsTouchEvent())
{
Reply.SetMousePos(CachedMousePosition);
}
if (DistanceDragged < FSlateApplication::Get().GetDragTriggerDistance())
{
EnterTextMode();
Reply.SetUserFocus(EditableText.ToSharedRef(), EFocusCause::SetDirectly);
}
return Reply;
}
return FReply::Unhandled();
}
template <typename NumericType>
void SSpinBox<NumericType>::ApplySliderMaxValueChanged(float SliderDeltaToAdd, bool UpdateOnlyIfHigher)
{
check(SupportDynamicSliderMaxValue.Get());
NumericType NewMaxSliderValue = std::numeric_limits<NumericType>::min();
if (MaxSliderValue.IsSet() && MaxSliderValue.Get().IsSet())
{
NewMaxSliderValue = GetMaxSliderValue();
if ((NewMaxSliderValue + (NumericType)SliderDeltaToAdd > GetMaxSliderValue() && UpdateOnlyIfHigher) || !UpdateOnlyIfHigher)
{
NewMaxSliderValue += (NumericType)SliderDeltaToAdd;
if (!MaxSliderValue.IsBound()) // simple value so we can update it without breaking the mechanic otherwise it must be handle by the callback implementer
{
SetMaxSliderValue(NewMaxSliderValue);
}
}
}
if (OnDynamicSliderMaxValueChanged.IsBound())
{
OnDynamicSliderMaxValueChanged.Execute(NewMaxSliderValue, TWeakPtr<SWidget>(AsShared()), true, UpdateOnlyIfHigher);
}
}
template <typename NumericType>
void SSpinBox<NumericType>::ApplySliderMinValueChanged(float SliderDeltaToAdd, bool UpdateOnlyIfLower)
{
check(SupportDynamicSliderMaxValue.Get());
NumericType NewMinSliderValue = std::numeric_limits<NumericType>::min();
if (MinSliderValue.IsSet() && MinSliderValue.Get().IsSet())
{
NewMinSliderValue = GetMinSliderValue();
if ((NewMinSliderValue + (NumericType)SliderDeltaToAdd < GetMinSliderValue() && UpdateOnlyIfLower) || !UpdateOnlyIfLower)
{
NewMinSliderValue += (NumericType)SliderDeltaToAdd;
if (!MinSliderValue.IsBound()) // simple value so we can update it without breaking the mechanic otherwise it must be handle by the callback implementer
{
SetMinSliderValue(NewMinSliderValue);
}
}
}
if (OnDynamicSliderMinValueChanged.IsBound())
{
OnDynamicSliderMinValueChanged.Execute(NewMinSliderValue, TWeakPtr<SWidget>(AsShared()), true, UpdateOnlyIfLower);
}
}
template <typename NumericType>
FReply SSpinBox<NumericType>::OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
const bool bEnableSlider = GetEnableSlider();
if (PointerDraggingSliderIndex == MouseEvent.GetPointerIndex() && bEnableSlider)
{
if (!this->HasMouseCapture())
{
// Lost the mouse capture - ensure reset
bDragging = false;
PointerDraggingSliderIndex = INDEX_NONE;
return FReply::Unhandled();
}
if (!bDragging)
{
DistanceDragged += (float)FMath::Abs(MouseEvent.GetCursorDelta().X);
if (DistanceDragged > FSlateApplication::Get().GetDragTriggerDistance())
{
ExitTextMode();
bDragging = true;
OnBeginSliderMovement.ExecuteIfBound();
}
// Cache the mouse, even if not dragging cache it.
CachedMousePosition = MouseEvent.GetScreenSpacePosition().IntPoint();
}
else
{
double NewValue = 0.0;
// Increments the spin based on delta mouse movement.
// A minimum slider width to use for calculating deltas in the slider-range space
const float MinSliderWidth = 100.f;
float SliderWidthInSlateUnits = FMath::Max((float)MyGeometry.GetDrawSize().X, MinSliderWidth);
if (MouseEvent.IsAltDown())
{
float DeltaToAdd = (float)MouseEvent.GetCursorDelta().X / SliderWidthInSlateUnits;
if (SupportDynamicSliderMaxValue.Get() && (NumericType)InternalValue == GetMaxSliderValue())
{
ApplySliderMaxValueChanged(DeltaToAdd, false);
}
else if (SupportDynamicSliderMinValue.Get() && (NumericType)InternalValue == GetMinSliderValue())
{
ApplySliderMinValueChanged(DeltaToAdd, false);
}
}
ECommitMethod CommitMethod = MouseEvent.IsControlDown() || MouseEvent.IsShiftDown() ? CommittedViaSpinMultiplier : CommittedViaSpin;
double Step = GetDefaultStepSize(MouseEvent);
//if we have a range to draw in
if (!bUnlimitedSpinRange)
{
bool HasValidExponentNeutralValue = SliderExponentNeutralValue.IsSet() && SliderExponentNeutralValue.Get() > GetMinSliderValue() && SliderExponentNeutralValue.Get() < GetMaxSliderValue();
const float CachedSliderExponent = SliderExponent.Get();
// The amount currently filled in the spinbox, needs to be calculated to do deltas correctly.
float FractionFilled = Fraction(InternalValue, (double)GetMinSliderValue(), (double)GetMaxSliderValue());
if (!FMath::IsNearlyEqual(CachedSliderExponent, 1.0f))
{
if (HasValidExponentNeutralValue)
{
//Compute a log curve on both side of the neutral value
float StartFractionFilled = Fraction((double)SliderExponentNeutralValue.Get(), (double)GetMinSliderValue(), (double)GetMaxSliderValue());
FractionFilled = SpinBoxComputeExponentSliderFraction(FractionFilled, StartFractionFilled, CachedSliderExponent);
}
else
{
FractionFilled = 1.0f - FMath::Pow(1.0f - FractionFilled, CachedSliderExponent);
}
}
FractionFilled *= SliderWidthInSlateUnits;
// Now add the delta to the fraction filled, this causes the spin.
FractionFilled += (float)(MouseEvent.GetCursorDelta().X * Step);
// Clamp the fraction to be within the bounds of the geometry.
FractionFilled = FMath::Clamp(FractionFilled, 0.0f, SliderWidthInSlateUnits);
// Convert the fraction filled to a percent.
float Percent = FMath::Clamp(FractionFilled / SliderWidthInSlateUnits, 0.0f, 1.0f);
if (!FMath::IsNearlyEqual(CachedSliderExponent, 1.0f))
{
// Have to convert the percent to the proper value due to the exponent component to the spin.
if (HasValidExponentNeutralValue)
{
//Compute a log curve on both side of the neutral value
float StartFractionFilled = Fraction(SliderExponentNeutralValue.Get(), GetMinSliderValue(), GetMaxSliderValue());
Percent = SpinBoxComputeExponentSliderFraction(Percent, StartFractionFilled, 1.0f / CachedSliderExponent);
}
else
{
Percent = 1.0f - FMath::Pow(1.0f - Percent, 1.0f / CachedSliderExponent);
}
}
NewValue = FMath::LerpStable<double>((double)GetMinSliderValue(), (double)GetMaxSliderValue(), Percent);
}
else
{
// If this control has a specified delta and sensitivity then we use that instead of the current value for determining how much to change.
const double Sign = (MouseEvent.GetCursorDelta().X > 0) ? 1.0 : -1.0;
if (LinearDeltaSensitivity.IsSet() && LinearDeltaSensitivity.Get() != 0 && Delta.IsSet() && Delta.Get() > 0)
{
const double MouseDelta = FMath::Abs(MouseEvent.GetCursorDelta().X / (float)LinearDeltaSensitivity.Get());
NewValue = InternalValue + (Sign * MouseDelta * FMath::Pow((double)Delta.Get(), (double)SliderExponent.Get())) * Step;
}
else
{
const double MouseDelta = FMath::Abs(MouseEvent.GetCursorDelta().X / SliderWidthInSlateUnits);
const double CurrentValue = FMath::Clamp<double>(FMath::Abs(InternalValue), 1.0, (double)std::numeric_limits<NumericType>::max());
NewValue = InternalValue + (Sign * MouseDelta * FMath::Pow((double)CurrentValue, (double)SliderExponent.Get())) * Step;
}
}
if (SpinBoxPrivate::bUseSpinBoxMouseMoveOptimization)
{
if (CommitMethod == ECommitMethod::CommittedViaSpin)
{
NewValue = FMath::Clamp<double>(NewValue, (double)GetMinSliderValue(), (double)GetMaxSliderValue());
}
NewValue = FMath::Clamp<double>(NewValue, (double)GetMinValue(), (double)GetMaxValue());
InternalValue = NewValue;
PendingCommitValue.Emplace(FPendingCommitValue
{
.NewValue = NewValue,
.CommitMethod = CommitMethod
});
}
else
{
NumericType RoundedNewValue = RoundIfIntegerValue(NewValue);
CommitValue(RoundedNewValue, NewValue, CommitMethod, ETextCommit::OnEnter);
}
}
return FReply::Handled();
}
return FReply::Unhandled();
}
template <typename NumericType>
FReply SSpinBox<NumericType>::OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
if (bEnableWheel && PointerDraggingSliderIndex == INDEX_NONE && HasKeyboardFocus())
{
// If there is no WheelStep defined, we use StepSize (Or SmallStepSize if slider range is <= SmallStepSizeMax)
constexpr bool bIsIntegral = TIsIntegral<NumericType>::Value;
const bool bIsSmallStep = !bIsIntegral && (GetMaxSliderValue() - GetMinSliderValue()) <= SmallStepSizeMax;
double Step = WheelStep.IsSet() && WheelStep.Get().IsSet() ? WheelStep.Get().GetValue() : (bIsSmallStep ? SmallStepSize : StepSize);
if (MouseEvent.IsControlDown())
{
// If no value is set for WheelSmallStep, we use the DefaultStep multiplied by the CtrlMultiplier
Step *= CtrlMultiplier.Get();
}
else if (MouseEvent.IsShiftDown())
{
// If no value is set for WheelBigStep, we use the DefaultStep multiplied by the ShiftMultiplier
Step *= ShiftMultiplier.Get();
}
const double Sign = (MouseEvent.GetWheelDelta() > 0) ? 1.0 : -1.0;
const double NewValue = InternalValue + (Sign * Step);
const NumericType RoundedNewValue = RoundIfIntegerValue(NewValue);
TSharedPtr<INumericTypeInterface<NumericType>> Interface = InterfaceAttr.Get();
// First SetEditableText is to update the value before calling CommitValue. Otherwise, when the text lose
// focus from the CommitValue, it will override the value we just committed.
// The second SetEditableText is to update the text to the InternalValue since it could have been clamped.
EditableText->SetEditableText(FText::FromString(Interface->ToString((NumericType)NewValue)));
CommitValue(RoundedNewValue, NewValue, CommittedViaSpin, ETextCommit::OnEnter);
EditableText->SetEditableText(FText::FromString(Interface->ToString((NumericType)InternalValue)));
return FReply::Handled();
}
return FReply::Unhandled();
}
template <typename NumericType>
FCursorReply SSpinBox<NumericType>::OnCursorQuery(const FGeometry& MyGeometry, const FPointerEvent& CursorEvent) const
{
const bool bEnableSlider = GetEnableSlider();
if (!bEnableSlider)
{
return FCursorReply::Cursor(EMouseCursor::Default);
}
return bDragging ?
FCursorReply::Cursor(EMouseCursor::None) :
FCursorReply::Cursor(EMouseCursor::ResizeLeftRight);
}
template <typename NumericType>
bool SSpinBox<NumericType>::SupportsKeyboardFocus() const
{
// SSpinBox is focusable.
return true;
}
template <typename NumericType>
FReply SSpinBox<NumericType>::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent)
{
if (!bDragging && (InFocusEvent.GetCause() == EFocusCause::Navigation || InFocusEvent.GetCause() == EFocusCause::SetDirectly))
{
EnterTextMode();
return FReply::Handled().SetUserFocus(EditableText.ToSharedRef(), InFocusEvent.GetCause());
}
else
{
return FReply::Unhandled();
}
}
template <typename NumericType>
FReply SSpinBox<NumericType>::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent)
{
const FKey Key = InKeyEvent.GetKey();
if (Key == EKeys::Escape && HasMouseCapture())
{
CancelMouseCapture();
return FReply::Handled().ReleaseMouseCapture().SetMousePos(CachedMousePosition);
}
else if (Key == EKeys::Up || Key == EKeys::Right)
{
const NumericType LocalValueAttribute = ValueAttribute.Get();
const NumericType LocalDelta = Delta.Get() != 0 ? Delta.Get() : (NumericType)GetDefaultStepSize(InKeyEvent);
InternalValue = (double)LocalValueAttribute;
CommitValue(LocalValueAttribute + LocalDelta, InternalValue + (double)LocalDelta, CommittedViaArrowKey, ETextCommit::OnEnter);
ExitTextMode();
return FReply::Handled();
}
else if (Key == EKeys::Down || Key == EKeys::Left)
{
const NumericType LocalValueAttribute = ValueAttribute.Get();
const NumericType LocalDelta = Delta.Get() != 0 ? Delta.Get() : (NumericType)GetDefaultStepSize(InKeyEvent);
InternalValue = (double)LocalValueAttribute;
CommitValue(LocalValueAttribute - LocalDelta, InternalValue + (double)LocalDelta, CommittedViaArrowKey, ETextCommit::OnEnter);
ExitTextMode();
return FReply::Handled();
}
else if (Key == EKeys::Enter)
{
InternalValue = (double)ValueAttribute.Get();
EnterTextMode();
return FReply::Handled().SetUserFocus(EditableText.ToSharedRef(), EFocusCause::Navigation);
}
else
{
return FReply::Unhandled();
}
}
template <typename NumericType>
bool SSpinBox<NumericType>::HasKeyboardFocus() const
{
// The spinbox is considered focused when we are typing it text.
return SCompoundWidget::HasKeyboardFocus() || (EditableText.IsValid() && EditableText->HasKeyboardFocus());
}
template <typename NumericType>
TAttribute<NumericType> SSpinBox<NumericType>::GetValueAttribute() const
{ return ValueAttribute; }
template <typename NumericType>
NumericType SSpinBox<NumericType>::GetValue() const
{ return ValueAttribute.Get(); }
template <typename NumericType>
void SSpinBox<NumericType>::SetValue(const TAttribute<NumericType>& InValueAttribute)
{
ValueAttribute = InValueAttribute;
const NumericType LocalValueAttribute = ValueAttribute.Get();
CommitValue(LocalValueAttribute, (double)LocalValueAttribute, ECommitMethod::CommittedViaCode, ETextCommit::Default);
}
template <typename NumericType>
NumericType SSpinBox<NumericType>::GetMinValue() const
{ return MinValue.Get().Get(std::numeric_limits<NumericType>::lowest()); }
template <typename NumericType>
void SSpinBox<NumericType>::SetMinValue(const TAttribute<TOptional<NumericType>>& InMinValue)
{
MinValue = InMinValue;
UpdateIsSpinRangeUnlimited();
}
template <typename NumericType>
NumericType SSpinBox<NumericType>::GetMaxValue() const
{ return MaxValue.Get().Get(std::numeric_limits<NumericType>::max()); }
template <typename NumericType>
void SSpinBox<NumericType>::SetMaxValue(const TAttribute<TOptional<NumericType>>& InMaxValue)
{
MaxValue = InMaxValue;
UpdateIsSpinRangeUnlimited();
}
template <typename NumericType>
bool SSpinBox<NumericType>::IsMinSliderValueBound() const
{ return MinSliderValue.IsBound(); }
template <typename NumericType>
NumericType SSpinBox<NumericType>::GetMinSliderValue() const
{ return MinSliderValue.Get().Get(std::numeric_limits<NumericType>::lowest()); }
template <typename NumericType>
void SSpinBox<NumericType>::SetMinSliderValue(const TAttribute<TOptional<NumericType>>& InMinSliderValue)
{
MinSliderValue = (InMinSliderValue.Get().IsSet()) ? InMinSliderValue : MinValue;
UpdateIsSpinRangeUnlimited();
}
template <typename NumericType>
bool SSpinBox<NumericType>::IsMaxSliderValueBound() const
{ return MaxSliderValue.IsBound(); }
template <typename NumericType>
NumericType SSpinBox<NumericType>::GetMaxSliderValue() const
{ return MaxSliderValue.Get().Get(std::numeric_limits<NumericType>::max()); }
template <typename NumericType>
void SSpinBox<NumericType>::SetMaxSliderValue(const TAttribute<TOptional<NumericType>>& InMaxSliderValue)
{
MaxSliderValue = (InMaxSliderValue.Get().IsSet()) ? InMaxSliderValue : MaxValue;;
UpdateIsSpinRangeUnlimited();
}
template <typename NumericType>
int32 SSpinBox<NumericType>::GetMinFractionalDigits() const
{ return InterfaceAttr.Get()->GetMinFractionalDigits(); }
template <typename NumericType>
void SSpinBox<NumericType>::SetMinFractionalDigits(const TAttribute<TOptional<int32>>& InMinFractionalDigits)
{
InterfaceAttr.Get()->SetMinFractionalDigits((InMinFractionalDigits.Get().IsSet()) ? InMinFractionalDigits.Get() : MinFractionalDigits);
bCachedValueStringDirty = true;
}
template <typename NumericType>
int32 SSpinBox<NumericType>::GetMaxFractionalDigits() const
{ return InterfaceAttr.Get()->GetMaxFractionalDigits(); }
template <typename NumericType>
void SSpinBox<NumericType>::SetMaxFractionalDigits(const TAttribute<TOptional<int32>>& InMaxFractionalDigits)
{
InterfaceAttr.Get()->SetMaxFractionalDigits((InMaxFractionalDigits.Get().IsSet()) ? InMaxFractionalDigits.Get() : MaxFractionalDigits);
bCachedValueStringDirty = true;
}
template <typename NumericType>
bool SSpinBox<NumericType>::GetAlwaysUsesDeltaSnap() const
{ return AlwaysUsesDeltaSnap.Get(); }
template <typename NumericType>
void SSpinBox<NumericType>::SetAlwaysUsesDeltaSnap(bool bNewValue)
{ AlwaysUsesDeltaSnap.Set(bNewValue); }
template <typename NumericType>
bool SSpinBox<NumericType>::GetEnableSlider() const
{ return EnableSlider.Get(); }
template <typename NumericType>
void SSpinBox<NumericType>::SetEnableSlider(bool bNewValue)
{ EnableSlider.Set(bNewValue); }
template <typename NumericType>
NumericType SSpinBox<NumericType>::GetDelta() const
{ return Delta.Get(); }
template <typename NumericType>
void SSpinBox<NumericType>::SetDelta(NumericType InDelta)
{ Delta.Set(InDelta); }
template <typename NumericType>
float SSpinBox<NumericType>::GetSliderExponent() const
{ return SliderExponent.Get(); }
template <typename NumericType>
void SSpinBox<NumericType>::SetSliderExponent(const TAttribute<float>& InSliderExponent)
{ SliderExponent = InSliderExponent; }
template <typename NumericType>
float SSpinBox<NumericType>::GetMinDesiredWidth() const
{ return MinDesiredWidth.Get(); }
template <typename NumericType>
void SSpinBox<NumericType>::SetMinDesiredWidth(const TAttribute<float>& InMinDesiredWidth)
{ MinDesiredWidth = InMinDesiredWidth; }
template <typename NumericType>
const FSpinBoxStyle* SSpinBox<NumericType>::GetWidgetStyle() const
{ return Style; }
template <typename NumericType>
void SSpinBox<NumericType>::SetWidgetStyle(const FSpinBoxStyle* InStyle)
{ Style = InStyle; }
template <typename NumericType>
void SSpinBox<NumericType>::InvalidateStyle()
{ Invalidate(EInvalidateWidgetReason::Layout); }
template <typename NumericType>
void SSpinBox<NumericType>::SetTextBlockFont(FSlateFontInfo InFont)
{ EditableText->SetFont(InFont); TextBlock->SetFont(InFont); }
template <typename NumericType>
void SSpinBox<NumericType>::SetTextJustification(ETextJustify::Type InJustification)
{ EditableText->SetJustification(InJustification); TextBlock->SetJustification(InJustification); }
template <typename NumericType>
void SSpinBox<NumericType>::SetTextClearKeyboardFocusOnCommit(bool bNewValue)
{ EditableText->SetClearKeyboardFocusOnCommit(bNewValue); }
template <typename NumericType>
void SSpinBox<NumericType>::SetTextSelectAllTextOnCommit(bool bNewValue)
{ EditableText->SetSelectAllTextOnCommit(bNewValue); }
template <typename NumericType>
void SSpinBox<NumericType>::SetTextRevertTextOnEscape(bool bNewValue)
{ EditableText->SetRevertTextOnEscape(bNewValue); }
template <typename NumericType>
void SSpinBox<NumericType>::EnterTextMode()
{
TextBlock->SetVisibility(EVisibility::Collapsed);
EditableText->SetVisibility(EVisibility::Visible);
}
template <typename NumericType>
void SSpinBox<NumericType>::ExitTextMode()
{
TextBlock->SetVisibility(EVisibility::Visible);
EditableText->SetVisibility(EVisibility::Collapsed);
}
template <typename NumericType>
FString SSpinBox<NumericType>::GetValueAsString() const
{
NumericType CurrentValue = ValueAttribute.Get();
if (!bCachedValueStringDirty && CurrentValue == CachedExternalValue)
{
return CachedValueString;
}
bCachedValueStringDirty = false;
return InterfaceAttr.Get()->ToString(CurrentValue);
}
template <typename NumericType>
FText SSpinBox<NumericType>::GetValueAsText() const
{
return FText::FromString(GetValueAsString());
}
template <typename NumericType>
FText SSpinBox<NumericType>::GetDisplayValue() const
{
if (OnGetDisplayValue.IsBound())
{
const TOptional<FText> OverrideValue = OnGetDisplayValue.Execute(ValueAttribute.Get());
if (OverrideValue.IsSet())
{
return OverrideValue.GetValue();
}
}
return FText::FromString(GetValueAsString());
}
template <typename NumericType>
void SSpinBox<NumericType>::TextField_OnTextChanged(const FText& NewText)
{
if (!bIsTextChanging)
{
TGuardValue<bool> TextChangedGuard(bIsTextChanging, true);
// Validate the text on change, and only accept text up until the first invalid character
FString Data = NewText.ToString();
int32 NumValidChars = Data.Len();
TSharedPtr<INumericTypeInterface<NumericType>> Interface = InterfaceAttr.Get();
for (int32 Index = 0; Index < Data.Len(); ++Index)
{
if (!Interface->IsCharacterValid(Data[Index]))
{
NumValidChars = Index;
break;
}
}
if (NumValidChars < Data.Len())
{
FString ValidData = NumValidChars > 0 ? Data.Left(NumValidChars) : GetValueAsString();
EditableText->SetEditableText(FText::FromString(ValidData));
}
// we check that the input is numeric, as we don't want to commit the new value on every change when an expression like *= is entered
if (bBroadcastValueChangesPerKey && FCString::IsNumeric(*Data))
{
TOptional<NumericType> NewValue = Interface->FromString(Data, ValueAttribute.Get());
if (NewValue.IsSet())
{
CommitValue(NewValue.GetValue(), static_cast<double>(NewValue.GetValue()), CommittedViaCode, ETextCommit::Default);
}
}
}
}
template <typename NumericType>
void SSpinBox<NumericType>::TextField_OnTextCommitted(const FText& NewText, ETextCommit::Type CommitInfo)
{
if (CommitInfo != ETextCommit::OnEnter)
{
ExitTextMode();
}
TSharedPtr<INumericTypeInterface<NumericType>> Interface = InterfaceAttr.Get();
TOptional<NumericType> NewValue = Interface->FromString(NewText.ToString(), ValueAttribute.Get());
if (NewValue.IsSet())
{
CommitValue(NewValue.GetValue(), (double)NewValue.GetValue(), CommittedViaTypeIn, CommitInfo);
}
}
template <typename NumericType>
void SSpinBox<NumericType>::CommitValue(NumericType NewValue, double NewSpinValue, ECommitMethod CommitMethod, ETextCommit::Type OriginalCommitInfo)
{
TRACE_CPUPROFILER_EVENT_SCOPE(SSpinBox_CommitValue);
if (CommitMethod == CommittedViaSpin || CommitMethod == CommittedViaArrowKey)
{
const NumericType LocalMinSliderValue = GetMinSliderValue();
const NumericType LocalMaxSliderValue = GetMaxSliderValue();
NewValue = FMath::Clamp<NumericType>(NewValue, LocalMinSliderValue, LocalMaxSliderValue);
NewSpinValue = FMath::Clamp<double>(NewSpinValue, (double)LocalMinSliderValue, (double)LocalMaxSliderValue);
}
{
const NumericType LocalMinValue = GetMinValue();
const NumericType LocalMaxValue = GetMaxValue();
NewValue = FMath::Clamp<NumericType>(NewValue, LocalMinValue, LocalMaxValue);
NewSpinValue = FMath::Clamp<double>(NewSpinValue, (double)LocalMinValue, (double)LocalMaxValue);
}
if (!ValueAttribute.IsBound())
{
ValueAttribute.Set(NewValue);
}
// If not in spin mode, there is no need to jump to the value from the external source, continue to use the committed value.
if (CommitMethod == CommittedViaSpin)
{
const NumericType CurrentValue = ValueAttribute.Get();
// This will detect if an external force has changed the value. Internally it will abandon the delta calculated this tick and update the internal value instead.
if (CurrentValue != CachedExternalValue)
{
NewValue = CurrentValue;
NewSpinValue = (double)CurrentValue;
}
}
// Update the internal value, this needs to be done before rounding.
InternalValue = NewSpinValue;
const bool bAlwaysUsesDeltaSnap = GetAlwaysUsesDeltaSnap();
// If needed, round this value to the delta. Internally the value is not held to the Delta but externally it appears to be.
if (CommitMethod == CommittedViaSpin || CommitMethod == CommittedViaArrowKey || bAlwaysUsesDeltaSnap)
{
NumericType CurrentDelta = Delta.Get();
if (CurrentDelta != NumericType())
{
NewValue = FMath::GridSnap<NumericType>(NewValue, CurrentDelta); // snap numeric point value to nearest Delta
}
}
// Update the max slider value based on the current value if we're in dynamic mode
if (SupportDynamicSliderMaxValue.Get() && ValueAttribute.Get() > GetMaxSliderValue())
{
ApplySliderMaxValueChanged(float(ValueAttribute.Get() - GetMaxSliderValue()), true);
}
else if (SupportDynamicSliderMinValue.Get() && ValueAttribute.Get() < GetMinSliderValue())
{
ApplySliderMinValueChanged(float(ValueAttribute.Get() - GetMinSliderValue()), true);
}
if (CommitMethod == CommittedViaTypeIn || CommitMethod == CommittedViaArrowKey)
{
OnValueCommitted.ExecuteIfBound(NewValue, OriginalCommitInfo);
}
OnValueChanged.ExecuteIfBound(NewValue);
if (!ValueAttribute.IsBound())
{
ValueAttribute.Set(NewValue);
}
// Update the cache of the external value to what the user believes the value is now.
const NumericType CurrentValue = ValueAttribute.Get();
if (CachedExternalValue != CurrentValue || bCachedValueStringDirty)
{
TSharedPtr<INumericTypeInterface<NumericType>> Interface = InterfaceAttr.Get();
CachedExternalValue = ValueAttribute.Get();
CachedValueString = Interface->ToString(CachedExternalValue);
bCachedValueStringDirty = false;
}
// This ensures that dragging is cleared if focus has been removed from this widget in one of the delegate calls, such as when spawning a modal dialog.
if (!this->HasMouseCapture())
{
bDragging = false;
PointerDraggingSliderIndex = INDEX_NONE;
}
}
template <typename NumericType>
void SSpinBox<NumericType>::NotifyValueCommitted(NumericType CurrentValue) const
{
// The internal value will have been clamped and rounded to the delta at this point, but integer values may still need to be rounded
// if the delta is 0.
OnValueCommitted.ExecuteIfBound(CurrentValue, ETextCommit::OnEnter);
OnEndSliderMovement.ExecuteIfBound(CurrentValue);
}
template <typename NumericType>
bool SSpinBox<NumericType>::IsInTextMode() const
{
return (EditableText->GetVisibility() == EVisibility::Visible);
}
template <typename NumericType>
float SSpinBox<NumericType>::Fraction(double InValue, double InMinValue, double InMaxValue)
{
const double HalfMax = InMaxValue * 0.5;
const double HalfMin = InMinValue * 0.5;
const double HalfVal = InValue * 0.5;
return (float)FMath::Clamp((HalfVal - HalfMin) / (HalfMax - HalfMin), 0.0, 1.0);
}
template <typename NumericType>
void SSpinBox<NumericType>::UpdateIsSpinRangeUnlimited()
{
bUnlimitedSpinRange = !((MinValue.Get().IsSet() && MaxValue.Get().IsSet()) || (MinSliderValue.Get().IsSet() && MaxSliderValue.Get().IsSet()));
}
template <typename NumericType>
float SSpinBox<NumericType>::GetTextMinDesiredWidth() const
{
return FMath::Max(0.0f, MinDesiredWidth.Get() - (float)Style->ArrowsImage.ImageSize.X);
}
template <typename NumericType>
bool SSpinBox<NumericType>::IsCharacterValid(TCHAR InChar) const
{
return InterfaceAttr.Get()->IsCharacterValid(InChar);
}
template <typename NumericType>
NumericType SSpinBox<NumericType>::RoundIfIntegerValue(double ValueToRound) const
{
constexpr bool bIsIntegral = TIsIntegral<NumericType>::Value;
constexpr bool bCanBeRepresentedInDouble = std::numeric_limits<double>::digits >= std::numeric_limits<NumericType>::digits;
if (bIsIntegral && !bCanBeRepresentedInDouble)
{
return (NumericType)FMath::Clamp<double>(FMath::FloorToDouble(ValueToRound + 0.5), -1.0 * (double)(1ll << std::numeric_limits<double>::digits), (double)(1ll << std::numeric_limits<double>::digits));
}
else if (bIsIntegral)
{
return (NumericType)FMath::Clamp<double>(FMath::FloorToDouble(ValueToRound + 0.5), (double)std::numeric_limits<NumericType>::lowest(), (double)std::numeric_limits<NumericType>::max());
}
else
{
return (NumericType)FMath::Clamp<double>(ValueToRound, (double)std::numeric_limits<NumericType>::lowest(), (double)std::numeric_limits<NumericType>::max());
}
}
template <typename NumericType>
void SSpinBox<NumericType>::CancelMouseCapture()
{
bDragging = false;
PointerDraggingSliderIndex = INDEX_NONE;
InternalValue = (double)PreDragValue;
NotifyValueCommitted(PreDragValue);
}
template<typename NumericType>
double SSpinBox<NumericType>::GetDefaultStepSize(const FInputEvent& InputEvent)
{
const bool bIsSmallStep = (GetMaxSliderValue() - GetMinSliderValue()) <= SmallStepSizeMax;
double Step = bIsSmallStep ? SmallStepSize : StepSize;
if (InputEvent.IsControlDown())
{
Step *= CtrlMultiplier.Get();
}
else if (InputEvent.IsShiftDown())
{
Step *= ShiftMultiplier.Get();
}
return Step;
}
template <typename NumericType>
void SSpinBox<NumericType>::ResetCachedValueString()
{
const NumericType CurrentValue = ValueAttribute.Get();
CachedExternalValue = CurrentValue;
CachedValueString = InterfaceAttr.Get()->ToString(CachedExternalValue);
}
// Explicit instantiation of the numeric types valid for this template
template class SSpinBox<double>;
template class SSpinBox<float>;
template class SSpinBox<uint64>;
template class SSpinBox<uint32>;
template class SSpinBox<uint16>;
template class SSpinBox<uint8>;
template class SSpinBox<int64>;
template class SSpinBox<int32>;
template class SSpinBox<int16>;
template class SSpinBox<int8>;
// template struct SSpinBox<double>::FArguments;
// template struct SSpinBox<float>::FArguments;
// template struct SSpinBox<uint64>::FArguments;
// template struct SSpinBox<uint32>::FArguments;
// template struct SSpinBox<uint16>::FArguments;
// template struct SSpinBox<uint8>::FArguments;
// template struct SSpinBox<int64>::FArguments;
// template struct SSpinBox<int32>::FArguments;
// template struct SSpinBox<int16>::FArguments;
// template struct SSpinBox<int8>::FArguments;