Files
UnrealEngineUWP/Engine/Source/Editor/DetailCustomizations/Private/ComponentTransformDetails.cpp
JeanMichel Dignard e6d45383d5 Copying //UE4/Dev-Enterprise to //UE4/Dev-Main (Source: //UE4/Dev-Enterprise @ 4341740)
#lockdown Nick.Penwarden
#rb none

============================
  MAJOR FEATURES & CHANGES
============================

Change 4280523 by Patrick.Boutot

	Add option in AjaCustomTimeStep to wait until the frame to be ready. Previously, the frame was there but not yet processed so it was possible that it was not ready by the time we wanted to read it. It won't work with interlaced because the 2 fields are processed at the same time. In interlaced, will get a 30fps behaviour when we actually want a 60fps.
	Fix bug that didn't set and reset bIsOwned properly when it was first initialized as not owned.

Change 4280526 by Patrick.Boutot

	Add accessor to get the leaf media source or output.

Change 4280624 by Patrick.Boutot

	Add timecode acessor to media samples

Change 4280626 by Patrick.Boutot

	Rework the timing for AJA Media Player. Previously, we took the timing of the frame. That was a bad idea because if 2 incomings video frames were coming a the same time, you would only show one. Making the buffering system useless.
	That affects the Custom Time Step since it was waiting for the interrupt signal and in some behavior we would like the frame to be ready to be used by UE. Same the timecode in the MediaSample because we may not used it to stamps the frame.

Change 4283022 by Patrick.Boutot

	[EditorScriptingUtilitites] Check folder names invalid characters separatly from the object's name.
	#jira UE-59886,  UE-62333

Change 4283112 by Patrick.Boutot

	Remove MediaFrameworkUtilititesModule dependency to the Settings module at runtime.
	Rename TimemanagemenetEditor module names.

Change 4283426 by JeanLuc.Corenthin

	Fix crash with FBX file

	#jira UE-62501

Change 4284940 by Patrick.Boutot

	A widget that let you select a single permutation from a list. It groups the values into categories and removes duplicates inside that category.

Change 4285471 by Patrick.Boutot

	Remove MediaFrameworkUtilititesModule dependency to the Settings module at runtime.

Change 4286925 by Patrick.Boutot

	[AJA] Add support to read LTC from the reference In.
	Add more detail on video format and the device.
	MediaSource use the Permutations Selection widget to select his mode and device.
	Remove debugging option to trigger an AJA custom time step and timecode provider.
	Remove the UYVY pixel option from AJA. It's better do to the conversion on the AJA card that on the GPU.
	Change the tooltip and category for some AjaMediaSource properties.

Change 4287026 by Julien.StJean

	Modifed the file STimeCodeProviderTab.cpp to fix the position of a SComboButton that wasn't properly place.

Change 4287663 by Jon.Nabozny

	Add timecode messages into nDisplay, and sync those between Master and Slave

Change 4287884 by Jon.Nabozny

	Create a TimecodeProvider for SystemTime and introduce a notion for DefaultTimecodeProvider in Engine.

Change 4288050 by Jon.Nabozny

	Rework the TimeSynchronization implementation for usability and functionality.

Change 4288283 by Jon.Nabozny

	Fixed swapped MetaClass and DisplayName options on UEngine::DefaultTimecodeProviderClassName;

Change 4288352 by Jon.Nabozny

	Set TimecodeProviderClassName and DefaultTimecodeProviderClassName in BaseEngine.ini

Change 4288378 by Jon.Nabozny

	Fixup some issues in TimecodeSynchronizer where code was reset improperly due to multiple unshelves / resolves.

Change 4288394 by Jon.Nabozny

	Add TimeSync functionality into LiveLink. Also add test cases for this. This should allow us to easily synchronize multiple LiveLink sources together, as well as synchronize those to anything else using the sync system (Relies on CL-4235417)

Change 4288899 by Patrick.Boutot

	Fix initialization order of FMediaIOCorePlayerBase variables

Change 4289157 by Patrick.Boutot

	Allow the user to change the source of a capture without stopping the current capture.
	[AJA] AjaMediaCapture, add support for UpdateSceneViewport & UpdateRenderTarget
	@made by julien.stjean

