// Copyright Epic Games, Inc. All Rights Reserved. #include "RangeStructCustomization.h" #include "Containers/BitArray.h" #include "Containers/Set.h" #include "Containers/SparseArray.h" #include "Containers/UnrealString.h" #include "Delegates/Delegate.h" #include "DetailLayoutBuilder.h" #include "DetailWidgetRow.h" #include "Editor.h" #include "Editor/EditorEngine.h" #include "Fonts/SlateFontInfo.h" #include "HAL/Platform.h" #include "HAL/PlatformCrt.h" #include "Internationalization/Internationalization.h" #include "Layout/Margin.h" #include "Math/RangeBound.h" #include "Misc/AssertionMacros.h" #include "Misc/Attribute.h" #include "PropertyEditorModule.h" #include "PropertyHandle.h" #include "Serialization/Archive.h" #include "SlotBase.h" #include "Templates/UnrealTemplate.h" #include "Types/SlateStructs.h" #include "UObject/UnrealType.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/Input/SComboBox.h" #include "Widgets/Input/SNumericEntryBox.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SSpacer.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Text/STextBlock.h" class SWidget; #define LOCTEXT_NAMESPACE "RangeStructCustomization" /* Helper traits for getting a metadata property based on the template parameter type *****************************************************************************/ namespace { template struct FGetMetaDataHelper { }; template <> struct FGetMetaDataHelper { static float GetMetaData(const TSharedRef& Property, const TCHAR* Key) { return Property->GetFloatMetaData(Key); } }; template <> struct FGetMetaDataHelper { static double GetMetaData(const TSharedRef& Property, const TCHAR* Key) { return Property->GetDoubleMetaData(Key); } }; template <> struct FGetMetaDataHelper { static int32 GetMetaData(const TSharedRef& Property, const TCHAR* Key) { return Property->GetIntMetaData(Key); } }; } /* FRangeStructCustomization static interface *****************************************************************************/ template TSharedRef FRangeStructCustomization::MakeInstance() { return MakeShareable(new FRangeStructCustomization); } /* IPropertyTypeCustomization interface *****************************************************************************/ template void FRangeStructCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { // Get handles to the properties we're interested in LowerBoundStructHandle = StructPropertyHandle->GetChildHandle(TEXT("LowerBound")); UpperBoundStructHandle = StructPropertyHandle->GetChildHandle(TEXT("UpperBound")); check(LowerBoundStructHandle.IsValid()); check(UpperBoundStructHandle.IsValid()); LowerBoundValueHandle = LowerBoundStructHandle->GetChildHandle(TEXT("Value")); UpperBoundValueHandle = UpperBoundStructHandle->GetChildHandle(TEXT("Value")); LowerBoundTypeHandle = LowerBoundStructHandle->GetChildHandle(TEXT("Type")); UpperBoundTypeHandle = UpperBoundStructHandle->GetChildHandle(TEXT("Type")); check(LowerBoundValueHandle.IsValid()); check(UpperBoundValueHandle.IsValid()); check(LowerBoundTypeHandle.IsValid()); check(UpperBoundTypeHandle.IsValid()); // Get min/max metadata values if defined if (StructPropertyHandle->HasMetaData(TEXT("UIMin"))) { MinAllowedValue = TOptional(FGetMetaDataHelper::GetMetaData(StructPropertyHandle, TEXT("UIMin"))); } if (StructPropertyHandle->HasMetaData(TEXT("UIMax"))) { MaxAllowedValue = TOptional(FGetMetaDataHelper::GetMetaData(StructPropertyHandle, TEXT("UIMax"))); } // Make weak pointers to be passed as payloads to the widgets TWeakPtr LowerBoundValueWeakPtr = LowerBoundValueHandle; TWeakPtr UpperBoundValueWeakPtr = UpperBoundValueHandle; TWeakPtr LowerBoundTypeWeakPtr = LowerBoundTypeHandle; TWeakPtr UpperBoundTypeWeakPtr = UpperBoundTypeHandle; // Generate a list of enum values for the combo box from the LowerBound.Type property TArray RestrictedList; LowerBoundTypeHandle->GeneratePossibleValues(ComboBoxList, ComboBoxToolTips, RestrictedList); // Get initial values for the combo box uint8 LowerBoundType; TSharedPtr LowerBoundTypeSelectedItem; if (ensure(LowerBoundTypeHandle->GetValue(LowerBoundType) == FPropertyAccess::Success)) { check(LowerBoundType < ComboBoxList.Num()); LowerBoundTypeSelectedItem = ComboBoxList[LowerBoundType]; } uint8 UpperBoundType; TSharedPtr UpperBoundTypeSelectedItem; if (ensure(UpperBoundTypeHandle->GetValue(UpperBoundType) == FPropertyAccess::Success)) { check(UpperBoundType < ComboBoxList.Num()); UpperBoundTypeSelectedItem = ComboBoxList[UpperBoundType]; } // Build the widgets HeaderRow.NameContent() [ StructPropertyHandle->CreatePropertyNameWidget() ] .ValueContent() .MinDesiredWidth(200.0f) .MaxDesiredWidth(200.0f) [ SNew(SVerticalBox) +SVerticalBox::Slot() .Padding(FMargin(0.0f, 3.0f, 0.0f, 2.0f)) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .Padding(FMargin(0.0f, 0.0f, 6.0f, 0.0f)) .AutoWidth() .VAlign(VAlign_Center) [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(LOCTEXT("MinimumBoundLabel", "Min")) ] +SHorizontalBox::Slot() .Padding(FMargin(0.0f, 0.0f, 3.0f, 0.0f)) .VAlign(VAlign_Center) [ SNew(SNumericEntryBox) .Value(this, &FRangeStructCustomization::OnGetValue, LowerBoundValueWeakPtr, LowerBoundTypeWeakPtr) .MinValue(MinAllowedValue) .MinSliderValue(MinAllowedValue) .MaxValue(this, &FRangeStructCustomization::OnGetValue, UpperBoundValueWeakPtr, UpperBoundTypeWeakPtr) .MaxSliderValue(this, &FRangeStructCustomization::OnGetValue, UpperBoundValueWeakPtr, UpperBoundTypeWeakPtr) .OnValueCommitted(this, &FRangeStructCustomization::OnValueCommitted, LowerBoundValueWeakPtr) .OnValueChanged(this, &FRangeStructCustomization::OnValueChanged, LowerBoundValueWeakPtr) .OnBeginSliderMovement(this, &FRangeStructCustomization::OnBeginSliderMovement) .OnEndSliderMovement(this, &FRangeStructCustomization::OnEndSliderMovement) .IsEnabled(this, &FRangeStructCustomization::OnQueryIfEnabled, LowerBoundTypeWeakPtr) .Font(IDetailLayoutBuilder::GetDetailFont()) .AllowSpin(true) ] +SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() [ SNew(SComboBox>) .OptionsSource(&ComboBoxList) .OnGenerateWidget(this, &FRangeStructCustomization::OnGenerateComboWidget) .OnSelectionChanged(this, &FRangeStructCustomization::OnComboSelectionChanged, LowerBoundTypeWeakPtr) .InitiallySelectedItem(LowerBoundTypeSelectedItem) [ // combo box button intentionally blank to avoid displaying excessive details SNew(SSpacer) ] ] ] +SVerticalBox::Slot() .Padding(FMargin(0.0f, 2.0f, 0.0f, 3.0f)) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(FMargin(0.0f, 0.0f, 3.0f, 0.0f)) .AutoWidth() .VAlign(VAlign_Center) [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(LOCTEXT("MaximumBoundLabel", "Max")) ] +SHorizontalBox::Slot() .Padding(FMargin(0.0f, 0.0f, 3.0f, 0.0f)) .VAlign(VAlign_Center) [ SNew(SNumericEntryBox) .Value(this, &FRangeStructCustomization::OnGetValue, UpperBoundValueWeakPtr, UpperBoundTypeWeakPtr) .MinValue(this, &FRangeStructCustomization::OnGetValue, LowerBoundValueWeakPtr, LowerBoundTypeWeakPtr) .MinSliderValue(this, &FRangeStructCustomization::OnGetValue, LowerBoundValueWeakPtr, LowerBoundTypeWeakPtr) .MaxValue(MaxAllowedValue) .MaxSliderValue(MaxAllowedValue) .OnValueCommitted(this, &FRangeStructCustomization::OnValueCommitted, UpperBoundValueWeakPtr) .OnValueChanged(this, &FRangeStructCustomization::OnValueChanged, UpperBoundValueWeakPtr) .OnBeginSliderMovement(this, &FRangeStructCustomization::OnBeginSliderMovement) .OnEndSliderMovement(this, &FRangeStructCustomization::OnEndSliderMovement) .IsEnabled(this, &FRangeStructCustomization::OnQueryIfEnabled, UpperBoundTypeWeakPtr) .Font(IDetailLayoutBuilder::GetDetailFont()) .AllowSpin(true) ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SComboBox< TSharedPtr >) .OptionsSource(&ComboBoxList) .OnGenerateWidget(this, &FRangeStructCustomization::OnGenerateComboWidget) .OnSelectionChanged(this, &FRangeStructCustomization::OnComboSelectionChanged, UpperBoundTypeWeakPtr) .InitiallySelectedItem(UpperBoundTypeSelectedItem) [ // combo box button intentionally blank to avoid displaying excessive details SNew(SSpacer) ] ] ] ]; } template void FRangeStructCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { // Don't display children, as editing them directly can break the constraints } /* FRangeStructCustomization callbacks *****************************************************************************/ template TOptional FRangeStructCustomization::OnGetValue(TWeakPtr ValueWeakPtr, TWeakPtr TypeWeakPtr) const { auto ValueSharedPtr = ValueWeakPtr.Pin(); auto TypeSharedPtr = TypeWeakPtr.Pin(); if (TypeSharedPtr.IsValid()) { uint8 Type; if (ensure(TypeSharedPtr->GetValue(Type) == FPropertyAccess::Success)) { if (Type != ERangeBoundTypes::Open && ValueSharedPtr.IsValid()) { NumericType Value; if (ensure(ValueSharedPtr->GetValue(Value) == FPropertyAccess::Success)) { return TOptional(Value); } } } } // Value couldn't be accessed, or was bound type 'open'. Return an unset value return TOptional(); } template void FRangeStructCustomization::OnValueCommitted(NumericType NewValue, ETextCommit::Type CommitType, TWeakPtr HandleWeakPtr) { auto HandleSharedPtr = HandleWeakPtr.Pin(); if (HandleSharedPtr.IsValid() && (!bIsUsingSlider || (bIsUsingSlider && ShouldAllowSpin()))) { ensure(HandleSharedPtr->SetValue(NewValue) == FPropertyAccess::Success); } } template void FRangeStructCustomization::OnValueChanged(NumericType NewValue, TWeakPtr HandleWeakPtr) { if (bIsUsingSlider && ShouldAllowSpin()) { auto HandleSharedPtr = HandleWeakPtr.Pin(); if (HandleSharedPtr.IsValid()) { ensure(HandleSharedPtr->SetValue(NewValue, EPropertyValueSetFlags::InteractiveChange) == FPropertyAccess::Success); } } } template void FRangeStructCustomization::OnBeginSliderMovement() { bIsUsingSlider = true; if (ShouldAllowSpin()) { GEditor->BeginTransaction(LOCTEXT("SetRangeProperty", "Set Range Property")); } } template void FRangeStructCustomization::OnEndSliderMovement(NumericType /*NewValue*/) { bIsUsingSlider = false; if (ShouldAllowSpin()) { GEditor->EndTransaction(); } } template bool FRangeStructCustomization::OnQueryIfEnabled(TWeakPtr HandleWeakPtr) const { auto PropertyHandle = HandleWeakPtr.Pin(); if (PropertyHandle.IsValid()) { uint8 BoundType; if (ensure(PropertyHandle->GetValue(BoundType) == FPropertyAccess::Success)) { return (BoundType != ERangeBoundTypes::Open); } } return false; } template bool FRangeStructCustomization::ShouldAllowSpin() const { uint8 LowerBoundType; uint8 UpperBoundType; if (ensure(LowerBoundTypeHandle->GetValue(LowerBoundType) == FPropertyAccess::Success) && ensure(UpperBoundTypeHandle->GetValue(UpperBoundType) == FPropertyAccess::Success)) { return (LowerBoundType != ERangeBoundTypes::Open && UpperBoundType != ERangeBoundTypes::Open); } return false; } template TSharedRef FRangeStructCustomization::OnGenerateComboWidget(TSharedPtr InComboString) { FText ToolTip; // A list of tool tips should have been populated in a 1 to 1 correspondence check(ComboBoxList.Num() == ComboBoxToolTips.Num()); if (ComboBoxToolTips.Num() > 0) { int32 Index = ComboBoxList.IndexOfByKey(InComboString); if (ensure(Index >= 0)) { ToolTip = ComboBoxToolTips[Index]; } } return SNew(SBox) .WidthOverride(150.0f) [ SNew(STextBlock) .Text(FText::FromString(*InComboString)) .ToolTipText(ToolTip) .Font(IDetailLayoutBuilder::GetDetailFont()) .IsEnabled(true) ]; } template void FRangeStructCustomization::OnComboSelectionChanged(TSharedPtr InSelectedItem, ESelectInfo::Type SelectInfo, TWeakPtr HandleWeakPtr) { auto PropertyHandle = HandleWeakPtr.Pin(); if (PropertyHandle.IsValid()) { int32 Index = ComboBoxList.IndexOfByKey(InSelectedItem); if (ensure(Index >= 0)) { ensure(PropertyHandle->SetValue(static_cast(Index)) == FPropertyAccess::Success); } } } /* Only explicitly instantiate the types which are supported *****************************************************************************/ template class FRangeStructCustomization; template class FRangeStructCustomization; template class FRangeStructCustomization; #undef LOCTEXT_NAMESPACE