// Copyright Epic Games, Inc. All Rights Reserved. #include "RawDistributionVectorStructCustomization.h" #include "Delegates/Delegate.h" #include "DetailLayoutBuilder.h" #include "DetailWidgetRow.h" #include "Editor.h" #include "Editor/EditorEngine.h" #include "Engine/Engine.h" #include "Fonts/SlateFontInfo.h" #include "Framework/SlateDelegates.h" #include "HAL/PlatformCrt.h" #include "IDetailChildrenBuilder.h" #include "IDetailCustomNodeBuilder.h" #include "Input/Events.h" #include "Input/Reply.h" #include "InputCoreTypes.h" #include "Internationalization/Internationalization.h" #include "Internationalization/Text.h" #include "Math/Color.h" #include "Math/UnrealMathSSE.h" #include "Math/Vector.h" #include "Math/Vector2D.h" #include "Misc/AssertionMacros.h" #include "Misc/Attribute.h" #include "Misc/Optional.h" #include "PropertyCustomizationHelpers.h" #include "PropertyEditorModule.h" #include "PropertyHandle.h" #include "Types/SlateEnums.h" #include "UObject/Class.h" #include "UObject/Field.h" #include "UObject/NameTypes.h" #include "UObject/UnrealNames.h" #include "UObject/UnrealType.h" #include "Widgets/Colors/SColorBlock.h" #include "Widgets/Colors/SColorPicker.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/Input/SNumericEntryBox.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Text/STextBlock.h" class SWidget; struct FGeometry; #define LOCTEXT_NAMESPACE "RawDistributionVectorStructCustomization" class FReplaceVectorWithLinearColorBuilder : public IDetailCustomNodeBuilder, public TSharedFromThis { public: /** * Constructor * * @param The property handle representing this item */ explicit FReplaceVectorWithLinearColorBuilder(TSharedRef InPropertyHandle); /** * Sets a delegate that should be used when the custom node needs to rebuild children * * @param A delegate to invoke when the tree should rebuild this nodes children */ virtual void SetOnRebuildChildren(FSimpleDelegate InOnRegenerateChildren) override {} /** * Called to generate content in the header of this node ( the actual node content ). * Only called if HasHeaderRow is true * * @param NodeRow The row to put content in */ virtual void GenerateHeaderRowContent(FDetailWidgetRow& NodeRow) override; /** * Called to generate child content of this node * * @param OutChildRows An array of rows to add children to */ virtual void GenerateChildContent(IDetailChildrenBuilder& ChildrenBuilder) override; /** * Called each tick if ReqiresTick is true */ virtual void Tick(float DeltaTime) override {} /** * @return true if this node requires tick, false otherwise */ virtual bool RequiresTick() const override { return false; } /** * @return true if this node should be collapsed in the tree */ virtual bool InitiallyCollapsed() const override { return false; } /** * @return The name of this custom builder. This is used as an identifier to save expansion state if needed */ virtual FName GetName() const override; protected: /** * Creates a widget representing the FVector being pointed to as a FLinearColor */ TSharedRef CreateColorWidget(const TSharedPtr& StructHandle); /** * Adds a child widget representing a component of an FLinearColor * * @param StructHandle Handle of the FVector being represented as a FLinearColor * @param Text Label text * @param ElementIndex Element of the FVector it represents (0-2) * @param ChildrenBuilder The builder object */ void AddColorChildProperty(const TSharedPtr& StructHandle, const FText& Text, int32 ElementIndex, IDetailChildrenBuilder& ChildrenBuilder); /** * Generates a child property, handling nested arrays and structs by creating a child property builder. * * @param Handle Property handle of the property being generated * @param ChildrenBuilder The current builder object */ void GeneratePropertyContent(const TSharedRef& Handle, IDetailChildrenBuilder& ChildrenBuilder); /** * Called by the array builder when it needs to generate a new child widget * * @param ElementProperty Property handle of the array element * @param ElementIndex Index of the element in the array * @param ChildrenBuilder The current builder object */ void OnGenerateArrayElementWidget(TSharedRef ElementProperty, int32 ElementIndex, IDetailChildrenBuilder& ChildrenBuilder); /** * Called by the color element widgets to determine the current color element value * * @param StructHandle Property handle of the FVector representing the color * @param ElementIndex Element of the FVector it represents (0-2) */ TOptional OnGetColorElementValue(TSharedPtr StructHandle, int32 ElementIndex) const; /** * Called when a numeric component is set. * * @param NewValue New element value * @param CommitType Specifies how the result was entered * @param StructHandle Property handle of the FVector representing the color * @param ElementIndex Element of the FVector it represents (0-2) */ void OnColorElementValueCommitted(float NewValue, ETextCommit::Type CommitType, TSharedPtr StructHandle, int32 ElementIndex); /** * Called when a numeric component is changed. * * @param NewValue New element value * @param StructHandle Property handle of the FVector representing the color * @param ElementIndex Element of the FVector it represents (0-2) */ void OnColorElementValueChanged(float NewValue, TSharedPtr StructHandle, int32 ElementIndex); /** Called when the slider is moved on a numeric entry box */ void OnBeginSliderMovement(); /** Called when the slider is released on a numeric entry box */ void OnEndSliderMovement(float NewValue); /** Returns the FLinearColor represented by the FVector pointed to by the property handle */ FLinearColor OnGetColorForColorBlock(TSharedPtr StructHandle) const; /** * Called when mouse is clicked on the color widget */ FReply OnMouseButtonDownColorBlock(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent, TSharedPtr StructHandle); /** Creates a color picker window */ void CreateColorPicker(const TSharedPtr& StructHandle); /** * Called when a color is chosen from the color picker * * @param NewColor Color that was chosen * @param StructHandle Property handle of the FVector representing the color */ void OnSetColorFromColorPicker(FLinearColor NewColor, TSharedPtr StructHandle); /** * Called when the color picker is cancelled * * @param OriginalColor Original value of the property * @param StructHandle Property handle of the FVector representing the color */ void OnColorPickerCancelled(FLinearColor OriginalColor, TSharedPtr StructHandle); /** Called when a drag starts in the color picker */ void OnColorPickerInteractiveBegin(); /** Called when a drag ends in the color picker */ void OnColorPickerInteractiveEnd(); private: /** Holds the property handle being referenced by this builder object */ TSharedRef PropertyHandle; /** true if the property is an FVector, and hence needs customization */ bool bIsVectorProperty; /** true if the slider is being dragged in a numeric entry box */ bool bIsUsingSlider; /** Original value of the property, prior to using the color picker */ FLinearColor OldColorValue; /** Widget the Color Picker is parented to */ TSharedPtr ColorPickerParentWidget; }; // Some helper functions for obtaining values from the FVector property, and converting them to FLinearColor namespace { float GetColorElementValue(const TSharedPtr& StructHandle, int32 ElementIndex) { FVector Result; ensure(StructHandle->GetValue(Result) == FPropertyAccess::Success); return static_cast(Result[ElementIndex]); } void SetColorElementValue(const TSharedPtr& StructHandle, int32 ElementIndex, float Value) { FVector Result; ensure(StructHandle->GetValue(Result) == FPropertyAccess::Success); Result[ElementIndex] = Value; ensure(StructHandle->SetValue(Result) == FPropertyAccess::Success); } FLinearColor GetColorValue(const TSharedPtr& StructHandle) { FVector Result; ensure(StructHandle->GetValue(Result) == FPropertyAccess::Success); return FLinearColor(static_cast(Result.X), static_cast(Result.Y), static_cast(Result.Z), 1.0f); } void SetColorValue(const TSharedPtr& StructHandle, FLinearColor Value) { FVector ValueAsVector(Value.R, Value.G, Value.B); ensure(StructHandle->SetValue(ValueAsVector) == FPropertyAccess::Success); } } FReplaceVectorWithLinearColorBuilder::FReplaceVectorWithLinearColorBuilder(TSharedRef InPropertyHandle) : PropertyHandle(InPropertyHandle) , bIsVectorProperty(false) , bIsUsingSlider(false) { // Determine if this is an FVector - if so it will be specialized FProperty* Property = InPropertyHandle->GetProperty(); if (FStructProperty* StructProperty = CastField(Property)) { FName StructType = StructProperty->Struct->GetFName(); bIsVectorProperty = (StructType == NAME_Vector); } } void FReplaceVectorWithLinearColorBuilder::GenerateHeaderRowContent(FDetailWidgetRow& NodeRow) { // Only generate a header row if the handle has a valid FProperty. // Note that it's possible for the Property to be NULL if the property node is an FObjectPropertyNode - however we still want to create children in this case. if (PropertyHandle->GetProperty() != nullptr) { NodeRow.NameContent() [ PropertyHandle->CreatePropertyNameWidget() ]; if (bIsVectorProperty) { // Customization - make FVector look like an FLinearColor NodeRow.ValueContent() .MinDesiredWidth(250.0f) .MaxDesiredWidth(250.0f) [ CreateColorWidget(PropertyHandle) ]; } else { // Otherwise, use the default property widget NodeRow.ValueContent() .MinDesiredWidth(1) .MaxDesiredWidth(4096) [ PropertyHandle->CreatePropertyValueWidget() ]; } } } void FReplaceVectorWithLinearColorBuilder::GenerateChildContent(IDetailChildrenBuilder& ChildrenBuilder) { if (bIsVectorProperty) { // Customization - children of an FVector are made to look like color components of an FLinearColor AddColorChildProperty(PropertyHandle, LOCTEXT("RedComponent", "R"), 0, ChildrenBuilder); AddColorChildProperty(PropertyHandle, LOCTEXT("GreenComponent", "G"), 1, ChildrenBuilder); AddColorChildProperty(PropertyHandle, LOCTEXT("BlueComponent", "B"), 2, ChildrenBuilder); } else { // Otherwise, go through the child properties and render them as normal uint32 NumChildren; ensure(PropertyHandle->GetNumChildren(NumChildren) == FPropertyAccess::Success); for (uint32 ChildIndex = 0; ChildIndex < NumChildren; ChildIndex++) { TSharedPtr ChildHandle = PropertyHandle->GetChildHandle(ChildIndex); check(ChildHandle.IsValid()); GeneratePropertyContent(ChildHandle.ToSharedRef(), ChildrenBuilder); } } } FName FReplaceVectorWithLinearColorBuilder::GetName() const { if (PropertyHandle->GetProperty() != nullptr) { return PropertyHandle->GetProperty()->GetFName(); } return NAME_None; } TSharedRef FReplaceVectorWithLinearColorBuilder::CreateColorWidget(const TSharedPtr& StructHandle) { FSlateFontInfo NormalText = IDetailLayoutBuilder::GetDetailFont(); return SNew(SBox) .VAlign(VAlign_Center) [ SAssignNew(ColorPickerParentWidget, SColorBlock) .Color(this, &FReplaceVectorWithLinearColorBuilder::OnGetColorForColorBlock, StructHandle) .ShowBackgroundForAlpha(false) .AlphaDisplayMode(EColorBlockAlphaDisplayMode::Ignore) .OnMouseButtonDown(this, &FReplaceVectorWithLinearColorBuilder::OnMouseButtonDownColorBlock, StructHandle) .Size(FVector2D(70.0f, 12.0f)) ]; } void FReplaceVectorWithLinearColorBuilder::AddColorChildProperty(const TSharedPtr& StructHandle, const FText& Text, int32 ElementIndex, IDetailChildrenBuilder& ChildrenBuilder) { ChildrenBuilder.AddCustomRow(LOCTEXT("Color", "Color")) .NameContent() [ SNew(STextBlock) .Text(Text) .Font(IPropertyTypeCustomizationUtils::GetRegularFont()) ] .ValueContent() .MinDesiredWidth(100.0f) .MaxDesiredWidth(100.0f) [ SNew(SNumericEntryBox) .Font(IPropertyTypeCustomizationUtils::GetRegularFont()) .Value(this, &FReplaceVectorWithLinearColorBuilder::OnGetColorElementValue, StructHandle, ElementIndex) .OnValueCommitted(this, &FReplaceVectorWithLinearColorBuilder::OnColorElementValueCommitted, StructHandle, ElementIndex) .OnValueChanged(this, &FReplaceVectorWithLinearColorBuilder::OnColorElementValueChanged, StructHandle, ElementIndex) .OnBeginSliderMovement(this, &FReplaceVectorWithLinearColorBuilder::OnBeginSliderMovement) .OnEndSliderMovement(this, &FReplaceVectorWithLinearColorBuilder::OnEndSliderMovement) .AllowSpin(true) .MinSliderValue(0.0f) .MaxSliderValue(1.0f) ]; } void FReplaceVectorWithLinearColorBuilder::GeneratePropertyContent(const TSharedRef& Handle, IDetailChildrenBuilder& ChildrenBuilder) { // Add to the current builder, depending on the property type. uint32 NumChildren = 0; ensure(Handle->GetNumChildren(NumChildren) == FPropertyAccess::Success); bool bHasChildren = (NumChildren > 0); bool bIsArray = Handle->AsArray().IsValid(); if (bIsArray) { // Arrays need special handling and will create an array builder TSharedRef ArrayBuilder = MakeShareable(new FDetailArrayBuilder(Handle)); ArrayBuilder->OnGenerateArrayElementWidget(FOnGenerateArrayElementWidget::CreateSP(this, &FReplaceVectorWithLinearColorBuilder::OnGenerateArrayElementWidget)); ChildrenBuilder.AddCustomBuilder(ArrayBuilder); } else if (bHasChildren) { // If there are children, we invoke a new instance of our custom builder for recursive handling // Note, if this is an FVector, it will be handled specially by the implementation of the IDetailCustomNodeBuilder interface. TSharedRef StructBuilder = MakeShareable(new FReplaceVectorWithLinearColorBuilder(Handle)); ChildrenBuilder.AddCustomBuilder(StructBuilder); } else { // No children - just add the property. ChildrenBuilder.AddProperty(Handle); } } void FReplaceVectorWithLinearColorBuilder::OnGenerateArrayElementWidget(TSharedRef ElementProperty, int32 ElementIndex, IDetailChildrenBuilder& ChildrenBuilder) { GeneratePropertyContent(ElementProperty, ChildrenBuilder); } TOptional FReplaceVectorWithLinearColorBuilder::OnGetColorElementValue(TSharedPtr StructHandle, int32 ElementIndex) const { return GetColorElementValue(StructHandle, ElementIndex); } void FReplaceVectorWithLinearColorBuilder::OnColorElementValueCommitted(float NewValue, ETextCommit::Type CommitType, TSharedPtr StructHandle, int32 ElementIndex) { SetColorElementValue(StructHandle, ElementIndex, NewValue); } void FReplaceVectorWithLinearColorBuilder::OnColorElementValueChanged(float NewValue, TSharedPtr StructHandle, int32 ElementIndex) { if (bIsUsingSlider) { SetColorElementValue(StructHandle, ElementIndex, NewValue); } } FLinearColor FReplaceVectorWithLinearColorBuilder::OnGetColorForColorBlock(TSharedPtr StructHandle) const { return GetColorValue(StructHandle); } void FReplaceVectorWithLinearColorBuilder::OnBeginSliderMovement() { GEditor->BeginTransaction(LOCTEXT("SetColorProperty", "Set Color Property")); bIsUsingSlider = true; } void FReplaceVectorWithLinearColorBuilder::OnEndSliderMovement(float NewValue) { bIsUsingSlider = false; GEditor->EndTransaction(); } FReply FReplaceVectorWithLinearColorBuilder::OnMouseButtonDownColorBlock(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent, TSharedPtr StructHandle) { if (MouseEvent.GetEffectingButton() != EKeys::LeftMouseButton) { return FReply::Unhandled(); } CreateColorPicker(StructHandle); return FReply::Handled(); } void FReplaceVectorWithLinearColorBuilder::CreateColorPicker(const TSharedPtr& StructHandle) { OldColorValue = GetColorValue(StructHandle); FColorPickerArgs PickerArgs; PickerArgs.bUseAlpha = false; PickerArgs.bOnlyRefreshOnMouseUp = false; PickerArgs.bOnlyRefreshOnOk = false; PickerArgs.DisplayGamma = TAttribute::Create(TAttribute::FGetter::CreateUObject(GEngine, &UEngine::GetDisplayGamma)); PickerArgs.OnColorCommitted = FOnLinearColorValueChanged::CreateSP(this, &FReplaceVectorWithLinearColorBuilder::OnSetColorFromColorPicker, StructHandle); PickerArgs.OnColorPickerCancelled = FOnColorPickerCancelled::CreateSP(this, &FReplaceVectorWithLinearColorBuilder::OnColorPickerCancelled, StructHandle); PickerArgs.OnInteractivePickBegin = FSimpleDelegate::CreateSP(this, &FReplaceVectorWithLinearColorBuilder::OnColorPickerInteractiveBegin); PickerArgs.OnInteractivePickEnd = FSimpleDelegate::CreateSP(this, &FReplaceVectorWithLinearColorBuilder::OnColorPickerInteractiveEnd); PickerArgs.InitialColor = OldColorValue; PickerArgs.ParentWidget = ColorPickerParentWidget; OpenColorPicker(PickerArgs); } void FReplaceVectorWithLinearColorBuilder::OnSetColorFromColorPicker(FLinearColor NewColor, TSharedPtr StructHandle) { SetColorValue(StructHandle, NewColor); } void FReplaceVectorWithLinearColorBuilder::OnColorPickerCancelled(FLinearColor OriginalColor, TSharedPtr StructHandle) { SetColorValue(StructHandle, OldColorValue); } void FReplaceVectorWithLinearColorBuilder::OnColorPickerInteractiveBegin() { GEditor->BeginTransaction(LOCTEXT("SetColorProperty", "Set Color Property")); } void FReplaceVectorWithLinearColorBuilder::OnColorPickerInteractiveEnd() { GEditor->EndTransaction(); } TSharedRef FRawDistributionVectorStructCustomization::MakeInstance() { return MakeShareable(new FRawDistributionVectorStructCustomization); } void FRawDistributionVectorStructCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { HeaderRow .NameContent() [ StructPropertyHandle->CreatePropertyNameWidget() ] .ValueContent() .MinDesiredWidth(1) .MaxDesiredWidth(4096) [ StructPropertyHandle->CreatePropertyValueWidget() ]; } void FRawDistributionVectorStructCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { // Determine from the metadata whether we should treat vectors as FLinearColors or not bool bTreatAsColor = StructPropertyHandle->HasMetaData("TreatAsColor"); uint32 NumChildren; ensure(StructPropertyHandle->GetNumChildren(NumChildren) == FPropertyAccess::Success); // Now recurse through all children, creating a custom builder for each which will either add the default property row, or // a property row exposing a FLinearColor type customization which maps directly to the elements of the original FVector. for (uint32 ChildIndex = 0; ChildIndex < NumChildren; ChildIndex++) { TSharedPtr ChildHandle = StructPropertyHandle->GetChildHandle(ChildIndex); check(ChildHandle.IsValid()); if (bTreatAsColor) { TSharedRef CustomBuilder = MakeShareable(new FReplaceVectorWithLinearColorBuilder(ChildHandle.ToSharedRef())); StructBuilder.AddCustomBuilder(CustomBuilder); } else { StructBuilder.AddProperty(ChildHandle.ToSharedRef()); } } } #undef LOCTEXT_NAMESPACE