// Copyright Epic Games, Inc. All Rights Reserved. #include "Customizations/MathStructCustomizations.h" #include "UObject/UnrealType.h" #include "Widgets/Text/STextBlock.h" #include "Editor.h" #include "Misc/ConfigCacheIni.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SCheckBox.h" #include "IDetailChildrenBuilder.h" #include "DetailWidgetRow.h" #include "DetailLayoutBuilder.h" #include "Widgets/Input/SNumericEntryBox.h" #define LOCTEXT_NAMESPACE "FMathStructCustomization" TSharedRef FMathStructCustomization::MakeInstance() { return MakeShareable(new FMathStructCustomization); } void FMathStructCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { SortedChildHandles.Empty(); GetSortedChildren(StructPropertyHandle, SortedChildHandles); MakeHeaderRow(StructPropertyHandle, HeaderRow); } void FMathStructCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { for (int32 ChildIndex = 0; ChildIndex < SortedChildHandles.Num(); ++ChildIndex) { TSharedRef ChildHandle = SortedChildHandles[ChildIndex]; // Add the individual properties as children as well so the vector can be expanded for more room StructBuilder.AddProperty(ChildHandle); } } void FMathStructCustomization::MakeHeaderRow(TSharedRef& StructPropertyHandle, FDetailWidgetRow& Row) { TWeakPtr StructWeakHandlePtr = StructPropertyHandle; TSharedPtr HorizontalBox; Row.NameContent() [ StructPropertyHandle->CreatePropertyNameWidget() ] .ValueContent() // Make enough space for each child handle .MinDesiredWidth(125.0f * SortedChildHandles.Num()) .MaxDesiredWidth(125.0f * SortedChildHandles.Num()) [ SAssignNew(HorizontalBox, SHorizontalBox) .IsEnabled(this, &FMathStructCustomization::IsValueEnabled, StructWeakHandlePtr) ]; for (int32 ChildIndex = 0; ChildIndex < SortedChildHandles.Num(); ++ChildIndex) { TSharedRef ChildHandle = SortedChildHandles[ChildIndex]; // 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("ShiftMouseMovePixelPerDelta"), StructPropertyHandle->GetMetaData(TEXT("ShiftMouseMovePixelPerDelta"))); 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; // Make a widget for each property. The vector component properties will be displayed in the header TSharedRef NumericEntryBox = MakeChildWidget(StructPropertyHandle, ChildHandle); NumericEntryBoxWidgetList.Add(NumericEntryBox); HorizontalBox->AddSlot() .Padding(FMargin(0.0f, 2.0f, bLastChild ? 0.0f : 3.0f, 2.0f)) [ NumericEntryBox ]; } if (StructPropertyHandle->GetProperty()->HasMetaData("AllowPreserveRatio")) { if (!GConfig->GetBool(TEXT("SelectionDetails"), *(StructPropertyHandle->GetProperty()->GetName() + TEXT("_PreserveScaleRatio")), bPreserveScaleRatio, GEditorPerProjectIni)) { bPreserveScaleRatio = true; } HorizontalBox->AddSlot() .AutoWidth() .MaxWidth(18.0f) .VAlign(VAlign_Center) [ // Add a checkbox to toggle between preserving the ratio of x,y,z components of scale when a value is entered SNew(SCheckBox) .IsChecked(this, &FMathStructCustomization::IsPreserveScaleRatioChecked) .OnCheckStateChanged(this, &FMathStructCustomization::OnPreserveScaleRatioToggled, StructWeakHandlePtr) .Style(FAppStyle::Get(), "TransparentCheckBox") .ToolTipText(LOCTEXT("PreserveScaleToolTip", "When locked, scales uniformly based on the current xyz scale values so the object maintains its shape in each direction when scaled")) [ SNew(SImage) .Image(this, &FMathStructCustomization::GetPreserveScaleRatioImage) .ColorAndOpacity(FSlateColor::UseForeground()) ] ]; } } const FSlateBrush* FMathStructCustomization::GetPreserveScaleRatioImage() const { return bPreserveScaleRatio ? FAppStyle::GetBrush(TEXT("Icons.Lock")) : FAppStyle::GetBrush(TEXT("Icons.Unlock")); } ECheckBoxState FMathStructCustomization::IsPreserveScaleRatioChecked() const { return bPreserveScaleRatio ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } void FMathStructCustomization::OnPreserveScaleRatioToggled(ECheckBoxState NewState, TWeakPtr PropertyHandle) { bPreserveScaleRatio = (NewState == ECheckBoxState::Checked) ? true : false; if (PropertyHandle.IsValid() && PropertyHandle.Pin()->GetProperty()) { FString SettingKey = (PropertyHandle.Pin()->GetProperty()->GetName() + TEXT("_PreserveScaleRatio")); GConfig->SetBool(TEXT("SelectionDetails"), *SettingKey, bPreserveScaleRatio, GEditorPerProjectIni); } } void FMathStructCustomization::GetSortedChildren(TSharedRef StructPropertyHandle, TArray< TSharedRef >& OutChildren) { uint32 NumChildren; StructPropertyHandle->GetNumChildren(NumChildren); for (uint32 ChildIndex = 0; ChildIndex < NumChildren; ++ChildIndex) { OutChildren.Add(StructPropertyHandle->GetChildHandle(ChildIndex).ToSharedRef()); } } // Deprecated overload, to be removed. template void FMathStructCustomization::ExtractNumericMetadata(TSharedRef& PropertyHandle, TOptional& MinValue, TOptional& MaxValue, TOptional& SliderMinValue, TOptional& SliderMaxValue, NumericType& SliderExponent, NumericType& Delta, int32& ShiftMouseMovePixelPerDelta, bool& bSupportDynamicSliderMaxValue, bool& bSupportDynamicSliderMinValue) { FNumericMetadata Metadata; ExtractNumericMetadata(PropertyHandle, Metadata); MinValue = Metadata.MinValue; MaxValue = Metadata.MaxValue; SliderMinValue = Metadata.SliderMinValue; SliderMaxValue = Metadata.SliderMaxValue; SliderExponent = Metadata.SliderExponent; Delta = Metadata.Delta; ShiftMouseMovePixelPerDelta = Metadata.ShiftMouseMovePixelPerDelta; bSupportDynamicSliderMaxValue = Metadata.bSupportDynamicSliderMaxValue; bSupportDynamicSliderMinValue = Metadata.bSupportDynamicSliderMinValue; } // Explicitly instantiate the deprecated overload for the four types that we had implicit instantiations // for at time of deprecation (float, double, int32, uint8), since we no longer implicitly instantiate // it in this file due to using the other overload. template void FMathStructCustomization::ExtractNumericMetadata(TSharedRef& PropertyHandle, TOptional& MinValue, TOptional& MaxValue, TOptional& SliderMinValue, TOptional& SliderMaxValue, float& SliderExponent, float& Delta, int32& ShiftMouseMovePixelPerDelta, bool& bSupportDynamicSliderMaxValue, bool& bSupportDynamicSliderMinValue); template void FMathStructCustomization::ExtractNumericMetadata(TSharedRef& PropertyHandle, TOptional& MinValue, TOptional& MaxValue, TOptional& SliderMinValue, TOptional& SliderMaxValue, double& SliderExponent, double& Delta, int32& ShiftMouseMovePixelPerDelta, bool& bSupportDynamicSliderMaxValue, bool& bSupportDynamicSliderMinValue); template void FMathStructCustomization::ExtractNumericMetadata(TSharedRef& PropertyHandle, TOptional& MinValue, TOptional& MaxValue, TOptional& SliderMinValue, TOptional& SliderMaxValue, int32& SliderExponent, int32& Delta, int32& ShiftMouseMovePixelPerDelta, bool& bSupportDynamicSliderMaxValue, bool& bSupportDynamicSliderMinValue); template void FMathStructCustomization::ExtractNumericMetadata(TSharedRef& PropertyHandle, TOptional& MinValue, TOptional& MaxValue, TOptional& SliderMinValue, TOptional& SliderMaxValue, uint8& SliderExponent, uint8& Delta, int32& ShiftMouseMovePixelPerDelta, bool& bSupportDynamicSliderMaxValue, bool& bSupportDynamicSliderMinValue); template void FMathStructCustomization::ExtractNumericMetadata(TSharedRef& PropertyHandle, FNumericMetadata& MetadataOut) { 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& LinearDeltaSensitivityString = Property->GetMetaData(TEXT("LinearDeltaSensitivity")); const FString& ShiftMouseMovePixelPerDeltaString = Property->GetMetaData(TEXT("ShiftMouseMovePixelPerDelta")); 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; NumericType ClampMin = TNumericLimits::Lowest(); NumericType ClampMax = TNumericLimits::Max(); if (!ClampMinString.IsEmpty()) { TTypeFromString::FromString(ClampMin, *ClampMinString); } if (!ClampMaxString.IsEmpty()) { TTypeFromString::FromString(ClampMax, *ClampMaxString); } NumericType UIMin = TNumericLimits::Lowest(); NumericType UIMax = TNumericLimits::Max(); TTypeFromString::FromString(UIMin, *UIMinString); TTypeFromString::FromString(UIMax, *UIMaxString); MetadataOut.SliderExponent = NumericType(1); if (SliderExponentString.Len()) { TTypeFromString::FromString(MetadataOut.SliderExponent, *SliderExponentString); } MetadataOut.Delta = NumericType(0); if (DeltaString.Len()) { TTypeFromString::FromString(MetadataOut.Delta, *DeltaString); } MetadataOut.LinearDeltaSensitivity = 0; if (LinearDeltaSensitivityString.Len()) { TTypeFromString::FromString(MetadataOut.LinearDeltaSensitivity, *LinearDeltaSensitivityString); } // LinearDeltaSensitivity only works in SSpinBox if delta is provided, so add it in if it wasn't. MetadataOut.Delta = (MetadataOut.LinearDeltaSensitivity != 0 && MetadataOut.Delta == NumericType(0)) ? NumericType(1) : MetadataOut.Delta; MetadataOut.ShiftMouseMovePixelPerDelta = 1; if (ShiftMouseMovePixelPerDeltaString.Len()) { TTypeFromString::FromString(MetadataOut.ShiftMouseMovePixelPerDelta, *ShiftMouseMovePixelPerDeltaString); //The value should be greater or equal to 1 // 1 is neutral since it is a multiplier of the mouse drag pixel if (MetadataOut.ShiftMouseMovePixelPerDelta < 1) { MetadataOut.ShiftMouseMovePixelPerDelta = 1; } } if (ClampMin >= ClampMax && (ClampMinString.Len() || ClampMaxString.Len())) { //UE_LOG(LogPropertyNode, Warning, TEXT("Clamp Min (%s) >= Clamp Max (%s) for Ranged Numeric"), *ClampMinString, *ClampMaxString); } const NumericType ActualUIMin = FMath::Max(UIMin, ClampMin); const NumericType ActualUIMax = FMath::Min(UIMax, ClampMax); MetadataOut.MinValue = ClampMinString.Len() ? ClampMin : TOptional(); MetadataOut.MaxValue = ClampMaxString.Len() ? ClampMax : TOptional(); MetadataOut.SliderMinValue = (UIMinString.Len()) ? ActualUIMin : TOptional(); MetadataOut.SliderMaxValue = (UIMaxString.Len()) ? ActualUIMax : TOptional(); if (ActualUIMin >= ActualUIMax && (MetaUIMinString.Len() || MetaUIMaxString.Len())) { //UE_LOG(LogPropertyNode, Warning, TEXT("UI Min (%s) >= UI Max (%s) for Ranged Numeric"), *UIMinString, *UIMaxString); } MetadataOut.bSupportDynamicSliderMaxValue = SupportDynamicSliderMaxValueString.Len() > 0 && SupportDynamicSliderMaxValueString.ToBool(); MetadataOut.bSupportDynamicSliderMinValue = SupportDynamicSliderMinValueString.Len() > 0 && SupportDynamicSliderMinValueString.ToBool(); } template TSharedRef FMathStructCustomization::MakeNumericWidget( TSharedRef& StructurePropertyHandle, TSharedRef& PropertyHandle) { FNumericMetadata Metadata; ExtractNumericMetadata(StructurePropertyHandle, Metadata); TWeakPtr WeakHandlePtr = PropertyHandle; return SNew(SNumericEntryBox) .IsEnabled(this, &FMathStructCustomization::IsValueEnabled, WeakHandlePtr) .EditableTextBoxStyle(&FCoreStyle::Get().GetWidgetStyle("NormalEditableTextBox")) .Value(this, &FMathStructCustomization::OnGetValue, WeakHandlePtr) .Font(IDetailLayoutBuilder::GetDetailFont()) .UndeterminedString(NSLOCTEXT("PropertyEditor", "MultipleValues", "Multiple Values")) .OnValueCommitted(this, &FMathStructCustomization::OnValueCommitted, WeakHandlePtr) .OnValueChanged(this, &FMathStructCustomization::OnValueChanged, WeakHandlePtr) .OnBeginSliderMovement(this, &FMathStructCustomization::OnBeginSliderMovement) .OnEndSliderMovement(this, &FMathStructCustomization::OnEndSliderMovement) // Only allow spin on handles with one object. Otherwise it is not clear what value to spin .AllowSpin(PropertyHandle->GetNumOuterObjects() < 2) .ShiftMouseMovePixelPerDelta(Metadata.ShiftMouseMovePixelPerDelta) .SupportDynamicSliderMaxValue(Metadata.bSupportDynamicSliderMaxValue) .SupportDynamicSliderMinValue(Metadata.bSupportDynamicSliderMinValue) .OnDynamicSliderMaxValueChanged(this, &FMathStructCustomization::OnDynamicSliderMaxValueChanged) .OnDynamicSliderMinValueChanged(this, &FMathStructCustomization::OnDynamicSliderMinValueChanged) .MinValue(Metadata.MinValue) .MaxValue(Metadata.MaxValue) .MinSliderValue(Metadata.SliderMinValue) .MaxSliderValue(Metadata.SliderMaxValue) .SliderExponent(Metadata.SliderExponent) .Delta(Metadata.Delta) // LinearDeltaSensitivity must be left unset if not provided, rather than being set to some default .LinearDeltaSensitivity(Metadata.LinearDeltaSensitivity != 0 ? Metadata.LinearDeltaSensitivity : TAttribute()) .ToolTipText(this, &FMathStructCustomization::OnGetValueToolTip, WeakHandlePtr); } template void FMathStructCustomization::OnDynamicSliderMaxValueChanged(NumericType 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); } } template void FMathStructCustomization::OnDynamicSliderMinValueChanged(NumericType 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); } } TSharedRef FMathStructCustomization::MakeChildWidget( TSharedRef& StructurePropertyHandle, TSharedRef& PropertyHandle) { const FFieldClass* PropertyClass = PropertyHandle->GetPropertyClass(); if (PropertyClass == FFloatProperty::StaticClass()) { return MakeNumericWidget(StructurePropertyHandle, PropertyHandle); } if (PropertyClass == FDoubleProperty::StaticClass()) { return MakeNumericWidget(StructurePropertyHandle, PropertyHandle); } if (PropertyClass == FIntProperty::StaticClass()) { return MakeNumericWidget(StructurePropertyHandle, PropertyHandle); } if (PropertyClass == FByteProperty::StaticClass()) { return MakeNumericWidget(StructurePropertyHandle, PropertyHandle); } if (PropertyClass == FEnumProperty::StaticClass()) { const FEnumProperty* EnumPropertyClass = static_cast(PropertyHandle->GetProperty()); const FProperty* Enum = EnumPropertyClass->GetUnderlyingProperty(); const FFieldClass* EnumClass = Enum->GetClass(); if (EnumClass == FByteProperty::StaticClass()) { return MakeNumericWidget(StructurePropertyHandle, PropertyHandle); } else if (EnumClass == FIntProperty::StaticClass()) { return MakeNumericWidget(StructurePropertyHandle, PropertyHandle); } } check(0); // Unsupported class return SNullWidget::NullWidget; } template TOptional FMathStructCustomization::OnGetValue(TWeakPtr WeakHandlePtr) const { NumericType NumericVal = 0; if (WeakHandlePtr.Pin()->GetValue(NumericVal) == FPropertyAccess::Success) { return TOptional(NumericVal); } // Value couldn't be accessed. Return an unset value return TOptional(); } template void FMathStructCustomization::OnValueCommitted(NumericType NewValue, ETextCommit::Type CommitType, TWeakPtr WeakHandlePtr) { EPropertyValueSetFlags::Type Flags = EPropertyValueSetFlags::DefaultFlags; SetValue(NewValue, Flags, WeakHandlePtr); } template void FMathStructCustomization::OnValueChanged(NumericType NewValue, TWeakPtr WeakHandlePtr) { if (bIsUsingSlider) { EPropertyValueSetFlags::Type Flags = EPropertyValueSetFlags::InteractiveChange | EPropertyValueSetFlags::NotTransactable; SetValue(NewValue, Flags, WeakHandlePtr); } } template void FMathStructCustomization::SetValue(NumericType NewValue, EPropertyValueSetFlags::Type Flags, TWeakPtr WeakHandlePtr) { if (bPreserveScaleRatio) { // Get the value for each object for the modified component TArray OldValues; if (WeakHandlePtr.Pin()->GetPerObjectValues(OldValues) == FPropertyAccess::Success) { // Loop through each object and scale based on the new ratio for each object individually for (int32 OutputIndex = 0; OutputIndex < OldValues.Num(); ++OutputIndex) { NumericType OldValue; TTypeFromString::FromString(OldValue, *OldValues[OutputIndex]); // Account for the previous scale being zero. Just set to the new value in that case? NumericType Ratio = OldValue == 0 ? NewValue : NewValue / OldValue; if (Ratio == 0) { Ratio = NewValue; } // Loop through all the child handles (each component of the math struct, like X, Y, Z...etc) for (int32 ChildIndex = 0; ChildIndex < SortedChildHandles.Num(); ++ChildIndex) { // Ignore scaling our selves. TSharedRef ChildHandle = SortedChildHandles[ChildIndex]; if (ChildHandle != WeakHandlePtr.Pin()) { // Get the value for each object. TArray ObjectChildValues; if (ChildHandle->GetPerObjectValues(ObjectChildValues) == FPropertyAccess::Success) { // Individually scale each object's components by the same ratio. for (int32 ChildOutputIndex = 0; ChildOutputIndex < ObjectChildValues.Num(); ++ChildOutputIndex) { NumericType ChildOldValue; TTypeFromString::FromString(ChildOldValue, *ObjectChildValues[ChildOutputIndex]); NumericType ChildNewValue = ChildOldValue * Ratio; ObjectChildValues[ChildOutputIndex] = TTypeToString::ToSanitizedString(ChildNewValue); } ChildHandle->SetPerObjectValues(ObjectChildValues); } } } } } } WeakHandlePtr.Pin()->SetValue(NewValue, Flags); } template FText FMathStructCustomization::OnGetValueToolTip(TWeakPtr WeakHandlePtr) const { if(TSharedPtr PropertyHandle = WeakHandlePtr.Pin()) { TOptional Value = OnGetValue(WeakHandlePtr); if (Value.IsSet()) { return FText::Format(LOCTEXT("ValueToolTip", "{0}: {1}"), PropertyHandle->GetPropertyDisplayName(), FText::AsNumber(Value.GetValue())); } } return FText::GetEmpty(); } bool FMathStructCustomization::IsValueEnabled(TWeakPtr WeakHandlePtr) const { if (WeakHandlePtr.IsValid()) { return !WeakHandlePtr.Pin()->IsEditConst(); } return false; } void FMathStructCustomization::OnBeginSliderMovement() { bIsUsingSlider = true; GEditor->BeginTransaction(LOCTEXT("SetVectorProperty", "Set Vector Property")); } template void FMathStructCustomization::OnEndSliderMovement(NumericType NewValue) { bIsUsingSlider = false; GEditor->EndTransaction(); } #undef LOCTEXT_NAMESPACE