// Copyright Epic Games, Inc. All Rights Reserved. #include "IntervalStructCustomization.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/PlatformCrt.h" #include "Internationalization/Internationalization.h" #include "Layout/Margin.h" #include "Math/UnrealMathSSE.h" #include "Misc/AssertionMacros.h" #include "Misc/Attribute.h" #include "PropertyEditorModule.h" #include "SlotBase.h" #include "Templates/UnrealTemplate.h" #include "UObject/UnrealType.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/Input/SNumericEntryBox.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Text/STextBlock.h" #include #define LOCTEXT_NAMESPACE "IntervalStructCustomization" /* Helper traits for getting a metadata property based on the template parameter type *****************************************************************************/ namespace IntervalMetadata { template struct FGetHelper { }; template <> struct FGetHelper { static float GetMetaData(const FProperty* Property, const TCHAR* Key) { return Property->GetFloatMetaData(Key); } }; template <> struct FGetHelper { static int32 GetMetaData(const FProperty* Property, const TCHAR* Key) { return Property->GetIntMetaData(Key); } }; } /* FIntervalStructCustomization static interface *****************************************************************************/ template TSharedRef FIntervalStructCustomization::MakeInstance() { return MakeShareable(new FIntervalStructCustomization); } /* IPropertyTypeCustomization interface *****************************************************************************/ template void FIntervalStructCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { // Get handles to the properties we're interested in MinValueHandle = StructPropertyHandle->GetChildHandle(TEXT("Min")); MaxValueHandle = StructPropertyHandle->GetChildHandle(TEXT("Max")); check(MinValueHandle.IsValid()); check(MaxValueHandle.IsValid()); // Get min/max metadata values if defined auto Property = StructPropertyHandle->GetProperty(); check(Property != nullptr); const FString& MetaUIMinString = Property->GetMetaData(TEXT("UIMin")); const FString& MetaUIMaxString = Property->GetMetaData(TEXT("UIMax")); const FString& MetaClampMinString = Property->GetMetaData(TEXT("ClampMin")); const FString& MetaClampMaxString = Property->GetMetaData(TEXT("ClampMax")); const FString& UIMinString = MetaUIMinString.Len() ? MetaUIMinString : MetaClampMinString; const FString& UIMaxString = MetaUIMaxString.Len() ? MetaUIMaxString : MetaClampMaxString; NumericType ClampMin = std::numeric_limits::lowest(); NumericType ClampMax = std::numeric_limits::max(); TTypeFromString::FromString(ClampMin, *MetaClampMinString); TTypeFromString::FromString(ClampMax, *MetaClampMaxString); NumericType UIMin = std::numeric_limits::lowest(); NumericType UIMax = std::numeric_limits::max(); TTypeFromString::FromString(UIMin, *UIMinString); TTypeFromString::FromString(UIMax, *UIMaxString); const NumericType ActualUIMin = FMath::Max(UIMin, ClampMin); const NumericType ActualUIMax = FMath::Min(UIMax, ClampMax); MinAllowedValue = MetaClampMinString.Len() ? ClampMin : TOptional(); MaxAllowedValue = MetaClampMaxString.Len() ? ClampMax : TOptional(); MinAllowedSliderValue = (UIMinString.Len()) ? ActualUIMin : TOptional(); MaxAllowedSliderValue = (UIMaxString.Len()) ? ActualUIMax : TOptional(); bAllowInvertedInterval = Property->HasMetaData(TEXT("AllowInvertedInterval")); bClampToMinMaxLimits = Property->HasMetaData(TEXT("ClampToMinMaxLimits")); // Build the widgets HeaderRow.NameContent() [ StructPropertyHandle->CreatePropertyNameWidget() ] .ValueContent() .MinDesiredWidth(251.0f) .MaxDesiredWidth(251.0f) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(FMargin(0.0f, 0.0f, 3.0f, 0.0f)) .AutoWidth() [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(LOCTEXT("MinLabel", "Min")) ] +SHorizontalBox::Slot() .Padding(FMargin(0.0f, 0.0f, 3.0f, 0.0f)) .VAlign(VAlign_Center) [ SNew(SNumericEntryBox) .Value(this, &FIntervalStructCustomization::OnGetValue, EIntervalField::Min) .MinValue(MinAllowedValue) .MinSliderValue(MinAllowedSliderValue) .MaxValue(this, &FIntervalStructCustomization::OnGetMaxValue) .MaxSliderValue(this, &FIntervalStructCustomization::OnGetMaxSliderValue) .OnValueCommitted(this, &FIntervalStructCustomization::OnValueCommitted, EIntervalField::Min) .OnValueChanged(this, &FIntervalStructCustomization::OnValueChanged, EIntervalField::Min) .OnBeginSliderMovement(this, &FIntervalStructCustomization::OnBeginSliderMovement) .OnEndSliderMovement(this, &FIntervalStructCustomization::OnEndSliderMovement) .UndeterminedString(LOCTEXT("MultipleValues", "Multiple Values")) .Font(IDetailLayoutBuilder::GetDetailFont()) .AllowSpin(true) .IsEnabled(this, &FIntervalStructCustomization::IsPropertyEnabled, EIntervalField::Min) ] +SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(2.0f, 0.0f) .AutoWidth() [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(LOCTEXT("MaxLabel", "Max")) ] +SHorizontalBox::Slot() .Padding(FMargin(0.0f, 0.0f, 3.0f, 0.0f)) .VAlign(VAlign_Center) [ SNew(SNumericEntryBox) .Value(this, &FIntervalStructCustomization::OnGetValue, EIntervalField::Max) .MinValue(this, &FIntervalStructCustomization::OnGetMinValue) .MinSliderValue(this, &FIntervalStructCustomization::OnGetMinSliderValue) .MaxValue(MaxAllowedValue) .MaxSliderValue(MaxAllowedSliderValue) .OnValueCommitted(this, &FIntervalStructCustomization::OnValueCommitted, EIntervalField::Max) .OnValueChanged(this, &FIntervalStructCustomization::OnValueChanged, EIntervalField::Max) .OnBeginSliderMovement(this, &FIntervalStructCustomization::OnBeginSliderMovement) .OnEndSliderMovement(this, &FIntervalStructCustomization::OnEndSliderMovement) .UndeterminedString(LOCTEXT("MultipleValues", "Multiple Values")) .Font(IDetailLayoutBuilder::GetDetailFont()) .AllowSpin(true) .IsEnabled(this, &FIntervalStructCustomization::IsPropertyEnabled, EIntervalField::Max) ] ]; } template void FIntervalStructCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { // Don't display children, as editing them directly can break the constraints } /* FIntervalStructCustomization callbacks *****************************************************************************/ template static TOptional GetValue(IPropertyHandle* Handle) { NumericType Value; if (Handle->GetValue(Value) == FPropertyAccess::Success) { return TOptional(Value); } return TOptional(); } template TOptional FIntervalStructCustomization::OnGetValue(EIntervalField Field) const { return GetValue((Field == EIntervalField::Min) ? MinValueHandle.Get() : MaxValueHandle.Get()); } template TOptional FIntervalStructCustomization::OnGetMinValue() const { if (bClampToMinMaxLimits) { return GetValue(MinValueHandle.Get()); } return MinAllowedValue; } template TOptional FIntervalStructCustomization::OnGetMinSliderValue() const { if (bClampToMinMaxLimits) { return GetValue(MinValueHandle.Get()); } return MinAllowedSliderValue; } template TOptional FIntervalStructCustomization::OnGetMaxValue() const { if (bClampToMinMaxLimits) { return GetValue(MaxValueHandle.Get()); } return MaxAllowedValue; } template TOptional FIntervalStructCustomization::OnGetMaxSliderValue() const { if (bClampToMinMaxLimits) { return GetValue(MaxValueHandle.Get()); } return MaxAllowedSliderValue; } template void FIntervalStructCustomization::SetValue(NumericType NewValue, EIntervalField Field, EPropertyValueSetFlags::Type Flags) { IPropertyHandle* Handle = (Field == EIntervalField::Min) ? MinValueHandle.Get() : MaxValueHandle.Get(); IPropertyHandle* OtherHandle = (Field == EIntervalField::Min) ? MaxValueHandle.Get() : MinValueHandle.Get(); const TOptional OtherValue = GetValue(OtherHandle); const bool bOutOfRange = OtherValue.IsSet() && ((Field == EIntervalField::Min && NewValue > OtherValue.GetValue()) || (Field == EIntervalField::Max && NewValue < OtherValue.GetValue())); // The order of execution of the Intertactive change vs commit is super important for the Undo history // So Interactive we must do, Handle, Other Handle // For Commit: we must do the reverse to properly close the scope of traction if (!bOutOfRange || bAllowInvertedInterval) { if (Flags == EPropertyValueSetFlags::InteractiveChange) { ensure(Handle->SetValue(NewValue, Flags) == FPropertyAccess::Success); if (OtherValue.IsSet()) { ensure(OtherHandle->SetValue(OtherValue.GetValue(), Flags) == FPropertyAccess::Success); } } else { if (OtherValue.IsSet()) { ensure(OtherHandle->SetValue(OtherValue.GetValue(), Flags) == FPropertyAccess::Success); } ensure(Handle->SetValue(NewValue, Flags) == FPropertyAccess::Success); } } else if (!bClampToMinMaxLimits) { if (Flags == EPropertyValueSetFlags::InteractiveChange) { ensure(Handle->SetValue(NewValue, Flags) == FPropertyAccess::Success); ensure(OtherHandle->SetValue(NewValue, Flags) == FPropertyAccess::Success); } else { ensure(OtherHandle->SetValue(NewValue, Flags) == FPropertyAccess::Success); ensure(Handle->SetValue(NewValue, Flags) == FPropertyAccess::Success); } } } template void FIntervalStructCustomization::OnValueCommitted(NumericType NewValue, ETextCommit::Type CommitType, EIntervalField Field) { if (!bIsUsingSlider || (bIsUsingSlider && ShouldAllowSpin())) { SetValue(NewValue, Field); } } template void FIntervalStructCustomization::OnValueChanged(NumericType NewValue, EIntervalField Field) { if (bIsUsingSlider && ShouldAllowSpin()) { SetValue(NewValue, Field, EPropertyValueSetFlags::InteractiveChange); } } template void FIntervalStructCustomization::OnBeginSliderMovement() { bIsUsingSlider = true; if (ShouldAllowSpin()) { GEditor->BeginTransaction(LOCTEXT("SetIntervalProperty", "Set Interval Property")); } } template void FIntervalStructCustomization::OnEndSliderMovement(NumericType /*NewValue*/) { bIsUsingSlider = false; if (ShouldAllowSpin()) { GEditor->EndTransaction(); } } template bool FIntervalStructCustomization::ShouldAllowSpin() const { return true; } template bool FIntervalStructCustomization::IsPropertyEnabled(EIntervalField Field) const { IPropertyHandle* Handle = (Field == EIntervalField::Min) ? MinValueHandle.Get() : MaxValueHandle.Get(); if (Handle == nullptr) { return false; } return !Handle->IsEditConst(); } /* Only explicitly instantiate the types which are supported *****************************************************************************/ template class FIntervalStructCustomization; template class FIntervalStructCustomization; template class FIntervalStructCustomization; #undef LOCTEXT_NAMESPACE