// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "Misc/Attribute.h" #include "InputCoreTypes.h" #include "Layout/Margin.h" #include "Fonts/SlateFontInfo.h" #include "Layout/Visibility.h" #include "Widgets/SNullWidget.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Styling/SlateColor.h" #include "Input/Events.h" #include "Input/Reply.h" #include "Widgets/SWidget.h" #include "Widgets/SCompoundWidget.h" #include "Styling/CoreStyle.h" #include "Widgets/Input/NumericTypeInterface.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Layout/SBorder.h" #include "Styling/SlateTypes.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Input/SEditableText.h" #include "Widgets/Input/SSpinBox.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/Colors/SColorBlock.h" #include "Containers/UnrealString.h" /** * Implementation for a box that only accepts a numeric value or that can display an undetermined value via a string * Supports an optional spin box for manipulating a value by dragging with the mouse * Supports an optional label inset in the text box */ template class SNumericEntryBox : public SCompoundWidget { public: static const FLinearColor RedLabelBackgroundColor; static const FLinearColor GreenLabelBackgroundColor; static const FLinearColor BlueLabelBackgroundColor; static const FLinearColor LilacLabelBackgroundColor; static const FText DefaultUndeterminedString; /** Notification for numeric value change */ DECLARE_DELEGATE_OneParam(FOnValueChanged, NumericType /*NewValue*/); /** Notification for numeric value committed */ DECLARE_DELEGATE_TwoParams(FOnValueCommitted, NumericType /*NewValue*/, ETextCommit::Type /*CommitType*/); /** Notification for change of undetermined values */ DECLARE_DELEGATE_OneParam(FOnUndeterminedValueChanged, FText /*NewValue*/ ); /** Notification for committing undetermined values */ DECLARE_DELEGATE_TwoParams(FOnUndeterminedValueCommitted, FText /*NewValue*/, ETextCommit::Type /*CommitType*/); /** Notification when the max/min spinner values are changed (only apply if SupportDynamicSliderMaxValue or SupportDynamicSliderMinValue are true) */ DECLARE_DELEGATE_FourParams(FOnDynamicSliderMinMaxValueChanged, NumericType, TWeakPtr, bool, bool); enum class ELabelLocation { // Outside the bounds of the editable area of this box. Usually preferred for text based labels Outside, // Inside the bounds of the editable area of this box. Usually preferred for non-text based labels // when a spin box is used the label will appear on top of the spin box in this case Inside }; public: SLATE_BEGIN_ARGS( SNumericEntryBox ) : _EditableTextBoxStyle( &FAppStyle::Get().GetWidgetStyle("NormalEditableTextBox") ) , _SpinBoxStyle(&FAppStyle::Get().GetWidgetStyle("NumericEntrySpinBox") ) , _Label() , _LabelVAlign( VAlign_Fill ) , _Justification(ETextJustify::Left) , _LabelLocation(ELabelLocation::Outside) , _LabelPadding(FMargin(3.f,0.f) ) , _BorderForegroundColor(FAppStyle::Get().GetWidgetStyle("NumericEntrySpinBox").ForegroundColor) , _BorderBackgroundColor(FLinearColor::White) , _UndeterminedString( SNumericEntryBox::DefaultUndeterminedString ) , _AllowSpin(false) , _ShiftMultiplier(10.f) , _CtrlMultiplier(0.1f) , _SupportDynamicSliderMaxValue(false) , _SupportDynamicSliderMinValue(false) , _Delta(NumericType(0)) , _MinFractionalDigits(DefaultMinFractionalDigits) , _MaxFractionalDigits(DefaultMaxFractionalDigits) , _MinValue(TNumericLimits::Lowest()) , _MaxValue(TNumericLimits::Max()) , _MinSliderValue(NumericType(0)) , _MaxSliderValue(NumericType(100)) , _SliderExponent(1.f) , _AllowWheel(true) , _PreventThrottling(false) , _BroadcastValueChangesPerKey(false) , _MinDesiredValueWidth(0.f) , _DisplayToggle(false) , _ToggleChecked(ECheckBoxState::Checked) , _TogglePadding(FMargin(1.f,0.f,1.f,0.f) ) , _ToolTipTextFormat(TOptional()) {} /** Style to use for the editable text box within this widget */ SLATE_STYLE_ARGUMENT( FEditableTextBoxStyle, EditableTextBoxStyle ) /** Style to use for the spin box within this widget */ SLATE_STYLE_ARGUMENT( FSpinBoxStyle, SpinBoxStyle ) /** Slot for this button's content (optional) */ SLATE_NAMED_SLOT( FArguments, Label ) /** Vertical alignment of the label content */ SLATE_ARGUMENT( EVerticalAlignment, LabelVAlign ) /** How should the value be justified in the editable text field. */ SLATE_ATTRIBUTE(ETextJustify::Type, Justification) SLATE_ARGUMENT(ELabelLocation, LabelLocation) /** Padding around the label content */ SLATE_ARGUMENT( FMargin, LabelPadding ) /** Border Foreground Color */ SLATE_ARGUMENT( FSlateColor, BorderForegroundColor ) /** Border Background Color */ SLATE_ARGUMENT( FSlateColor, BorderBackgroundColor ) /** The value that should be displayed. This value is optional in the case where a value cannot be determined */ SLATE_ATTRIBUTE( TOptional, Value ) /** The string to display if the value cannot be determined */ SLATE_ARGUMENT( FText, UndeterminedString ) /** Font color and opacity */ SLATE_ATTRIBUTE( FSlateFontInfo, Font ) /** Whether or not the user should be able to change the value by dragging with the mouse cursor */ SLATE_ARGUMENT( bool, AllowSpin ) SLATE_ATTRIBUTE_DEPRECATED( int32, ShiftMouseMovePixelPerDelta, 5.4, "Shift Mouse Move Pixel Per Delta is deprecated, please use ShiftMultiplier" ) /** Multiplier to use when shift is held down */ SLATE_ATTRIBUTE( float, ShiftMultiplier ) /** Multiplier to use when ctrl is held down */ SLATE_ATTRIBUTE( float, CtrlMultiplier ) /** If we're an unbounded spinbox, what value do we divide mouse movement by before multiplying by Delta. Requires Delta to be set. */ SLATE_ATTRIBUTE( int32, LinearDeltaSensitivity) /** Tell us if we want to support dynamically changing of the max value using ctrl (only use if there is a spinbox allow) */ SLATE_ATTRIBUTE(bool, SupportDynamicSliderMaxValue) /** Tell us if we want to support dynamically changing of the min value using ctrl (only use if there is a spinbox allow) */ SLATE_ATTRIBUTE(bool, SupportDynamicSliderMinValue) /** Called right after the spinner max value is changed (only relevant if SupportDynamicSliderMaxValue is true) */ SLATE_EVENT(FOnDynamicSliderMinMaxValueChanged, OnDynamicSliderMaxValueChanged) /** Called right after the spinner min value is changed (only relevant if SupportDynamicSliderMinValue is true) */ SLATE_EVENT(FOnDynamicSliderMinMaxValueChanged, OnDynamicSliderMinValueChanged) /** Delta to increment the value as the slider moves. If not specified will determine automatically */ SLATE_ATTRIBUTE( NumericType, Delta ) /** The minimum fractional digits the spin box displays, defaults to 1 */ SLATE_ATTRIBUTE(TOptional< int32 >, MinFractionalDigits) /** The maximum fractional digits the spin box displays, defaults to 6 */ SLATE_ATTRIBUTE(TOptional< int32 >, MaxFractionalDigits) /** The minimum value that can be entered into the text edit box */ SLATE_ATTRIBUTE( TOptional< NumericType >, MinValue ) /** The maximum value that can be entered into the text edit box */ SLATE_ATTRIBUTE( TOptional< NumericType >, MaxValue ) /** The minimum value that can be specified by using the slider */ SLATE_ATTRIBUTE( TOptional< NumericType >, MinSliderValue ) /** The maximum value that can be specified by using the slider */ SLATE_ATTRIBUTE( TOptional< NumericType >, MaxSliderValue ) /** Use exponential scale for the slider */ SLATE_ATTRIBUTE( float, SliderExponent ) /** When using exponential scale specify a neutral value where we want the maximum precision (by default it is the smallest slider value)*/ SLATE_ATTRIBUTE(NumericType, SliderExponentNeutralValue ) /** Whether this spin box should have mouse wheel feature enabled, defaults to true */ SLATE_ARGUMENT( bool, AllowWheel ) /** If refresh requests for the viewport should happen for all value changes **/ SLATE_ARGUMENT(bool, PreventThrottling) /** True to broadcast every time we type */ SLATE_ARGUMENT( bool, BroadcastValueChangesPerKey) /** Step to increment or decrement the value by when scrolling the mouse wheel. If not specified will determine automatically */ SLATE_ATTRIBUTE( TOptional< NumericType >, WheelStep ) /** The minimum desired width for the value portion of the control. */ SLATE_ATTRIBUTE( float, MinDesiredValueWidth ) /** The text margin to use if overridden. */ SLATE_ATTRIBUTE( FMargin, OverrideTextMargin ) /** Called whenever the text is changed programmatically or interactively by the user */ SLATE_EVENT( FOnValueChanged, OnValueChanged ) /** Called whenever the text is committed. This happens when the user presses enter or the text box loses focus. */ SLATE_EVENT( FOnValueCommitted, OnValueCommitted ) /** Called whenever the text is changed programmatically or interactively by the user */ SLATE_EVENT( FOnUndeterminedValueChanged, OnUndeterminedValueChanged ) /** Called whenever the text is committed. This happens when the user presses enter or the text box loses focus. */ SLATE_EVENT( FOnUndeterminedValueCommitted, OnUndeterminedValueCommitted ) /** Called right before the slider begins to move */ SLATE_EVENT( FSimpleDelegate, OnBeginSliderMovement ) /** Called right after the slider handle is released by the user */ SLATE_EVENT( FOnValueChanged, OnEndSliderMovement ) /** Menu extender for right-click context menu */ SLATE_EVENT( FMenuExtensionDelegate, ContextMenuExtender ) /** Provide custom type conversion functionality to this spin box */ SLATE_ARGUMENT( TSharedPtr< INumericTypeInterface >, TypeInterface ) /** Whether or not to include a toggle checkbox to the left of the widget */ SLATE_ARGUMENT( bool, DisplayToggle ) /** The value of the toggle checkbox */ SLATE_ATTRIBUTE( ECheckBoxState, ToggleChecked ) /** Called whenever the toggle changes state */ SLATE_EVENT( FOnCheckStateChanged, OnToggleChanged ) /** Padding around the toggle checkbox */ SLATE_ARGUMENT( FMargin, TogglePadding ) /** An optional format pattern that is used to format the ToolTipText. If set, this is used with a single argument ({0} = the display value). */ SLATE_ATTRIBUTE(TOptional, ToolTipTextFormat) SLATE_END_ARGS() SNumericEntryBox() { } void Construct( const FArguments& InArgs ) { check(InArgs._EditableTextBoxStyle); OnValueChanged = InArgs._OnValueChanged; OnValueCommitted = InArgs._OnValueCommitted; OnUndeterminedValueChanged = InArgs._OnUndeterminedValueChanged; OnUndeterminedValueCommitted = InArgs._OnUndeterminedValueCommitted; ValueAttribute = InArgs._Value; UndeterminedString = InArgs._UndeterminedString; MinDesiredValueWidth = InArgs._MinDesiredValueWidth; BorderImageNormal = &InArgs._EditableTextBoxStyle->BackgroundImageNormal; BorderImageHovered = &InArgs._EditableTextBoxStyle->BackgroundImageHovered; BorderImageFocused = &InArgs._EditableTextBoxStyle->BackgroundImageFocused; Interface = InArgs._TypeInterface.IsValid() ? InArgs._TypeInterface : MakeShareable( new TDefaultNumericTypeInterface ); if (InArgs._TypeInterface.IsValid() && Interface->GetOnSettingChanged()) { Interface->GetOnSettingChanged()->AddSP(this, &SNumericEntryBox::ResetCachedValueString); } MinFractionalDigits = (InArgs._MinFractionalDigits.Get().IsSet()) ? InArgs._MinFractionalDigits : DefaultMinFractionalDigits; MaxFractionalDigits = (InArgs._MaxFractionalDigits.Get().IsSet()) ? InArgs._MaxFractionalDigits : DefaultMaxFractionalDigits; SetMinFractionalDigits(MinFractionalDigits); SetMaxFractionalDigits(MaxFractionalDigits); ToolTipTextFormat = InArgs._ToolTipTextFormat; CachedExternalValue = ValueAttribute.Get(); if (CachedExternalValue.IsSet()) { CachedValueString = Interface->ToString(CachedExternalValue.GetValue()); } bCachedValueStringDirty = false; const bool bDisplayToggle = InArgs._DisplayToggle; if(bDisplayToggle) { SAssignNew(ToggleCheckBox, SCheckBox) .Padding(FMargin(0.f, 0.f, 2.f, 0.f)) .IsChecked(InArgs._ToggleChecked) .OnCheckStateChanged(this, &SNumericEntryBox::HandleToggleCheckBoxChanged, InArgs._OnToggleChanged); } const bool bAllowSpin = InArgs._AllowSpin; TSharedPtr FinalWidget; if( bAllowSpin ) { SAssignNew(SpinBox, SSpinBox) .Style(InArgs._SpinBoxStyle) .Font(InArgs._Font.IsSet() ? InArgs._Font : InArgs._EditableTextBoxStyle->TextStyle.Font) .Value(this, &SNumericEntryBox::OnGetValueForSpinBox) .Delta(InArgs._Delta) .ShiftMultiplier(InArgs._ShiftMultiplier) .CtrlMultiplier(InArgs._CtrlMultiplier) .LinearDeltaSensitivity(InArgs._LinearDeltaSensitivity) .SupportDynamicSliderMaxValue(InArgs._SupportDynamicSliderMaxValue) .SupportDynamicSliderMinValue(InArgs._SupportDynamicSliderMinValue) .OnDynamicSliderMaxValueChanged(InArgs._OnDynamicSliderMaxValueChanged) .OnDynamicSliderMinValueChanged(InArgs._OnDynamicSliderMinValueChanged) .OnValueChanged(OnValueChanged) .OnValueCommitted(OnValueCommitted) .MinFractionalDigits(MinFractionalDigits) .MaxFractionalDigits(MaxFractionalDigits) .MinSliderValue(InArgs._MinSliderValue) .MaxSliderValue(InArgs._MaxSliderValue) .MaxValue(InArgs._MaxValue) .MinValue(InArgs._MinValue) .ContextMenuExtender(InArgs._ContextMenuExtender) .SliderExponent(InArgs._SliderExponent) .SliderExponentNeutralValue(InArgs._SliderExponentNeutralValue) .EnableWheel(InArgs._AllowWheel) .PreventThrottling(InArgs._PreventThrottling) .BroadcastValueChangesPerKey(InArgs._BroadcastValueChangesPerKey) .WheelStep(InArgs._WheelStep) .OnBeginSliderMovement(InArgs._OnBeginSliderMovement) .OnEndSliderMovement(InArgs._OnEndSliderMovement) .MinDesiredWidth(InArgs._MinDesiredValueWidth) .TypeInterface(Interface) .ToolTipText(this, &SNumericEntryBox::GetToolTipText); } // Always create an editable text box. In the case of an undetermined value being passed in, we cant use the spinbox. SAssignNew(EditableText, SEditableText) .Text(this, &SNumericEntryBox::OnGetValueForTextBox) .ToolTipText(this, &SNumericEntryBox::GetToolTipText) .ColorAndOpacity(InArgs._EditableTextBoxStyle->ForegroundColor) .Visibility(bAllowSpin ? EVisibility::Collapsed : EVisibility::Visible) .Font(InArgs._Font.IsSet() ? InArgs._Font : InArgs._EditableTextBoxStyle->TextStyle.Font) .SelectAllTextWhenFocused(true) .ClearKeyboardFocusOnCommit(false) .OnTextChanged(this, &SNumericEntryBox::OnTextChanged) .OnTextCommitted(this, &SNumericEntryBox::OnTextCommitted) .SelectAllTextOnCommit(true) .ContextMenuExtender(InArgs._ContextMenuExtender) .Justification(InArgs._Justification) .MinDesiredWidth(InArgs._MinDesiredValueWidth); TSharedRef Overlay = SNew(SOverlay); // Add the spin box if we have one if( bAllowSpin ) { Overlay->AddSlot() .HAlign(HAlign_Fill) .VAlign(VAlign_Center) [ SpinBox.ToSharedRef() ]; } TAttribute TextMargin = InArgs._OverrideTextMargin.IsSet() ? InArgs._OverrideTextMargin : InArgs._EditableTextBoxStyle->Padding; Overlay->AddSlot() .HAlign(HAlign_Fill) .VAlign(VAlign_Center) .Padding(TextMargin) [ EditableText.ToSharedRef() ]; TSharedPtr MainContents; const bool bHasLabel = InArgs._Label.Widget != SNullWidget::NullWidget; bool bHasInsideLabel = false; if (bHasLabel && InArgs._LabelLocation == ELabelLocation::Inside) { bHasInsideLabel = true; Overlay->AddSlot() .HAlign(HAlign_Left) .VAlign(InArgs._LabelVAlign) .Padding(InArgs._LabelPadding) [ InArgs._Label.Widget ]; } if (bAllowSpin && !bHasInsideLabel) { MainContents = Overlay; } else { MainContents = SNew(SBorder) .BorderImage(this, &SNumericEntryBox::GetBorderImage) .BorderBackgroundColor(InArgs._BorderBackgroundColor) .ForegroundColor(InArgs._BorderForegroundColor) .Padding(0.f) [ Overlay ]; } if(!bHasLabel || bHasInsideLabel) { if(bDisplayToggle) { ChildSlot [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .HAlign(HAlign_Fill) .VAlign(VAlign_Center) .Padding(InArgs._TogglePadding) [ ToggleCheckBox.ToSharedRef() ] + SHorizontalBox::Slot() [ MainContents.ToSharedRef() ] ]; } else { ChildSlot [ MainContents.ToSharedRef() ]; } } else { TSharedRef HorizontalBox = SNew(SHorizontalBox); HorizontalBox->AddSlot() .AutoWidth() .HAlign(HAlign_Left) .VAlign(InArgs._LabelVAlign) .Padding(InArgs._LabelPadding) [ InArgs._Label.Widget ]; if(bDisplayToggle) { HorizontalBox->AddSlot() .AutoWidth() .HAlign(HAlign_Fill) .VAlign(VAlign_Center) .Padding(InArgs._TogglePadding) [ ToggleCheckBox.ToSharedRef() ]; } HorizontalBox->AddSlot() [ MainContents.ToSharedRef() ]; ChildSlot [ HorizontalBox ]; } if (bDisplayToggle) { HandleToggleCheckBoxChanged(InArgs._ToggleChecked.Get(), FOnCheckStateChanged()); } } static TSharedRef BuildLabel(TAttribute LabelText, const FSlateColor& ForegroundColor, const FSlateColor& BackgroundColor) { return SNew(SBorder) .Visibility(EVisibility::HitTestInvisible) .BorderImage(FCoreStyle::Get().GetBrush("NumericEntrySpinBox.Decorator")) .BorderBackgroundColor(BackgroundColor) .ForegroundColor(ForegroundColor) .VAlign(VAlign_Center) .HAlign(HAlign_Left) .Padding(FMargin(1.f, 0.f, 6.f, 0.f)) [ SNew(STextBlock) .Text(LabelText) ]; } static TSharedRef BuildNarrowColorLabel(FLinearColor LabelColor) { return SNew(SBorder) .Visibility(EVisibility::HitTestInvisible) .BorderImage(FAppStyle::Get().GetBrush("NumericEntrySpinBox.NarrowDecorator")) .BorderBackgroundColor(LabelColor) .HAlign(HAlign_Left) .Padding(FMargin(2.0f, 0.0f, 0.0f, 0.0f)); } /** Return the internally created SpinBox if bAllowSpin is true */ TSharedPtr GetSpinBox() const { return SpinBox; } /** See the MinFractionalDigits attribute */ int32 GetMinFractionalDigits() const { return Interface->GetMinFractionalDigits(); } void SetMinFractionalDigits(const TAttribute>& InMinFractionalDigits) { Interface->SetMinFractionalDigits((InMinFractionalDigits.Get().IsSet()) ? InMinFractionalDigits.Get() : MinFractionalDigits); bCachedValueStringDirty = true; } /** See the MaxFractionalDigits attribute */ int32 GetMaxFractionalDigits() const { return Interface->GetMaxFractionalDigits(); } void SetMaxFractionalDigits(const TAttribute>& InMaxFractionalDigits) { Interface->SetMaxFractionalDigits((InMaxFractionalDigits.Get().IsSet()) ? InMaxFractionalDigits.Get() : MaxFractionalDigits); bCachedValueStringDirty = true; } private: //~ SWidget Interface virtual bool SupportsKeyboardFocus() const override { return StaticCastSharedPtr(EditableText)->SupportsKeyboardFocus(); } virtual FReply OnFocusReceived( const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent ) override { FReply Reply = FReply::Handled(); // The widget to forward focus to changes depending on whether we have a SpinBox or not. TSharedPtr FocusWidget; if (SpinBox.IsValid() && SpinBox->GetVisibility() == EVisibility::Visible) { FocusWidget = SpinBox; } else { FocusWidget = EditableText; } if ( InFocusEvent.GetCause() != EFocusCause::Cleared ) { // Forward keyboard focus to our chosen widget Reply.SetUserFocus(FocusWidget.ToSharedRef(), InFocusEvent.GetCause()); } return Reply; } virtual FReply OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent ) override { FKey Key = InKeyEvent.GetKey(); if( Key == EKeys::Escape && EditableText->HasKeyboardFocus() ) { return FReply::Handled().SetUserFocus(SharedThis(this), EFocusCause::Cleared); } return FReply::Unhandled(); } private: /** * @return the Label that should be displayed */ FString GetLabel() const { // Should always be set if this is being called return LabelAttribute.Get().GetValue(); } /** * Called to get the value for the spin box */ NumericType OnGetValueForSpinBox() const { const auto& Value = ValueAttribute.Get(); // Get the value or 0 if its not set if( Value.IsSet() == true ) { return Value.GetValue(); } return 0; } void SetCachedString(const NumericType CurrentValue) { if (!CachedExternalValue.IsSet() || CachedExternalValue.GetValue() != CurrentValue || bCachedValueStringDirty) { CachedExternalValue = CurrentValue; CachedValueString = Interface->ToString(CurrentValue); bCachedValueStringDirty = false; } } FString GetCachedString(const NumericType CurrentValue) const { const bool bUseCachedString = CachedExternalValue.IsSet() && CurrentValue == CachedExternalValue.GetValue() && !bCachedValueStringDirty; return bUseCachedString ? CachedValueString : Interface->ToString(CurrentValue); } FText GetToolTipText() const { const TOptional& Value = ValueAttribute.Get(); #if WITH_EDITOR if constexpr (std::is_floating_point_v) { if (Value.IsSet() && CachedValueString.Contains(TEXT("..."))) { NumericType NumericValue = Value.GetValue(); FString ValueScientificStr = FString::Printf(TEXT("%e"), NumericValue); // Dev Note: If you are seeing this message when not expected it can mean the following: // 1. Simple content / value change is needed assuming the ~1e-16 off value is not desired. // 2. If the above doesn't fix the issue, then there probably is an editor customization / some data writer that needs fixing // 3. If the value is a true denorm, smaller than 1.175e-38 / 2.225e-307 for floats / doubles respectively. There is a larger issue. // In this case, it is possible that you may be reading from garbage / random memory not intended for floating point types. return FText::Format(NSLOCTEXT("SNumericEntryBox", "NearZeroWarning_ToolTip", "Exact value: {0}. Set value or reimport after updating source content to change."), FText::FromString(ValueScientificStr)); } } #endif // WITH_EDITOR if (Value.IsSet() == true) { NumericType CurrentValue = Value.GetValue(); FText CurrentValueText = FText::FromString(GetCachedString(CurrentValue)); if (ToolTipTextFormat.Get().IsSet()) { return FText::Format(ToolTipTextFormat.Get().GetValue(), CurrentValueText); } return CurrentValueText; } return FText::GetEmpty(); } /** * Called to get the value for the text box as FText */ FText OnGetValueForTextBox() const { if( EditableText->GetVisibility() == EVisibility::Visible ) { const TOptional& Value = ValueAttribute.Get(); if (Value.IsSet() == true) { return FText::FromString(GetCachedString(Value.GetValue())); } else { return UndeterminedString; } } // The box isnt visible, just return an empty Text return FText::GetEmpty(); } /** * Called when the text changes in the text box */ void OnTextChanged( const FText& NewValue ) { const auto& Value = ValueAttribute.Get(); if (Value.IsSet() || !OnUndeterminedValueChanged.IsBound()) { SendChangesFromText( NewValue, false, ETextCommit::Default ); } else { OnUndeterminedValueChanged.Execute(NewValue); } } /** * Called when the text is committed from the text box */ void OnTextCommitted( const FText& NewValue, ETextCommit::Type CommitInfo ) { const auto& Value = ValueAttribute.Get(); if (Value.IsSet() || !OnUndeterminedValueCommitted.IsBound()) { SendChangesFromText( NewValue, true, CommitInfo ); } else { OnUndeterminedValueCommitted.Execute(NewValue, CommitInfo); } } /** * Called to get the border image of the box */ const FSlateBrush* GetBorderImage() const { TSharedPtr EditingWidget; if (SpinBox.IsValid() && SpinBox->GetVisibility() == EVisibility::Visible) { EditingWidget = SpinBox; } else { EditingWidget = EditableText; } if ( EditingWidget->HasKeyboardFocus() ) { return BorderImageFocused; } if ( EditingWidget->IsHovered() ) { return BorderImageHovered; } return BorderImageNormal; } /** * Calls the value commit or changed delegate set for this box when the value is set from a string * * @param NewValue The new value as a string * @param bCommit Whether or not to call the commit or changed delegate */ void SendChangesFromText( const FText& NewValue, bool bCommit, ETextCommit::Type CommitInfo ) { if (NewValue.IsEmpty()) { return; } TOptional ExistingValue = ValueAttribute.Get(); TOptional NumericValue = Interface->FromString(NewValue.ToString(), ExistingValue.Get(0)); if (NumericValue.IsSet()) { SetCachedString(NumericValue.GetValue()); if (bCommit) { OnValueCommitted.ExecuteIfBound(NumericValue.GetValue(), CommitInfo); } else { OnValueChanged.ExecuteIfBound(NumericValue.GetValue()); } } } /** * Caches the value and performs widget visibility maintenance */ virtual void Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) override { // Update the cached value, if needed. #if WITH_EDITOR if constexpr (std::is_floating_point_v) { if (!GetPersistentState().bIsInGameLayer && !Interface->GetIndicateNearlyInteger()) { Interface->SetIndicateNearlyInteger(true); bCachedValueStringDirty = true; if (SpinBox.IsValid()) { SpinBox->ResetCachedValueString(); } } } #endif // WITH_EDITOR const TOptional& Value = ValueAttribute.Get(); if (Value.IsSet() == true) { SetCachedString(Value.GetValue()); } // Visibility toggle only matters if the spinbox is used if (SpinBox.IsValid()) { if (Value.IsSet() == true) { if (SpinBox->GetVisibility() != EVisibility::Visible) { // Set the visibility of the spinbox to visible if we have a valid value SpinBox->SetVisibility( EVisibility::Visible ); // The text box should be invisible EditableText->SetVisibility( EVisibility::Collapsed ); } } else { // The value isn't set so the spinbox should be hidden and the text box shown SpinBox->SetVisibility(EVisibility::Collapsed); EditableText->SetVisibility(EVisibility::Visible); } } } bool IsToggleEnabled() const { if(!IsEnabled()) { return false; } return ToggleCheckBox->IsChecked(); } void HandleToggleCheckBoxChanged(ECheckBoxState InCheckState, FOnCheckStateChanged OnToggleChanged) const { if(SpinBox.IsValid()) { SpinBox->SetEnabled(InCheckState == ECheckBoxState::Checked); } if(EditableText.IsValid()) { EditableText->SetEnabled(InCheckState == ECheckBoxState::Checked); } if(OnToggleChanged.IsBound()) { OnToggleChanged.Execute(InCheckState); } } void ResetCachedValueString() { if (ValueAttribute.Get().IsSet()) { bCachedValueStringDirty = true; } } private: /** The default minimum fractional digits */ static const int32 DefaultMinFractionalDigits; /** The default maximum fractional digits */ static const int32 DefaultMaxFractionalDigits; /** Attribute for getting the label */ TAttribute< TOptional > LabelAttribute; /** Attribute for getting the value. If the value is not set we display the undetermined string */ TAttribute< TOptional > ValueAttribute; /** Toggle checkbox */ TSharedPtr ToggleCheckBox; /** Spinbox widget */ TSharedPtr> SpinBox; /** Editable widget */ TSharedPtr EditableText; /** Delegate to call when the value changes */ FOnValueChanged OnValueChanged; /** Delegate to call when the value is committed */ FOnValueCommitted OnValueCommitted; /** Delegate to call when an undetermined value changes */ FOnUndeterminedValueChanged OnUndeterminedValueChanged; /** Delegate to call when an undetermined is committed */ FOnUndeterminedValueCommitted OnUndeterminedValueCommitted; /** The undetermined string to display when needed */ FText UndeterminedString; /** Styling: border image to draw when not hovered or focused */ const FSlateBrush* BorderImageNormal; /** Styling: border image to draw when hovered */ const FSlateBrush* BorderImageHovered; /** Styling: border image to draw when focused */ const FSlateBrush* BorderImageFocused; /** Prevents the value portion of the control from being smaller than desired in certain cases. */ TAttribute MinDesiredValueWidth; /** Type interface that defines how we should deal with the templated numeric type. Always valid after construction. */ TSharedPtr< INumericTypeInterface > Interface; /** Cached value of entry box, updated on set & per tick */ TOptional CachedExternalValue; /** Used to prevent per-frame re-conversion of the cached numeric value to a string. */ FString CachedValueString; /** Whetever the interfaced setting changed and the CachedValueString needs to be recomputed. */ bool bCachedValueStringDirty; TAttribute< TOptional > MinFractionalDigits; TAttribute< TOptional > MaxFractionalDigits; TAttribute< TOptional > ToolTipTextFormat; }; template const FLinearColor SNumericEntryBox::RedLabelBackgroundColor(0.594f,0.0197f,0.0f); template const FLinearColor SNumericEntryBox::GreenLabelBackgroundColor(0.1349f,0.3959f,0.0f); template const FLinearColor SNumericEntryBox::BlueLabelBackgroundColor(0.0251f,0.207f,0.85f); template const FLinearColor SNumericEntryBox::LilacLabelBackgroundColor(0.8f,0.121f,0.8f); template const FText SNumericEntryBox::DefaultUndeterminedString = FText::FromString(TEXT("---")); template const int32 SNumericEntryBox::DefaultMinFractionalDigits = 1; template const int32 SNumericEntryBox::DefaultMaxFractionalDigits = 6;