Change 4291328 by Jon.Nabozny

	Report the Skeleton Guid with TimeSyncData and track sync state in LiveLinkTimeSynchronizationSource.
	This prevents a crash that can happen if a source is quickly cleared and reset before the next tick of Time Synchronization.

Change 4296294 by Jon.Nabozny

	Fixup errors when TimecodeProviderClassName is empty. It's valid to leave this empty.

Change 4297122 by Patrick.Boutot

	Media Profile with timecode provider & custom time step

Change 4301855 by Austin.Crismore

	Fix for movment scaling and virtual joystick controls. Movement scaling in for truck and dolly is locked to the world xy plane, and virtual joysticks use their own method for movement scaling now.

	#jira UE-61762, UE-62187

Change 4301856 by Austin.Crismore

	Virtual sequence level controller now listens to on object spawned, so that it can intercept the camera actor and disable attatching to HMD to prevent camera movement that isn't from the level sequence

	#jira UE-61766

Change 4301860 by Austin.Crismore

	Fix for touch scrubbing. Added default values back in. Added logic to only allow scrubbing when touch focus was off.

	#jira UE-61865

Change 4302294 by Jamie.Dale

	Added functions to get your the localized spoken and subtitle text from a dialogue wave

Change 4304393 by Jamie.Dale

	Added support for BlueprintAssignable properties in Python

Change 4305852 by Jamie.Dale

	Removed hard-dependency between EditorScriptingUtilities and PythonScriptPlugin

	Backed-out changelist 4259264 and query Python availability based on whether anything is available to handle the command

	#jira UE-62318

Change 4308550 by Jamie.Dale

	Fixed crash when passing a null world to Python actor iterators

Change 4311867 by Homam.Bahnassi

	Revit master material with exposed parameters matching the API when possible.

Change 4314428 by Francis.Hurteau

	Made the usage of the bBuildDeveloperTools switch independent of the bCompileAgainstEngine switch.
	Changed bBuildDeveloperTools TargetRule in UnrealBuildTool to a nullable to keep the old behavior in case where bBuildDeveloperTools wasn't explicitly set in TargetRules

Change 4315134 by Jamie.Dale

	Defer editable text focus selection until mouse-up to allow the user to make an initial selection

	#jira UE-58086

Change 4318615 by Johan.Duparc

	EditorFactories: consistent return values after asset import.

