// 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 SSpinBox::SSpinBox() { } template SSpinBox::~SSpinBox() { if (bDragging || PointerDraggingSliderIndex != INDEX_NONE) { CancelMouseCapture(); } } template void SSpinBox::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>(); } TSharedPtr> 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::GetDisplayValue) .MinDesiredWidth(this, &SSpinBox::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::GetValueAsText) .RevertTextOnEscape(InArgs._RevertTextOnEscape) .OnIsTypedCharValid(this, &SSpinBox::IsCharacterValid) .OnTextChanged(this, &SSpinBox::TextField_OnTextChanged) .OnTextCommitted(this, &SSpinBox::TextField_OnTextCommitted) .ClearKeyboardFocusOnCommit(InArgs._ClearKeyboardFocusOnCommit) .SelectAllTextOnCommit(InArgs._SelectAllTextOnCommit) .MinDesiredWidth(this, &SSpinBox::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 int32 SSpinBox::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(), Style->InsetPadding.GetTotalSpaceAlong()), 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 void SSpinBox::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 const bool SSpinBox::CommitWithMultiplier(const FPointerEvent& MouseEvent) { return MouseEvent.IsShiftDown() || MouseEvent.IsControlDown(); } template FReply SSpinBox::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 FReply SSpinBox::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 void SSpinBox::ApplySliderMaxValueChanged(float SliderDeltaToAdd, bool UpdateOnlyIfHigher) { check(SupportDynamicSliderMaxValue.Get()); NumericType NewMaxSliderValue = std::numeric_limits::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(AsShared()), true, UpdateOnlyIfHigher); } } template void SSpinBox::ApplySliderMinValueChanged(float SliderDeltaToAdd, bool UpdateOnlyIfLower) { check(SupportDynamicSliderMaxValue.Get()); NumericType NewMinSliderValue = std::numeric_limits::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(AsShared()), true, UpdateOnlyIfLower); } } template FReply SSpinBox::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)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(FMath::Abs(InternalValue), 1.0, (double)std::numeric_limits::max()); NewValue = InternalValue + (Sign * MouseDelta * FMath::Pow((double)CurrentValue, (double)SliderExponent.Get())) * Step; } } if (SpinBoxPrivate::bUseSpinBoxMouseMoveOptimization) { if (CommitMethod == ECommitMethod::CommittedViaSpin) { NewValue = FMath::Clamp(NewValue, (double)GetMinSliderValue(), (double)GetMaxSliderValue()); } NewValue = FMath::Clamp(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 FReply SSpinBox::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::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> 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 FCursorReply SSpinBox::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 bool SSpinBox::SupportsKeyboardFocus() const { // SSpinBox is focusable. return true; } template FReply SSpinBox::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 FReply SSpinBox::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 bool SSpinBox::HasKeyboardFocus() const { // The spinbox is considered focused when we are typing it text. return SCompoundWidget::HasKeyboardFocus() || (EditableText.IsValid() && EditableText->HasKeyboardFocus()); } template TAttribute SSpinBox::GetValueAttribute() const { return ValueAttribute; } template NumericType SSpinBox::GetValue() const { return ValueAttribute.Get(); } template void SSpinBox::SetValue(const TAttribute& InValueAttribute) { ValueAttribute = InValueAttribute; const NumericType LocalValueAttribute = ValueAttribute.Get(); CommitValue(LocalValueAttribute, (double)LocalValueAttribute, ECommitMethod::CommittedViaCode, ETextCommit::Default); } template NumericType SSpinBox::GetMinValue() const { return MinValue.Get().Get(std::numeric_limits::lowest()); } template void SSpinBox::SetMinValue(const TAttribute>& InMinValue) { MinValue = InMinValue; UpdateIsSpinRangeUnlimited(); } template NumericType SSpinBox::GetMaxValue() const { return MaxValue.Get().Get(std::numeric_limits::max()); } template void SSpinBox::SetMaxValue(const TAttribute>& InMaxValue) { MaxValue = InMaxValue; UpdateIsSpinRangeUnlimited(); } template bool SSpinBox::IsMinSliderValueBound() const { return MinSliderValue.IsBound(); } template NumericType SSpinBox::GetMinSliderValue() const { return MinSliderValue.Get().Get(std::numeric_limits::lowest()); } template void SSpinBox::SetMinSliderValue(const TAttribute>& InMinSliderValue) { MinSliderValue = (InMinSliderValue.Get().IsSet()) ? InMinSliderValue : MinValue; UpdateIsSpinRangeUnlimited(); } template bool SSpinBox::IsMaxSliderValueBound() const { return MaxSliderValue.IsBound(); } template NumericType SSpinBox::GetMaxSliderValue() const { return MaxSliderValue.Get().Get(std::numeric_limits::max()); } template void SSpinBox::SetMaxSliderValue(const TAttribute>& InMaxSliderValue) { MaxSliderValue = (InMaxSliderValue.Get().IsSet()) ? InMaxSliderValue : MaxValue;; UpdateIsSpinRangeUnlimited(); } template int32 SSpinBox::GetMinFractionalDigits() const { return InterfaceAttr.Get()->GetMinFractionalDigits(); } template void SSpinBox::SetMinFractionalDigits(const TAttribute>& InMinFractionalDigits) { InterfaceAttr.Get()->SetMinFractionalDigits((InMinFractionalDigits.Get().IsSet()) ? InMinFractionalDigits.Get() : MinFractionalDigits); bCachedValueStringDirty = true; } template int32 SSpinBox::GetMaxFractionalDigits() const { return InterfaceAttr.Get()->GetMaxFractionalDigits(); } template void SSpinBox::SetMaxFractionalDigits(const TAttribute>& InMaxFractionalDigits) { InterfaceAttr.Get()->SetMaxFractionalDigits((InMaxFractionalDigits.Get().IsSet()) ? InMaxFractionalDigits.Get() : MaxFractionalDigits); bCachedValueStringDirty = true; } template bool SSpinBox::GetAlwaysUsesDeltaSnap() const { return AlwaysUsesDeltaSnap.Get(); } template void SSpinBox::SetAlwaysUsesDeltaSnap(bool bNewValue) { AlwaysUsesDeltaSnap.Set(bNewValue); } template bool SSpinBox::GetEnableSlider() const { return EnableSlider.Get(); } template void SSpinBox::SetEnableSlider(bool bNewValue) { EnableSlider.Set(bNewValue); } template NumericType SSpinBox::GetDelta() const { return Delta.Get(); } template void SSpinBox::SetDelta(NumericType InDelta) { Delta.Set(InDelta); } template float SSpinBox::GetSliderExponent() const { return SliderExponent.Get(); } template void SSpinBox::SetSliderExponent(const TAttribute& InSliderExponent) { SliderExponent = InSliderExponent; } template float SSpinBox::GetMinDesiredWidth() const { return MinDesiredWidth.Get(); } template void SSpinBox::SetMinDesiredWidth(const TAttribute& InMinDesiredWidth) { MinDesiredWidth = InMinDesiredWidth; } template const FSpinBoxStyle* SSpinBox::GetWidgetStyle() const { return Style; } template void SSpinBox::SetWidgetStyle(const FSpinBoxStyle* InStyle) { Style = InStyle; } template void SSpinBox::InvalidateStyle() { Invalidate(EInvalidateWidgetReason::Layout); } template void SSpinBox::SetTextBlockFont(FSlateFontInfo InFont) { EditableText->SetFont(InFont); TextBlock->SetFont(InFont); } template void SSpinBox::SetTextJustification(ETextJustify::Type InJustification) { EditableText->SetJustification(InJustification); TextBlock->SetJustification(InJustification); } template void SSpinBox::SetTextClearKeyboardFocusOnCommit(bool bNewValue) { EditableText->SetClearKeyboardFocusOnCommit(bNewValue); } template void SSpinBox::SetTextSelectAllTextOnCommit(bool bNewValue) { EditableText->SetSelectAllTextOnCommit(bNewValue); } template void SSpinBox::SetTextRevertTextOnEscape(bool bNewValue) { EditableText->SetRevertTextOnEscape(bNewValue); } template void SSpinBox::EnterTextMode() { TextBlock->SetVisibility(EVisibility::Collapsed); EditableText->SetVisibility(EVisibility::Visible); } template void SSpinBox::ExitTextMode() { TextBlock->SetVisibility(EVisibility::Visible); EditableText->SetVisibility(EVisibility::Collapsed); } template FString SSpinBox::GetValueAsString() const { NumericType CurrentValue = ValueAttribute.Get(); if (!bCachedValueStringDirty && CurrentValue == CachedExternalValue) { return CachedValueString; } bCachedValueStringDirty = false; return InterfaceAttr.Get()->ToString(CurrentValue); } template FText SSpinBox::GetValueAsText() const { return FText::FromString(GetValueAsString()); } template FText SSpinBox::GetDisplayValue() const { if (OnGetDisplayValue.IsBound()) { const TOptional OverrideValue = OnGetDisplayValue.Execute(ValueAttribute.Get()); if (OverrideValue.IsSet()) { return OverrideValue.GetValue(); } } return FText::FromString(GetValueAsString()); } template void SSpinBox::TextField_OnTextChanged(const FText& NewText) { if (!bIsTextChanging) { TGuardValue 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> 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 NewValue = Interface->FromString(Data, ValueAttribute.Get()); if (NewValue.IsSet()) { CommitValue(NewValue.GetValue(), static_cast(NewValue.GetValue()), CommittedViaCode, ETextCommit::Default); } } } } template void SSpinBox::TextField_OnTextCommitted(const FText& NewText, ETextCommit::Type CommitInfo) { if (CommitInfo != ETextCommit::OnEnter) { ExitTextMode(); } TSharedPtr> Interface = InterfaceAttr.Get(); TOptional NewValue = Interface->FromString(NewText.ToString(), ValueAttribute.Get()); if (NewValue.IsSet()) { CommitValue(NewValue.GetValue(), (double)NewValue.GetValue(), CommittedViaTypeIn, CommitInfo); } } template void SSpinBox::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(NewValue, LocalMinSliderValue, LocalMaxSliderValue); NewSpinValue = FMath::Clamp(NewSpinValue, (double)LocalMinSliderValue, (double)LocalMaxSliderValue); } { const NumericType LocalMinValue = GetMinValue(); const NumericType LocalMaxValue = GetMaxValue(); NewValue = FMath::Clamp(NewValue, LocalMinValue, LocalMaxValue); NewSpinValue = FMath::Clamp(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(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> 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 void SSpinBox::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 bool SSpinBox::IsInTextMode() const { return (EditableText->GetVisibility() == EVisibility::Visible); } template float SSpinBox::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 void SSpinBox::UpdateIsSpinRangeUnlimited() { bUnlimitedSpinRange = !((MinValue.Get().IsSet() && MaxValue.Get().IsSet()) || (MinSliderValue.Get().IsSet() && MaxSliderValue.Get().IsSet())); } template float SSpinBox::GetTextMinDesiredWidth() const { return FMath::Max(0.0f, MinDesiredWidth.Get() - (float)Style->ArrowsImage.ImageSize.X); } template bool SSpinBox::IsCharacterValid(TCHAR InChar) const { return InterfaceAttr.Get()->IsCharacterValid(InChar); } template NumericType SSpinBox::RoundIfIntegerValue(double ValueToRound) const { constexpr bool bIsIntegral = TIsIntegral::Value; constexpr bool bCanBeRepresentedInDouble = std::numeric_limits::digits >= std::numeric_limits::digits; if (bIsIntegral && !bCanBeRepresentedInDouble) { return (NumericType)FMath::Clamp(FMath::FloorToDouble(ValueToRound + 0.5), -1.0 * (double)(1ll << std::numeric_limits::digits), (double)(1ll << std::numeric_limits::digits)); } else if (bIsIntegral) { return (NumericType)FMath::Clamp(FMath::FloorToDouble(ValueToRound + 0.5), (double)std::numeric_limits::lowest(), (double)std::numeric_limits::max()); } else { return (NumericType)FMath::Clamp(ValueToRound, (double)std::numeric_limits::lowest(), (double)std::numeric_limits::max()); } } template void SSpinBox::CancelMouseCapture() { bDragging = false; PointerDraggingSliderIndex = INDEX_NONE; InternalValue = (double)PreDragValue; NotifyValueCommitted(PreDragValue); } template double SSpinBox::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 void SSpinBox::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; template class SSpinBox; template class SSpinBox; template class SSpinBox; template class SSpinBox; template class SSpinBox; template class SSpinBox; template class SSpinBox; template class SSpinBox; template class SSpinBox; // template struct SSpinBox::FArguments; // template struct SSpinBox::FArguments; // template struct SSpinBox::FArguments; // template struct SSpinBox::FArguments; // template struct SSpinBox::FArguments; // template struct SSpinBox::FArguments; // template struct SSpinBox::FArguments; // template struct SSpinBox::FArguments; // template struct SSpinBox::FArguments; // template struct SSpinBox::FArguments;