// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "Styling/AppStyle.h" #include "Styling/SegmentedControlStyle.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/Layout/SSpacer.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SUniformGridPanel.h" #include "Framework/Application/SlateApplication.h" /** * A Slate Segmented Control is functionally similar to a group of Radio Buttons. * Slots require a templated value to return when the segment is selected by the user. * Users can specify text, icon or provide custom content to each Segment. * * Note: It is currently not possible to add segments after initialization * (i.e. there is no AddSlot). */ template< typename OptionType > class SSegmentedControl : public SCompoundWidget { public: /** Stores the per-child info for this panel type */ struct FSlot : public TSlotBase, public TAlignmentWidgetSlotMixin { FSlot(const OptionType& InValue) : TSlotBase() , TAlignmentWidgetSlotMixin(HAlign_Center, VAlign_Fill) , _Text() , _Tooltip() , _Icon(nullptr) , _Value(InValue) { } SLATE_SLOT_BEGIN_ARGS_OneMixin(FSlot, TSlotBase, TAlignmentWidgetSlotMixin) SLATE_ATTRIBUTE(FText, Text) SLATE_ATTRIBUTE(FText, ToolTip) SLATE_ATTRIBUTE(TSharedPtr, ToolTipWidget) SLATE_ATTRIBUTE(const FSlateBrush*, Icon) SLATE_ARGUMENT(TOptional, Value) SLATE_SLOT_END_ARGS() void Construct(const FChildren& SlotOwner, FSlotArguments&& InArgs) { TSlotBase::Construct(SlotOwner, MoveTemp(InArgs)); TAlignmentWidgetSlotMixin::ConstructMixin(SlotOwner, MoveTemp(InArgs)); if (InArgs._Text.IsSet()) { _Text = MoveTemp(InArgs._Text); } if (InArgs._ToolTip.IsSet()) { _Tooltip = MoveTemp(InArgs._ToolTip); } if (InArgs._ToolTipWidget.IsSet()) { _ToolTipWidget = MoveTemp(InArgs._ToolTipWidget); } if (InArgs._Icon.IsSet()) { _Icon = MoveTemp(InArgs._Icon); } if (InArgs._Value.IsSet()) { _Value = MoveTemp(InArgs._Value.GetValue()); } } void SetText(TAttribute InText) { _Text = MoveTemp(InText); } FText GetText() const { return _Text.Get(); } void SetIcon(TAttribute InBrush) { _Icon = MoveTemp(InBrush); } const FSlateBrush* GetIcon() const { return _Icon.Get(); } void SetToolTip(TAttribute InTooltip) { _Tooltip = MoveTemp(InTooltip); } FText GetToolTip() const { return _Tooltip.Get(); } friend SSegmentedControl; private: TAttribute _Text; TAttribute _Tooltip; TAttribute> _ToolTipWidget; TAttribute _Icon; OptionType _Value; TWeakPtr _CheckBox; }; static typename FSlot::FSlotArguments Slot(const OptionType& InValue) { return typename FSlot::FSlotArguments(MakeUnique(InValue)); } DECLARE_DELEGATE_OneParam( FOnValueChanged, OptionType ); DECLARE_DELEGATE_OneParam( FOnValuesChanged, TArray ); DECLARE_DELEGATE_TwoParams( FOnValueChecked, OptionType, ECheckBoxState ); SLATE_BEGIN_ARGS( SSegmentedControl ) : _Style(&FAppStyle::Get().GetWidgetStyle("SegmentedControl")) , _TextStyle(&FAppStyle::Get().GetWidgetStyle("SmallButtonText")) , _SupportsMultiSelection(false) , _SupportsEmptySelection(false) , _MaxSegmentsPerLine(0) {} /** Slot type supported by this panel */ SLATE_SLOT_ARGUMENT(FSlot, Slots) /** Styling for this control */ SLATE_STYLE_ARGUMENT(FSegmentedControlStyle, Style) /** Styling for the text in each slot. If a custom widget is supplied for a slot this argument is not used */ SLATE_STYLE_ARGUMENT(FTextBlockStyle, TextStyle) /** * If enabled the widget will support multi selection. * For single selection the widget relies on the Value attribute, * for multi selection the widget relies on the MultiValue attribute. */ SLATE_ARGUMENT(bool, SupportsMultiSelection) /** * If enabled the widget will support an empty selection. * This is only enabled if SupportsMultiSelection is also enabled. */ SLATE_ARGUMENT(bool, SupportsEmptySelection) /** The current control value. */ SLATE_ATTRIBUTE(OptionType, Value) /** The current (multiple) control values (if SupportsMultiSelection is enabled) */ SLATE_ATTRIBUTE(TArray, Values) /** Padding to apply to each slot */ SLATE_ATTRIBUTE(FMargin, UniformPadding) /** Called when the (primary) value is changed */ SLATE_EVENT(FOnValueChanged, OnValueChanged) /** Called when the any value is changed */ SLATE_EVENT(FOnValuesChanged, OnValuesChanged) /** Called when the value is changed (useful for multi selection) */ SLATE_EVENT(FOnValueChecked, OnValueChecked) /** Optional maximum number of segments per line before the control wraps vertically to the next line. If this value is <= 0 no wrapping happens */ SLATE_ARGUMENT(int32, MaxSegmentsPerLine) SLATE_END_ARGS() SSegmentedControl() : Children(this) , CurrentValues(*this) {} void Construct( const FArguments& InArgs ) { check(InArgs._Style); Style = InArgs._Style; TextStyle = InArgs._TextStyle; SupportsMultiSelection = InArgs._SupportsMultiSelection; SupportsEmptySelection = InArgs._SupportsEmptySelection; CurrentValuesIsBound = false; // will be set by SetValue or SetValues if(InArgs._Value.IsBound() || InArgs._Value.IsSet()) { SetValue(InArgs._Value, false); } else if(InArgs._Values.IsBound() || InArgs._Values.IsSet()) { SetValues(InArgs._Values, false); } OnValueChanged = InArgs._OnValueChanged; OnValuesChanged = InArgs._OnValuesChanged; OnValueChecked = InArgs._OnValueChecked; UniformPadding = InArgs._UniformPadding; MaxSegmentsPerLine = InArgs._MaxSegmentsPerLine; Children.AddSlots(MoveTemp(const_cast&>(InArgs._Slots))); RebuildChildren(); } void RebuildChildren() { // The right padding will be applied later at the end so we dont accumulate left+right padding between all buttons FMargin SlotPadding = Style->UniformPadding; SlotPadding.Right = 0.0f; TSharedPtr UniformBox = SNew(SUniformGridPanel).SlotPadding(SlotPadding); const int32 NumSlots = Children.Num(); for ( int32 SlotIndex = 0; SlotIndex < NumSlots; ++SlotIndex ) { TSharedRef Child = Children[SlotIndex].GetWidget(); FSlot* ChildSlotPtr = &Children[SlotIndex]; const OptionType ChildValue = ChildSlotPtr->_Value; TAttribute SpacerLambda = FVector::ZeroVector; if (ChildSlotPtr->_Icon.IsBound() || ChildSlotPtr->_Text.IsBound()) { SpacerLambda = MakeAttributeLambda([ChildSlotPtr]() { return (ChildSlotPtr->_Icon.Get() != nullptr && !ChildSlotPtr->_Text.Get().IsEmpty()) ? FVector2D(8.0f, 1.0f) : FVector2D::ZeroVector; }); } else { SpacerLambda = (ChildSlotPtr->_Icon.Get() != nullptr && !ChildSlotPtr->_Text.Get().IsEmpty()) ? FVector2D(8.0f, 1.0f) : FVector2D::ZeroVector; } if (Child == SNullWidget::NullWidget) { Child = SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SImage) .ColorAndOpacity(FSlateColor::UseForeground()) .Image(ChildSlotPtr->_Icon) ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(SSpacer) .Size(SpacerLambda) ] +SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() [ SNew(STextBlock) .TextStyle(TextStyle) .Text(ChildSlotPtr->_Text) ]; } const FCheckBoxStyle* CheckBoxStyle = &Style->ControlStyle; if (SlotIndex == 0) { CheckBoxStyle = &Style->FirstControlStyle; } else if (SlotIndex == NumSlots - 1) { CheckBoxStyle = &Style->LastControlStyle; } const int32 ColumnIndex = MaxSegmentsPerLine > 0 ? SlotIndex % MaxSegmentsPerLine : SlotIndex; const int32 RowIndex = MaxSegmentsPerLine > 0 ? SlotIndex / MaxSegmentsPerLine : 0; // Note HAlignment is applied at the check box level because if it were applied here it would make the slots look physically disconnected from each other UniformBox->AddSlot(ColumnIndex, RowIndex) .VAlign(ChildSlotPtr->GetVerticalAlignment()) [ SAssignNew(ChildSlotPtr->_CheckBox, SCheckBox) .Clipping(EWidgetClipping::ClipToBounds) .HAlign(ChildSlotPtr->GetHorizontalAlignment()) .ToolTipText(ChildSlotPtr->_Tooltip) .ToolTip(ChildSlotPtr->_ToolTipWidget) .Style(CheckBoxStyle) .IsChecked(GetCheckBoxStateAttribute(ChildValue)) .OnCheckStateChanged(this, &SSegmentedControl::CommitValue, ChildValue) .Padding(UniformPadding) [ Child ] ]; } ChildSlot [ SNew(SBorder) .BorderImage(&Style->BackgroundBrush) .Padding(FMargin(0,0,Style->UniformPadding.Right,0)) [ UniformBox.ToSharedRef() ] ]; UpdateCheckboxValuesIfNeeded(); } // Slot Management using FScopedWidgetSlotArguments = typename TPanelChildren::FScopedWidgetSlotArguments; FScopedWidgetSlotArguments AddSlot(const OptionType& InValue, bool bRebuildChildren = true) { if (bRebuildChildren) { TWeakPtr AsWeak = SharedThis(this); return FScopedWidgetSlotArguments { MakeUnique(InValue), this->Children, INDEX_NONE, [AsWeak](const FSlot*, int32) { if (TSharedPtr SharedThis = AsWeak.Pin()) { SharedThis->RebuildChildren(); } }}; } else { return FScopedWidgetSlotArguments{ MakeUnique(InValue), this->Children, INDEX_NONE }; } } int32 NumSlots() const { return Children.Num(); } OptionType GetValue() const { const TArray Values = GetValues(); if(Values.IsEmpty()) { return OptionType(); } return Values[0]; } TArray GetValues() const { return CurrentValues.Get(); } bool HasValue(OptionType InValue) { const TArray Values = GetValues(); return Values.Contains(InValue); } /** See the Value attribute */ void SetValue(TAttribute InValue, bool bUpdateChildren = true) { if(InValue.IsBound()) { SetValues(TAttribute>::CreateLambda([InValue]() -> TArray { TArray Values; Values.Add(InValue.Get()); return Values; }), bUpdateChildren); } else if(InValue.IsSet()) { TArray Values = {InValue.Get()}; SetValues(TAttribute>(Values), bUpdateChildren); } else { SetValues(TAttribute>(), bUpdateChildren); } } /** See the Values attribute */ void SetValues(TAttribute> InValues, bool bUpdateChildren = true) { CurrentValuesIsBound = InValues.IsBound(); if(CurrentValuesIsBound) { CurrentValues.Assign(*this, InValues); } else if(InValues.IsSet()) { CurrentValues.Set(*this, InValues.Get()); } else { CurrentValues.Unbind(*this); }; if(bUpdateChildren) { UpdateCheckboxValuesIfNeeded(); } } static TSharedPtr> Create( const TArray& InKeys, const TArray& InLabels, const TArray& InTooltips, const TAttribute>& InValues, bool bSupportsMultiSelection = true, FOnValuesChanged OnValuesChanged = FOnValuesChanged()) { TSharedPtr> Widget; SAssignNew(Widget, SSegmentedControl) .SupportsMultiSelection(bSupportsMultiSelection) .Values(InValues) .OnValuesChanged(OnValuesChanged); ensure(InKeys.Num() == InLabels.Num()); ensure(InKeys.Num() == InTooltips.Num()); for(int32 Index = 0; Index < InKeys.Num(); Index++) { Widget->AddSlot(InKeys[Index], false) .Text(InLabels[Index]) .ToolTip(InTooltips[Index]); } Widget->RebuildChildren(); return Widget; } private: TAttribute GetCheckBoxStateAttribute(OptionType InValue) const { auto Lambda = [this, InValue]() { const TArray Values = GetValues(); return Values.Contains(InValue) ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }; if (CurrentValuesIsBound) { return MakeAttributeLambda(Lambda); } return Lambda(); } void UpdateCheckboxValuesIfNeeded() { if (!CurrentValuesIsBound) { const TArray Values = GetValues(); for (int32 Index = 0; Index < Children.Num(); ++Index) { const FSlot& Slot = Children[Index]; if (const TSharedPtr CheckBox = Slot._CheckBox.Pin()) { CheckBox->SetIsChecked(Values.Contains(Slot._Value) ? ECheckBoxState::Checked : ECheckBoxState::Unchecked); } } } } void CommitValue(const ECheckBoxState InCheckState, OptionType InValue) { const TArray PreviousValues = CurrentValues.Get(); TArray Values = PreviousValues; // don't allow to deselect the last checkbox if(InCheckState != ECheckBoxState::Checked && Values.Num() == 1) { if(!SupportsEmptySelection) { UpdateCheckboxValuesIfNeeded(); return; } } bool bModifierIsDown = false; if(SupportsMultiSelection) { bModifierIsDown = FSlateApplication::Get().GetModifierKeys().IsShiftDown() || FSlateApplication::Get().GetModifierKeys().IsControlDown(); } // if the attribute is not bound update our internal state if(bModifierIsDown) { if (InCheckState == ECheckBoxState::Checked) { Values.AddUnique(InValue); } else { Values.Remove(InValue); } } else { if((InCheckState == ECheckBoxState::Checked) || Values.Contains(InValue)) { Values.Reset(); Values.Add(InValue); } } if (!CurrentValuesIsBound) { CurrentValues.Set(*this, Values); UpdateCheckboxValuesIfNeeded(); } if(OnValueChecked.IsBound()) { if(!bModifierIsDown && InCheckState == ECheckBoxState::Checked) { for(const OptionType PreviousValue : PreviousValues) { if(!Values.Contains(PreviousValue)) { OnValueChecked.Execute(PreviousValue, ECheckBoxState::Unchecked); } } } OnValueChecked.Execute(InValue, InCheckState); } if (InCheckState == ECheckBoxState::Checked) { OnValueChanged.ExecuteIfBound(InValue); } OnValuesChanged.ExecuteIfBound(Values); } private: TPanelChildren Children; FOnValueChanged OnValueChanged; FOnValuesChanged OnValuesChanged; FOnValueChecked OnValueChecked; TSlateAttribute, EInvalidateWidgetReason::Paint> CurrentValues; TAttribute UniformPadding; const FSegmentedControlStyle* Style; const FTextBlockStyle* TextStyle; int32 MaxSegmentsPerLine = 0; bool CurrentValuesIsBound = false; bool SupportsMultiSelection = false; bool SupportsEmptySelection = false; };