// Copyright Epic Games, Inc. All Rights Reserved. #include "Customizations/MathStructProxyCustomizations.h" #include "Containers/ArrayView.h" #include "Containers/UnrealString.h" #include "CoreGlobals.h" #include "DetailLayoutBuilder.h" #include "DetailWidgetRow.h" #include "Editor.h" #include "Editor/EditorEngine.h" #include "Framework/Commands/UIAction.h" #include "HAL/PlatformApplicationMisc.h" #include "HAL/PlatformCrt.h" #include "IDetailChildrenBuilder.h" #include "IPropertyTypeCustomization.h" #include "IPropertyUtilities.h" #include "Internationalization/Internationalization.h" #include "Layout/Margin.h" #include "Math/Matrix.h" #include "Math/Quat.h" #include "Math/ScaleRotationTranslationMatrix.h" #include "Math/TransformVectorized.h" #include "Math/UnrealMathSSE.h" #include "Math/VectorRegister.h" #include "Misc/AssertionMacros.h" #include "Misc/AxisDisplayInfo.h" #include "PropertyHandle.h" #include "ScopedTransaction.h" #include "SlotBase.h" #include "Templates/UnrealTemplate.h" #include "UObject/Object.h" #include "UObject/OverridableManager.h" #include "UObject/UnrealType.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/Input/SNumericEntryBox.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SNullWidget.h" class SWidget; #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)) .ToolTipTextFormat(this, &FMathStructProxyCustomization::OnGetValueToolTipTextFormat, 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 { return FText::GetEmpty(); } TOptional FMathStructProxyCustomization::OnGetValueToolTipTextFormat(FText Label) const { if (!Label.IsEmptyOrWhitespace()) { TStringBuilder<32> ToolTipTextFormatString; ToolTipTextFormatString.Append(Label.ToString()); ToolTipTextFormatString.Append(TEXT(": {0}")); return TOptional(FText::FromString(ToolTipTextFormatString.ToString())); } return TOptional(); } 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; if (Row.IsPasteFromTextBound()) { Row.OnPasteFromTextDelegate.Pin()->AddSP(this, &FMatrixStructCustomization::OnPasteFromText, FTransformField::Location, WeakHandlePtr); } constexpr int32 NumComponents = 3; TStaticArray()>, NumComponents> ComponentConstructors = { [this, &StructPropertyHandle]()->TSharedRef { return MakeNumericProxyWidget, T>(StructPropertyHandle, CachedTranslationX, AxisDisplayInfo::GetAxisToolTip(EAxisList::Forward), false, AxisDisplayInfo::GetAxisColor(EAxisList::Forward)); }, [this, &StructPropertyHandle]()->TSharedRef { return MakeNumericProxyWidget, T>(StructPropertyHandle, CachedTranslationY, AxisDisplayInfo::GetAxisToolTip(EAxisList::Left), false, AxisDisplayInfo::GetAxisColor(EAxisList::Left)); }, [this, &StructPropertyHandle]()->TSharedRef { return MakeNumericProxyWidget, T>(StructPropertyHandle, CachedTranslationZ, AxisDisplayInfo::GetAxisToolTip(EAxisList::Up), false, AxisDisplayInfo::GetAxisColor(EAxisList::Up)); } }; const TStaticArray Paddings = { FMargin(0.0f, 2.0f, 3.0f, 2.0f), FMargin(0.0f, 2.0f, 3.0f, 2.0f), FMargin(0.0f, 2.0f, 0.0f, 2.0f) }; TSharedRef HBox = MakeShared(); FIntVector4 Swizzle = GetSwizzle(); for (int32 ComponentIndex = 0; ComponentIndex < NumComponents; ComponentIndex++) { TSharedRef Widget = SNullWidget::NullWidget; if (ensure(Swizzle[ComponentIndex] < NumComponents)) { const int32 SwizzledComponentIndex = Swizzle[ComponentIndex]; Widget = ComponentConstructors[SwizzledComponentIndex](); } HBox->AddSlot() .Padding(Paddings[ComponentIndex]) [ Widget ]; } 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) [ HBox ]; } template void FMatrixStructCustomization::CustomizeRotation(TSharedRef StructPropertyHandle, FDetailWidgetRow& Row) { TWeakPtr WeakHandlePtr = StructPropertyHandle; if (Row.IsPasteFromTextBound()) { Row.OnPasteFromTextDelegate.Pin()->AddSP(this, &FMatrixStructCustomization::OnPasteFromText, FTransformField::Rotation, WeakHandlePtr); } constexpr int32 NumComponents = 3; TStaticArray()>, NumComponents> ComponentConstructors = { [this, &StructPropertyHandle]()->TSharedRef { return MakeNumericProxyWidget, T>(StructPropertyHandle, CachedRotationRoll, AxisDisplayInfo::GetRotationAxisToolTip(EAxisList::Forward), true, AxisDisplayInfo::GetAxisColor(EAxisList::Forward)); }, [this, &StructPropertyHandle]()->TSharedRef { return MakeNumericProxyWidget, T>(StructPropertyHandle, CachedRotationPitch, AxisDisplayInfo::GetRotationAxisToolTip(EAxisList::Left), true, AxisDisplayInfo::GetAxisColor(EAxisList::Left)); }, [this, &StructPropertyHandle]()->TSharedRef { return MakeNumericProxyWidget, T>(StructPropertyHandle, CachedRotationYaw, AxisDisplayInfo::GetRotationAxisToolTip(EAxisList::Up), true, AxisDisplayInfo::GetAxisColor(EAxisList::Up)); } }; const TStaticArray Paddings = { FMargin(0.0f, 2.0f, 3.0f, 2.0f), FMargin(0.0f, 2.0f, 3.0f, 2.0f), FMargin(0.0f, 2.0f, 0.0f, 2.0f) }; TSharedRef HBox = MakeShared(); FIntVector4 Swizzle = GetSwizzle(); for (int32 ComponentIndex = 0; ComponentIndex < NumComponents; ComponentIndex++) { TSharedRef Widget = SNullWidget::NullWidget; if (ensure(Swizzle[ComponentIndex] < NumComponents)) { const int32 SwizzledComponentIndex = Swizzle[ComponentIndex]; Widget = ComponentConstructors[SwizzledComponentIndex](); } HBox->AddSlot() .Padding(Paddings[ComponentIndex]) [ Widget ]; } 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) [ HBox ]; } template void FMatrixStructCustomization::CustomizeScale(TSharedRef StructPropertyHandle, FDetailWidgetRow& Row) { TWeakPtr WeakHandlePtr = StructPropertyHandle; if (Row.IsPasteFromTextBound()) { Row.OnPasteFromTextDelegate.Pin()->AddSP(this, &FMatrixStructCustomization::OnPasteFromText, FTransformField::Scale, WeakHandlePtr); } constexpr int32 NumComponents = 3; TStaticArray()>, NumComponents> ComponentConstructors = { [this, &StructPropertyHandle]()->TSharedRef { return MakeNumericProxyWidget, T>(StructPropertyHandle, CachedScaleX, AxisDisplayInfo::GetAxisToolTip(EAxisList::Forward), false, AxisDisplayInfo::GetAxisColor(EAxisList::Forward)); }, [this, &StructPropertyHandle]()->TSharedRef { return MakeNumericProxyWidget, T>(StructPropertyHandle, CachedScaleY, AxisDisplayInfo::GetAxisToolTip(EAxisList::Left), false, AxisDisplayInfo::GetAxisColor(EAxisList::Left)); }, [this, &StructPropertyHandle]()->TSharedRef { return MakeNumericProxyWidget, T>(StructPropertyHandle, CachedScaleZ, AxisDisplayInfo::GetAxisToolTip(EAxisList::Up), false, AxisDisplayInfo::GetAxisColor(EAxisList::Up)); } }; const TStaticArray Paddings = { FMargin(0.0f, 2.0f, 3.0f, 2.0f), FMargin(0.0f, 2.0f, 3.0f, 2.0f), FMargin(0.0f, 2.0f, 0.0f, 2.0f) }; TSharedRef HBox = MakeShared(); FIntVector4 Swizzle = GetSwizzle(); for (int32 ComponentIndex = 0; ComponentIndex < NumComponents; ComponentIndex++) { TSharedRef Widget = SNullWidget::NullWidget; if (ensure(Swizzle[ComponentIndex] < NumComponents)) { const int32 SwizzledComponentIndex = Swizzle[ComponentIndex]; Widget = ComponentConstructors[SwizzledComponentIndex](); } HBox->AddSlot() .Padding(Paddings[ComponentIndex]) [ Widget ]; } 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) [ HBox ]; } template void FMatrixStructCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { FMathStructProxyCustomization::CustomizeChildren(StructPropertyHandle, StructBuilder, StructCustomizationUtils); TWeakPtr WeakHandlePtr = StructPropertyHandle; bUseLeftUpForwardAxisDisplayCoordinateSystem = AxisDisplayInfo::GetAxisDisplayCoordinateSystem() == EAxisList::LeftUpForward && !StructPropertyHandle->GetProperty()->HasAnyPropertyFlags(CPF_BlueprintVisible | CPF_BlueprintReadOnly); 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) { TSharedPtr 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) { FString PastedText; FPlatformApplicationMisc::ClipboardPaste(PastedText); PasteFromText(TEXT(""), PastedText, Type, PropertyHandlePtr); } template void FMatrixStructCustomization::OnPasteFromText( const FString& InTag, const FString& InText, const TOptional& InOperationId, FTransformField::Type Type, TWeakPtr PropertyHandlePtr) { PasteFromText(InTag, InText, Type, PropertyHandlePtr); } template void FMatrixStructCustomization::PasteFromText( const FString& InTag, const FString& InText, FTransformField::Type Type, TWeakPtr PropertyHandlePtr) { TSharedPtr PropertyHandle = PropertyHandlePtr.Pin(); if (!PropertyHandle.IsValid()) { return; } FString PastedText = InText; 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 { TSharedPtr 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()); if (bUseLeftUpForwardAxisDisplayCoordinateSystem) { CachedTranslationY->Set(-CachedTranslationY->Get()); } CachedRotation->Set(FirstMatrixValue->Rotator()); CachedScale->Set(FirstMatrixValue->GetScaleVector()); return true; } return false; } template bool FMatrixStructCustomization::FlushValues( TWeakPtr PropertyHandlePtr ) const { TSharedPtr 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() ? (bUseLeftUpForwardAxisDisplayCoordinateSystem ? -CachedTranslationY->Get() : 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) { if (!FOverridableManager::Get().IsEnabled(ArchetypeInstance)) { 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 FIntVector4 FMatrixStructCustomization::GetSwizzle() const { return {0, 1, 2, 3}; } template TSharedRef FTransformStructCustomization::MakeInstance() { return MakeShareable( new FTransformStructCustomization ); } template bool FTransformStructCustomization::CacheValues( TWeakPtr PropertyHandlePtr ) const { TSharedPtr 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()); if (this->bUseLeftUpForwardAxisDisplayCoordinateSystem) { this->CachedTranslationY->Set(-this->CachedTranslationY->Get()); } this->CachedRotation->Set(FirstTransformValue->GetRotation().Rotator()); this->CachedScale->Set(FirstTransformValue->GetScale3D()); return true; } return false; } template bool FTransformStructCustomization::FlushValues( TWeakPtr PropertyHandlePtr ) const { TSharedPtr 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[ValueIndex]); 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->bUseLeftUpForwardAxisDisplayCoordinateSystem ? -this->CachedTranslationY->Get() : 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) { if (!FOverridableManager::Get().IsEnabled(ArchetypeInstance)) { 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 FIntVector4 FTransformStructCustomization::GetSwizzle() const { return AxisDisplayInfo::GetTransformAxisSwizzle(); } 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 { TSharedPtr 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 { TSharedPtr 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) { if (!FOverridableManager::Get().IsEnabled(ArchetypeInstance)) { 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