// Copyright Epic Games, Inc. All Rights Reserved. #include "ChaosClothAsset/WeightedValueCustomization.h" #include "ChaosClothAsset/ClothAssetEditorStyle.h" #include "ChaosClothAsset/WeightedValue.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/Input/SNumericEntryBox.h" #include "Widgets/Input/SEditableTextBox.h" #include "DetailLayoutBuilder.h" #include "DetailWidgetRow.h" #include "Editor.h" #define LOCTEXT_NAMESPACE "ChaosClothAssetWeightedValueCustomization" namespace UE::Chaos::ClothAsset { namespace Private { static const FString ImportFabricBounds = TEXT("ImportFabricBounds"); static bool ImportFabricBoundsProperty(const TSharedPtr& Property) { const FStringView PropertyPath = Property ? Property->GetPropertyPath() : FStringView(); return PropertyPath.EndsWith(ImportFabricBounds, ESearchCase::CaseSensitive); } } TSharedRef FWeightedValueCustomization::MakeInstance() { return MakeShareable(new FWeightedValueCustomization); } FWeightedValueCustomization::FWeightedValueCustomization() = default; FWeightedValueCustomization::~FWeightedValueCustomization() = default; void FWeightedValueCustomization::MakeHeaderRow(TSharedRef& StructPropertyHandle, FDetailWidgetRow& Row) { const TWeakPtr StructWeakHandlePtr = StructPropertyHandle; TSharedPtr ValueHorizontalBox; TSharedPtr NameHorizontalBox; Row.NameContent() [ SAssignNew(NameHorizontalBox, SHorizontalBox) .IsEnabled(this, &FMathStructCustomization::IsValueEnabled, StructWeakHandlePtr) ] .ValueContent() // Make enough space for each child handle .MinDesiredWidth(125.f * SortedChildHandles.Num()) .MaxDesiredWidth(125.f * SortedChildHandles.Num()) [ SAssignNew(ValueHorizontalBox, SHorizontalBox) .IsEnabled(this, &FMathStructCustomization::IsValueEnabled, StructWeakHandlePtr) ]; for (int32 ChildIndex = 0; ChildIndex < SortedChildHandles.Num(); ++ChildIndex) { TSharedRef ChildHandle = SortedChildHandles[ChildIndex]; if (CouldUseFabricsProperty(ChildHandle)) { bool bValue = false; ChildHandle->GetValue(bValue); if(!bValue) { break; } } if (Private::ImportFabricBoundsProperty(ChildHandle)) { AddToggledCheckBox(ChildHandle, NameHorizontalBox, FAppStyle::Get().GetBrush("Icons.Import")); } else if (BuildFabricMapsProperty(ChildHandle)) { AddToggledCheckBox(ChildHandle, NameHorizontalBox, UE::Chaos::ClothAsset::FClothAssetEditorStyle::Get().GetBrush("ClassIcon.ChaosClothPreset")); } } NameHorizontalBox->AddSlot().VAlign(VAlign_Center) .Padding(FMargin(4.f, 2.f, 4.0f, 2.f)) .HAlign(HAlign_Right) .AutoWidth() [ StructPropertyHandle->CreatePropertyNameWidget() ]; for (int32 ChildIndex = 0; ChildIndex < SortedChildHandles.Num(); ++ChildIndex) { TSharedRef ChildHandle = SortedChildHandles[ChildIndex]; PRAGMA_DISABLE_DEPRECATION_WARNINGS if (IsOverrideProperty(ChildHandle)) { continue; // Skip overrides } PRAGMA_ENABLE_DEPRECATION_WARNINGS // Propagate metadata to child properties so that it's reflected in the nested, individual spin boxes ChildHandle->SetInstanceMetaData(TEXT("UIMin"), StructPropertyHandle->GetMetaData(TEXT("UIMin"))); ChildHandle->SetInstanceMetaData(TEXT("UIMax"), StructPropertyHandle->GetMetaData(TEXT("UIMax"))); ChildHandle->SetInstanceMetaData(TEXT("SliderExponent"), StructPropertyHandle->GetMetaData(TEXT("SliderExponent"))); ChildHandle->SetInstanceMetaData(TEXT("Delta"), StructPropertyHandle->GetMetaData(TEXT("Delta"))); ChildHandle->SetInstanceMetaData(TEXT("LinearDeltaSensitivity"), StructPropertyHandle->GetMetaData(TEXT("LinearDeltaSensitivity"))); ChildHandle->SetInstanceMetaData(TEXT("ShiftMultiplier"), StructPropertyHandle->GetMetaData(TEXT("ShiftMultiplier"))); ChildHandle->SetInstanceMetaData(TEXT("CtrlMultiplier"), StructPropertyHandle->GetMetaData(TEXT("CtrlMultiplier"))); ChildHandle->SetInstanceMetaData(TEXT("SupportDynamicSliderMaxValue"), StructPropertyHandle->GetMetaData(TEXT("SupportDynamicSliderMaxValue"))); ChildHandle->SetInstanceMetaData(TEXT("SupportDynamicSliderMinValue"), StructPropertyHandle->GetMetaData(TEXT("SupportDynamicSliderMinValue"))); ChildHandle->SetInstanceMetaData(TEXT("ClampMin"), StructPropertyHandle->GetMetaData(TEXT("ClampMin"))); ChildHandle->SetInstanceMetaData(TEXT("ClampMax"), StructPropertyHandle->GetMetaData(TEXT("ClampMax"))); const bool bLastChild = SortedChildHandles.Num() - 1 == ChildIndex; TSharedRef ChildWidget = MakeChildWidget(StructPropertyHandle, ChildHandle); if(ChildWidget != SNullWidget::NullWidget) { if (ChildHandle->GetPropertyClass() == FBoolProperty::StaticClass()) { ValueHorizontalBox->AddSlot() .Padding(FMargin(0.f, 2.f, bLastChild ? 0.f : 3.f, 2.f)) .AutoWidth() // keep the check box slots small [ ChildWidget ]; } else { if (ChildHandle->GetPropertyClass() == FFloatProperty::StaticClass()) { NumericEntryBoxWidgetList.Add(ChildWidget); } ValueHorizontalBox->AddSlot() .Padding(FMargin(0.f, 2.f, bLastChild ? 0.f : 3.f, 2.f)) [ ChildWidget ]; } } } } TSharedRef FWeightedValueCustomization::MakeChildWidget( TSharedRef& StructurePropertyHandle, TSharedRef& PropertyHandle) { const FFieldClass* PropertyClass = PropertyHandle->GetPropertyClass(); if (PropertyClass == FFloatProperty::StaticClass()) { return MakeFloatWidget(StructurePropertyHandle, PropertyHandle); } if (PropertyClass == FBoolProperty::StaticClass()) { TWeakPtr WeakHandlePtr = PropertyHandle; if (!Private::ImportFabricBoundsProperty(PropertyHandle) && !BuildFabricMapsProperty(PropertyHandle) && !CouldUseFabricsProperty(PropertyHandle)) { return SNew(SCheckBox) .ToolTipText(PropertyHandle->GetToolTipText()) .Type(ESlateCheckBoxType::CheckBox) .IsChecked_Lambda([WeakHandlePtr]()->ECheckBoxState { bool bValue = false; WeakHandlePtr.Pin()->GetValue(bValue); return bValue ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }) .OnCheckStateChanged_Lambda([WeakHandlePtr](ECheckBoxState CheckBoxState) { WeakHandlePtr.Pin()->SetValue(CheckBoxState == ECheckBoxState::Checked, EPropertyValueSetFlags::DefaultFlags); }); } } return FConnectableValueCustomization::MakeChildWidget(StructurePropertyHandle, PropertyHandle); } TSharedRef FWeightedValueCustomization::MakeFloatWidget( TSharedRef& StructurePropertyHandle, TSharedRef& PropertyHandle) { TOptional MinValue, MaxValue, SliderMinValue, SliderMaxValue; float SliderExponent, Delta; float ShiftMultiplier = 10.f; float CtrlMultiplier = 0.1f; bool SupportDynamicSliderMaxValue = false; bool SupportDynamicSliderMinValue = false; ExtractFloatMetadata(StructurePropertyHandle, MinValue, MaxValue, SliderMinValue, SliderMaxValue, SliderExponent, Delta, ShiftMultiplier, CtrlMultiplier, SupportDynamicSliderMaxValue, SupportDynamicSliderMinValue); TWeakPtr WeakHandlePtr = PropertyHandle; return SNew(SNumericEntryBox) .IsEnabled(this, &FMathStructCustomization::IsValueEnabled, WeakHandlePtr) .EditableTextBoxStyle(&FCoreStyle::Get().GetWidgetStyle("NormalEditableTextBox")) .Value_Lambda([WeakHandlePtr]() { float Value = 0.; return (WeakHandlePtr.Pin()->GetValue(Value) == FPropertyAccess::Success) ? TOptional(Value) : TOptional(); // Value couldn't be accessed, return an unset value }) .Font(IDetailLayoutBuilder::GetDetailFont()) .UndeterminedString(NSLOCTEXT("PropertyEditor", "MultipleValues", "Multiple Values")) .OnValueCommitted_Lambda([WeakHandlePtr](float Value, ETextCommit::Type) { WeakHandlePtr.Pin()->SetValue(Value, EPropertyValueSetFlags::DefaultFlags); }) .OnValueChanged_Lambda([this, WeakHandlePtr](float Value) { if (bIsUsingSlider) { WeakHandlePtr.Pin()->SetValue(Value, EPropertyValueSetFlags::InteractiveChange | EPropertyValueSetFlags::NotTransactable); } }) .OnBeginSliderMovement_Lambda([this]() { bIsUsingSlider = true; GEditor->BeginTransaction(LOCTEXT("SetVectorProperty", "Set Vector Property")); }) .OnEndSliderMovement_Lambda([this](float Value) { bIsUsingSlider = false; GEditor->EndTransaction(); }) .LabelVAlign(VAlign_Center) // Only allow spin on handles with one object. Otherwise it is not clear what value to spin .AllowSpin(PropertyHandle->GetNumOuterObjects() < 2) .ShiftMultiplier(ShiftMultiplier) .CtrlMultiplier(CtrlMultiplier) .SupportDynamicSliderMaxValue(SupportDynamicSliderMaxValue) .SupportDynamicSliderMinValue(SupportDynamicSliderMinValue) .OnDynamicSliderMaxValueChanged(this, &FWeightedValueCustomization::OnDynamicSliderMaxValueChanged) .OnDynamicSliderMinValueChanged(this, &FWeightedValueCustomization::OnDynamicSliderMinValueChanged) .MinValue(MinValue) .MaxValue(MaxValue) .MinSliderValue(SliderMinValue) .MaxSliderValue(SliderMaxValue) .SliderExponent(SliderExponent) .Delta(Delta) .Label() [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(FText::FromString(PropertyHandle->GetMetaData(TEXT("ChaosClothAssetShortName")))) // Case specific metadata, uses ChaosCloth prefix to avoid conflict with future engine extensions ]; } // The following code is just a plain copy of FMathStructCustomization which // would need changes to be able to serve as a base class for this customization. void FWeightedValueCustomization::ExtractFloatMetadata(TSharedRef& PropertyHandle, TOptional& MinValue, TOptional& MaxValue, TOptional& SliderMinValue, TOptional& SliderMaxValue, float& SliderExponent, float& Delta, float& ShiftMultiplier, float& CtrlMultiplier, bool& SupportDynamicSliderMaxValue, bool& SupportDynamicSliderMinValue) { FProperty* Property = PropertyHandle->GetProperty(); const FString& MetaUIMinString = Property->GetMetaData(TEXT("UIMin")); const FString& MetaUIMaxString = Property->GetMetaData(TEXT("UIMax")); const FString& SliderExponentString = Property->GetMetaData(TEXT("SliderExponent")); const FString& DeltaString = Property->GetMetaData(TEXT("Delta")); const FString& ShiftMultiplierString = Property->GetMetaData(TEXT("ShiftMultiplier")); const FString& CtrlMultiplierString = Property->GetMetaData(TEXT("CtrlMultiplier")); const FString& SupportDynamicSliderMaxValueString = Property->GetMetaData(TEXT("SupportDynamicSliderMaxValue")); const FString& SupportDynamicSliderMinValueString = Property->GetMetaData(TEXT("SupportDynamicSliderMinValue")); const FString& ClampMinString = Property->GetMetaData(TEXT("ClampMin")); const FString& ClampMaxString = Property->GetMetaData(TEXT("ClampMax")); // If no UIMin/Max was specified then use the clamp string const FString& UIMinString = MetaUIMinString.Len() ? MetaUIMinString : ClampMinString; const FString& UIMaxString = MetaUIMaxString.Len() ? MetaUIMaxString : ClampMaxString; float ClampMin = TNumericLimits::Lowest(); float ClampMax = TNumericLimits::Max(); if (!ClampMinString.IsEmpty()) { TTypeFromString::FromString(ClampMin, *ClampMinString); } if (!ClampMaxString.IsEmpty()) { TTypeFromString::FromString(ClampMax, *ClampMaxString); } float UIMin = TNumericLimits::Lowest(); float UIMax = TNumericLimits::Max(); TTypeFromString::FromString(UIMin, *UIMinString); TTypeFromString::FromString(UIMax, *UIMaxString); SliderExponent = float(1); if (SliderExponentString.Len()) { TTypeFromString::FromString(SliderExponent, *SliderExponentString); } Delta = float(0); if (DeltaString.Len()) { TTypeFromString::FromString(Delta, *DeltaString); } ShiftMultiplier = 10.f; if (ShiftMultiplierString.Len()) { TTypeFromString::FromString(ShiftMultiplier, *ShiftMultiplierString); } CtrlMultiplier = 0.1f; if (CtrlMultiplierString.Len()) { TTypeFromString::FromString(CtrlMultiplier, *CtrlMultiplierString); } const float ActualUIMin = FMath::Max(UIMin, ClampMin); const float ActualUIMax = FMath::Min(UIMax, ClampMax); MinValue = ClampMinString.Len() ? ClampMin : TOptional(); MaxValue = ClampMaxString.Len() ? ClampMax : TOptional(); SliderMinValue = (UIMinString.Len()) ? ActualUIMin : TOptional(); SliderMaxValue = (UIMaxString.Len()) ? ActualUIMax : TOptional(); SupportDynamicSliderMaxValue = SupportDynamicSliderMaxValueString.Len() > 0 && SupportDynamicSliderMaxValueString.ToBool(); SupportDynamicSliderMinValue = SupportDynamicSliderMinValueString.Len() > 0 && SupportDynamicSliderMinValueString.ToBool(); } void FWeightedValueCustomization::OnDynamicSliderMaxValueChanged(float NewMaxSliderValue, TWeakPtr InValueChangedSourceWidget, bool IsOriginator, bool UpdateOnlyIfHigher) { for (TWeakPtr& Widget : NumericEntryBoxWidgetList) { TSharedPtr> NumericBox = StaticCastSharedPtr>(Widget.Pin()); if (NumericBox.IsValid()) { TSharedPtr> SpinBox = StaticCastSharedPtr>(NumericBox->GetSpinBox()); if (SpinBox.IsValid()) { if (SpinBox != InValueChangedSourceWidget) { if ((NewMaxSliderValue > SpinBox->GetMaxSliderValue() && UpdateOnlyIfHigher) || !UpdateOnlyIfHigher) { // Make sure the max slider value is not a getter otherwise we will break the link! verifySlow(!SpinBox->IsMaxSliderValueBound()); SpinBox->SetMaxSliderValue(NewMaxSliderValue); } } } } } if (IsOriginator) { OnNumericEntryBoxDynamicSliderMaxValueChanged.Broadcast((float)NewMaxSliderValue, InValueChangedSourceWidget, false, UpdateOnlyIfHigher); } } void FWeightedValueCustomization::OnDynamicSliderMinValueChanged(float NewMinSliderValue, TWeakPtr InValueChangedSourceWidget, bool IsOriginator, bool UpdateOnlyIfLower) { for (TWeakPtr& Widget : NumericEntryBoxWidgetList) { TSharedPtr> NumericBox = StaticCastSharedPtr>(Widget.Pin()); if (NumericBox.IsValid()) { TSharedPtr> SpinBox = StaticCastSharedPtr>(NumericBox->GetSpinBox()); if (SpinBox.IsValid()) { if (SpinBox != InValueChangedSourceWidget) { if ((NewMinSliderValue < SpinBox->GetMinSliderValue() && UpdateOnlyIfLower) || !UpdateOnlyIfLower) { // Make sure the min slider value is not a getter otherwise we will break the link! verifySlow(!SpinBox->IsMinSliderValueBound()); SpinBox->SetMinSliderValue(NewMinSliderValue); } } } } } if (IsOriginator) { OnNumericEntryBoxDynamicSliderMinValueChanged.Broadcast((float)NewMinSliderValue, InValueChangedSourceWidget, false, UpdateOnlyIfLower); } } } // End namespace UE::Chaos::ClothAsset #undef LOCTEXT_NAMESPACE