// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "MathStructProxyCustomizations.h" #include "Framework/Commands/UIAction.h" #include "UObject/UnrealType.h" #include "Editor.h" #include "IDetailChildrenBuilder.h" #include "DetailWidgetRow.h" #include "DetailLayoutBuilder.h" #include "IPropertyUtilities.h" #include "ScopedTransaction.h" #include "Widgets/Input/SNumericEntryBox.h" #include "HAL/PlatformApplicationMisc.h" void FMathStructProxyCustomization::CustomizeChildren( TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils ) { PropertyUtilities = StructCustomizationUtils.GetPropertyUtilities(); } void FMathStructProxyCustomization::MakeHeaderRow( TSharedRef& StructPropertyHandle, FDetailWidgetRow& Row ) { } template TSharedRef FMathStructProxyCustomization::MakeNumericProxyWidget(TSharedRef& StructPropertyHandle, TSharedRef< TProxyProperty >& ProxyValue, const FText& Label, bool bRotationInDegrees, const FLinearColor& LabelColor, const FLinearColor& LabelBackgroundColor) { TWeakPtr WeakHandlePtr = StructPropertyHandle; return SNew( SNumericEntryBox ) .IsEnabled( this, &FMathStructProxyCustomization::IsValueEnabled, WeakHandlePtr ) .Value( this, &FMathStructProxyCustomization::OnGetValue, WeakHandlePtr, ProxyValue ) .Font( IDetailLayoutBuilder::GetDetailFont() ) .UndeterminedString( NSLOCTEXT("PropertyEditor", "MultipleValues", "Multiple Values") ) .OnValueCommitted( this, &FMathStructProxyCustomization::OnValueCommitted, WeakHandlePtr, ProxyValue ) .OnValueChanged( this, &FMathStructProxyCustomization::OnValueChanged, WeakHandlePtr, ProxyValue ) .OnBeginSliderMovement( this, &FMathStructProxyCustomization::OnBeginSliderMovement ) .OnEndSliderMovement( this, &FMathStructProxyCustomization::OnEndSliderMovement, WeakHandlePtr, ProxyValue ) .LabelVAlign(VAlign_Fill) .LabelPadding(0) // Only allow spin on handles with one object. Otherwise it is not clear what value to spin .AllowSpin( StructPropertyHandle->GetNumOuterObjects() == 1 ) .MinValue(TOptional()) .MaxValue(TOptional()) .MaxSliderValue(bRotationInDegrees ? 360.0f : TOptional()) .MinSliderValue(bRotationInDegrees ? 0.0f : TOptional()) .Label() [ SNumericEntryBox::BuildLabel( Label, LabelColor, LabelBackgroundColor ) ]; } template TOptional FMathStructProxyCustomization::OnGetValue( TWeakPtr WeakHandlePtr, TSharedRef< TProxyProperty > ProxyValue ) const { if(CacheValues(WeakHandlePtr)) { return ProxyValue->Get(); } return TOptional(); } template void FMathStructProxyCustomization::OnValueCommitted( NumericType NewValue, ETextCommit::Type CommitType, TWeakPtr WeakHandlePtr, TSharedRef< TProxyProperty > ProxyValue ) { if (!bIsUsingSlider && !GIsTransacting) { ProxyValue->Set(NewValue); FlushValues(WeakHandlePtr); } } template void FMathStructProxyCustomization::OnValueChanged( NumericType NewValue, TWeakPtr WeakHandlePtr, TSharedRef< TProxyProperty > ProxyValue ) { if( bIsUsingSlider ) { ProxyValue->Set(NewValue); FlushValues(WeakHandlePtr); } } void FMathStructProxyCustomization::OnBeginSliderMovement() { bIsUsingSlider = true; } template void FMathStructProxyCustomization::OnEndSliderMovement( NumericType NewValue, TWeakPtr WeakHandlePtr, TSharedRef< TProxyProperty > ProxyValue ) { bIsUsingSlider = false; ProxyValue->Set(NewValue); FlushValues(WeakHandlePtr); } #define LOCTEXT_NAMESPACE "MatrixStructCustomization" TSharedRef FMatrixStructCustomization::MakeInstance() { return MakeShareable( new FMatrixStructCustomization ); } void FMatrixStructCustomization::MakeHeaderRow(TSharedRef& StructPropertyHandle, FDetailWidgetRow& Row) { Row .NameContent() [ StructPropertyHandle->CreatePropertyNameWidget() ] .ValueContent() .MinDesiredWidth(0.0f) .MaxDesiredWidth(0.0f) [ SNullWidget::NullWidget ]; } void FMatrixStructCustomization::CustomizeLocation(TSharedRef StructPropertyHandle, FDetailWidgetRow& Row) { TWeakPtr WeakHandlePtr = StructPropertyHandle; Row .CopyAction(FUIAction(FExecuteAction::CreateSP(this, &FMatrixStructCustomization::OnCopy, FTransformField::Location, WeakHandlePtr))) .PasteAction(FUIAction(FExecuteAction::CreateSP(this, &FMatrixStructCustomization::OnPaste, FTransformField::Location, WeakHandlePtr))) .NameContent() [ StructPropertyHandle->CreatePropertyNameWidget(LOCTEXT("LocationLabel", "Location")) ] .ValueContent() .MinDesiredWidth(375.0f) .MaxDesiredWidth(375.0f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(FMargin(0.0f, 2.0f, 3.0f, 2.0f)) [ MakeNumericProxyWidget(StructPropertyHandle, CachedTranslationX, LOCTEXT("TranslationX", "X"), false, FLinearColor::White, SNumericEntryBox::RedLabelBackgroundColor) ] + SHorizontalBox::Slot() .Padding(FMargin(0.0f, 2.0f, 3.0f, 2.0f)) [ MakeNumericProxyWidget(StructPropertyHandle, CachedTranslationY, LOCTEXT("TranslationY", "Y"), false, FLinearColor::White, SNumericEntryBox::GreenLabelBackgroundColor) ] + SHorizontalBox::Slot() .Padding(FMargin(0.0f, 2.0f, 0.0f, 2.0f)) [ MakeNumericProxyWidget(StructPropertyHandle, CachedTranslationZ, LOCTEXT("TranslationZ", "Z"), false, FLinearColor::White, SNumericEntryBox::BlueLabelBackgroundColor) ] ]; } void FMatrixStructCustomization::CustomizeRotation(TSharedRef StructPropertyHandle, FDetailWidgetRow& Row) { TWeakPtr WeakHandlePtr = StructPropertyHandle; Row .CopyAction(FUIAction(FExecuteAction::CreateSP(this, &FMatrixStructCustomization::OnCopy, FTransformField::Rotation, WeakHandlePtr))) .PasteAction(FUIAction(FExecuteAction::CreateSP(this, &FMatrixStructCustomization::OnPaste, FTransformField::Rotation, WeakHandlePtr))) .NameContent() [ StructPropertyHandle->CreatePropertyNameWidget(LOCTEXT("RotationLabel", "Rotation")) ] .ValueContent() .MinDesiredWidth(375.0f) .MaxDesiredWidth(375.0f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(FMargin(0.0f, 2.0f, 3.0f, 2.0f)) [ MakeNumericProxyWidget(StructPropertyHandle, CachedRotationRoll, LOCTEXT("RotationRoll", "X"), true, FLinearColor::White, SNumericEntryBox::RedLabelBackgroundColor) ] + SHorizontalBox::Slot() .Padding(FMargin(0.0f, 2.0f, 3.0f, 2.0f)) [ MakeNumericProxyWidget(StructPropertyHandle, CachedRotationPitch, LOCTEXT("RotationPitch", "Y"), true, FLinearColor::White, SNumericEntryBox::GreenLabelBackgroundColor) ] + SHorizontalBox::Slot() .Padding(FMargin(0.0f, 2.0f, 0.0f, 2.0f)) [ MakeNumericProxyWidget(StructPropertyHandle, CachedRotationYaw, LOCTEXT("RotationYaw", "Z"), true, FLinearColor::White, SNumericEntryBox::BlueLabelBackgroundColor) ] ]; } void FMatrixStructCustomization::CustomizeScale(TSharedRef StructPropertyHandle, FDetailWidgetRow& Row) { TWeakPtr WeakHandlePtr = StructPropertyHandle; Row .CopyAction(FUIAction(FExecuteAction::CreateSP(this, &FMatrixStructCustomization::OnCopy, FTransformField::Scale, WeakHandlePtr))) .PasteAction(FUIAction(FExecuteAction::CreateSP(this, &FMatrixStructCustomization::OnPaste, FTransformField::Scale, WeakHandlePtr))) .NameContent() [ StructPropertyHandle->CreatePropertyNameWidget(LOCTEXT("ScaleLabel", "Scale")) ] .ValueContent() .MinDesiredWidth(375.0f) .MaxDesiredWidth(375.0f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(FMargin(0.0f, 2.0f, 3.0f, 2.0f)) [ MakeNumericProxyWidget(StructPropertyHandle, CachedScaleX, LOCTEXT("ScaleX", "X"), false, FLinearColor::White, SNumericEntryBox::RedLabelBackgroundColor) ] + SHorizontalBox::Slot() .Padding(FMargin(0.0f, 2.0f, 3.0f, 2.0f)) [ MakeNumericProxyWidget(StructPropertyHandle, CachedScaleY, LOCTEXT("ScaleY", "Y"), false, FLinearColor::White, SNumericEntryBox::GreenLabelBackgroundColor) ] + SHorizontalBox::Slot() .Padding(FMargin(0.0f, 2.0f, 0.0f, 2.0f)) [ MakeNumericProxyWidget(StructPropertyHandle, CachedScaleZ, LOCTEXT("ScaleZ", "Z"), false, FLinearColor::White, SNumericEntryBox::BlueLabelBackgroundColor) ] ]; } void FMatrixStructCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { FMathStructProxyCustomization::CustomizeChildren(StructPropertyHandle, StructBuilder, StructCustomizationUtils); TWeakPtr WeakHandlePtr = StructPropertyHandle; CustomizeLocation(StructPropertyHandle, StructBuilder.AddCustomRow(LOCTEXT("RotationLabel", "Rotation"))); CustomizeRotation(StructPropertyHandle, StructBuilder.AddCustomRow(LOCTEXT("LocationLabel", "Location"))); CustomizeScale(StructPropertyHandle, StructBuilder.AddCustomRow(LOCTEXT("ScaleLabel", "Scale"))); } void FMatrixStructCustomization::OnCopy(FTransformField::Type Type, TWeakPtr PropertyHandlePtr) { auto PropertyHandle = PropertyHandlePtr.Pin(); if (!PropertyHandle.IsValid()) { return; } FString CopyStr; CacheValues(PropertyHandle); switch (Type) { case FTransformField::Location: { FVector Location = CachedTranslation->Get(); CopyStr = FString::Printf(TEXT("(X=%f,Y=%f,Z=%f)"), Location.X, Location.Y, Location.Z); break; } case FTransformField::Rotation: { FRotator Rotation = CachedRotation->Get(); CopyStr = FString::Printf(TEXT("(Pitch=%f,Yaw=%f,Roll=%f)"), Rotation.Pitch, Rotation.Yaw, Rotation.Roll); break; } case FTransformField::Scale: { FVector Scale = CachedScale->Get(); CopyStr = FString::Printf(TEXT("(X=%f,Y=%f,Z=%f)"), Scale.X, Scale.Y, Scale.Z); break; } } if (!CopyStr.IsEmpty()) { FPlatformApplicationMisc::ClipboardCopy(*CopyStr); } } void FMatrixStructCustomization::OnPaste(FTransformField::Type Type, TWeakPtr PropertyHandlePtr) { auto PropertyHandle = PropertyHandlePtr.Pin(); if (!PropertyHandle.IsValid()) { return; } FString PastedText; FPlatformApplicationMisc::ClipboardPaste(PastedText); switch (Type) { case FTransformField::Location: { FVector Location; if (Location.InitFromString(PastedText)) { FScopedTransaction Transaction(LOCTEXT("PasteLocation", "Paste Location")); CachedTranslationX->Set(Location.X); CachedTranslationY->Set(Location.Y); CachedTranslationZ->Set(Location.Z); FlushValues(PropertyHandle); } break; } case FTransformField::Rotation: { FRotator Rotation; PastedText.ReplaceInline(TEXT("Pitch="), TEXT("P=")); PastedText.ReplaceInline(TEXT("Yaw="), TEXT("Y=")); PastedText.ReplaceInline(TEXT("Roll="), TEXT("R=")); if (Rotation.InitFromString(PastedText)) { FScopedTransaction Transaction(LOCTEXT("PasteRotation", "Paste Rotation")); CachedRotationPitch->Set(Rotation.Pitch); CachedRotationYaw->Set(Rotation.Yaw); CachedRotationRoll->Set(Rotation.Roll); FlushValues(PropertyHandle); } break; } case FTransformField::Scale: { FVector Scale; if (Scale.InitFromString(PastedText)) { FScopedTransaction Transaction(LOCTEXT("PasteScale", "Paste Scale")); CachedScaleX->Set(Scale.X); CachedScaleY->Set(Scale.Y); CachedScaleZ->Set(Scale.Z); FlushValues(PropertyHandle); } break; } } } bool FMatrixStructCustomization::CacheValues( TWeakPtr PropertyHandlePtr ) const { auto PropertyHandle = PropertyHandlePtr.Pin(); if (!PropertyHandle.IsValid()) { return false; } TArray RawData; PropertyHandle->AccessRawData(RawData); if (RawData.Num() == 1) { FMatrix* MatrixValue = reinterpret_cast(RawData[0]); if (MatrixValue != NULL) { CachedTranslation->Set(MatrixValue->GetOrigin()); CachedRotation->Set(MatrixValue->Rotator()); CachedScale->Set(MatrixValue->GetScaleVector()); return true; } } return false; } bool FMatrixStructCustomization::FlushValues( TWeakPtr PropertyHandlePtr ) const { auto PropertyHandle = PropertyHandlePtr.Pin(); if (!PropertyHandle.IsValid()) { return false; } TArray RawData; PropertyHandle->AccessRawData(RawData); TArray OuterObjects; PropertyHandle->GetOuterObjects(OuterObjects); // The object array should either be empty or the same size as the raw data array. check(!OuterObjects.Num() || OuterObjects.Num() == RawData.Num()); // Persistent flag that's set when we're in the middle of an interactive change (note: assumes multiple interactive changes do not occur in parallel). static bool bIsInteractiveChangeInProgress = false; bool bNotifiedPreChange = false; for (int32 ValueIndex = 0; ValueIndex < RawData.Num(); ValueIndex++) { FMatrix* MatrixValue = reinterpret_cast(RawData[ValueIndex]); if (MatrixValue != NULL) { const FMatrix PreviousValue = *MatrixValue; const FRotator CurrentRotation = MatrixValue->Rotator(); const FVector CurrentTranslation = MatrixValue->GetOrigin(); const FVector CurrentScale = MatrixValue->GetScaleVector(); FRotator Rotation( CachedRotationPitch->IsSet() ? CachedRotationPitch->Get() : CurrentRotation.Pitch, CachedRotationYaw->IsSet() ? CachedRotationYaw->Get() : CurrentRotation.Yaw, CachedRotationRoll->IsSet() ? CachedRotationRoll->Get() : CurrentRotation.Roll ); FVector Translation( CachedTranslationX->IsSet() ? CachedTranslationX->Get() : CurrentTranslation.X, CachedTranslationY->IsSet() ? CachedTranslationY->Get() : CurrentTranslation.Y, CachedTranslationZ->IsSet() ? CachedTranslationZ->Get() : CurrentTranslation.Z ); FVector Scale( CachedScaleX->IsSet() ? CachedScaleX->Get() : CurrentScale.X, CachedScaleY->IsSet() ? CachedScaleY->Get() : CurrentScale.Y, CachedScaleZ->IsSet() ? CachedScaleZ->Get() : CurrentScale.Z ); const FMatrix NewValue = FScaleRotationTranslationMatrix(Scale, Rotation, Translation); if (!bNotifiedPreChange && (!MatrixValue->Equals(NewValue, 0.0f) || (!bIsUsingSlider && bIsInteractiveChangeInProgress))) { if (!bIsInteractiveChangeInProgress) { GEditor->BeginTransaction(FText::Format(LOCTEXT("SetPropertyValue", "Set {0}"), PropertyHandle->GetPropertyDisplayName())); } PropertyHandle->NotifyPreChange(); bNotifiedPreChange = true; bIsInteractiveChangeInProgress = bIsUsingSlider; } // Set the new value. *MatrixValue = NewValue; // Propagate default value changes after updating, for archetypes. As per usual, we only propagate the change if the instance matches the archetype's value. // Note: We cannot use the "normal" PropertyNode propagation logic here, because that is string-based and the decision to propagate relies on an exact value match. // Here, we're dealing with conversions between FMatrix and FVector/FRotator values, so there is some precision loss that requires a tolerance when comparing values. if (ValueIndex < OuterObjects.Num() && OuterObjects[ValueIndex]->IsTemplate()) { TArray ArchetypeInstances; OuterObjects[ValueIndex]->GetArchetypeInstances(ArchetypeInstances); for (UObject* ArchetypeInstance : ArchetypeInstances) { FMatrix* CurrentValue = reinterpret_cast(PropertyHandle->GetValueBaseAddress(reinterpret_cast(ArchetypeInstance))); if (CurrentValue && CurrentValue->Equals(PreviousValue)) { *CurrentValue = NewValue; } } } } } if (bNotifiedPreChange) { PropertyHandle->NotifyPostChange(bIsUsingSlider ? EPropertyChangeType::Interactive : EPropertyChangeType::ValueSet); if (!bIsUsingSlider) { GEditor->EndTransaction(); bIsInteractiveChangeInProgress = false; } } if (PropertyUtilities.IsValid() && !bIsInteractiveChangeInProgress) { FPropertyChangedEvent ChangeEvent(PropertyHandle->GetProperty(), EPropertyChangeType::ValueSet); PropertyUtilities->NotifyFinishedChangingProperties(ChangeEvent); } return true; } #undef LOCTEXT_NAMESPACE TSharedRef FTransformStructCustomization::MakeInstance() { return MakeShareable( new FTransformStructCustomization ); } bool FTransformStructCustomization::CacheValues( TWeakPtr PropertyHandlePtr ) const { auto PropertyHandle = PropertyHandlePtr.Pin(); if (!PropertyHandle.IsValid()) { return false; } TArray RawData; PropertyHandle->AccessRawData(RawData); if (RawData.Num() == 1) { FTransform* TransformValue = reinterpret_cast(RawData[0]); if (TransformValue != NULL) { CachedTranslation->Set(TransformValue->GetTranslation()); CachedRotation->Set(TransformValue->GetRotation().Rotator()); CachedScale->Set(TransformValue->GetScale3D()); return true; } } return false; } bool FTransformStructCustomization::FlushValues( TWeakPtr PropertyHandlePtr ) const { auto PropertyHandle = PropertyHandlePtr.Pin(); if (!PropertyHandle.IsValid()) { return false; } TArray RawData; PropertyHandle->AccessRawData(RawData); TArray OuterObjects; PropertyHandle->GetOuterObjects(OuterObjects); // The object array should either be empty or the same size as the raw data array. check(!OuterObjects.Num() || OuterObjects.Num() == RawData.Num()); // Persistent flag that's set when we're in the middle of an interactive change (note: assumes multiple interactive changes do not occur in parallel). static bool bIsInteractiveChangeInProgress = false; bool bNotifiedPreChange = false; for (int32 ValueIndex = 0; ValueIndex < RawData.Num(); ValueIndex++) { FTransform* TransformValue = reinterpret_cast(RawData[0]); if (TransformValue != NULL) { const FTransform PreviousValue = *TransformValue; const FRotator CurrentRotation = TransformValue->GetRotation().Rotator(); const FVector CurrentTranslation = TransformValue->GetTranslation(); const FVector CurrentScale = TransformValue->GetScale3D(); FRotator Rotation( CachedRotationPitch->IsSet() ? CachedRotationPitch->Get() : CurrentRotation.Pitch, CachedRotationYaw->IsSet() ? CachedRotationYaw->Get() : CurrentRotation.Yaw, CachedRotationRoll->IsSet() ? CachedRotationRoll->Get() : CurrentRotation.Roll ); FVector Translation( CachedTranslationX->IsSet() ? CachedTranslationX->Get() : CurrentTranslation.X, CachedTranslationY->IsSet() ? CachedTranslationY->Get() : CurrentTranslation.Y, CachedTranslationZ->IsSet() ? CachedTranslationZ->Get() : CurrentTranslation.Z ); FVector Scale( CachedScaleX->IsSet() ? CachedScaleX->Get() : CurrentScale.X, CachedScaleY->IsSet() ? CachedScaleY->Get() : CurrentScale.Y, CachedScaleZ->IsSet() ? CachedScaleZ->Get() : CurrentScale.Z ); const FTransform NewValue = FTransform(Rotation, Translation, Scale); if (!bNotifiedPreChange && (!TransformValue->Equals(NewValue, 0.0f) || (!bIsUsingSlider && bIsInteractiveChangeInProgress))) { if (!bIsInteractiveChangeInProgress) { GEditor->BeginTransaction(FText::Format(NSLOCTEXT("FTransformStructCustomization", "SetPropertyValue", "Set {0}"), PropertyHandle->GetPropertyDisplayName())); } PropertyHandle->NotifyPreChange(); bNotifiedPreChange = true; bIsInteractiveChangeInProgress = bIsUsingSlider; } // Set the new value. *TransformValue = NewValue; // Propagate default value changes after updating, for archetypes. As per usual, we only propagate the change if the instance matches the archetype's value. // Note: We cannot use the "normal" PropertyNode propagation logic here, because that is string-based and the decision to propagate relies on an exact value match. // Here, we're dealing with conversions between FTransform and FVector/FRotator values, so there is some precision loss that requires a tolerance when comparing values. if (ValueIndex < OuterObjects.Num() && OuterObjects[ValueIndex]->IsTemplate()) { TArray ArchetypeInstances; OuterObjects[ValueIndex]->GetArchetypeInstances(ArchetypeInstances); for (UObject* ArchetypeInstance : ArchetypeInstances) { FTransform* CurrentValue = reinterpret_cast(PropertyHandle->GetValueBaseAddress(reinterpret_cast(ArchetypeInstance))); if (CurrentValue && CurrentValue->Equals(PreviousValue)) { *CurrentValue = NewValue; } } } } } if (bNotifiedPreChange) { PropertyHandle->NotifyPostChange(bIsUsingSlider ? EPropertyChangeType::Interactive : EPropertyChangeType::ValueSet); if (!bIsUsingSlider) { GEditor->EndTransaction(); bIsInteractiveChangeInProgress = false; } } if (PropertyUtilities.IsValid() && !bIsInteractiveChangeInProgress) { FPropertyChangedEvent ChangeEvent(PropertyHandle->GetProperty(), EPropertyChangeType::ValueSet); PropertyUtilities->NotifyFinishedChangingProperties(ChangeEvent); } return true; } TSharedRef FQuatStructCustomization::MakeInstance() { return MakeShareable(new FQuatStructCustomization); } void FQuatStructCustomization::MakeHeaderRow(TSharedRef& InStructPropertyHandle, FDetailWidgetRow& Row) { CustomizeRotation(InStructPropertyHandle, Row); } void FQuatStructCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { FMathStructProxyCustomization::CustomizeChildren(StructPropertyHandle, StructBuilder, StructCustomizationUtils); } bool FQuatStructCustomization::CacheValues(TWeakPtr PropertyHandlePtr) const { auto PropertyHandle = PropertyHandlePtr.Pin(); if (!PropertyHandle.IsValid()) { return false; } TArray RawData; PropertyHandle->AccessRawData(RawData); if (RawData.Num() == 1) { FQuat* QuatValue = reinterpret_cast(RawData[0]); if (QuatValue != NULL) { CachedRotation->Set(QuatValue->Rotator()); return true; } } return false; } bool FQuatStructCustomization::FlushValues(TWeakPtr PropertyHandlePtr) const { auto PropertyHandle = PropertyHandlePtr.Pin(); if (!PropertyHandle.IsValid()) { return false; } TArray RawData; PropertyHandle->AccessRawData(RawData); TArray OuterObjects; PropertyHandle->GetOuterObjects(OuterObjects); // The object array should either be empty or the same size as the raw data array. check(!OuterObjects.Num() || OuterObjects.Num() == RawData.Num()); // Persistent flag that's set when we're in the middle of an interactive change (note: assumes multiple interactive changes do not occur in parallel). static bool bIsInteractiveChangeInProgress = false; bool bNotifiedPreChange = false; for (int32 ValueIndex = 0; ValueIndex < RawData.Num(); ValueIndex++) { FQuat* QuatValue = reinterpret_cast(RawData[0]); if (QuatValue != NULL) { const FQuat PreviousValue = *QuatValue; const FRotator CurrentRotation = QuatValue->Rotator(); FRotator Rotation( CachedRotationPitch->IsSet() ? CachedRotationPitch->Get() : CurrentRotation.Pitch, CachedRotationYaw->IsSet() ? CachedRotationYaw->Get() : CurrentRotation.Yaw, CachedRotationRoll->IsSet() ? CachedRotationRoll->Get() : CurrentRotation.Roll ); const FQuat NewValue = Rotation.Quaternion(); // In some cases the FQuat pointed to in RawData is no longer aligned to 16 bytes. // Make a local copy to guarantee the alignment criterions of the vector intrinsics inside FQuat::Equals const FQuat AlignedQuatValue = *QuatValue; if (!bNotifiedPreChange && (!AlignedQuatValue.Equals(NewValue, 0.0f) || (!bIsUsingSlider && bIsInteractiveChangeInProgress))) { if (!bIsInteractiveChangeInProgress) { GEditor->BeginTransaction(FText::Format(NSLOCTEXT("FQuatStructCustomization", "SetPropertyValue", "Set {0}"), PropertyHandle->GetPropertyDisplayName())); } PropertyHandle->NotifyPreChange(); bNotifiedPreChange = true; bIsInteractiveChangeInProgress = bIsUsingSlider; } // Set the new value. *QuatValue = NewValue; // Propagate default value changes after updating, for archetypes. As per usual, we only propagate the change if the instance matches the archetype's value. // Note: We cannot use the "normal" PropertyNode propagation logic here, because that is string-based and the decision to propagate relies on an exact value match. // Here, we're dealing with conversions between FQuat and FRotator values, so there is some precision loss that requires a tolerance when comparing values. if (ValueIndex < OuterObjects.Num() && OuterObjects[ValueIndex]->IsTemplate()) { TArray ArchetypeInstances; OuterObjects[ValueIndex]->GetArchetypeInstances(ArchetypeInstances); for (UObject* ArchetypeInstance : ArchetypeInstances) { FQuat* CurrentValue = reinterpret_cast(PropertyHandle->GetValueBaseAddress(reinterpret_cast(ArchetypeInstance))); if (CurrentValue && CurrentValue->Equals(PreviousValue)) { *CurrentValue = NewValue; } } } } } if (bNotifiedPreChange) { PropertyHandle->NotifyPostChange(bIsUsingSlider ? EPropertyChangeType::Interactive : EPropertyChangeType::ValueSet); if (!bIsUsingSlider) { GEditor->EndTransaction(); bIsInteractiveChangeInProgress = false; } } if (PropertyUtilities.IsValid() && !bIsInteractiveChangeInProgress) { FPropertyChangedEvent ChangeEvent(PropertyHandle->GetProperty(), EPropertyChangeType::ValueSet); PropertyUtilities->NotifyFinishedChangingProperties(ChangeEvent); } return true; }