Files
UnrealEngineUWP/Engine/Source/Editor/DetailCustomizations/Private/ComponentTransformDetails.cpp
jeanmichel dignard d81df94d30 ComponentTransformDetails: keep same world rotation and scale when going from relative to absolute and vice versa.
It was already the behavior for location but not for rotation and scale. With this change, all transform components will behave the same when switching from absolute to relative and vice versa.

#jira UE-193569
#rb aditya.ravichandran

[CL 27445738 by jeanmichel dignard in ue5-main branch]
2023-08-29 10:11:34 -04:00

1576 lines
56 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ComponentTransformDetails.h"
#include "Algo/Transform.h"
#include "Components/SceneComponent.h"
#include "Containers/ArrayView.h"
#include "Containers/Set.h"
#include "Containers/UnrealString.h"
#include "CoreGlobals.h"
#include "DetailCategoryBuilder.h"
#include "DetailLayoutBuilder.h"
#include "DetailWidgetRow.h"
#include "Editor.h"
#include "Editor/EditorEngine.h"
#include "Editor/UnrealEdEngine.h"
#include "Fonts/SlateFontInfo.h"
#include "Framework/Application/SlateApplication.h"
#include "Framework/Commands/UICommandInfo.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "GameFramework/Actor.h"
#include "HAL/PlatformApplicationMisc.h"
#include "IDetailChildrenBuilder.h"
#include "IDetailPropertyRow.h"
#include "Input/Events.h"
#include "Internationalization/Internationalization.h"
#include "IPropertyUtilities.h"
#include "Kismet2/ComponentEditorUtils.h"
#include "Layout/Margin.h"
#include "Math/Quat.h"
#include "Math/Transform.h"
#include "Math/UnitConversion.h"
#include "Misc/AssertionMacros.h"
#include "Misc/Attribute.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/NotifyHook.h"
#include "PropertyEditorCopyPaste.h"
#include "PropertyHandle.h"
#include "ScopedTransaction.h"
#include "Settings/EditorProjectSettings.h"
#include "SlateOptMacros.h"
#include "SlotBase.h"
#include "Styling/AppStyle.h"
#include "Styling/SlateColor.h"
#include "Templates/Casts.h"
#include "Templates/UnrealTemplate.h"
#include "Textures/SlateIcon.h"
#include "Types/SlateStructs.h"
#include "UnrealEdGlobals.h"
#include "UObject/Class.h"
#include "UObject/Object.h"
#include "UObject/ObjectMacros.h"
#include "UObject/ObjectPtr.h"
#include "UObject/Package.h"
#include "UObject/UnrealNames.h"
#include "UObject/UnrealType.h"
#include "UObject/UObjectGlobals.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Input/NumericUnitTypeInterface.inl"
#include "Widgets/Input/SCheckBox.h"
#include "Widgets/Input/SComboButton.h"
#include "Widgets/Input/SRotatorInputBox.h"
#include "Widgets/Input/SVectorInputBox.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/Text/STextBlock.h"
#include <utility>
class SWidget;
class UWorld;
struct FSlateBrush;
#define LOCTEXT_NAMESPACE "FComponentTransformDetails"
namespace UE::DetailsCustomizations::Internal
{
/** Lookup to get the property name for the given TransformField. */
static TMap<ETransformField::Type, FString> TransformFieldToPropertyNameString = {
{ ETransformField::Location, USceneComponent::GetRelativeLocationPropertyName().ToString() },
{ ETransformField::Rotation, USceneComponent::GetRelativeRotationPropertyName().ToString() },
{ ETransformField::Scale, USceneComponent::GetRelativeScale3DPropertyName().ToString() }
};
}
class FScopedSwitchWorldForObject
{
public:
FScopedSwitchWorldForObject( UObject* Object )
: PrevWorld( NULL )
{
bool bRequiresPlayWorld = false;
if( GUnrealEd->PlayWorld && !GIsPlayInEditorWorld )
{
UPackage* ObjectPackage = Object->GetOutermost();
bRequiresPlayWorld = ObjectPackage->HasAnyPackageFlags(PKG_PlayInEditor);
}
if( bRequiresPlayWorld )
{
PrevWorld = SetPlayInEditorWorld( GUnrealEd->PlayWorld );
}
}
~FScopedSwitchWorldForObject()
{
if( PrevWorld )
{
RestoreEditorWorld( PrevWorld );
}
}
private:
UWorld* PrevWorld;
};
static USceneComponent* GetSceneComponentFromDetailsObject(UObject* InObject)
{
AActor* Actor = Cast<AActor>(InObject);
if(Actor)
{
return Actor->GetRootComponent();
}
return Cast<USceneComponent>(InObject);
}
FComponentTransformDetails::FComponentTransformDetails( const TArray< TWeakObjectPtr<UObject> >& InSelectedObjects, const FSelectedActorInfo& InSelectedActorInfo, IDetailLayoutBuilder& DetailBuilder )
: TNumericUnitTypeInterface(GetDefault<UEditorProjectAppearanceSettings>()->bDisplayUnitsOnComponentTransforms ? EUnit::Centimeters : EUnit::Unspecified)
, SelectedActorInfo( InSelectedActorInfo )
, SelectedObjects( InSelectedObjects )
, NotifyHook( DetailBuilder.GetPropertyUtilities()->GetNotifyHook() )
, bPreserveScaleRatio( false )
, bEditingRotationInUI( false )
, bIsSliderTransaction( false )
, HiddenFieldMask( 0 )
{
GConfig->GetBool(TEXT("SelectionDetails"), TEXT("PreserveScaleRatio"), bPreserveScaleRatio, GEditorPerProjectIni);
FCoreUObjectDelegates::OnObjectsReplaced.AddRaw(this, &FComponentTransformDetails::OnObjectsReplaced);
}
FComponentTransformDetails::~FComponentTransformDetails()
{
FCoreUObjectDelegates::OnObjectsReplaced.RemoveAll(this);
}
TSharedRef<SWidget> FComponentTransformDetails::BuildTransformFieldLabel( ETransformField::Type TransformField )
{
FText Label;
switch( TransformField )
{
case ETransformField::Rotation:
Label = LOCTEXT( "RotationLabel", "Rotation");
break;
case ETransformField::Scale:
Label = LOCTEXT( "ScaleLabel", "Scale" );
break;
case ETransformField::Location:
default:
Label = LOCTEXT("LocationLabel", "Location");
break;
}
FMenuBuilder MenuBuilder( true, NULL, NULL );
FUIAction SetRelativeLocationAction
(
FExecuteAction::CreateSP( this, &FComponentTransformDetails::OnSetAbsoluteTransform, TransformField, false ),
FCanExecuteAction(),
FIsActionChecked::CreateSP( this, &FComponentTransformDetails::IsAbsoluteTransformChecked, TransformField, false )
);
FUIAction SetWorldLocationAction
(
FExecuteAction::CreateSP( this, &FComponentTransformDetails::OnSetAbsoluteTransform, TransformField, true ),
FCanExecuteAction(),
FIsActionChecked::CreateSP( this, &FComponentTransformDetails::IsAbsoluteTransformChecked, TransformField, true )
);
MenuBuilder.BeginSection( TEXT("TransformType"), FText::Format( LOCTEXT("TransformType", "{0} Type"), Label ) );
MenuBuilder.AddMenuEntry
(
FText::Format( LOCTEXT( "RelativeLabel", "Relative"), Label ),
FText::Format( LOCTEXT( "RelativeLabel_ToolTip", "{0} is relative to its parent"), Label ),
FSlateIcon(),
SetRelativeLocationAction,
NAME_None,
EUserInterfaceActionType::RadioButton
);
MenuBuilder.AddMenuEntry
(
FText::Format( LOCTEXT( "WorldLabel", "World"), Label ),
FText::Format( LOCTEXT( "WorldLabel_ToolTip", "{0} is relative to the world"), Label ),
FSlateIcon(),
SetWorldLocationAction,
NAME_None,
EUserInterfaceActionType::RadioButton
);
MenuBuilder.EndSection();
TSharedRef<SWidget> NameContent =
SNew(SComboButton)
.ContentPadding(0)
.MenuContent()
[
MenuBuilder.MakeWidget()
]
.ButtonContent()
[
SNew( SBox )
.Padding( FMargin( 0.0f, 0.0f, 2.0f, 0.0f ) )
.MinDesiredWidth(50.f)
[
SNew(STextBlock)
.Text(this, &FComponentTransformDetails::GetTransformFieldText, TransformField)
.Font(IDetailLayoutBuilder::GetDetailFont())
]
];
if(TransformField == ETransformField::Scale)
{
NameContent =
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.VAlign(VAlign_Center)
[
NameContent
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(FMargin(4.0f, 0.0f, 0.0f, 0.0f))
[
// 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, &FComponentTransformDetails::IsPreserveScaleRatioChecked)
.IsEnabled(this, &FComponentTransformDetails::GetIsEnabled)
.OnCheckStateChanged(this, &FComponentTransformDetails::OnPreserveScaleRatioToggled)
.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, &FComponentTransformDetails::GetPreserveScaleRatioImage)
.ColorAndOpacity(FSlateColor::UseForeground())
]
];
}
return NameContent;
}
FText FComponentTransformDetails::GetTransformFieldText( ETransformField::Type TransformField ) const
{
switch (TransformField)
{
case ETransformField::Location:
return GetLocationText();
break;
case ETransformField::Rotation:
return GetRotationText();
break;
case ETransformField::Scale:
return GetScaleText();
break;
default:
return FText::GetEmpty();
break;
}
}
bool FComponentTransformDetails::OnCanCopy( ETransformField::Type TransformField ) const
{
// We can only copy values if the whole field is set. If multiple values are defined we do not copy since we are unable to determine the value
switch (TransformField)
{
case ETransformField::Location:
return CachedLocation.IsSet();
break;
case ETransformField::Rotation:
return CachedRotation.IsSet();
break;
case ETransformField::Scale:
return CachedScale.IsSet();
break;
default:
return false;
break;
}
}
void FComponentTransformDetails::OnCopy( ETransformField::Type TransformField )
{
CacheTransform();
FString CopyStr;
switch (TransformField)
{
case ETransformField::Location:
CopyStr = FString::Printf(TEXT("(X=%f,Y=%f,Z=%f)"), CachedLocation.X.GetValue(), CachedLocation.Y.GetValue(), CachedLocation.Z.GetValue());
break;
case ETransformField::Rotation:
CopyStr = FString::Printf(TEXT("(Pitch=%f,Yaw=%f,Roll=%f)"), CachedRotation.Y.GetValue(), CachedRotation.Z.GetValue(), CachedRotation.X.GetValue());
break;
case ETransformField::Scale:
CopyStr = FString::Printf(TEXT("(X=%f,Y=%f,Z=%f)"), CachedScale.X.GetValue(), CachedScale.Y.GetValue(), CachedScale.Z.GetValue());
break;
default:
break;
}
if( !CopyStr.IsEmpty() )
{
FPlatformApplicationMisc::ClipboardCopy( *CopyStr );
}
}
void FComponentTransformDetails::OnPaste( ETransformField::Type TransformField )
{
FString PastedText;
FPlatformApplicationMisc::ClipboardPaste(PastedText);
PasteFromText(TEXT(""), PastedText, TransformField);
}
void FComponentTransformDetails::OnPasteFromText(
const FString& InTag,
const FString& InText,
const TOptional<FGuid>& InOperationId,
ETransformField::Type InTransformField)
{
PasteFromText(InTag, InText, InTransformField);
}
void FComponentTransformDetails::PasteFromText(
const FString& InTag,
const FString& InText,
ETransformField::Type InTransformField)
{
if (InText.IsEmpty())
{
return;
}
FString Text = InText;
if (!InTag.IsEmpty())
{
const FString PropertyPath = UE::PropertyEditor::GetPropertyPath(GetPropertyHandle());
// ensure that if tag is specified, that it matches the subscriber
if (!InTag.Equals(UE::DetailsCustomizations::Internal::TransformFieldToPropertyNameString[InTransformField]))
{
return;
}
}
switch (InTransformField)
{
case ETransformField::Location:
{
FVector Location;
if (Location.InitFromString(Text))
{
FScopedTransaction Transaction(LOCTEXT("PasteLocation", "Paste Location"));
OnSetTransform(ETransformField::Location, EAxisList::All, Location, false, true);
}
}
break;
case ETransformField::Rotation:
{
FRotator Rotation;
Text.ReplaceInline(TEXT("Pitch="), TEXT("P="));
Text.ReplaceInline(TEXT("Yaw="), TEXT("Y="));
Text.ReplaceInline(TEXT("Roll="), TEXT("R="));
if (Rotation.InitFromString(Text))
{
FScopedTransaction Transaction(LOCTEXT("PasteRotation", "Paste Rotation"));
OnSetTransform(ETransformField::Rotation, EAxisList::All, Rotation.Euler(), false, true);
}
}
break;
case ETransformField::Scale:
{
FVector Scale;
if (Scale.InitFromString(Text))
{
FScopedTransaction Transaction(LOCTEXT("PasteScale", "Paste Scale"));
OnSetTransform(ETransformField::Scale, EAxisList::All, Scale, false, true);
}
}
break;
default:
break;
}
}
FUIAction FComponentTransformDetails::CreateCopyAction( ETransformField::Type TransformField ) const
{
return
FUIAction
(
FExecuteAction::CreateSP(const_cast<FComponentTransformDetails*>(this), &FComponentTransformDetails::OnCopy, TransformField ),
FCanExecuteAction::CreateSP(const_cast<FComponentTransformDetails*>(this), &FComponentTransformDetails::OnCanCopy, TransformField )
);
}
FUIAction FComponentTransformDetails::CreatePasteAction( ETransformField::Type TransformField ) const
{
return
FUIAction( FExecuteAction::CreateSP(const_cast<FComponentTransformDetails*>(this), &FComponentTransformDetails::OnPaste, TransformField ) );
}
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void FComponentTransformDetails::GenerateChildContent( IDetailChildrenBuilder& ChildrenBuilder )
{
UClass* SceneComponentClass = USceneComponent::StaticClass();
FSlateFontInfo FontInfo = IDetailLayoutBuilder::GetDetailFont();
const bool bHideLocationField = ( HiddenFieldMask & ( 1 << ETransformField::Location ) ) != 0;
const bool bHideRotationField = ( HiddenFieldMask & ( 1 << ETransformField::Rotation ) ) != 0;
const bool bHideScaleField = ( HiddenFieldMask & ( 1 << ETransformField::Scale ) ) != 0;
IDetailCategoryBuilder& ParentCategory = ChildrenBuilder.GetParentCategory();
IDetailLayoutBuilder& LayoutBuilder = ParentCategory.GetParentLayout();
TSharedPtr<IPropertyHandle> LocationPropertyHandle = LayoutBuilder.GetProperty(USceneComponent::GetRelativeLocationPropertyName(), USceneComponent::StaticClass());
TSharedPtr<IPropertyHandle> RotationPropertyHandle = LayoutBuilder.GetProperty(USceneComponent::GetRelativeRotationPropertyName(), USceneComponent::StaticClass());
TSharedPtr<IPropertyHandle> ScalePropertyHandle = LayoutBuilder.GetProperty(USceneComponent::GetRelativeScale3DPropertyName(), USceneComponent::StaticClass());
const FString& MetaLocationDeltaString = LocationPropertyHandle->GetMetaData("Delta");
const FString& MetaRotationMinString = RotationPropertyHandle->GetMetaData("UIMin");
const FString& MetaRotationMaxString = RotationPropertyHandle->GetMetaData("UIMax");
const FString& MetaScaleDeltaString = ScalePropertyHandle->GetMetaData("Delta");
float LocationSpinDelta = !MetaLocationDeltaString.IsEmpty() ? FCString::Atof(*MetaLocationDeltaString) : 1.f;
float RotationMin = !MetaRotationMinString.IsEmpty() ? FCString::Atof(*MetaRotationMinString) : 0.f;
float RotationMax = !MetaRotationMaxString.IsEmpty() ? FCString::Atof(*MetaRotationMaxString) : 359.999f;
float ScaleSpinDelta = !MetaScaleDeltaString.IsEmpty() ? FCString::Atof(*MetaScaleDeltaString) : 0.0025f;
// Location
if(!bHideLocationField)
{
TSharedPtr<INumericTypeInterface<FVector::FReal>> TypeInterface;
if( FUnitConversion::Settings().ShouldDisplayUnits() )
{
TypeInterface = SharedThis(this);
}
ParentCategory.OnPasteFromText()->AddSP(this, &FComponentTransformDetails::OnPasteFromText, ETransformField::Location);
ChildrenBuilder.AddCustomRow( LOCTEXT("LocationFilter", "Location") )
.RowTag("Location")
.CopyAction( CreateCopyAction( ETransformField::Location ) )
.PasteAction( CreatePasteAction( ETransformField::Location ) )
.OverrideResetToDefault(FResetToDefaultOverride::Create(TAttribute<bool>(this, &FComponentTransformDetails::GetLocationResetVisibility), FSimpleDelegate::CreateSP(this, &FComponentTransformDetails::OnLocationResetClicked)))
.PropertyHandleList({ GeneratePropertyHandle(USceneComponent::GetRelativeLocationPropertyName(), ChildrenBuilder) })
.NameContent()
.VAlign(VAlign_Center)
[
BuildTransformFieldLabel( ETransformField::Location )
]
.ValueContent()
.MinDesiredWidth(125.0f * 3.0f)
.MaxDesiredWidth(125.0f * 3.0f)
.VAlign(VAlign_Center)
[
SNew(SNumericVectorInputBox<FVector::FReal>)
.X(this, &FComponentTransformDetails::GetLocationX)
.Y(this, &FComponentTransformDetails::GetLocationY)
.Z(this, &FComponentTransformDetails::GetLocationZ)
.bColorAxisLabels(true)
.IsEnabled(this, &FComponentTransformDetails::GetIsEnabled)
.OnXChanged(this, &FComponentTransformDetails::OnSetTransformAxis, ETextCommit::Default, ETransformField::Location, EAxisList::X, false)
.OnYChanged(this, &FComponentTransformDetails::OnSetTransformAxis, ETextCommit::Default, ETransformField::Location, EAxisList::Y, false)
.OnZChanged(this, &FComponentTransformDetails::OnSetTransformAxis, ETextCommit::Default, ETransformField::Location, EAxisList::Z, false)
.OnXCommitted(this, &FComponentTransformDetails::OnSetTransformAxis, ETransformField::Location, EAxisList::X, true)
.OnYCommitted(this, &FComponentTransformDetails::OnSetTransformAxis, ETransformField::Location, EAxisList::Y, true)
.OnZCommitted(this, &FComponentTransformDetails::OnSetTransformAxis, ETransformField::Location, EAxisList::Z, true)
.Font(FontInfo)
.TypeInterface(TypeInterface)
.AllowSpin(SelectedObjects.Num() == 1)
.SpinDelta(LocationSpinDelta)
.OnBeginSliderMovement(this, &FComponentTransformDetails::OnBeginLocationSlider)
.OnEndSliderMovement(this, &FComponentTransformDetails::OnEndLocationSlider)
];
}
// Rotation
if(!bHideRotationField)
{
TSharedPtr<INumericTypeInterface<FRotator::FReal>> TypeInterface;
if( FUnitConversion::Settings().ShouldDisplayUnits() )
{
TypeInterface = MakeShareable( new TNumericUnitTypeInterface<FRotator::FReal>(EUnit::Degrees) );
}
ParentCategory.OnPasteFromText()->AddSP(this, &FComponentTransformDetails::OnPasteFromText, ETransformField::Rotation);
ChildrenBuilder.AddCustomRow( LOCTEXT("RotationFilter", "Rotation") )
.RowTag("Rotation")
.CopyAction( CreateCopyAction(ETransformField::Rotation) )
.PasteAction( CreatePasteAction(ETransformField::Rotation) )
.OverrideResetToDefault(FResetToDefaultOverride::Create(TAttribute<bool>(this, &FComponentTransformDetails::GetRotationResetVisibility), FSimpleDelegate::CreateSP(this, &FComponentTransformDetails::OnRotationResetClicked)))
.PropertyHandleList({ GeneratePropertyHandle(USceneComponent::GetRelativeRotationPropertyName(), ChildrenBuilder) })
.NameContent()
.VAlign(VAlign_Center)
[
BuildTransformFieldLabel(ETransformField::Rotation)
]
.ValueContent()
.MinDesiredWidth(125.0f * 3.0f)
.MaxDesiredWidth(125.0f * 3.0f)
.VAlign(VAlign_Center)
[
SNew( SNumericRotatorInputBox<FRotator::FReal> )
.AllowSpin( SelectedObjects.Num() == 1 )
.MinSliderValue(RotationMin)
.MaxSliderValue(RotationMax)
.Roll( this, &FComponentTransformDetails::GetRotationX )
.Pitch( this, &FComponentTransformDetails::GetRotationY )
.Yaw( this, &FComponentTransformDetails::GetRotationZ )
.bColorAxisLabels( true )
.IsEnabled( this, &FComponentTransformDetails::GetIsEnabled )
.OnPitchBeginSliderMovement( this, &FComponentTransformDetails::OnBeginRotationSlider )
.OnYawBeginSliderMovement( this, &FComponentTransformDetails::OnBeginRotationSlider )
.OnRollBeginSliderMovement( this, &FComponentTransformDetails::OnBeginRotationSlider )
.OnPitchEndSliderMovement( this, &FComponentTransformDetails::OnEndRotationSlider )
.OnYawEndSliderMovement( this, &FComponentTransformDetails::OnEndRotationSlider )
.OnRollEndSliderMovement( this, &FComponentTransformDetails::OnEndRotationSlider )
.OnRollChanged( this, &FComponentTransformDetails::OnSetTransformAxis, ETextCommit::Default, ETransformField::Rotation, EAxisList::X, false )
.OnPitchChanged( this, &FComponentTransformDetails::OnSetTransformAxis, ETextCommit::Default, ETransformField::Rotation, EAxisList::Y, false )
.OnYawChanged( this, &FComponentTransformDetails::OnSetTransformAxis, ETextCommit::Default, ETransformField::Rotation, EAxisList::Z, false )
.OnRollCommitted( this, &FComponentTransformDetails::OnSetTransformAxis, ETransformField::Rotation, EAxisList::X, true )
.OnPitchCommitted( this, &FComponentTransformDetails::OnSetTransformAxis, ETransformField::Rotation, EAxisList::Y, true )
.OnYawCommitted( this, &FComponentTransformDetails::OnSetTransformAxis, ETransformField::Rotation, EAxisList::Z, true )
.TypeInterface( TypeInterface )
.Font( FontInfo )
];
}
// Scale
if(!bHideScaleField)
{
ParentCategory.OnPasteFromText()->AddSP(this, &FComponentTransformDetails::OnPasteFromText, ETransformField::Scale);
ChildrenBuilder.AddCustomRow( LOCTEXT("ScaleFilter", "Scale") )
.RowTag("Scale")
.CopyAction( CreateCopyAction(ETransformField::Scale) )
.PasteAction( CreatePasteAction(ETransformField::Scale) )
.OverrideResetToDefault(FResetToDefaultOverride::Create(TAttribute<bool>(this, &FComponentTransformDetails::GetScaleResetVisibility), FSimpleDelegate::CreateSP(this, &FComponentTransformDetails::OnScaleResetClicked)))
.PropertyHandleList({ GeneratePropertyHandle(USceneComponent::GetRelativeScale3DPropertyName(), ChildrenBuilder) })
.NameContent()
.VAlign(VAlign_Center)
[
BuildTransformFieldLabel(ETransformField::Scale)
]
.ValueContent()
.MinDesiredWidth(125.0f * 3.0f)
.MaxDesiredWidth(125.0f * 3.0f)
.VAlign(VAlign_Center)
[
SNew( SNumericVectorInputBox<FVector::FReal> )
.X( this, &FComponentTransformDetails::GetScaleX )
.Y( this, &FComponentTransformDetails::GetScaleY )
.Z( this, &FComponentTransformDetails::GetScaleZ )
.bColorAxisLabels( true )
.IsEnabled( this, &FComponentTransformDetails::GetIsEnabled )
.OnXChanged( this, &FComponentTransformDetails::OnSetTransformAxis, ETextCommit::Default, ETransformField::Scale, EAxisList::X, false )
.OnYChanged( this, &FComponentTransformDetails::OnSetTransformAxis, ETextCommit::Default, ETransformField::Scale, EAxisList::Y, false )
.OnZChanged( this, &FComponentTransformDetails::OnSetTransformAxis, ETextCommit::Default, ETransformField::Scale, EAxisList::Z, false )
.OnXCommitted( this, &FComponentTransformDetails::OnSetTransformAxis, ETransformField::Scale, EAxisList::X, true )
.OnYCommitted( this, &FComponentTransformDetails::OnSetTransformAxis, ETransformField::Scale, EAxisList::Y, true )
.OnZCommitted( this, &FComponentTransformDetails::OnSetTransformAxis, ETransformField::Scale, EAxisList::Z, true )
.ContextMenuExtenderX( this, &FComponentTransformDetails::ExtendXScaleContextMenu )
.ContextMenuExtenderY( this, &FComponentTransformDetails::ExtendYScaleContextMenu )
.ContextMenuExtenderZ( this, &FComponentTransformDetails::ExtendZScaleContextMenu )
.Font( FontInfo )
.AllowSpin( SelectedObjects.Num() == 1 )
.SpinDelta( ScaleSpinDelta )
.OnBeginSliderMovement( this, &FComponentTransformDetails::OnBeginScaleSlider )
.OnEndSliderMovement(this, &FComponentTransformDetails::OnEndScaleSlider)
];
}
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
void FComponentTransformDetails::Tick( float DeltaTime )
{
CacheTransform();
if (!FixedDisplayUnits.IsSet())
{
CacheCommonLocationUnits();
}
}
void FComponentTransformDetails::CacheCommonLocationUnits()
{
float LargestValue = 0.f;
if (CachedLocation.X.IsSet() && CachedLocation.X.GetValue() > LargestValue)
{
LargestValue = CachedLocation.X.GetValue();
}
if (CachedLocation.Y.IsSet() && CachedLocation.Y.GetValue() > LargestValue)
{
LargestValue = CachedLocation.Y.GetValue();
}
if (CachedLocation.Z.IsSet() && CachedLocation.Z.GetValue() > LargestValue)
{
LargestValue = CachedLocation.Z.GetValue();
}
SetupFixedDisplay(LargestValue);
}
TSharedPtr<IPropertyHandle> FComponentTransformDetails::GeneratePropertyHandle(FName PropertyName, IDetailChildrenBuilder& ChildrenBuilder)
{
// Try finding the property handle in the details panel's property map first.
IDetailLayoutBuilder& LayoutBuilder = ChildrenBuilder.GetParentCategory().GetParentLayout();
TSharedPtr<IPropertyHandle> PropertyHandle = LayoutBuilder.GetProperty(PropertyName, USceneComponent::StaticClass());
if (!PropertyHandle || !PropertyHandle->IsValidHandle())
{
// If it wasn't found, add a collapsed row which contains the property node.
TArray<UObject*> SceneComponents;
Algo::Transform(SelectedObjects, SceneComponents, [](TWeakObjectPtr<UObject> Obj) { return GetSceneComponentFromDetailsObject(Obj.Get()); });
PropertyHandle = LayoutBuilder.AddObjectPropertyData(SceneComponents, PropertyName);
CachedHandlesObjects.Append(SceneComponents);
}
PropertyHandles.Add(PropertyHandle);
return PropertyHandle;
}
void FComponentTransformDetails::UpdatePropertyHandlesObjects(const TArray<UObject*> NewSceneComponents)
{
// Cached the old handles objects.
CachedHandlesObjects.Reset(NewSceneComponents.Num());
Algo::Transform(NewSceneComponents, CachedHandlesObjects, [](UObject* Obj) { return TWeakObjectPtr<UObject>(Obj); });
for (TSharedPtr<IPropertyHandle>& Handle : PropertyHandles)
{
if (Handle && Handle->IsValidHandle())
{
Handle->ReplaceOuterObjects(NewSceneComponents);
}
}
}
bool FComponentTransformDetails::GetIsEnabled() const
{
return !GEditor->HasLockedActors() || SelectedActorInfo.NumSelected == 0;
}
const FSlateBrush* FComponentTransformDetails::GetPreserveScaleRatioImage() const
{
return bPreserveScaleRatio ? FAppStyle::GetBrush( TEXT("Icons.Lock") ) : FAppStyle::GetBrush( TEXT("Icons.Unlock") ) ;
}
ECheckBoxState FComponentTransformDetails::IsPreserveScaleRatioChecked() const
{
return bPreserveScaleRatio ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
}
void FComponentTransformDetails::OnPreserveScaleRatioToggled( ECheckBoxState NewState )
{
bPreserveScaleRatio = (NewState == ECheckBoxState::Checked) ? true : false;
GConfig->SetBool(TEXT("SelectionDetails"), TEXT("PreserveScaleRatio"), bPreserveScaleRatio, GEditorPerProjectIni);
}
FText FComponentTransformDetails::GetLocationText() const
{
return bAbsoluteLocation ? LOCTEXT( "AbsoluteLocation", "Absolute Location" ) : LOCTEXT( "Location", "Location" );
}
FText FComponentTransformDetails::GetRotationText() const
{
return bAbsoluteRotation ? LOCTEXT( "AbsoluteRotation", "Absolute Rotation" ) : LOCTEXT( "Rotation", "Rotation" );
}
FText FComponentTransformDetails::GetScaleText() const
{
return bAbsoluteScale ? LOCTEXT( "AbsoluteScale", "Absolute Scale" ) : LOCTEXT( "Scale", "Scale" );
}
void FComponentTransformDetails::OnSetAbsoluteTransform(ETransformField::Type TransformField, bool bAbsoluteEnabled)
{
FBoolProperty* AbsoluteProperty = nullptr;
FText TransactionText;
switch (TransformField)
{
case ETransformField::Location:
AbsoluteProperty = FindFProperty<FBoolProperty>(USceneComponent::StaticClass(), USceneComponent::GetAbsoluteLocationPropertyName());
TransactionText = LOCTEXT("ToggleAbsoluteLocation", "Toggle Absolute Location");
break;
case ETransformField::Rotation:
AbsoluteProperty = FindFProperty<FBoolProperty>(USceneComponent::StaticClass(), USceneComponent::GetAbsoluteRotationPropertyName());
TransactionText = LOCTEXT("ToggleAbsoluteRotation", "Toggle Absolute Rotation");
break;
case ETransformField::Scale:
AbsoluteProperty = FindFProperty<FBoolProperty>(USceneComponent::StaticClass(), USceneComponent::GetAbsoluteScalePropertyName());
TransactionText = LOCTEXT("ToggleAbsoluteScale", "Toggle Absolute Scale");
break;
default:
return;
}
bool bBeganTransaction = false;
TArray<UObject*> ModifiedObjects;
for (int32 ObjectIndex = 0; ObjectIndex < SelectedObjects.Num(); ++ObjectIndex)
{
TWeakObjectPtr<UObject> ObjectPtr = SelectedObjects[ObjectIndex];
if (ObjectPtr.IsValid())
{
UObject* Object = ObjectPtr.Get();
USceneComponent* SceneComponent = GetSceneComponentFromDetailsObject(Object);
if (SceneComponent)
{
bool bOldValue = TransformField == ETransformField::Location ? SceneComponent->IsUsingAbsoluteLocation() : (TransformField == ETransformField::Rotation ? SceneComponent->IsUsingAbsoluteRotation() : SceneComponent->IsUsingAbsoluteScale());
if (bOldValue == bAbsoluteEnabled)
{
// Already the desired value
continue;
}
if (!bBeganTransaction)
{
// NOTE: One transaction per change, not per actor
GEditor->BeginTransaction(TransactionText);
bBeganTransaction = true;
}
FScopedSwitchWorldForObject WorldSwitcher(Object);
if (SceneComponent->HasAnyFlags(RF_DefaultSubObject))
{
// Default subobjects must be included in any undo/redo operations
SceneComponent->SetFlags(RF_Transactional);
}
SceneComponent->PreEditChange(AbsoluteProperty);
if (NotifyHook)
{
NotifyHook->NotifyPreChange(AbsoluteProperty);
}
TOptional<FTransform> TransformToPreserve;
if (SceneComponent->GetAttachParent())
{
if (bAbsoluteEnabled)
{
TransformToPreserve = SceneComponent->GetComponentTransform();
}
else
{
FTransform ParentToWorld = SceneComponent->GetAttachParent()->GetSocketTransform(SceneComponent->GetAttachSocketName());
TransformToPreserve = SceneComponent->GetComponentTransform().GetRelativeTransform(ParentToWorld);
}
}
switch (TransformField)
{
case ETransformField::Location:
SceneComponent->SetUsingAbsoluteLocation(bAbsoluteEnabled);
if (TransformToPreserve.IsSet())
{
SceneComponent->SetRelativeLocation_Direct(TransformToPreserve->GetTranslation());
}
break;
case ETransformField::Rotation:
SceneComponent->SetUsingAbsoluteRotation(bAbsoluteEnabled);
if (TransformToPreserve.IsSet())
{
SceneComponent->SetRelativeRotation_Direct(FRotator(TransformToPreserve->GetRotation()));
}
break;
case ETransformField::Scale:
SceneComponent->SetUsingAbsoluteScale(bAbsoluteEnabled);
if (TransformToPreserve.IsSet())
{
SceneComponent->SetRelativeScale3D_Direct(TransformToPreserve->GetScale3D());
}
break;
}
ModifiedObjects.Add(Object);
}
}
}
if (bBeganTransaction)
{
FPropertyChangedEvent PropertyChangedEvent(AbsoluteProperty, EPropertyChangeType::ValueSet, MakeArrayView(ModifiedObjects));
for (UObject* Object : ModifiedObjects)
{
USceneComponent* SceneComponent = GetSceneComponentFromDetailsObject(Object);
if (SceneComponent)
{
SceneComponent->PostEditChangeProperty(PropertyChangedEvent);
// If it's a template, propagate the change out to any current instances of the object
if (SceneComponent->IsTemplate())
{
bool NewValue = bAbsoluteEnabled;
bool OldValue = !NewValue;
TSet<USceneComponent*> UpdatedInstances;
FComponentEditorUtils::PropagateDefaultValueChange(SceneComponent, AbsoluteProperty, OldValue, NewValue, UpdatedInstances);
}
}
}
if (NotifyHook)
{
NotifyHook->NotifyPostChange(PropertyChangedEvent, AbsoluteProperty);
}
GEditor->EndTransaction();
GUnrealEd->RedrawLevelEditingViewports();
}
}
bool FComponentTransformDetails::IsAbsoluteTransformChecked(ETransformField::Type TransformField, bool bAbsoluteEnabled) const
{
switch (TransformField)
{
case ETransformField::Location:
return bAbsoluteLocation == bAbsoluteEnabled;
break;
case ETransformField::Rotation:
return bAbsoluteRotation == bAbsoluteEnabled;
break;
case ETransformField::Scale:
return bAbsoluteScale == bAbsoluteEnabled;
break;
default:
return false;
break;
}
}
struct FGetRootComponentArchetype
{
static USceneComponent* Get(UObject* Object)
{
auto RootComponent = Object ? GetSceneComponentFromDetailsObject(Object) : nullptr;
return RootComponent ? Cast<USceneComponent>(RootComponent->GetArchetype()) : nullptr;
}
};
bool FComponentTransformDetails::GetLocationResetVisibility() const
{
const USceneComponent* Archetype = FGetRootComponentArchetype::Get(SelectedObjects[0].Get());
const FVector Data = Archetype ? Archetype->GetRelativeLocation() : FVector::ZeroVector;
// unset means multiple differing values, so show "Reset to Default" in that case
return CachedLocation.IsSet() && CachedLocation.X.GetValue() == Data.X && CachedLocation.Y.GetValue() == Data.Y && CachedLocation.Z.GetValue() == Data.Z ? false : true;
}
void FComponentTransformDetails::OnLocationResetClicked()
{
const FText TransactionName = LOCTEXT("ResetLocation", "Reset Location");
FScopedTransaction Transaction(TransactionName);
const USceneComponent* Archetype = FGetRootComponentArchetype::Get(SelectedObjects[0].Get());
const FVector Data = Archetype ? Archetype->GetRelativeLocation() : FVector::ZeroVector;
OnSetTransform(ETransformField::Location, EAxisList::All, Data, false, true);
}
bool FComponentTransformDetails::GetRotationResetVisibility() const
{
const USceneComponent* Archetype = FGetRootComponentArchetype::Get(SelectedObjects[0].Get());
const FVector Data = Archetype ? Archetype->GetRelativeRotation().Euler() : FVector::ZeroVector;
// unset means multiple differing values, so show "Reset to Default" in that case
return CachedRotation.IsSet() && CachedRotation.X.GetValue() == Data.X && CachedRotation.Y.GetValue() == Data.Y && CachedRotation.Z.GetValue() == Data.Z ? false : true;
}
void FComponentTransformDetails::OnRotationResetClicked()
{
const FText TransactionName = LOCTEXT("ResetRotation", "Reset Rotation");
FScopedTransaction Transaction(TransactionName);
const USceneComponent* Archetype = FGetRootComponentArchetype::Get(SelectedObjects[0].Get());
const FVector Data = Archetype ? Archetype->GetRelativeRotation().Euler() : FVector::ZeroVector;
OnSetTransform(ETransformField::Rotation, EAxisList::All, Data, false, true);
}
bool FComponentTransformDetails::GetScaleResetVisibility() const
{
const USceneComponent* Archetype = FGetRootComponentArchetype::Get(SelectedObjects[0].Get());
const FVector Data = Archetype ? Archetype->GetRelativeScale3D() : FVector(1.0f);
// unset means multiple differing values, so show "Reset to Default" in that case
return CachedScale.IsSet() && CachedScale.X.GetValue() == Data.X && CachedScale.Y.GetValue() == Data.Y && CachedScale.Z.GetValue() == Data.Z ? false : true;
}
void FComponentTransformDetails::OnScaleResetClicked()
{
const FText TransactionName = LOCTEXT("ResetScale", "Reset Scale");
FScopedTransaction Transaction(TransactionName);
const USceneComponent* Archetype = FGetRootComponentArchetype::Get(SelectedObjects[0].Get());
const FVector Data = Archetype ? Archetype->GetRelativeScale3D() : FVector(1.0f);
OnSetTransform(ETransformField::Scale, EAxisList::All, Data, false, true);
}
void FComponentTransformDetails::ExtendXScaleContextMenu( FMenuBuilder& MenuBuilder )
{
MenuBuilder.BeginSection( "ScaleOperations", LOCTEXT( "ScaleOperations", "Scale Operations" ) );
MenuBuilder.AddMenuEntry(
LOCTEXT( "MirrorValueX", "Mirror X" ),
LOCTEXT( "MirrorValueX_Tooltip", "Mirror scale value on the X axis" ),
FSlateIcon(),
FUIAction( FExecuteAction::CreateSP( this, &FComponentTransformDetails::OnXScaleMirrored ), FCanExecuteAction() )
);
MenuBuilder.EndSection();
}
void FComponentTransformDetails::ExtendYScaleContextMenu( FMenuBuilder& MenuBuilder )
{
MenuBuilder.BeginSection( "ScaleOperations", LOCTEXT( "ScaleOperations", "Scale Operations" ) );
MenuBuilder.AddMenuEntry(
LOCTEXT( "MirrorValueY", "Mirror Y" ),
LOCTEXT( "MirrorValueY_Tooltip", "Mirror scale value on the Y axis" ),
FSlateIcon(),
FUIAction( FExecuteAction::CreateSP( this, &FComponentTransformDetails::OnYScaleMirrored ), FCanExecuteAction() )
);
MenuBuilder.EndSection();
}
void FComponentTransformDetails::ExtendZScaleContextMenu( FMenuBuilder& MenuBuilder )
{
MenuBuilder.BeginSection( "ScaleOperations", LOCTEXT( "ScaleOperations", "Scale Operations" ) );
MenuBuilder.AddMenuEntry(
LOCTEXT( "MirrorValueZ", "Mirror Z" ),
LOCTEXT( "MirrorValueZ_Tooltip", "Mirror scale value on the Z axis" ),
FSlateIcon(),
FUIAction( FExecuteAction::CreateSP( this, &FComponentTransformDetails::OnZScaleMirrored ), FCanExecuteAction() )
);
MenuBuilder.EndSection();
}
void FComponentTransformDetails::OnXScaleMirrored()
{
FSlateApplication::Get().ClearKeyboardFocus(EFocusCause::Mouse);
FScopedTransaction Transaction(LOCTEXT("MirrorActorScaleX", "Mirror actor scale X"));
OnSetTransform(ETransformField::Scale, EAxisList::X, FVector(1.0f), true, true);
}
void FComponentTransformDetails::OnYScaleMirrored()
{
FSlateApplication::Get().ClearKeyboardFocus(EFocusCause::Mouse);
FScopedTransaction Transaction(LOCTEXT("MirrorActorScaleY", "Mirror actor scale Y"));
OnSetTransform(ETransformField::Scale, EAxisList::Y, FVector(1.0f), true, true);
}
void FComponentTransformDetails::OnZScaleMirrored()
{
FSlateApplication::Get().ClearKeyboardFocus(EFocusCause::Mouse);
FScopedTransaction Transaction(LOCTEXT("MirrorActorScaleZ", "Mirror actor scale Z"));
OnSetTransform(ETransformField::Scale, EAxisList::Z, FVector(1.0f), true, true);
}
void FComponentTransformDetails::CacheTransform()
{
FVector CurLoc;
FRotator CurRot;
FVector CurScale;
for( int32 ObjectIndex = 0; ObjectIndex < SelectedObjects.Num(); ++ObjectIndex )
{
TWeakObjectPtr<UObject> ObjectPtr = SelectedObjects[ObjectIndex];
if( ObjectPtr.IsValid() )
{
UObject* Object = ObjectPtr.Get();
USceneComponent* SceneComponent = GetSceneComponentFromDetailsObject( Object );
FVector Loc;
FRotator Rot;
FVector Scale;
if( SceneComponent )
{
Loc = SceneComponent->GetRelativeLocation();
FRotator* FoundRotator = ObjectToRelativeRotationMap.Find(SceneComponent);
Rot = (bEditingRotationInUI && !Object->IsTemplate() && FoundRotator) ? *FoundRotator : SceneComponent->GetRelativeRotation();
Scale = SceneComponent->GetRelativeScale3D();
if( ObjectIndex == 0 )
{
// Cache the current values from the first actor to see if any values differ among other actors
CurLoc = Loc;
CurRot = Rot;
CurScale = Scale;
CachedLocation.Set( Loc );
CachedRotation.Set( Rot );
CachedScale.Set( Scale );
bAbsoluteLocation = SceneComponent->IsUsingAbsoluteLocation();
bAbsoluteScale = SceneComponent->IsUsingAbsoluteScale();
bAbsoluteRotation = SceneComponent->IsUsingAbsoluteRotation();
}
else if( CurLoc != Loc || CurRot != Rot || CurScale != Scale )
{
// Check which values differ and unset the different values
CachedLocation.X = Loc.X == CurLoc.X && CachedLocation.X.IsSet() ? Loc.X : TOptional<FVector::FReal>();
CachedLocation.Y = Loc.Y == CurLoc.Y && CachedLocation.Y.IsSet() ? Loc.Y : TOptional<FVector::FReal>();
CachedLocation.Z = Loc.Z == CurLoc.Z && CachedLocation.Z.IsSet() ? Loc.Z : TOptional<FVector::FReal>();
CachedRotation.X = Rot.Roll == CurRot.Roll && CachedRotation.X.IsSet() ? Rot.Roll : TOptional<FRotator::FReal>();
CachedRotation.Y = Rot.Pitch == CurRot.Pitch && CachedRotation.Y.IsSet() ? Rot.Pitch : TOptional<FRotator::FReal>();
CachedRotation.Z = Rot.Yaw == CurRot.Yaw && CachedRotation.Z.IsSet() ? Rot.Yaw : TOptional<FRotator::FReal>();
CachedScale.X = Scale.X == CurScale.X && CachedScale.X.IsSet() ? Scale.X : TOptional<FVector::FReal>();
CachedScale.Y = Scale.Y == CurScale.Y && CachedScale.Y.IsSet() ? Scale.Y : TOptional<FVector::FReal>();
CachedScale.Z = Scale.Z == CurScale.Z && CachedScale.Z.IsSet() ? Scale.Z : TOptional<FVector::FReal>();
// If all values are unset all values are different and we can stop looking
const bool bAllValuesDiffer = !CachedLocation.IsSet() && !CachedRotation.IsSet() && !CachedScale.IsSet();
if( bAllValuesDiffer )
{
break;
}
}
}
}
}
}
FVector FComponentTransformDetails::GetAxisFilteredVector(EAxisList::Type Axis, const FVector& NewValue, const FVector& OldValue)
{
return FVector((Axis & EAxisList::X) ? NewValue.X : OldValue.X,
(Axis & EAxisList::Y) ? NewValue.Y : OldValue.Y,
(Axis & EAxisList::Z) ? NewValue.Z : OldValue.Z);
}
void FComponentTransformDetails::OnSetTransform(ETransformField::Type TransformField, EAxisList::Type Axis, FVector NewValue, bool bMirror, bool bCommitted)
{
if (!bCommitted && SelectedObjects.Num() > 1)
{
// Ignore interactive changes when we have more than one selected object
return;
}
FText TransactionText;
FProperty* ValueProperty = nullptr;
FProperty* AxisProperty = nullptr;
switch (TransformField)
{
case ETransformField::Location:
TransactionText = LOCTEXT("OnSetLocation", "Set Location");
ValueProperty = FindFProperty<FProperty>(USceneComponent::StaticClass(), USceneComponent::GetRelativeLocationPropertyName());
// Only set axis property for single axis set
if (Axis == EAxisList::X)
{
AxisProperty = FindFProperty<FFloatProperty>(TBaseStructure<FVector>::Get(), GET_MEMBER_NAME_CHECKED(FVector, X));
}
else if (Axis == EAxisList::Y)
{
AxisProperty = FindFProperty<FFloatProperty>(TBaseStructure<FVector>::Get(), GET_MEMBER_NAME_CHECKED(FVector, Y));
}
else if (Axis == EAxisList::Z)
{
AxisProperty = FindFProperty<FFloatProperty>(TBaseStructure<FVector>::Get(), GET_MEMBER_NAME_CHECKED(FVector, Z));
}
break;
case ETransformField::Rotation:
TransactionText = LOCTEXT("OnSetRotation", "Set Rotation");
ValueProperty = FindFProperty<FProperty>(USceneComponent::StaticClass(), USceneComponent::GetRelativeRotationPropertyName());
// Only set axis property for single axis set
if (Axis == EAxisList::X)
{
AxisProperty = FindFProperty<FFloatProperty>(TBaseStructure<FRotator>::Get(), GET_MEMBER_NAME_CHECKED(FRotator, Roll));
}
else if (Axis == EAxisList::Y)
{
AxisProperty = FindFProperty<FFloatProperty>(TBaseStructure<FRotator>::Get(), GET_MEMBER_NAME_CHECKED(FRotator, Pitch));
}
else if (Axis == EAxisList::Z)
{
AxisProperty = FindFProperty<FFloatProperty>(TBaseStructure<FRotator>::Get(), GET_MEMBER_NAME_CHECKED(FRotator, Yaw));
}
break;
case ETransformField::Scale:
TransactionText = LOCTEXT("OnSetScale", "Set Scale");
ValueProperty = FindFProperty<FProperty>(USceneComponent::StaticClass(), USceneComponent::GetRelativeScale3DPropertyName());
// If keep scale is set, don't set axis property
if (!bPreserveScaleRatio && Axis == EAxisList::X)
{
AxisProperty = FindFProperty<FFloatProperty>(TBaseStructure<FVector>::Get(), GET_MEMBER_NAME_CHECKED(FVector, X));
}
else if (!bPreserveScaleRatio && Axis == EAxisList::Y)
{
AxisProperty = FindFProperty<FFloatProperty>(TBaseStructure<FVector>::Get(), GET_MEMBER_NAME_CHECKED(FVector, Y));
}
else if (!bPreserveScaleRatio && Axis == EAxisList::Z)
{
AxisProperty = FindFProperty<FFloatProperty>(TBaseStructure<FVector>::Get(), GET_MEMBER_NAME_CHECKED(FVector, Z));
}
break;
default:
return;
}
bool bBeganTransaction = false;
TArray<UObject*> ModifiedObjects;
FPropertyChangedEvent PropertyChangedEvent(ValueProperty, !bCommitted ? EPropertyChangeType::Interactive : EPropertyChangeType::ValueSet, MakeArrayView(ModifiedObjects));
FEditPropertyChain PropertyChain;
if (AxisProperty)
{
PropertyChain.AddHead(AxisProperty);
}
PropertyChain.AddHead(ValueProperty);
FPropertyChangedChainEvent PropertyChangedChainEvent(PropertyChain, PropertyChangedEvent);
for (int32 ObjectIndex = 0; ObjectIndex < SelectedObjects.Num(); ++ObjectIndex)
{
TWeakObjectPtr<UObject> ObjectPtr = SelectedObjects[ObjectIndex];
if (ObjectPtr.IsValid())
{
UObject* Object = ObjectPtr.Get();
USceneComponent* SceneComponent = GetSceneComponentFromDetailsObject(Object);
if (SceneComponent)
{
AActor* EditedActor = SceneComponent->GetOwner();
const bool bIsEditingTemplateObject = Object->IsTemplate();
FVector OldComponentValue;
FVector NewComponentValue;
switch (TransformField)
{
case ETransformField::Location:
OldComponentValue = SceneComponent->GetRelativeLocation();
break;
case ETransformField::Rotation:
// Pull from the actual component or from the cache
OldComponentValue = SceneComponent->GetRelativeRotation().Euler();
if (bEditingRotationInUI && !bIsEditingTemplateObject && ObjectToRelativeRotationMap.Find(SceneComponent))
{
OldComponentValue = ObjectToRelativeRotationMap.Find(SceneComponent)->Euler();
}
break;
case ETransformField::Scale:
OldComponentValue = SceneComponent->GetRelativeScale3D();
break;
}
// Set the incoming value
if (bMirror)
{
NewComponentValue = GetAxisFilteredVector(Axis, -OldComponentValue, OldComponentValue);
}
else
{
NewComponentValue = GetAxisFilteredVector(Axis, NewValue, OldComponentValue);
}
// If we're committing during a slider transaction then we need to force it, in order that PostEditChangeChainProperty be called.
// Note: this will even happen if the slider hasn't changed the value.
if (OldComponentValue != NewComponentValue || (bCommitted && bIsSliderTransaction))
{
if (!bBeganTransaction && bCommitted)
{
// NOTE: One transaction per change, not per actor
GEditor->BeginTransaction(TransactionText);
bBeganTransaction = true;
}
FScopedSwitchWorldForObject WorldSwitcher(Object);
if (bCommitted)
{
if (!bIsEditingTemplateObject)
{
// Broadcast the first time an actor is about to move
GEditor->BroadcastBeginObjectMovement(*SceneComponent);
if (EditedActor && EditedActor->GetRootComponent() == SceneComponent)
{
GEditor->BroadcastBeginObjectMovement(*EditedActor);
}
}
if (SceneComponent->HasAnyFlags(RF_DefaultSubObject))
{
// Default subobjects must be included in any undo/redo operations
SceneComponent->SetFlags(RF_Transactional);
}
}
// Have to downcast here because of function overloading and inheritance not playing nicely
((UObject*)SceneComponent)->PreEditChange(PropertyChain);
if (EditedActor && EditedActor->GetRootComponent() == SceneComponent)
{
((UObject*)EditedActor)->PreEditChange(PropertyChain);
}
if (NotifyHook)
{
NotifyHook->NotifyPreChange(ValueProperty);
}
switch (TransformField)
{
case ETransformField::Location:
{
if (!bIsEditingTemplateObject)
{
// Update local cache for restoring later
ObjectToRelativeRotationMap.FindOrAdd(SceneComponent) = SceneComponent->GetRelativeRotation();
}
SceneComponent->SetRelativeLocation(NewComponentValue);
// Also forcibly set it as the cache may have changed it slightly
SceneComponent->SetRelativeLocation_Direct(NewComponentValue);
// If it's a template, propagate the change out to any current instances of the object
if (bIsEditingTemplateObject)
{
TSet<USceneComponent*> UpdatedInstances;
FComponentEditorUtils::PropagateDefaultValueChange(SceneComponent, ValueProperty, OldComponentValue, NewComponentValue, UpdatedInstances);
}
break;
}
case ETransformField::Rotation:
{
FRotator NewRotation = FRotator::MakeFromEuler(NewComponentValue);
if (!bIsEditingTemplateObject)
{
// Update local cache for restoring later
ObjectToRelativeRotationMap.FindOrAdd(SceneComponent) = NewRotation;
}
SceneComponent->SetRelativeRotationExact(NewRotation);
// If it's a template, propagate the change out to any current instances of the object
if (bIsEditingTemplateObject)
{
TSet<USceneComponent*> UpdatedInstances;
FComponentEditorUtils::PropagateDefaultValueChange(SceneComponent, ValueProperty, FRotator::MakeFromEuler(OldComponentValue), NewRotation, UpdatedInstances);
}
break;
}
case ETransformField::Scale:
{
if (bPreserveScaleRatio)
{
// If we set a single axis, scale the others
float Ratio = 0.0f;
switch (Axis)
{
case EAxisList::X:
if (bIsSliderTransaction)
{
Ratio = SliderScaleRatio.X == 0.0f ? SliderScaleRatio.Y : (SliderScaleRatio.Y / SliderScaleRatio.X);
NewComponentValue.Y = NewComponentValue.X * Ratio;
Ratio = SliderScaleRatio.X == 0.0f ? SliderScaleRatio.Z : (SliderScaleRatio.Z / SliderScaleRatio.X);
NewComponentValue.Z = NewComponentValue.X * Ratio;
}
else
{
Ratio = OldComponentValue.X == 0.0f ? NewComponentValue.Z : NewComponentValue.X / OldComponentValue.X;
NewComponentValue.Y *= Ratio;
NewComponentValue.Z *= Ratio;
}
break;
case EAxisList::Y:
if (bIsSliderTransaction)
{
Ratio = SliderScaleRatio.Y == 0.0f ? SliderScaleRatio.X : (SliderScaleRatio.X / SliderScaleRatio.Y);
NewComponentValue.X = NewComponentValue.Y * Ratio;
Ratio = SliderScaleRatio.Y == 0.0f ? SliderScaleRatio.Z : (SliderScaleRatio.Z / SliderScaleRatio.Y);
NewComponentValue.Z = NewComponentValue.Y * Ratio;
}
else
{
Ratio = OldComponentValue.Y == 0.0f ? NewComponentValue.Z : NewComponentValue.Y / OldComponentValue.Y;
NewComponentValue.X *= Ratio;
NewComponentValue.Z *= Ratio;
}
break;
case EAxisList::Z:
if (bIsSliderTransaction)
{
Ratio = SliderScaleRatio.Z == 0.0f ? SliderScaleRatio.X : (SliderScaleRatio.X / SliderScaleRatio.Z);
NewComponentValue.X = NewComponentValue.Z * Ratio;
Ratio = SliderScaleRatio.Z == 0.0f ? SliderScaleRatio.Y : (SliderScaleRatio.Y / SliderScaleRatio.Z);
NewComponentValue.Y = NewComponentValue.Z * Ratio;
}
else
{
Ratio = OldComponentValue.Z == 0.0f ? NewComponentValue.Z : NewComponentValue.Z / OldComponentValue.Z;
NewComponentValue.X *= Ratio;
NewComponentValue.Y *= Ratio;
}
break;
default:
// Do nothing, this set multiple axis at once
break;
}
}
SceneComponent->SetRelativeScale3D(NewComponentValue);
// If it's a template, propagate the change out to any current instances of the object
if (bIsEditingTemplateObject)
{
TSet<USceneComponent*> UpdatedInstances;
FComponentEditorUtils::PropagateDefaultValueChange(SceneComponent, ValueProperty, OldComponentValue, NewComponentValue, UpdatedInstances);
}
break;
}
}
ModifiedObjects.Add(Object);
}
}
}
}
if (ModifiedObjects.Num())
{
for (UObject* Object : ModifiedObjects)
{
USceneComponent* SceneComponent = GetSceneComponentFromDetailsObject(Object);
USceneComponent* OldSceneComponent = SceneComponent;
if (SceneComponent)
{
AActor* EditedActor = SceneComponent->GetOwner();
FString SceneComponentPath = SceneComponent->GetPathName(EditedActor);
// This can invalidate OldSceneComponent
OldSceneComponent->PostEditChangeChainProperty(PropertyChangedChainEvent);
if (!bCommitted)
{
const FProperty* ConstValueProperty = ValueProperty;
SnapshotTransactionBuffer(OldSceneComponent, MakeArrayView(&ConstValueProperty, 1));
}
SceneComponent = FindObject<USceneComponent>(EditedActor, *SceneComponentPath);
if (EditedActor && EditedActor->GetRootComponent() == SceneComponent)
{
EditedActor->PostEditChangeChainProperty(PropertyChangedChainEvent);
SceneComponent = FindObject<USceneComponent>(EditedActor, *SceneComponentPath);
if (!bCommitted && OldSceneComponent != SceneComponent)
{
const FProperty* ConstValueProperty = ValueProperty;
SnapshotTransactionBuffer(SceneComponent, MakeArrayView(&ConstValueProperty, 1));
}
}
if (!Object->IsTemplate())
{
if (TransformField == ETransformField::Rotation || TransformField == ETransformField::Location)
{
FRotator* FoundRotator = ObjectToRelativeRotationMap.Find(OldSceneComponent);
if (FoundRotator)
{
FQuat OldQuat = FoundRotator->GetDenormalized().Quaternion();
FQuat NewQuat = SceneComponent->GetRelativeRotation().GetDenormalized().Quaternion();
if (OldQuat.Equals(NewQuat))
{
// Need to restore the manually set rotation as it was modified by quat conversion
SceneComponent->SetRelativeRotation_Direct(*FoundRotator);
}
}
}
if (bCommitted)
{
// Broadcast when the actor is done moving
GEditor->BroadcastEndObjectMovement(*SceneComponent);
if (EditedActor && EditedActor->GetRootComponent() == SceneComponent)
{
GEditor->BroadcastEndObjectMovement(*EditedActor);
}
}
}
}
}
if (NotifyHook)
{
NotifyHook->NotifyPostChange(PropertyChangedEvent, ValueProperty);
}
}
if (bCommitted && bBeganTransaction)
{
GEditor->EndTransaction();
CacheTransform();
}
GUnrealEd->UpdatePivotLocationForSelection();
GUnrealEd->SetPivotMovedIndependently(false);
// Redraw
GUnrealEd->RedrawLevelEditingViewports();
}
void FComponentTransformDetails::OnSetTransformAxis(FVector::FReal NewValue, ETextCommit::Type CommitInfo, ETransformField::Type TransformField, EAxisList::Type Axis, bool bCommitted)
{
FVector NewVector = GetAxisFilteredVector(Axis, FVector(NewValue), FVector::ZeroVector);
OnSetTransform(TransformField, Axis, NewVector, false, bCommitted);
}
void FComponentTransformDetails::BeginSliderTransaction(FText ActorTransaction, FText ComponentTransaction) const
{
bool bBeganTransaction = false;
for (TWeakObjectPtr<UObject> ObjectPtr : SelectedObjects)
{
if (ObjectPtr.IsValid())
{
UObject* Object = ObjectPtr.Get();
// Start a new transaction when a slider begins to change
// We'll end it when the slider is released
// NOTE: One transaction per change, not per actor
if (!bBeganTransaction)
{
if (Object->IsA<AActor>())
{
GEditor->BeginTransaction(ActorTransaction);
}
else
{
GEditor->BeginTransaction(ComponentTransaction);
}
bBeganTransaction = true;
}
USceneComponent* SceneComponent = GetSceneComponentFromDetailsObject(Object);
if (SceneComponent)
{
FScopedSwitchWorldForObject WorldSwitcher(Object);
if (SceneComponent->HasAnyFlags(RF_DefaultSubObject))
{
// Default subobjects must be included in any undo/redo operations
SceneComponent->SetFlags(RF_Transactional);
}
// Call modify but not PreEdit, we don't do the proper "Edit" until it's committed
SceneComponent->Modify();
}
}
}
// Just in case we couldn't start a new transaction for some reason
if (!bBeganTransaction)
{
GEditor->BeginTransaction(ActorTransaction);
}
}
void FComponentTransformDetails::OnBeginRotationSlider()
{
FText ActorTransaction = LOCTEXT("OnSetRotation", "Set Rotation");
FText ComponentTransaction = LOCTEXT("OnSetRotation_ComponentDirect", "Modify Component(s)");
BeginSliderTransaction(ActorTransaction, ComponentTransaction);
bEditingRotationInUI = true;
bIsSliderTransaction = true;
for (TWeakObjectPtr<UObject> ObjectPtr : SelectedObjects)
{
if (ObjectPtr.IsValid())
{
UObject* Object = ObjectPtr.Get();
USceneComponent* SceneComponent = GetSceneComponentFromDetailsObject(Object);
if (SceneComponent)
{
FScopedSwitchWorldForObject WorldSwitcher(Object);
// Add/update cached rotation value prior to slider interaction
ObjectToRelativeRotationMap.FindOrAdd(SceneComponent) = SceneComponent->GetRelativeRotation();
}
}
}
}
void FComponentTransformDetails::OnEndRotationSlider(FRotator::FReal NewValue)
{
// Commit gets called right before this, only need to end the transaction
bEditingRotationInUI = false;
bIsSliderTransaction = false;
GEditor->EndTransaction();
}
void FComponentTransformDetails::OnBeginLocationSlider()
{
bIsSliderTransaction = true;
FText ActorTransaction = LOCTEXT("OnSetLocation", "Set Location");
FText ComponentTransaction = LOCTEXT("OnSetLocation_ComponentDirect", "Modify Component Location");
BeginSliderTransaction(ActorTransaction, ComponentTransaction);
}
void FComponentTransformDetails::OnEndLocationSlider(FVector::FReal NewValue)
{
bIsSliderTransaction = false;
GEditor->EndTransaction();
}
void FComponentTransformDetails::OnBeginScaleSlider()
{
// Assumption: slider isn't usable if multiple objects are selected
SliderScaleRatio.X = CachedScale.X.GetValue();
SliderScaleRatio.Y = CachedScale.Y.GetValue();
SliderScaleRatio.Z = CachedScale.Z.GetValue();
bIsSliderTransaction = true;
FText ActorTransaction = LOCTEXT("OnSetScale", "Set Scale");
FText ComponentTransaction = LOCTEXT("OnSetScale_ComponentDirect", "Modify Component Scale");
BeginSliderTransaction(ActorTransaction, ComponentTransaction);
}
void FComponentTransformDetails::OnEndScaleSlider(FVector::FReal NewValue)
{
bIsSliderTransaction = false;
GEditor->EndTransaction();
}
void FComponentTransformDetails::OnObjectsReplaced(const TMap<UObject*, UObject*>& ReplacementMap)
{
TArray<UObject*> NewSceneComponents;
for (const TWeakObjectPtr<UObject> Obj : CachedHandlesObjects)
{
if (UObject* Replacement = ReplacementMap.FindRef(Obj.GetEvenIfUnreachable()))
{
NewSceneComponents.Add(Replacement);
}
}
if (NewSceneComponents.Num())
{
UpdatePropertyHandlesObjects(NewSceneComponents);
}
}
#undef LOCTEXT_NAMESPACE