// Copyright Epic Games, Inc. All Rights Reserved. #include "Customizations/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" #define LOCTEXT_NAMESPACE "MatrixStructCustomization" 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& 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) // 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()) .LabelPadding(FMargin(3)) .ToolTipText(this, &FMathStructProxyCustomization::OnGetValueToolTip, WeakHandlePtr, ProxyValue, Label) .LabelLocation(SNumericEntryBox::ELabelLocation::Inside) .Label() [ SNumericEntryBox::BuildNarrowColorLabel(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); } template FText FMathStructProxyCustomization::OnGetValueToolTip(TWeakPtr WeakHandlePtr, TSharedRef> ProxyValue, FText Label) const { TOptional Value = OnGetValue(WeakHandlePtr, ProxyValue); if (Value.IsSet()) { return FText::Format(LOCTEXT("ValueToolTip", "{0}: {1}"), Label, FText::AsNumber(Value.GetValue())); } return FText::GetEmpty(); } template TSharedRef FMatrixStructCustomization::MakeInstance() { return MakeShareable( new FMatrixStructCustomization ); } template void FMatrixStructCustomization::MakeHeaderRow(TSharedRef& StructPropertyHandle, FDetailWidgetRow& Row) { Row .NameContent() [ StructPropertyHandle->CreatePropertyNameWidget() ] .ValueContent() .MinDesiredWidth(0.0f) .MaxDesiredWidth(0.0f) [ SNullWidget::NullWidget ]; } template 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, T>(StructPropertyHandle, CachedTranslationX, LOCTEXT("TranslationX", "X"), false, SNumericEntryBox::RedLabelBackgroundColor) ] + SHorizontalBox::Slot() .Padding(FMargin(0.0f, 2.0f, 3.0f, 2.0f)) [ MakeNumericProxyWidget, T>(StructPropertyHandle, CachedTranslationY, LOCTEXT("TranslationY", "Y"), false, SNumericEntryBox::GreenLabelBackgroundColor) ] + SHorizontalBox::Slot() .Padding(FMargin(0.0f, 2.0f, 0.0f, 2.0f)) [ MakeNumericProxyWidget, T>(StructPropertyHandle, CachedTranslationZ, LOCTEXT("TranslationZ", "Z"), false, SNumericEntryBox::BlueLabelBackgroundColor) ] ]; } template 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, T>(StructPropertyHandle, CachedRotationRoll, LOCTEXT("RotationRoll", "X"), true, SNumericEntryBox::RedLabelBackgroundColor) ] + SHorizontalBox::Slot() .Padding(FMargin(0.0f, 2.0f, 3.0f, 2.0f)) [ MakeNumericProxyWidget, T>(StructPropertyHandle, CachedRotationPitch, LOCTEXT("RotationPitch", "Y"), true, SNumericEntryBox::GreenLabelBackgroundColor) ] + SHorizontalBox::Slot() .Padding(FMargin(0.0f, 2.0f, 0.0f, 2.0f)) [ MakeNumericProxyWidget, T>(StructPropertyHandle, CachedRotationYaw, LOCTEXT("RotationYaw", "Z"), true, SNumericEntryBox::BlueLabelBackgroundColor) ] ]; } template 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, T>(StructPropertyHandle, CachedScaleX, LOCTEXT("ScaleX", "X"), false, SNumericEntryBox::RedLabelBackgroundColor) ] + SHorizontalBox::Slot() .Padding(FMargin(0.0f, 2.0f, 3.0f, 2.0f)) [ MakeNumericProxyWidget, T>(StructPropertyHandle, CachedScaleY, LOCTEXT("ScaleY", "Y"), false, SNumericEntryBox::GreenLabelBackgroundColor) ] + SHorizontalBox::Slot() .Padding(FMargin(0.0f, 2.0f, 0.0f, 2.0f)) [ MakeNumericProxyWidget, T>(StructPropertyHandle, CachedScaleZ, LOCTEXT("ScaleZ", "Z"), false, SNumericEntryBox::BlueLabelBackgroundColor) ] ]; } template 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"))); } template 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: { UE::Math::TVector Location = CachedTranslation->Get(); CopyStr = FString::Printf(TEXT("(X=%f,Y=%f,Z=%f)"), Location.X, Location.Y, Location.Z); break; } case FTransformField::Rotation: { UE::Math::TRotator Rotation = CachedRotation->Get(); CopyStr = FString::Printf(TEXT("(Pitch=%f,Yaw=%f,Roll=%f)"), Rotation.Pitch, Rotation.Yaw, Rotation.Roll); break; } case FTransformField::Scale: { UE::Math::TVector 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); } } template 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: { UE::Math::TVector 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: { UE::Math::TRotator 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: { UE::Math::TVector 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; } } } template bool FMatrixStructCustomization::CacheValues( TWeakPtr PropertyHandlePtr ) const { auto PropertyHandle = PropertyHandlePtr.Pin(); if (!PropertyHandle.IsValid()) { return false; } TArray RawData; PropertyHandle->AccessRawData(RawData); const UE::Math::TMatrix* FirstMatrixValue = nullptr; for(void* RawDataPtr : RawData) { UE::Math::TMatrix* MatrixValue = reinterpret_cast*>(RawDataPtr); if (MatrixValue == nullptr) { return false; } if(FirstMatrixValue) { if(!FirstMatrixValue->Equals(*MatrixValue, 0.0001f)) { return false; } } else { FirstMatrixValue = MatrixValue; } } if(FirstMatrixValue) { CachedTranslation->Set(FirstMatrixValue->GetOrigin()); CachedRotation->Set(FirstMatrixValue->Rotator()); CachedScale->Set(FirstMatrixValue->GetScaleVector()); return true; } return false; } template 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++) { UE::Math::TMatrix* MatrixValue = reinterpret_cast*>(RawData[ValueIndex]); if (MatrixValue != NULL) { const UE::Math::TMatrix PreviousValue = *MatrixValue; const UE::Math::TRotator CurrentRotation = MatrixValue->Rotator(); const UE::Math::TVector CurrentTranslation = MatrixValue->GetOrigin(); const UE::Math::TVector CurrentScale = MatrixValue->GetScaleVector(); UE::Math::TRotator Rotation( CachedRotationPitch->IsSet() ? CachedRotationPitch->Get() : CurrentRotation.Pitch, CachedRotationYaw->IsSet() ? CachedRotationYaw->Get() : CurrentRotation.Yaw, CachedRotationRoll->IsSet() ? CachedRotationRoll->Get() : CurrentRotation.Roll ); UE::Math::TVector Translation( CachedTranslationX->IsSet() ? CachedTranslationX->Get() : CurrentTranslation.X, CachedTranslationY->IsSet() ? CachedTranslationY->Get() : CurrentTranslation.Y, CachedTranslationZ->IsSet() ? CachedTranslationZ->Get() : CurrentTranslation.Z ); UE::Math::TVector Scale( CachedScaleX->IsSet() ? CachedScaleX->Get() : CurrentScale.X, CachedScaleY->IsSet() ? CachedScaleY->Get() : CurrentScale.Y, CachedScaleZ->IsSet() ? CachedScaleZ->Get() : CurrentScale.Z ); const UE::Math::TMatrix NewValue = UE::Math::TScaleRotationTranslationMatrix(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 UE::Math::TMatrix and UE::Math::TVector/UE::Math::TRotator 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) { UE::Math::TMatrix* 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, OuterObjects); PropertyUtilities->NotifyFinishedChangingProperties(ChangeEvent); } return true; } template TSharedRef FTransformStructCustomization::MakeInstance() { return MakeShareable( new FTransformStructCustomization ); } template bool FTransformStructCustomization::CacheValues( TWeakPtr PropertyHandlePtr ) const { auto PropertyHandle = PropertyHandlePtr.Pin(); if (!PropertyHandle.IsValid()) { return false; } TArray RawData; PropertyHandle->AccessRawData(RawData); const UE::Math::TTransform* FirstTransformValue = nullptr; for(void* RawDataPtr : RawData) { UE::Math::TTransform* TransformValue = reinterpret_cast*>(RawDataPtr); if (TransformValue == nullptr) { return false; } if(FirstTransformValue) { if(!FirstTransformValue->Equals(*TransformValue, 0.0001f)) { return false; } } else { FirstTransformValue = TransformValue; } } if(FirstTransformValue) { this->CachedTranslation->Set(FirstTransformValue->GetTranslation()); this->CachedRotation->Set(FirstTransformValue->GetRotation().Rotator()); this->CachedScale->Set(FirstTransformValue->GetScale3D()); return true; } return false; } template 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++) { UE::Math::TTransform* TransformValue = reinterpret_cast*>(RawData[0]); if (TransformValue != NULL) { const UE::Math::TTransform PreviousValue = *TransformValue; const UE::Math::TRotator CurrentRotation = TransformValue->GetRotation().Rotator(); const UE::Math::TVector CurrentTranslation = TransformValue->GetTranslation(); const UE::Math::TVector CurrentScale = TransformValue->GetScale3D(); UE::Math::TRotator Rotation( this->CachedRotationPitch->IsSet() ? this->CachedRotationPitch->Get() : CurrentRotation.Pitch, this->CachedRotationYaw->IsSet() ? this->CachedRotationYaw->Get() : CurrentRotation.Yaw, this->CachedRotationRoll->IsSet() ? this->CachedRotationRoll->Get() : CurrentRotation.Roll ); UE::Math::TVector Translation( this->CachedTranslationX->IsSet() ? this->CachedTranslationX->Get() : CurrentTranslation.X, this->CachedTranslationY->IsSet() ? this->CachedTranslationY->Get() : CurrentTranslation.Y, this->CachedTranslationZ->IsSet() ? this->CachedTranslationZ->Get() : CurrentTranslation.Z ); UE::Math::TVector Scale( this->CachedScaleX->IsSet() ? this->CachedScaleX->Get() : CurrentScale.X, this->CachedScaleY->IsSet() ? this->CachedScaleY->Get() : CurrentScale.Y, this->CachedScaleZ->IsSet() ? this->CachedScaleZ->Get() : CurrentScale.Z ); const UE::Math::TTransform NewValue = UE::Math::TTransform(Rotation, Translation, Scale); if (!bNotifiedPreChange && (!TransformValue->Equals(NewValue, 0.0f) || (!this->bIsUsingSlider && bIsInteractiveChangeInProgress))) { if (!bIsInteractiveChangeInProgress) { GEditor->BeginTransaction(FText::Format(NSLOCTEXT("FTransformStructCustomization", "SetPropertyValue", "Set {0}"), PropertyHandle->GetPropertyDisplayName())); } PropertyHandle->NotifyPreChange(); bNotifiedPreChange = true; bIsInteractiveChangeInProgress = this->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 UE::Math::TTransform and UE::Math::TVector/UE::Math::TRotator 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) { UE::Math::TTransform* CurrentValue = reinterpret_cast*>(PropertyHandle->GetValueBaseAddress(reinterpret_cast(ArchetypeInstance))); if (CurrentValue && CurrentValue->Equals(PreviousValue)) { *CurrentValue = NewValue; } } } } } if (bNotifiedPreChange) { PropertyHandle->NotifyPostChange(this->bIsUsingSlider ? EPropertyChangeType::Interactive : EPropertyChangeType::ValueSet); if (!this->bIsUsingSlider) { GEditor->EndTransaction(); bIsInteractiveChangeInProgress = false; } } if (this->PropertyUtilities.IsValid() && !bIsInteractiveChangeInProgress) { FPropertyChangedEvent ChangeEvent(PropertyHandle->GetProperty(), EPropertyChangeType::ValueSet, OuterObjects); this->PropertyUtilities->NotifyFinishedChangingProperties(ChangeEvent); } return true; } template TSharedRef FQuatStructCustomization::MakeInstance() { return MakeShareable(new FQuatStructCustomization); } template void FQuatStructCustomization::MakeHeaderRow(TSharedRef& InStructPropertyHandle, FDetailWidgetRow& Row) { this->CustomizeRotation(InStructPropertyHandle, Row); } template void FQuatStructCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { FMathStructProxyCustomization::CustomizeChildren(StructPropertyHandle, StructBuilder, StructCustomizationUtils); } template bool FQuatStructCustomization::CacheValues(TWeakPtr PropertyHandlePtr) const { auto PropertyHandle = PropertyHandlePtr.Pin(); if (!PropertyHandle.IsValid()) { return false; } TArray RawData; PropertyHandle->AccessRawData(RawData); if (RawData.Num() == 1) { UE::Math::TQuat* QuatValue = reinterpret_cast*>(RawData[0]); if (QuatValue != NULL) { this->CachedRotation->Set(QuatValue->Rotator()); return true; } } return false; } template 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++) { UE::Math::TQuat* QuatValue = reinterpret_cast*>(RawData[0]); if (QuatValue != NULL) { const UE::Math::TQuat PreviousValue = *QuatValue; const UE::Math::TRotator CurrentRotation = QuatValue->Rotator(); UE::Math::TRotator Rotation( this->CachedRotationPitch->IsSet() ? this->CachedRotationPitch->Get() : CurrentRotation.Pitch, this->CachedRotationYaw->IsSet() ? this->CachedRotationYaw->Get() : CurrentRotation.Yaw, this->CachedRotationRoll->IsSet() ? this->CachedRotationRoll->Get() : CurrentRotation.Roll ); const UE::Math::TQuat NewValue = Rotation.Quaternion(); // In some cases the UE::Math::TQuat 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 UE::Math::TQuat::Equals const UE::Math::TQuat AlignedQuatValue = *QuatValue; if (!bNotifiedPreChange && (!AlignedQuatValue.Equals(NewValue, 0.0f) || (!this->bIsUsingSlider && bIsInteractiveChangeInProgress))) { if (!bIsInteractiveChangeInProgress) { GEditor->BeginTransaction(FText::Format(NSLOCTEXT("FQuatStructCustomization", "SetPropertyValue", "Set {0}"), PropertyHandle->GetPropertyDisplayName())); } PropertyHandle->NotifyPreChange(); bNotifiedPreChange = true; bIsInteractiveChangeInProgress = this->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 UE::Math::TQuat and UE::Math::TRotator 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) { UE::Math::TQuat* CurrentValue = reinterpret_cast*>(PropertyHandle->GetValueBaseAddress(reinterpret_cast(ArchetypeInstance))); if (CurrentValue && CurrentValue->Equals(PreviousValue)) { *CurrentValue = NewValue; } } } } } if (bNotifiedPreChange) { PropertyHandle->NotifyPostChange(this->bIsUsingSlider ? EPropertyChangeType::Interactive : EPropertyChangeType::ValueSet); if (!this->bIsUsingSlider) { GEditor->EndTransaction(); bIsInteractiveChangeInProgress = false; } } if (this->PropertyUtilities.IsValid() && !bIsInteractiveChangeInProgress) { FPropertyChangedEvent ChangeEvent(PropertyHandle->GetProperty(), EPropertyChangeType::ValueSet, OuterObjects); this->PropertyUtilities->NotifyFinishedChangingProperties(ChangeEvent); } return true; } // Instantiate for linker template class FMatrixStructCustomization; template class FMatrixStructCustomization; template class FTransformStructCustomization; template class FTransformStructCustomization; template class FQuatStructCustomization; template class FQuatStructCustomization; #undef LOCTEXT_NAMESPACE