Change 4322459 by Jamie.Dale

	Made SequencerScripting an Editor plugin as it depends on PythonScriptPlugin which is an Editor plugin

	This was causing issues at runtime when SequencerScripting was enabled, as it failed to load PythonScriptPlugin (which hadn't been built).

Change 4323341 by Francis.Hurteau

	Implement proper message bus protocol version negociation with static nodes

Change 4323733 by Francis.Hurteau

	Fix VR Pausing Sequence Scrubbing just setting playback speed to 0.0

Change 4324319 by Jamie.Dale

	Exposed transactions to Blueprints

Change 4325847 by Alistair.White

	Copying //Tasks/UE4/Private-PixelStreaming@4325566 to Dev-Enterprise-Minimal (//UE4/Dev-Enterprise-Minimal)

	This adds the new experimental PixelStreaming plugin to allow streaming of an Unreal client's audio & video stream to a browser through the WebRTC protocol to support new uses for enterprise customers.

Change 4326282 by Simon.Tourangeau

	nDisplay native present handler

Change 4326581 by Jamie.Dale

	Replacing FDateTime with int64 Ticks value to workaround UE-63485

Change 4326599 by Homam.Bahnassi

	Moving texture coords outside UVEdit function to allow using different UV channels.

Change 4333250 by Francis.Hurteau

	Small TFuture changes:
	* cleans up TFuture::Then with usage of TUniqueFunction
	* added TFuture::Reset to invalidate it and remove continuation from a future shared state

Change 4333359 by Homam.Bahnassi

	Support scaling and rotating UVs around arbitrary pivot

Change 4333566 by Johan.Duparc

	Expose ProxyLOD functionalities to Scripting
	#jira UEENT-1788

Change 4333988 by Jamie.Dale

	Allow UHT to parse FText default parameter values

	INVTEXT, NSLOCTEXT, LOCTABLE, and FText::GetEmpty() are supported. LOCTEXT isn't as it relies on an external macro that is known to C++ but not to UHT (NSLOCTEXT can easily be used instead).

Change 4335020 by Francis.Hurteau

	Uncomment MessageBus::Send deprecation notice for 4.21
	Update MessageBus Send usage to new API

Change 4335195 by JeanMichel.Dignard

	Add a SetLodFromStaticMesh script utility function

	#jira UEENT-1789

Change 4335231 by Anousack.Kitisa

	Added functions to generate planar, cylindrical, box UV mapping.

	#jira UEENT-1598

Change 4335373 by Jamie.Dale

	Cleaned up some places creating empty literal texts

Change 4335458 by Jamie.Dale

	Allow UHT to parse FText() as an alias of FText::GetEmpty() when processing default values

Change 4335875 by Max.Chen

	Sequencer: Clear RF_Transient on pasted tracks/sections

	#jira UE-63537

Change 4336497 by Johan.Duparc

	ProxyLOD: Fix progress bar issue
	- removed duplicated code
	- removed duplicated LongTask object
	#jira UEENT-1788

Change 4336723 by Jamie.Dale

	Ensure that Python generated types create their CDO at the correct point

	#jira UE-62895

Change 4340594 by Ben.Marsh

	Fix manifest being invalidated when building two enterprise targets in a row. Fixes CIS error.

	#jira UE-63644

[CL 4342443 by JeanMichel Dignard in Main branch]
2018-09-04 16:35:02 -04:00

1367 lines
46 KiB
C++

// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.
#include "ComponentTransformDetails.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "Widgets/SBoxPanel.h"
#include "Textures/SlateIcon.h"
#include "EditorStyleSet.h"
#include "IDetailChildrenBuilder.h"
#include "DetailWidgetRow.h"
#include "UObject/UnrealType.h"
#include "Components/SceneComponent.h"
#include "GameFramework/Actor.h"
#include "Misc/ConfigCacheIni.h"
#include "SlateOptMacros.h"
#include "Framework/Application/SlateApplication.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Layout/SBox.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SComboButton.h"
#include "Widgets/Input/SCheckBox.h"
#include "Editor/UnrealEdEngine.h"
#include "Kismet2/ComponentEditorUtils.h"
#include "Editor.h"
#include "UnrealEdGlobals.h"
#include "DetailLayoutBuilder.h"
#include "Widgets/Input/SVectorInputBox.h"
#include "Widgets/Input/SRotatorInputBox.h"
#include "ScopedTransaction.h"
#include "IPropertyUtilities.h"
#include "Math/UnitConversion.h"
#include "Widgets/Input/NumericUnitTypeInterface.inl"
#include "Settings/EditorProjectSettings.h"
#include "HAL/PlatformApplicationMisc.h"
#define LOCTEXT_NAMESPACE "FComponentTransformDetails"
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 )
, HiddenFieldMask( 0 )
{
GConfig->GetBool(TEXT("SelectionDetails"), TEXT("PreserveScaleRatio"), bPreserveScaleRatio, GEditorPerProjectIni);
}
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();
return
SNew(SComboButton)
.ContentPadding( 0 )
.ButtonStyle( FEditorStyle::Get(), "NoBorder" )
.ForegroundColor( FSlateColor::UseForeground() )
.MenuContent()
[
MenuBuilder.MakeWidget()
]
.ButtonContent()
[
SNew( SBox )
.Padding( FMargin( 0.0f, 0.0f, 2.0f, 0.0f ) )
[
SNew(STextBlock)
.Text(this, &FComponentTransformDetails::GetTransformFieldText, TransformField)
.Font(IDetailLayoutBuilder::GetDetailFont())
]
];
}
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);
switch (TransformField)
{
case ETransformField::Location:
{
FVector Location;
if (Location.InitFromString(PastedText))
{
FScopedTransaction Transaction(LOCTEXT("PasteLocation", "Paste Location"));
OnSetTransform(ETransformField::Location, EAxisList::All, Location, false, true);
}
}
break;
case ETransformField::Rotation:
{
FRotator Rotation;
PastedText.ReplaceInline(TEXT("Pitch="), TEXT("P="));
PastedText.ReplaceInline(TEXT("Yaw="), TEXT("Y="));
PastedText.ReplaceInline(TEXT("Roll="), TEXT("R="));
if (Rotation.InitFromString(PastedText))
{
FScopedTransaction Transaction(LOCTEXT("PasteRotation", "Paste Rotation"));
OnSetTransform(ETransformField::Rotation, EAxisList::All, Rotation.Euler(), false, true);
}
}
break;
case ETransformField::Scale:
{
FVector Scale;
if (Scale.InitFromString(PastedText))
{
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(this, &FComponentTransformDetails::OnCopy, TransformField ),
FCanExecuteAction::CreateSP(this, &FComponentTransformDetails::OnCanCopy, TransformField )
);
}
FUIAction FComponentTransformDetails::CreatePasteAction( ETransformField::Type TransformField ) const
{
return
FUIAction( FExecuteAction::CreateSP(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;
// Location
if(!bHideLocationField)
{
TSharedPtr<INumericTypeInterface<float>> TypeInterface;
if( FUnitConversion::Settings().ShouldDisplayUnits() )
{
TypeInterface = SharedThis(this);
}
ChildrenBuilder.AddCustomRow( LOCTEXT("LocationFilter", "Location") )
.CopyAction( CreateCopyAction( ETransformField::Location ) )
.PasteAction( CreatePasteAction( ETransformField::Location ) )
.NameContent()
.HAlign(HAlign_Left)
.VAlign(VAlign_Center)
[
BuildTransformFieldLabel( ETransformField::Location )
]
.ValueContent()
.MinDesiredWidth(125.0f * 3.0f)
.MaxDesiredWidth(125.0f * 3.0f)
[
SNew( SHorizontalBox )
+SHorizontalBox::Slot()
.FillWidth(1)
.VAlign( VAlign_Center )
[
SNew( SVectorInputBox )
.X( this, &FComponentTransformDetails::GetLocationX )
.Y( this, &FComponentTransformDetails::GetLocationY )
.Z( this, &FComponentTransformDetails::GetLocationZ )
.bColorAxisLabels( true )
.AllowResponsiveLayout( true )
.IsEnabled( this, &FComponentTransformDetails::GetIsEnabled )
.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(false)
]
+SHorizontalBox::Slot()
.AutoWidth()
[
// Just take up space for alignment
SNew( SBox )
.WidthOverride( 18.0f )
]
+SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.AutoWidth()
[
SNew(SButton)
.OnClicked(this, &FComponentTransformDetails::OnLocationResetClicked)
.Visibility(this, &FComponentTransformDetails::GetLocationResetVisibility)
.ContentPadding(FMargin(5.f, 0.f))
.ToolTipText(LOCTEXT("ResetToDefaultToolTip", "Reset to Default"))
.ButtonStyle( FEditorStyle::Get(), "NoBorder" )
.Content()
[
SNew(SImage)
.Image( FEditorStyle::GetBrush("PropertyWindow.DiffersFromDefault") )
]
]
];
}
// Rotation
if(!bHideRotationField)
{
TSharedPtr<INumericTypeInterface<float>> TypeInterface;
if( FUnitConversion::Settings().ShouldDisplayUnits() )
{
TypeInterface = MakeShareable( new TNumericUnitTypeInterface<float>(EUnit::Degrees) );
}
ChildrenBuilder.AddCustomRow( LOCTEXT("RotationFilter", "Rotation") )
.CopyAction( CreateCopyAction(ETransformField::Rotation) )
.PasteAction( CreatePasteAction(ETransformField::Rotation) )
.NameContent()
.HAlign(HAlign_Left)
.VAlign(VAlign_Center)
[
BuildTransformFieldLabel(ETransformField::Rotation)
]
.ValueContent()
.MinDesiredWidth(125.0f * 3.0f)
.MaxDesiredWidth(125.0f * 3.0f)
[
SNew( SHorizontalBox )
+SHorizontalBox::Slot()
.FillWidth(1)
.VAlign( VAlign_Center )
[
SNew( SRotatorInputBox )
.AllowSpin( SelectedObjects.Num() == 1 )
.Roll( this, &FComponentTransformDetails::GetRotationX )
.Pitch( this, &FComponentTransformDetails::GetRotationY )
.Yaw( this, &FComponentTransformDetails::GetRotationZ )
.AllowResponsiveLayout( true )
.bColorAxisLabels( true )
.IsEnabled( this, &FComponentTransformDetails::GetIsEnabled )
.OnBeginSliderMovement( this, &FComponentTransformDetails::OnBeginRotatonSlider )
.OnEndSliderMovement( 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 )
]
+SHorizontalBox::Slot()
.AutoWidth()
[
// Just take up space for alignment
SNew( SBox )
.WidthOverride( 18.0f )
]
+SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.AutoWidth()
[
SNew(SButton)
.OnClicked(this, &FComponentTransformDetails::OnRotationResetClicked)
.Visibility(this, &FComponentTransformDetails::GetRotationResetVisibility)
.ContentPadding(FMargin(5.f, 0.f))
.ToolTipText(LOCTEXT("ResetToDefaultToolTip", "Reset to Default"))
.ButtonStyle( FEditorStyle::Get(), "NoBorder" )
.Content()
[
SNew(SImage)
.Image( FEditorStyle::GetBrush("PropertyWindow.DiffersFromDefault") )
]
]
];
}
// Scale
if(!bHideScaleField)
{
ChildrenBuilder.AddCustomRow( LOCTEXT("ScaleFilter", "Scale") )
.CopyAction( CreateCopyAction(ETransformField::Scale) )
.PasteAction( CreatePasteAction(ETransformField::Scale) )
.NameContent()
.HAlign(HAlign_Left)
.VAlign(VAlign_Center)
[
BuildTransformFieldLabel(ETransformField::Scale)
]
.ValueContent()
.MinDesiredWidth(125.0f * 3.0f)
.MaxDesiredWidth(125.0f * 3.0f)
[
SNew( SHorizontalBox )
+SHorizontalBox::Slot()
.VAlign( VAlign_Center )
.FillWidth(1.0f)
[
SNew( SVectorInputBox )
.X( this, &FComponentTransformDetails::GetScaleX )
.Y( this, &FComponentTransformDetails::GetScaleY )
.Z( this, &FComponentTransformDetails::GetScaleZ )
.bColorAxisLabels( true )
.AllowResponsiveLayout( true )
.IsEnabled( this, &FComponentTransformDetails::GetIsEnabled )
.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(false)
]
+SHorizontalBox::Slot()
.AutoWidth()
.MaxWidth( 18.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( FEditorStyle::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() )
]
]
+SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.AutoWidth()
[
SNew(SButton)
.OnClicked(this, &FComponentTransformDetails::OnScaleResetClicked)
.Visibility(this, &FComponentTransformDetails::GetScaleResetVisibility)
.ContentPadding(FMargin(5.f, 0.f))
.ToolTipText(LOCTEXT("ResetToDefaultToolTip", "Reset to Default"))
.ButtonStyle( FEditorStyle::Get(), "NoBorder" )
.Content()
[
SNew(SImage)
.Image( FEditorStyle::GetBrush("PropertyWindow.DiffersFromDefault") )
]
]
];
}
}
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);
}
bool FComponentTransformDetails::GetIsEnabled() const
{
return !GEditor->HasLockedActors() || SelectedActorInfo.NumSelected == 0;
}
const FSlateBrush* FComponentTransformDetails::GetPreserveScaleRatioImage() const
{
return bPreserveScaleRatio ? FEditorStyle::GetBrush( TEXT("GenericLock") ) : FEditorStyle::GetBrush( TEXT("GenericUnlock") ) ;
}
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)
{
UBoolProperty* AbsoluteProperty = nullptr;
FText TransactionText;
switch (TransformField)
{
case ETransformField::Location:
AbsoluteProperty = FindField<UBoolProperty>(USceneComponent::StaticClass(), GET_MEMBER_NAME_CHECKED(USceneComponent, bAbsoluteLocation));
TransactionText = LOCTEXT("ToggleAbsoluteLocation", "Toggle Absolute Location");
break;
case ETransformField::Rotation:
AbsoluteProperty = FindField<UBoolProperty>(USceneComponent::StaticClass(), GET_MEMBER_NAME_CHECKED(USceneComponent, bAbsoluteRotation));
TransactionText = LOCTEXT("ToggleAbsoluteRotation", "Toggle Absolute Rotation");
break;
case ETransformField::Scale:
AbsoluteProperty = FindField<UBoolProperty>(USceneComponent::StaticClass(), GET_MEMBER_NAME_CHECKED(USceneComponent, bAbsoluteScale));
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->bAbsoluteLocation : (TransformField == ETransformField::Rotation ? SceneComponent->bAbsoluteRotation : SceneComponent->bAbsoluteScale);
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);
}
switch (TransformField)
{
case ETransformField::Location:
SceneComponent->bAbsoluteLocation = bAbsoluteEnabled;
// Update RelativeLocation to maintain/stabilize position when switching between relative and world.
if (SceneComponent->GetAttachParent())
{
if (SceneComponent->bAbsoluteLocation)
{
SceneComponent->RelativeLocation = SceneComponent->GetComponentTransform().GetTranslation();
}
else
{
FTransform ParentToWorld = SceneComponent->GetAttachParent()->GetSocketTransform(SceneComponent->GetAttachSocketName());
FTransform RelativeTM = SceneComponent->GetComponentTransform().GetRelativeTransform(ParentToWorld);
SceneComponent->RelativeLocation = RelativeTM.GetTranslation();
}
}
break;
case ETransformField::Rotation:
SceneComponent->bAbsoluteRotation = bAbsoluteEnabled;
break;
case ETransformField::Scale:
SceneComponent->bAbsoluteScale = bAbsoluteEnabled;
break;
}
ModifiedObjects.Add(Object);
}
}
}
if (bBeganTransaction)
{
FPropertyChangedEvent PropertyChangedEvent(AbsoluteProperty, EPropertyChangeType::ValueSet, (const TArray<const UObject*>*)&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;
}
};
EVisibility FComponentTransformDetails::GetLocationResetVisibility() const
{
const USceneComponent* Archetype = FGetRootComponentArchetype::Get(SelectedObjects[0].Get());
const FVector Data = Archetype ? Archetype->RelativeLocation : 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 ? EVisibility::Hidden : EVisibility::Visible;
}
FReply 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->RelativeLocation : FVector::ZeroVector;
OnSetTransform(ETransformField::Location, EAxisList::All, Data, false, true);
return FReply::Handled();
}
EVisibility FComponentTransformDetails::GetRotationResetVisibility() const
{
const USceneComponent* Archetype = FGetRootComponentArchetype::Get(SelectedObjects[0].Get());
const FVector Data = Archetype ? Archetype->RelativeRotation.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 ? EVisibility::Hidden : EVisibility::Visible;
}
FReply 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->RelativeRotation.Euler() : FVector::ZeroVector;
OnSetTransform(ETransformField::Rotation, EAxisList::All, Data, false, true);
return FReply::Handled();
}
EVisibility FComponentTransformDetails::GetScaleResetVisibility() const
{
const USceneComponent* Archetype = FGetRootComponentArchetype::Get(SelectedObjects[0].Get());
const FVector Data = Archetype ? Archetype->RelativeScale3D : 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 ? EVisibility::Hidden : EVisibility::Visible;
}
FReply 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->RelativeScale3D : FVector(1.0f);
OnSetTransform(ETransformField::Scale, EAxisList::All, Data, false, true);
return FReply::Handled();
}
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::X, 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::X, 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->RelativeLocation;
FRotator* FoundRotator = ObjectToRelativeRotationMap.Find(SceneComponent);
Rot = (bEditingRotationInUI && !Object->IsTemplate() && FoundRotator) ? *FoundRotator : SceneComponent->RelativeRotation;
Scale = SceneComponent->RelativeScale3D;
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->bAbsoluteLocation;
bAbsoluteScale = SceneComponent->bAbsoluteScale;
bAbsoluteRotation = SceneComponent->bAbsoluteRotation;
}
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<float>();
CachedLocation.Y = Loc.Y == CurLoc.Y && CachedLocation.Y.IsSet() ? Loc.Y : TOptional<float>();
CachedLocation.Z = Loc.Z == CurLoc.Z && CachedLocation.Z.IsSet() ? Loc.Z : TOptional<float>();
CachedRotation.X = Rot.Roll == CurRot.Roll && CachedRotation.X.IsSet() ? Rot.Roll : TOptional<float>();
CachedRotation.Y = Rot.Pitch == CurRot.Pitch && CachedRotation.Y.IsSet() ? Rot.Pitch : TOptional<float>();
CachedRotation.Z = Rot.Yaw == CurRot.Yaw && CachedRotation.Z.IsSet() ? Rot.Yaw : TOptional<float>();
CachedScale.X = Scale.X == CurScale.X && CachedScale.X.IsSet() ? Scale.X : TOptional<float>();
CachedScale.Y = Scale.Y == CurScale.Y && CachedScale.Y.IsSet() ? Scale.Y : TOptional<float>();
CachedScale.Z = Scale.Z == CurScale.Z && CachedScale.Z.IsSet() ? Scale.Z : TOptional<float>();
// 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;
UProperty* ValueProperty = nullptr;
UProperty* AxisProperty = nullptr;
switch (TransformField)
{
case ETransformField::Location:
TransactionText = LOCTEXT("OnSetLocation", "Set Location");
ValueProperty = FindField<UProperty>(USceneComponent::StaticClass(), GET_MEMBER_NAME_CHECKED(USceneComponent, RelativeLocation));
// Only set axis property for single axis set
if (Axis == EAxisList::X)
{
AxisProperty = FindField<UFloatProperty>(TBaseStructure<FVector>::Get(), GET_MEMBER_NAME_CHECKED(FVector, X));
}
else if (Axis == EAxisList::Y)
{
AxisProperty = FindField<UFloatProperty>(TBaseStructure<FVector>::Get(), GET_MEMBER_NAME_CHECKED(FVector, Y));
}
else if (Axis == EAxisList::Z)
{
AxisProperty = FindField<UFloatProperty>(TBaseStructure<FVector>::Get(), GET_MEMBER_NAME_CHECKED(FVector, Z));
}
break;
case ETransformField::Rotation:
TransactionText = LOCTEXT("OnSetRotation", "Set Rotation");
ValueProperty = FindField<UProperty>(USceneComponent::StaticClass(), GET_MEMBER_NAME_CHECKED(USceneComponent, RelativeRotation));
// Only set axis property for single axis set
if (Axis == EAxisList::X)
{
AxisProperty = FindField<UFloatProperty>(TBaseStructure<FRotator>::Get(), GET_MEMBER_NAME_CHECKED(FRotator, Roll));
}
else if (Axis == EAxisList::Y)
{
AxisProperty = FindField<UFloatProperty>(TBaseStructure<FRotator>::Get(), GET_MEMBER_NAME_CHECKED(FRotator, Pitch));
}
else if (Axis == EAxisList::Z)
{
AxisProperty = FindField<UFloatProperty>(TBaseStructure<FRotator>::Get(), GET_MEMBER_NAME_CHECKED(FRotator, Yaw));
}
break;
case ETransformField::Scale:
TransactionText = LOCTEXT("OnSetScale", "Set Scale");
ValueProperty = FindField<UProperty>(USceneComponent::StaticClass(), GET_MEMBER_NAME_CHECKED(USceneComponent, RelativeScale3D));
// If keep scale is set, don't set axis property
if (!bPreserveScaleRatio && Axis == EAxisList::X)
{
AxisProperty = FindField<UFloatProperty>(TBaseStructure<FVector>::Get(), GET_MEMBER_NAME_CHECKED(FVector, X));
}
else if (!bPreserveScaleRatio && Axis == EAxisList::Y)
{
AxisProperty = FindField<UFloatProperty>(TBaseStructure<FVector>::Get(), GET_MEMBER_NAME_CHECKED(FVector, Y));
}
else if (!bPreserveScaleRatio && Axis == EAxisList::Z)
{
AxisProperty = FindField<UFloatProperty>(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, (const TArray<const UObject*>*)&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->RelativeLocation;
break;
case ETransformField::Rotation:
// Pull from the actual component or from the cache
OldComponentValue = SceneComponent->RelativeRotation.Euler();
if (bEditingRotationInUI && !bIsEditingTemplateObject && ObjectToRelativeRotationMap.Find(SceneComponent))
{
OldComponentValue = ObjectToRelativeRotationMap.Find(SceneComponent)->Euler();
}
break;
case ETransformField::Scale:
OldComponentValue = SceneComponent->RelativeScale3D;
break;
}
// Set the incoming value
if (bMirror)
{
NewComponentValue = -OldComponentValue;
}
else
{
NewComponentValue = GetAxisFilteredVector(Axis, NewValue, OldComponentValue);
}
// If we're committing during a rotation edit then we need to force it
if (OldComponentValue != NewComponentValue || (bCommitted && bEditingRotationInUI))
{
if (!bBeganTransaction && bCommitted)
{
// Begin a transaction the first time an actors rotation is about to change.
// 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
// We don't call PreEditChange for non commit changes because most classes implement the version that doesn't check the interaction type
((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->RelativeRotation;
}
SceneComponent->SetRelativeLocation(NewComponentValue);
// Also forcibly set it as the cache may have changed it slightly
SceneComponent->RelativeLocation = 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:
// Account for the previous scale being zero. Just set to the new value in that case?
Ratio = OldComponentValue.X == 0.0f ? NewComponentValue.X : NewComponentValue.X / OldComponentValue.X;
NewComponentValue.Y *= Ratio;
NewComponentValue.Z *= Ratio;
break;
case EAxisList::Y:
Ratio = OldComponentValue.Y == 0.0f ? NewComponentValue.Y : NewComponentValue.Y / OldComponentValue.Y;
NewComponentValue.X *= Ratio;
NewComponentValue.Z *= Ratio;
break;
case EAxisList::Z:
Ratio = OldComponentValue.Z == 0.0f ? NewComponentValue.Z : NewComponentValue.Z / OldComponentValue.Z;
NewComponentValue.X *= Ratio;
NewComponentValue.Y *= Ratio;
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);
if (bCommitted)
{
// This can invalidate OldSceneComponent
// We don't call PostEditChange for non commit changes because most classes implement the version that doesn't check the interaction type
OldSceneComponent->PostEditChangeChainProperty(PropertyChangedChainEvent);
}
else
{
SnapshotTransactionBuffer(OldSceneComponent);
}
SceneComponent = FindObject<USceneComponent>(EditedActor, *SceneComponentPath);
if (EditedActor && EditedActor->GetRootComponent() == SceneComponent)
{
if (bCommitted)
{
EditedActor->PostEditChangeChainProperty(PropertyChangedChainEvent);
SceneComponent = FindObject<USceneComponent>(EditedActor, *SceneComponentPath);
}
else
{
SnapshotTransactionBuffer(EditedActor);
}
}
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->RelativeRotation.GetDenormalized().Quaternion();
if (OldQuat.Equals(NewQuat))
{
// Need to restore the manually set rotation as it was modified by quat conversion
SceneComponent->RelativeRotation = *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(float 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::OnBeginRotatonSlider()
{
bEditingRotationInUI = true;
bool bBeganTransaction = false;
for( int32 ObjectIndex = 0; ObjectIndex < SelectedObjects.Num(); ++ObjectIndex )
{
TWeakObjectPtr<UObject> ObjectPtr = SelectedObjects[ObjectIndex];
if( ObjectPtr.IsValid() )
{
UObject* Object = ObjectPtr.Get();
// Start a new transation when a rotator slider begins to change
// We'll end it when the slider is release
// NOTE: One transaction per change, not per actor
if(!bBeganTransaction)
{
if(Object->IsA<AActor>())
{
GEditor->BeginTransaction( LOCTEXT( "OnSetRotation", "Set Rotation" ) );
}
else
{
GEditor->BeginTransaction( LOCTEXT( "OnSetRotation_ComponentDirect", "Modify Component(s)") );
}
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();
// Add/update cached rotation value prior to slider interaction
ObjectToRelativeRotationMap.FindOrAdd(SceneComponent) = SceneComponent->RelativeRotation;
}
}
}
// Just in case we couldn't start a new transaction for some reason
if(!bBeganTransaction)
{
GEditor->BeginTransaction( LOCTEXT( "OnSetRotation", "Set Rotation" ) );
}
}
void FComponentTransformDetails::OnEndRotationSlider(float NewValue)
{
// Commit gets called right before this, only need to end the transaction
bEditingRotationInUI = false;
GEditor->EndTransaction();
}
#undef LOCTEXT_NAMESPACE