Files
2025-05-18 13:04:45 +08:00

1183 lines
44 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ActorMode.h"
#include "ActorHierarchy.h"
#include "Engine/Selection.h"
#include "Editor/UnrealEdEngine.h"
#include "SceneOutlinerDelegates.h"
#include "ActorEditorUtils.h"
#include "LevelUtils.h"
#include "GameFramework/WorldSettings.h"
#include "DragAndDrop/ActorDragDropOp.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "ActorTreeItem.h"
#include "LevelTreeItem.h"
#include "FolderTreeItem.h"
#include "ComponentTreeItem.h"
#include "ActorDescTreeItem.h"
#include "EditorActorFolders.h"
#include "EditorFolderUtils.h"
#include "EditorLevelUtils.h"
#include "EditorModeManager.h"
#include "Engine/GameViewportClient.h"
#include "WorldTreeItem.h"
#include "LevelInstance/LevelInstanceInterface.h"
#include "LevelInstance/LevelInstanceSubsystem.h"
#include "LevelInstance/LevelInstanceEditorInstanceActor.h"
#include "LevelEditor.h"
#include "Logging/MessageLog.h"
#include "DragAndDrop/ActorDragDropGraphEdOp.h"
#include "DragAndDrop/FolderDragDropOp.h"
#include "Modules/ModuleManager.h"
#include "ScopedTransaction.h"
#include "SSocketChooser.h"
#include "UnrealClient.h"
DEFINE_LOG_CATEGORY_STATIC(LogActorMode, Log, All);
static int32 GSceneOutlinerAutoRepresentingWorldNetMode = NM_Client;
static FAutoConsoleVariableRef CVarAutoRepresentingWorldNetMode(
TEXT("SceneOutliner.AutoRepresentingWorldNetMode"),
GSceneOutlinerAutoRepresentingWorldNetMode,
TEXT("The preferred NetMode of the world shown in the scene outliner when the 'Auto' option is chosen: 0=Standalone, 1=DedicatedServer, 2=ListenServer, 3=Client"));
#define LOCTEXT_NAMESPACE "SceneOutliner_ActorMode"
using FActorFilter = TSceneOutlinerPredicateFilter<FActorTreeItem>;
using FFolderFilter = TSceneOutlinerPredicateFilter<FFolderTreeItem>;
using FComponentFilter = TSceneOutlinerPredicateFilter<FComponentTreeItem>;
namespace SceneOutliner
{
bool FWeakActorSelector::operator()(const TWeakPtr<ISceneOutlinerTreeItem>& Item, TWeakObjectPtr<AActor>& DataOut) const
{
if (TSharedPtr<ISceneOutlinerTreeItem> ItemPtr = Item.Pin())
{
if (FActorTreeItem* ActorItem = ItemPtr->CastTo<FActorTreeItem>())
{
if (ActorItem->IsValid())
{
DataOut = ActorItem->Actor;
return true;
}
}
}
return false;
}
bool FActorSelector::operator()(const TWeakPtr<ISceneOutlinerTreeItem>& Item, AActor*& ActorPtrOut) const
{
if (TSharedPtr<ISceneOutlinerTreeItem> ItemPtr = Item.Pin())
{
if (FActorTreeItem* ActorItem = ItemPtr->CastTo<FActorTreeItem>())
{
if (ActorItem->IsValid())
{
AActor* Actor = ActorItem->Actor.Get();
if (Actor)
{
ActorPtrOut = Actor;
return true;
}
}
}
// If a component is selected, we meant for the owning actor to be selected
else if (FComponentTreeItem* ComponentItem = ItemPtr->CastTo<FComponentTreeItem>())
{
if (ComponentItem->IsValid())
{
AActor* Actor = ComponentItem->Component->GetOwner();
if (Actor)
{
ActorPtrOut = Actor;
return true;
}
}
}
}
return false;
}
bool FActorHandleSelector::operator()(const TWeakPtr<ISceneOutlinerTreeItem>& Item, FWorldPartitionHandle& ActorHandleOut) const
{
if (TSharedPtr<ISceneOutlinerTreeItem> ItemPtr = Item.Pin())
{
if (FActorDescTreeItem* ActorDescItem = ItemPtr->CastTo<FActorDescTreeItem>())
{
ActorHandleOut = ActorDescItem->ActorDescHandle;
return true;
}
}
return false;
}
}
FActorMode::FActorMode(const FActorModeParams& Params)
: ISceneOutlinerMode(Params.SceneOutliner)
, SpecifiedWorldToDisplay(Params.SpecifiedWorldToDisplay)
, bHideComponents(Params.bHideComponents)
, bHideActorWithNoComponent(Params.bHideActorWithNoComponent)
, bHideLevelInstanceHierarchy(Params.bHideLevelInstanceHierarchy)
, bHideUnloadedActors(Params.bHideUnloadedActors)
, bHideEmptyFolders(Params.bHideEmptyFolders)
, bCanInteractWithSelectableActorsOnly(Params.bCanInteractWithSelectableActorsOnly)
, bShouldUpdateContentWhileInPIEFocused(Params.bShouldUpdateContentWhileInPIEFocused)
, bSearchComponentsByActorName(Params.bSearchComponentsByActorName)
{
SceneOutliner->AddFilter(MakeShared<FActorFilter>(FActorTreeItem::FFilterPredicate::CreateLambda([this](const AActor* Actor)
{
return IsActorDisplayable(Actor);
}), FSceneOutlinerFilter::EDefaultBehaviour::Pass));
// Don't show components if the owner actor is not displayable
SceneOutliner->AddFilter(MakeShared<FComponentFilter>(FComponentTreeItem::FFilterPredicate::CreateLambda([this](const UActorComponent* ActorComponent)
{
return ActorComponent && IsActorDisplayable(ActorComponent->GetOwner());
}), FSceneOutlinerFilter::EDefaultBehaviour::Pass));
auto FolderPassesFilter = [this](const FFolder& InFolder, bool bInCheckHideLevelInstanceFlag)
{
if (ILevelInstanceInterface* LevelInstance = Cast<ILevelInstanceInterface>(InFolder.GetRootObjectPtr()))
{
if (LevelInstance->IsEditing())
{
return true;
}
if (bInCheckHideLevelInstanceFlag)
{
return !bHideLevelInstanceHierarchy;
}
}
if (ULevel* Level = Cast<ULevel>(InFolder.GetRootObjectPtr()))
{
return true;
}
return false;
};
SceneOutliner->AddFilter(MakeShared<FFolderFilter>(FFolderTreeItem::FFilterPredicate::CreateLambda([FolderPassesFilter](const FFolder& InFolder)
{
return FolderPassesFilter(InFolder, /*bCheckHideLevelInstanceFlag*/true);
}), FSceneOutlinerFilter::EDefaultBehaviour::Pass));
SceneOutliner->AddInteractiveFilter(MakeShared<FFolderFilter>(FFolderTreeItem::FFilterPredicate::CreateLambda([FolderPassesFilter](const FFolder& InFolder)
{
return FolderPassesFilter(InFolder, /*bCheckHideLevelInstanceFlag*/false);
}), FSceneOutlinerFilter::EDefaultBehaviour::Pass));
}
FActorMode::~FActorMode()
{
}
TUniquePtr<ISceneOutlinerHierarchy> FActorMode::CreateHierarchy()
{
TUniquePtr<FActorHierarchy> ActorHierarchy = FActorHierarchy::Create(this, RepresentingWorld);
ActorHierarchy->SetShowingComponents(!bHideComponents);
ActorHierarchy->SetShowingOnlyActorWithValidComponents(!bHideComponents && bHideActorWithNoComponent);
ActorHierarchy->SetShowingLevelInstances(!bHideLevelInstanceHierarchy);
ActorHierarchy->SetShowingUnloadedActors(!bHideUnloadedActors);
ActorHierarchy->SetShowingEmptyFolders(!bHideEmptyFolders);
ActorHierarchy->SetSearchComponentsByActorName(bSearchComponentsByActorName);
return ActorHierarchy;
}
void FActorMode::Rebuild()
{
ChooseRepresentingWorld();
Hierarchy = CreateHierarchy();
}
void FActorMode::ChooseRepresentingWorld()
{
// Select a world to represent
RepresentingWorld = nullptr;
// If a specified world was provided, represent it
if (SpecifiedWorldToDisplay.IsValid())
{
RepresentingWorld = SpecifiedWorldToDisplay.Get();
}
// check if the user-chosen world is valid and in the editor contexts
if (!RepresentingWorld.IsValid() && UserChosenWorld.IsValid())
{
for (const FWorldContext& Context : GEngine->GetWorldContexts())
{
if (UserChosenWorld.Get() == Context.World())
{
RepresentingWorld = UserChosenWorld.Get();
break;
}
}
}
// If the user did not manually select a world, try to pick the most suitable world context
if (!RepresentingWorld.IsValid())
{
// Ideally we want a PIE world that is standalone or the first client, unless the preferred NetMode is overridden by CVar
int32 LowestPIEInstanceSeen = MAX_int32;
for (const FWorldContext& Context : GEngine->GetWorldContexts())
{
UWorld* World = Context.World();
if (World && Context.WorldType == EWorldType::PIE)
{
if (World->GetNetMode() == NM_Standalone)
{
RepresentingWorld = World;
break;
}
else if ((World->GetNetMode() == ENetMode(GSceneOutlinerAutoRepresentingWorldNetMode)) && (Context.PIEInstance < LowestPIEInstanceSeen))
{
RepresentingWorld = World;
LowestPIEInstanceSeen = Context.PIEInstance;
}
}
}
}
if (RepresentingWorld == nullptr)
{
// If there is still no world, we query the Level Editor, which prefers the PIE world over the Editor world
TWeakPtr<ILevelEditor> LevelEditor = FModuleManager::GetModuleChecked<FLevelEditorModule>(TEXT("LevelEditor")).GetLevelEditorInstance();
if (TSharedPtr<ILevelEditor> LevelEditorPin = LevelEditor.Pin())
{
RepresentingWorld = LevelEditorPin->GetEditorModeManager().GetWorld();
}
}
}
void FActorMode::BuildWorldPickerMenu(FMenuBuilder& MenuBuilder)
{
MenuBuilder.BeginSection("Worlds", LOCTEXT("WorldsHeading", "Worlds"));
{
MenuBuilder.AddMenuEntry(
LOCTEXT("AutoWorld", "Auto"),
LOCTEXT("AutoWorldToolTip", "Automatically pick the world to display based on context."),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateRaw(this, &FActorMode::OnSelectWorld, TWeakObjectPtr<UWorld>()),
FCanExecuteAction(),
FIsActionChecked::CreateRaw(this, &FActorMode::IsWorldChecked, TWeakObjectPtr<UWorld>())
),
NAME_None,
EUserInterfaceActionType::RadioButton
);
for (const FWorldContext& Context : GEngine->GetWorldContexts())
{
UWorld* World = Context.World();
if (World && (World->WorldType == EWorldType::PIE || Context.WorldType == EWorldType::Editor))
{
MenuBuilder.AddMenuEntry(
SceneOutliner::GetWorldDescription(World),
LOCTEXT("ChooseWorldToolTip", "Display actors for this world."),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateRaw(this, &FActorMode::OnSelectWorld, MakeWeakObjectPtr(World)),
FCanExecuteAction(),
FIsActionChecked::CreateRaw(this, &FActorMode::IsWorldChecked, MakeWeakObjectPtr(World))
),
NAME_None,
EUserInterfaceActionType::RadioButton
);
}
}
}
MenuBuilder.EndSection();
}
void FActorMode::OnSelectWorld(TWeakObjectPtr<UWorld> World)
{
UserChosenWorld = World;
SceneOutliner->FullRefresh();
}
bool FActorMode::IsWorldChecked(TWeakObjectPtr<UWorld> World) const
{
return (UserChosenWorld == World) || (World.IsExplicitlyNull() && !UserChosenWorld.IsValid());
}
void FActorMode::SynchronizeActorSelection()
{
USelection* SelectedActors = GEditor->GetSelectedActors();
// Deselect actors in the tree that are no longer selected in the world
const FSceneOutlinerItemSelection Selection(SceneOutliner->GetSelection());
auto DeselectActors = [this](FActorTreeItem& Item)
{
if (!Item.Actor.IsValid() || !Item.Actor.Get()->IsSelected())
{
SceneOutliner->SetItemSelection(Item.AsShared(), false);
}
};
Selection.ForEachItem<FActorTreeItem>(DeselectActors);
// Show actor selection but only if sub objects are not selected
if (!Selection.Has<FComponentTreeItem>())
{
// See if the tree view selector is pointing at a selected item
bool bSelectorInSelectionSet = false;
TArray<FSceneOutlinerTreeItemPtr> ActorItems;
for (FSelectionIterator SelectionIt(*SelectedActors); SelectionIt; ++SelectionIt)
{
AActor* Actor = CastChecked< AActor >(*SelectionIt);
if (FSceneOutlinerTreeItemPtr ActorItem = SceneOutliner->GetTreeItem(Actor))
{
if (!bSelectorInSelectionSet && SceneOutliner->HasSelectorFocus(ActorItem))
{
bSelectorInSelectionSet = true;
}
ActorItems.Add(ActorItem);
}
}
// If NOT bSelectorInSelectionSet then we want to just move the selector to the first selected item.
ESelectInfo::Type SelectInfo = bSelectorInSelectionSet ? ESelectInfo::Direct : ESelectInfo::OnMouseClick;
SceneOutliner->AddToSelection(ActorItems, SelectInfo);
}
FSceneOutlinerDelegates::Get().SelectionChanged.Broadcast();
}
bool FActorMode::IsActorDisplayable(const AActor* Actor) const
{
return FActorMode::IsActorDisplayable(SceneOutliner, Actor, !bHideLevelInstanceHierarchy);
}
FFolder::FRootObject FActorMode::GetRootObject() const
{
return FFolder::GetWorldRootFolder(RepresentingWorld.Get()).GetRootObject();
}
FFolder::FRootObject FActorMode::GetPasteTargetRootObject() const
{
if (UWorld* World = RepresentingWorld.Get())
{
return FFolder::GetOptionalFolderRootObject(World->GetCurrentLevel()).Get(FFolder::GetWorldRootFolder(World).GetRootObject());
}
return FFolder::GetInvalidRootObject();
}
bool FActorMode::IsActorDisplayable(const SSceneOutliner* SceneOutliner, const AActor* Actor, bool bShowLevelInstanceContent)
{
bool bIsActorDisplayable = Actor &&
!SceneOutliner->GetSharedData().bOnlyShowFolders && // Don't show actors if we're only showing folders
Actor->IsEditable() && // Only show actors that are allowed to be selected and drawn in editor
Actor->IsListedInSceneOutliner();
if(bIsActorDisplayable)
{
if (Actor->HasAnyFlags(RF_Transient))
{
// Level Instance transient actors are shown based on passed in bShowLevelInstanceContent flag
if (Actor->IsInLevelInstance())
{
bIsActorDisplayable = bShowLevelInstanceContent;
}
else
{
// Don't show transient actors in non-play worlds, except if bShowTransient is true
bIsActorDisplayable = SceneOutliner->GetSharedData().bShowTransient || (Actor->GetWorld() && Actor->GetWorld()->IsPlayInEditor());
}
}
}
return bIsActorDisplayable && // Previous results
!Actor->IsTemplate() && // Should never happen, but we never want CDOs displayed
!FActorEditorUtils::IsABuilderBrush(Actor) && // Don't show the builder brush
!Actor->IsA(AWorldSettings::StaticClass()) && // Don't show the WorldSettings actor, even though it is technically editable
IsValidChecked(Actor) && // We don't want to show actors that are about to go away
FLevelUtils::IsLevelVisible(Actor->GetLevel()); // Only show Actors whose level is visible
}
bool FActorMode::CanInteract(const ISceneOutlinerTreeItem& Item) const
{
if (bCanInteractWithSelectableActorsOnly)
{
AActor* FoundActor = nullptr;
if (const FActorTreeItem* ActorTreeItem = Item.CastTo<FActorTreeItem>())
{
FoundActor = ActorTreeItem->Actor.Get();
}
else if (const FComponentTreeItem* ComponentTreeItem = Item.CastTo<FComponentTreeItem>())
{
if (UActorComponent* Component = ComponentTreeItem->Component.Get())
{
FoundActor = Component->GetOwner();
}
}
if (FoundActor)
{
const bool bInSelected = true;
const bool bSelectEvenIfHidden = true; // @todo outliner: Is this actually OK?
if (!GEditor->CanSelectActor(FoundActor, bInSelected, bSelectEvenIfHidden))
{
return false;
}
}
}
return true;
}
bool FActorMode::CanPopulate() const
{
if (!bShouldUpdateContentWhileInPIEFocused)
{
if (UGameViewportClient* GameViewport = RepresentingWorld->GetGameViewport())
{
return !GameViewport->Viewport || !GameViewport->Viewport->HasFocus();
}
}
return true;
}
bool FActorMode::IsActorLevelDisplayable(ULevel* InLevel)
{
// Don't show level tree item for the persistent level
return (InLevel && !InLevel->IsPersistentLevel());
}
void FActorMode::OnFilterTextChanged(const FText& InFilterText)
{
// Scroll last item (if it passes the filter) into view - this means if we are multi-selecting, we show newest selection that passes the filter
if (const AActor* LastSelectedActor = GEditor->GetSelectedActors()->GetBottom<AActor>())
{
// This part is different than that of OnLevelSelectionChanged(nullptr) because IsItemVisible(TreeItem) & ScrollItemIntoView(TreeItem) are applied to
// the current visual state, not to the one after applying the filter. Thus, the scroll would go to the place where the object was located
// before applying the FilterText
// If the object is already in the list, but it does not passes the filter, then we do not want to re-add it, because it will be removed by the filter
const FSceneOutlinerTreeItemPtr TreeItem = SceneOutliner->GetTreeItem(LastSelectedActor);
if (!TreeItem.IsValid() || !SceneOutliner->PassesTextFilter(TreeItem))
{
return;
}
// If the object is not in the list, and it does not passes the filter, then we should not re-add it, because it would be removed by the filter again. Unfortunately,
// there is no code to check if a future element (i.e., one that is currently not in the TreeItemMap list) will pass the filter. Therefore, we kind of overkill it
// by re-adding that element (even though it will be removed). However, AddItemToTree(FSceneOutlinerTreeItemRef Item) and similar functions already check the element before
// adding it. So this solution is fine.
// This solution might affect the performance of the World Outliner when a key is pressed, but it will still work properly when the remove/del keys are pressed. Not
// updating the filter when !TreeItem.IsValid() would result in the focus not being updated when the remove/del keys are pressed.
// In any other case (i.e., if the object passes the current filter), re-add it
SceneOutliner->ScrollItemIntoView(TreeItem);
SetAsMostRecentOutliner();
}
}
void FActorMode::SetAsMostRecentOutliner() const
{
TWeakPtr<ILevelEditor> LevelEditor = FModuleManager::GetModuleChecked<FLevelEditorModule>(TEXT("LevelEditor")).GetLevelEditorInstance();
if(TSharedPtr<ILevelEditor> LevelEditorPin = LevelEditor.Pin())
{
LevelEditorPin->SetMostRecentlyUsedSceneOutliner(SceneOutliner->GetOutlinerIdentifier());
}
}
int32 FActorMode::GetTypeSortPriority(const ISceneOutlinerTreeItem& Item) const
{
if (Item.IsA<FWorldTreeItem>())
{
return EItemSortOrder::World;
}
else if (Item.IsA<FLevelTreeItem>())
{
return EItemSortOrder::Level;
}
else if (Item.IsA<FFolderTreeItem>())
{
return EItemSortOrder::Folder;
}
else if (Item.IsA<FActorTreeItem>() || Item.IsA<FComponentTreeItem>() || Item.IsA<FActorDescTreeItem>())
{
return EItemSortOrder::Actor;
}
// Warning: using actor mode with an unsupported item type!
check(false);
return -1;
}
FSceneOutlinerDragValidationInfo FActorMode::ValidateDrop(const ISceneOutlinerTreeItem& DropTarget, const FSceneOutlinerDragDropPayload& Payload) const
{
if (Payload.Has<FFolderTreeItem>())
{
FFolder::FRootObject TargetRootObject = DropTarget.GetRootObject();
FFolder::FRootObject CommonPayloadFoldersRootObject;
TArray<FName> PayloadFolders;
const bool bHasCommonRootObject = GetFolderNamesFromPayload(Payload, PayloadFolders, CommonPayloadFoldersRootObject);
if (!bHasCommonRootObject)
{
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric, LOCTEXT("CantMoveFoldersWithMultipleRoots", "Cannot move folders with multiple roots"));
}
else if (CommonPayloadFoldersRootObject != TargetRootObject)
{
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric, LOCTEXT("CantChangeFoldersRoot", "Cannot change folders root"));
}
}
if (const FActorTreeItem* ActorItem = DropTarget.CastTo<FActorTreeItem>())
{
const AActor* ActorTarget = ActorItem->Actor.Get();
if (!ActorTarget)
{
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric, FText());
}
const ILevelInstanceInterface* LevelInstanceTarget = Cast<ILevelInstanceInterface>(ActorTarget);
const ULevelInstanceSubsystem* LevelInstanceSubsystem = RepresentingWorld->GetSubsystem<ULevelInstanceSubsystem>();
if (LevelInstanceTarget)
{
check(LevelInstanceSubsystem);
if (!LevelInstanceTarget->IsEditing())
{
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric, LOCTEXT("Error_AttachToClosedLevelInstance", "Cannot attach to LevelInstance which is not being edited"));
}
}
else
{
if (Payload.Has<FFolderTreeItem>())
{
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric, LOCTEXT("FoldersOnActorError", "Cannot attach folders to actors"));
}
if (!Payload.Has<FActorTreeItem>())
{
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric, FText());
}
}
FText AttachErrorMsg;
bool bCanAttach = true;
bool bDraggedOntoAttachmentParent = true;
const auto& DragActors = Payload.GetData<TWeakObjectPtr<AActor>>(SceneOutliner::FWeakActorSelector());
for (const auto& DragActorPtr : DragActors)
{
AActor* DragActor = DragActorPtr.Get();
if (DragActor)
{
if (bCanAttach)
{
if (LevelInstanceSubsystem)
{
// Either all actors must be in a LevelInstance or none of them
if (const ILevelInstanceInterface* ParentLevelInstance = LevelInstanceSubsystem->GetParentLevelInstance(DragActor))
{
if (!ParentLevelInstance->IsEditing())
{
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric, LOCTEXT("Error_RemoveEditingLevelInstance", "Cannot detach from a LevelInstance which is not being edited"));
}
}
if (!LevelInstanceSubsystem->CanMoveActorToLevel(DragActor, &AttachErrorMsg))
{
bCanAttach = bDraggedOntoAttachmentParent = false;
break;
}
}
if (DragActor->IsChildActor())
{
AttachErrorMsg = FText::Format(LOCTEXT("Error_AttachChildActor", "Cannot move {0} as it is a child actor."), FText::FromString(DragActor->GetActorLabel()));
bCanAttach = bDraggedOntoAttachmentParent = false;
break;
}
if (!LevelInstanceTarget && !GEditor->CanParentActors(ActorTarget, DragActor, &AttachErrorMsg))
{
bCanAttach = false;
}
}
if (DragActor->GetSceneOutlinerParent() != ActorTarget)
{
bDraggedOntoAttachmentParent = false;
}
}
}
const FText ActorLabel = FText::FromString(ActorTarget->GetActorLabel());
if (bDraggedOntoAttachmentParent)
{
for (const auto& DragActorPtr : DragActors)
{
AActor* DragActor = DragActorPtr.Get();
if (!DragActor->EditorCanDetachFrom(DragActor->GetSceneOutlinerParent(), AttachErrorMsg))
{
// Cannot detach from parent into root
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric, AttachErrorMsg);
}
}
if (DragActors.Num() == 1)
{
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::CompatibleDetach, ActorLabel);
}
else
{
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::CompatibleMultipleDetach, ActorLabel);
}
}
else if (bCanAttach)
{
if (DragActors.Num() == 1)
{
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::CompatibleAttach, ActorLabel);
}
else
{
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::CompatibleMultipleAttach, ActorLabel);
}
}
else
{
if (DragActors.Num() == 1)
{
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric, AttachErrorMsg);
}
else
{
const FText ReasonText = FText::Format(LOCTEXT("DropOntoText", "{0}. {1}"), ActorLabel, AttachErrorMsg);
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleMultipleAttach, ReasonText);
}
}
}
else if (DropTarget.IsA<FFolderTreeItem>() || DropTarget.IsA<FWorldTreeItem>() || DropTarget.IsA<FLevelTreeItem>())
{
const FFolderTreeItem* FolderItem = DropTarget.CastTo<FFolderTreeItem>();
const FWorldTreeItem* WorldItem = DropTarget.CastTo<FWorldTreeItem>();
const FLevelTreeItem* LevelItem = DropTarget.CastTo<FLevelTreeItem>();
// WorldTreeItem and LevelTreeItem are treated as root folders (path = none), with the difference that LevelTreeItem has a RootObject.
const FFolder DestinationPath = FolderItem ? FolderItem->GetFolder() : (LevelItem ? FFolder(FFolder::GetOptionalFolderRootObject(LevelItem->Level.Get()).Get(FFolder::GetInvalidRootObject())) : GetWorldDefaultRootFolder());
const FFolder::FRootObject& DestinationRootObject = DestinationPath.GetRootObject();
ILevelInstanceInterface* LevelInstanceTarget = Cast<ILevelInstanceInterface>(DestinationPath.GetRootObjectPtr());
if (LevelInstanceTarget && !LevelInstanceTarget->IsEditing())
{
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric, LOCTEXT("Error_DragInNonEditingLevelInstance", "Cannot drag into a LevelInstance which is not being edited"));
}
if (Payload.Has<FFolderTreeItem>())
{
FFolder::FRootObject CommonFolderRootObject;
TArray<FName> DraggedFolders;
if (GetFolderNamesFromPayload(Payload, DraggedFolders, CommonFolderRootObject))
{
// Iterate over all the folders that have been dragged
for (const FName& DraggedFolder : DraggedFolders)
{
const FName Leaf = FEditorFolderUtils::GetLeafName(DraggedFolder);
const FName Parent = FEditorFolderUtils::GetParentPath(DraggedFolder);
if ((CommonFolderRootObject != DestinationRootObject) && FFolder::IsRootObjectValid(CommonFolderRootObject) && FFolder::IsRootObjectValid(DestinationRootObject))
{
FFormatNamedArguments Args;
Args.Add(TEXT("SourceName"), FText::FromName(Leaf));
FText Text = FText::Format(LOCTEXT("CantChangeFolderRoot", "Cannot change {SourceName} folder root"), Args);
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric, Text);
}
if (Parent == DestinationPath.GetPath())
{
FFormatNamedArguments Args;
Args.Add(TEXT("SourceName"), FText::FromName(Leaf));
FText Text;
if (DestinationPath.IsNone())
{
Text = FText::Format(LOCTEXT("FolderAlreadyAssignedRoot", "{SourceName} is already assigned to root"), Args);
}
else
{
Args.Add(TEXT("DestPath"), FText::FromName(DestinationPath.GetPath()));
Text = FText::Format(LOCTEXT("FolderAlreadyAssigned", "{SourceName} is already assigned to {DestPath}"), Args);
}
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric, Text);
}
const FString DragFolderPath = DraggedFolder.ToString();
const FString LeafName = Leaf.ToString();
const FString DstFolderPath = DestinationPath.IsNone() ? FString() : DestinationPath.ToString();
const FString NewPath = DstFolderPath / LeafName;
if (FActorFolders::Get().ContainsFolder(*RepresentingWorld, FFolder(DestinationRootObject, FName(*NewPath))))
{
// The folder already exists
FFormatNamedArguments Args;
Args.Add(TEXT("DragName"), FText::FromString(LeafName));
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric,
FText::Format(LOCTEXT("FolderAlreadyExistsRoot", "A folder called \"{DragName}\" already exists at this level"), Args));
}
else if (DragFolderPath == DstFolderPath || DstFolderPath.StartsWith(DragFolderPath + "/"))
{
// Cannot drag as a child of itself
FFormatNamedArguments Args;
Args.Add(TEXT("FolderPath"), FText::FromName(DraggedFolder));
return FSceneOutlinerDragValidationInfo(
ESceneOutlinerDropCompatibility::IncompatibleGeneric,
FText::Format(LOCTEXT("ChildOfItself", "Cannot move \"{FolderPath}\" to be a child of itself"), Args));
}
}
}
}
if (Payload.Has<FActorTreeItem>())
{
const ULevelInstanceSubsystem* LevelInstanceSubsystem = RepresentingWorld->GetSubsystem<ULevelInstanceSubsystem>();
// Iterate over all the actors that have been dragged
for (const TWeakObjectPtr<AActor>& WeakActor : Payload.GetData<TWeakObjectPtr<AActor>>(SceneOutliner::FWeakActorSelector()))
{
const AActor* Actor = WeakActor.Get();
bool bActorContainedInLevelInstance = false;
if (LevelInstanceSubsystem)
{
if (const ILevelInstanceInterface* ParentLevelInstance = LevelInstanceSubsystem->GetParentLevelInstance(Actor))
{
if (!ParentLevelInstance->IsEditing())
{
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric, LOCTEXT("Error_RemoveEditingLevelInstance", "Cannot detach from a LevelInstance which is not being edited"));
}
bActorContainedInLevelInstance = true;
}
if (const ILevelInstanceInterface* LevelInstance = Cast<ILevelInstanceInterface>(Actor))
{
FText Reason;
if (!LevelInstanceSubsystem->CanMoveActorToLevel(Actor, &Reason))
{
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric, Reason);
}
}
}
if (Actor->IsChildActor())
{
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric, FText::Format(LOCTEXT("Error_AttachChildActor", "Cannot move {0} as it is a child actor."), FText::FromString(Actor->GetActorLabel())));
}
else if ((Actor->GetFolderRootObject() != DestinationRootObject) && FFolder::IsRootObjectValid(Actor->GetFolderRootObject()) && FFolder::IsRootObjectValid(DestinationRootObject))
{
FFormatNamedArguments Args;
Args.Add(TEXT("SourceName"), FText::FromString(Actor->GetActorLabel()));
FText Text = FText::Format(LOCTEXT("CantChangeActorRoot", "Cannot change {SourceName} folder root"), Args);
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric, Text);
}
else if (Actor->GetFolder() == DestinationPath && !Actor->GetSceneOutlinerParent() && !bActorContainedInLevelInstance)
{
FFormatNamedArguments Args;
Args.Add(TEXT("SourceName"), FText::FromString(Actor->GetActorLabel()));
FText Text;
if (DestinationPath.IsNone())
{
Text = FText::Format(LOCTEXT("FolderAlreadyAssignedRoot", "{SourceName} is already assigned to root"), Args);
}
else
{
Args.Add(TEXT("DestPath"), FText::FromName(DestinationPath.GetPath()));
Text = FText::Format(LOCTEXT("FolderAlreadyAssigned", "{SourceName} is already assigned to {DestPath}"), Args);
}
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric, Text);
}
else if (Actor->GetSceneOutlinerParent())
{
FText DetachErrorMsg;
if (!Actor->EditorCanDetachFrom(Actor->GetSceneOutlinerParent(), DetachErrorMsg))
{
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric, DetachErrorMsg);
}
}
}
}
// Everything else is a valid operation
if (DestinationPath.IsNone())
{
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::CompatibleGeneric, LOCTEXT("MoveToRoot", "Move to root"));
}
else
{
FFormatNamedArguments Args;
Args.Add(TEXT("DestPath"), FText::FromName(DestinationPath.GetPath()));
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::CompatibleGeneric, FText::Format(LOCTEXT("MoveInto", "Move into \"{DestPath}\""), Args));
}
}
else if (DropTarget.IsA<FComponentTreeItem>())
{
// we don't allow drag and drop on components for now
return FSceneOutlinerDragValidationInfo(ESceneOutlinerDropCompatibility::IncompatibleGeneric, FText());
}
return FSceneOutlinerDragValidationInfo::Invalid();
}
TSharedPtr<FDragDropOperation> FActorMode::CreateDragDropOperation(const FPointerEvent& MouseEvent, const TArray<FSceneOutlinerTreeItemPtr>& InTreeItems) const
{
FSceneOutlinerDragDropPayload DraggedObjects(InTreeItems);
// If the drag contains only actors, we shortcut and create a simple FActorDragDropGraphEdOp rather than an FSceneOutlinerDragDrop composite op.
if (DraggedObjects.Has<FActorTreeItem>() && !DraggedObjects.Has<FFolderTreeItem>())
{
return FActorDragDropGraphEdOp::New(DraggedObjects.GetData<TWeakObjectPtr<AActor>>(SceneOutliner::FWeakActorSelector()));
}
TSharedPtr<FSceneOutlinerDragDropOp> OutlinerOp = MakeShareable(new FSceneOutlinerDragDropOp());
if (DraggedObjects.Has<FActorTreeItem>())
{
TSharedPtr<FActorDragDropOp> ActorOperation = MakeShareable(new FActorDragDropGraphEdOp);
ActorOperation->Init(DraggedObjects.GetData<TWeakObjectPtr<AActor>>(SceneOutliner::FWeakActorSelector()));
OutlinerOp->AddSubOp(ActorOperation);
}
if (DraggedObjects.Has<FFolderTreeItem>())
{
FFolder::FRootObject CommonRootObject;
TArray<FName> DraggedFolders;
if (GetFolderNamesFromPayload(DraggedObjects, DraggedFolders, CommonRootObject))
{
TSharedPtr<FFolderDragDropOp> FolderOperation = MakeShareable(new FFolderDragDropOp);
FolderOperation->Init(DraggedFolders, RepresentingWorld.Get(), CommonRootObject);
OutlinerOp->AddSubOp(FolderOperation);
}
}
OutlinerOp->Construct();
return OutlinerOp;
}
bool FActorMode::ParseDragDrop(FSceneOutlinerDragDropPayload& OutPayload, const FDragDropOperation& Operation) const
{
if (Operation.IsOfType<FSceneOutlinerDragDropOp>())
{
const auto& OutlinerOp = static_cast<const FSceneOutlinerDragDropOp&>(Operation);
if (const auto& FolderOp = OutlinerOp.GetSubOp<FFolderDragDropOp>())
{
for (const auto& Folder : FolderOp->Folders)
{
OutPayload.DraggedItems.Add(SceneOutliner->GetTreeItem(FFolder(FolderOp->RootObject, Folder)));
}
}
if (const auto& ActorOp = OutlinerOp.GetSubOp<FActorDragDropOp>())
{
for (const TWeakObjectPtr<AActor>& Actor : ActorOp->Actors)
{
if (!Actor.IsValid())
{
continue;
}
OutPayload.DraggedItems.Add(SceneOutliner->GetTreeItem(Actor.Get()));
}
}
return true;
}
else if (Operation.IsOfType<FActorDragDropOp>())
{
for (const TWeakObjectPtr<AActor>& Actor : static_cast<const FActorDragDropOp&>(Operation).Actors)
{
if (!Actor.IsValid())
{
continue;
}
OutPayload.DraggedItems.Add(SceneOutliner->GetTreeItem(Actor.Get()));
}
return true;
}
return false;
}
void FActorMode::OnDrop(ISceneOutlinerTreeItem& DropTarget, const FSceneOutlinerDragDropPayload& Payload, const FSceneOutlinerDragValidationInfo& ValidationInfo) const
{
if (const FActorTreeItem* ActorItem = DropTarget.CastTo<FActorTreeItem>())
{
AActor* DropActor = ActorItem->Actor.Get();
if (!DropActor)
{
return;
}
FMessageLog EditorErrors("EditorErrors");
EditorErrors.NewPage(LOCTEXT("ActorAttachmentsPageLabel", "Actor attachment"));
if (ValidationInfo.CompatibilityType == ESceneOutlinerDropCompatibility::CompatibleMultipleDetach || ValidationInfo.CompatibilityType == ESceneOutlinerDropCompatibility::CompatibleDetach)
{
const FScopedTransaction Transaction(LOCTEXT("UndoAction_DetachActors", "Detach actors"));
TArray<TWeakObjectPtr<AActor>> DraggedActors = Payload.GetData<TWeakObjectPtr<AActor>>(SceneOutliner::FWeakActorSelector());
for (const auto& WeakActor : DraggedActors)
{
if (auto* DragActor = WeakActor.Get())
{
// Detach from parent
USceneComponent* RootComp = DragActor->GetRootComponent();
if (RootComp && RootComp->GetAttachParent())
{
AActor* OldParent = RootComp->GetAttachParent()->GetOwner();
// Attachment is persisted on the child so modify both actors for Undo/Redo but do not mark the Parent package dirty
OldParent->Modify(/*bAlwaysMarkDirty=*/false);
RootComp->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
DragActor->SetFolderPath_Recursively(OldParent->GetFolderPath());
}
}
}
}
else if (ValidationInfo.CompatibilityType == ESceneOutlinerDropCompatibility::CompatibleMultipleAttach || ValidationInfo.CompatibilityType == ESceneOutlinerDropCompatibility::CompatibleAttach)
{
// Show socket chooser if we have sockets to select
if (ILevelInstanceInterface* TargetLevelInstance = Cast<ILevelInstanceInterface>(DropActor))
{
check(TargetLevelInstance->IsEditing());
const FScopedTransaction Transaction(LOCTEXT("UndoAction_MoveActorsToLevelInstance", "Move actors to LevelInstance"));
const FFolder DestinationPath = FFolder(FFolder::FRootObject(DropActor));
auto MoveToDestination = [&DestinationPath](FFolderTreeItem& Item)
{
Item.MoveTo(DestinationPath);
};
Payload.ForEachItem<FFolderTreeItem>(MoveToDestination);
// Since target root is directly the Level Instance, clear folder path
TArray<AActor*> DraggedActors = Payload.GetData<AActor*>(SceneOutliner::FActorSelector());
for (auto& Actor : DraggedActors)
{
Actor->SetFolderPath_Recursively(FName());
}
ULevelInstanceSubsystem* LevelInstanceSubsystem = RepresentingWorld->GetSubsystem<ULevelInstanceSubsystem>();
check(LevelInstanceSubsystem);
LevelInstanceSubsystem->MoveActorsTo(TargetLevelInstance, DraggedActors);
}
else
{
auto PerformAttachment = [this](FName SocketName, TWeakObjectPtr<AActor> Parent, const TArray<TWeakObjectPtr<AActor>> NewAttachments)
{
AActor* ParentActor = Parent.Get();
if (ParentActor)
{
TArray<TWeakObjectPtr<AActor>> AttachedActors;
// modify parent and child
const FScopedTransaction Transaction(LOCTEXT("UndoAction_PerformAttachment", "Attach actors"));
// Attach each child
for (auto& Child : NewAttachments)
{
AActor* ChildActor = Child.Get();
if (GEditor->CanParentActors(ParentActor, ChildActor))
{
GEditor->ParentActors(ParentActor, ChildActor, SocketName);
AttachedActors.Add(ChildActor);
}
}
OnActorsAttached(ParentActor, AttachedActors);
}
};
TArray<TWeakObjectPtr<AActor>> DraggedActors = Payload.GetData<TWeakObjectPtr<AActor>>(SceneOutliner::FWeakActorSelector());
//@TODO: Should create a menu for each component that contains sockets, or have some form of disambiguation within the menu (like a fully qualified path)
// Instead, we currently only display the sockets on the root component
USceneComponent* Component = DropActor->GetRootComponent();
if ((Component != NULL) && (Component->HasAnySockets()))
{
// Create the popup
FSlateApplication::Get().PushMenu(
SceneOutliner->AsShared(),
FWidgetPath(),
SNew(SSocketChooserPopup)
.SceneComponent(Component)
.OnSocketChosen_Lambda(PerformAttachment, DropActor, MoveTemp(DraggedActors)),
FSlateApplication::Get().GetCursorPos(),
FPopupTransitionEffect(FPopupTransitionEffect::TypeInPopup)
);
}
else
{
PerformAttachment(NAME_None, DropActor, MoveTemp(DraggedActors));
}
}
}
// Report errors
EditorErrors.Notify(NSLOCTEXT("ActorAttachmentError", "AttachmentsFailed", "Attachments Failed!"));
}
else if (DropTarget.IsA<FFolderTreeItem>() || DropTarget.IsA<FWorldTreeItem>() || DropTarget.IsA<FLevelTreeItem>())
{
const FFolderTreeItem* FolderItem = DropTarget.CastTo<FFolderTreeItem>();
const FWorldTreeItem* WorldItem = DropTarget.CastTo<FWorldTreeItem>();
const FLevelTreeItem* LevelItem = DropTarget.CastTo<FLevelTreeItem>();
// WorldTreeItem and LevelTreeItem are treated as root folders (path = none), with the difference that LevelTreeItem has a RootObject.
const FFolder DestinationPath = FolderItem ? FolderItem->GetFolder() : (LevelItem ? FFolder(FFolder::GetOptionalFolderRootObject(LevelItem->Level.Get()).Get(FFolder::GetInvalidRootObject())) : GetWorldDefaultRootFolder());
const FScopedTransaction Transaction(LOCTEXT("MoveOutlinerItems", "Move World Outliner Items"));
auto MoveToDestination = [&DestinationPath](FFolderTreeItem& Item)
{
Item.MoveTo(DestinationPath);
};
Payload.ForEachItem<FFolderTreeItem>(MoveToDestination);
// Set the folder path on all the dragged actors, and detach any that need to be moved
if (Payload.Has<FActorTreeItem>())
{
TSet<const AActor*> ParentActors;
TSet<const AActor*> ChildActors;
TArray<AActor*> MovingActorsToValidRootObject;
Payload.ForEachItem<FActorTreeItem>([&DestinationPath, &ParentActors, &ChildActors, &MovingActorsToValidRootObject](const FActorTreeItem& ActorItem)
{
AActor* Actor = ActorItem.Actor.Get();
if (Actor)
{
// First mark this object as a parent, then set its children's path
ParentActors.Add(Actor);
const FFolder SrcFolder = Actor->GetFolder();
// If the folder root object changes, 1st pass will put actors at root. 2nd pass will set the destination path.
FName NewPath = (SrcFolder.GetRootObject() == DestinationPath.GetRootObject()) ? DestinationPath.GetPath() : NAME_None;
Actor->SetFolderPath(NewPath);
FActorEditorUtils::TraverseActorTree_ParentFirst(Actor, [&](AActor* InActor) {
ChildActors.Add(InActor);
InActor->SetFolderPath(NewPath);
return true;
}, false);
if ((Actor->GetFolderRootObject() != DestinationPath.GetRootObject()) && SrcFolder.IsRootObjectPersistentLevel() && (DestinationPath.IsRootObjectValid() && !DestinationPath.IsRootObjectPersistentLevel()))
{
MovingActorsToValidRootObject.Add(Actor);
}
}
});
// Detach parent actors
for (const AActor* Parent : ParentActors)
{
auto* RootComp = Parent->GetRootComponent();
// We don't detach if it's a child of another that's been dragged
if (RootComp && RootComp->GetAttachParent() && !ChildActors.Contains(Parent))
{
if (AActor* OldParentActor = RootComp->GetAttachParent()->GetOwner())
{
// Attachment is persisted on the child so modify both actors for Undo/Redo but do not mark the Parent package dirty
OldParentActor->Modify(/*bAlwaysMarkDirty=*/false);
}
RootComp->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
}
}
auto MoveActorsToLevel = [](const TArray<AActor*>& InActorsToMove, ULevel* InDestLevel, const FName& InDestinationPath)
{
// We are moving actors to another level
const bool bWarnAboutReferences = true;
const bool bWarnAboutRenaming = true;
const bool bMoveAllOrFail = true;
TArray<AActor*> MovedActors;
if (!EditorLevelUtils::MoveActorsToLevel(InActorsToMove, InDestLevel, bWarnAboutReferences, bWarnAboutRenaming, bMoveAllOrFail, &MovedActors))
{
UE_LOG(LogActorMode, Warning, TEXT("Failed to move actors because not all actors could be moved"));
}
// Once moved, update actors folder path
for (AActor* Actor : MovedActors)
{
Actor->SetFolderPath_Recursively(InDestinationPath);
}
};
if (DestinationPath.IsRootObjectPersistentLevel())
{
const ULevelInstanceSubsystem* LevelInstanceSubsystem = RepresentingWorld->GetSubsystem<ULevelInstanceSubsystem>();
check(LevelInstanceSubsystem);
ULevel* DestinationLevel = RepresentingWorld->PersistentLevel;
check(DestinationLevel);
TArray<AActor*> LevelInstanceActorsToMove;
TArray<AActor*> ActorsToMoveToPersistentLevel;
Payload.ForEachItem<FActorTreeItem>([LevelInstanceSubsystem, &LevelInstanceActorsToMove, &ActorsToMoveToPersistentLevel](const FActorTreeItem& ActorItem)
{
AActor* Actor = ActorItem.Actor.Get();
if (const ILevelInstanceInterface* ParentLevelInstance = LevelInstanceSubsystem->GetParentLevelInstance(Actor))
{
check(ParentLevelInstance->IsEditing());
LevelInstanceActorsToMove.Add(Actor);
}
else
{
const FFolder ActorSrcFolder = Actor->GetFolder();
if (ActorSrcFolder.IsRootObjectValid() && !ActorSrcFolder.IsRootObjectPersistentLevel())
{
ActorsToMoveToPersistentLevel.Add(Actor);
}
}
});
// We are moving actors outside of an editing level instance to a folder (or root) into the persistent level.
if (LevelInstanceActorsToMove.Num() > 0)
{
TArray<AActor*> MovedActors;
LevelInstanceSubsystem->MoveActorsToLevel(LevelInstanceActorsToMove, DestinationLevel, &MovedActors);
// Once moved, update actors folder path
for (AActor* Actor : MovedActors)
{
Actor->SetFolderPath_Recursively(DestinationPath.GetPath());
}
}
if (ActorsToMoveToPersistentLevel.Num() > 0)
{
MoveActorsToLevel(ActorsToMoveToPersistentLevel, DestinationLevel, DestinationPath.GetPath());
}
}
else if (MovingActorsToValidRootObject.Num())
{
if (ILevelInstanceInterface* TargetLevelInstance = Cast<ILevelInstanceInterface>(DestinationPath.GetRootObjectPtr()))
{
// We are moving actors inside an editing level instance
check(TargetLevelInstance->IsEditing());
ULevelInstanceSubsystem* LevelInstanceSubsystem = RepresentingWorld->GetSubsystem<ULevelInstanceSubsystem>();
check(LevelInstanceSubsystem);
TArray<AActor*> MovedActors;
LevelInstanceSubsystem->MoveActorsTo(TargetLevelInstance, MovingActorsToValidRootObject, &MovedActors);
// Once moved, update actors folder path
for (AActor* Actor : MovedActors)
{
Actor->SetFolderPath_Recursively(DestinationPath.GetPath());
}
}
else if (ULevel* DestinationLevel = Cast<ULevel>(DestinationPath.GetRootObjectPtr()))
{
MoveActorsToLevel(MovingActorsToValidRootObject, DestinationLevel, DestinationPath.GetPath());
}
}
}
}
}
bool FActorMode::GetFolderNamesFromPayload(const FSceneOutlinerDragDropPayload& InPayload, TArray<FName>& OutFolders, FFolder::FRootObject& OutCommonRootObject) const
{
return FFolder::GetFolderPathsAndCommonRootObject(InPayload.GetData<FFolder>(SceneOutliner::FFolderPathSelector()), OutFolders, OutCommonRootObject);
}
FFolder FActorMode::GetWorldDefaultRootFolder() const
{
return FFolder::GetWorldRootFolder(RepresentingWorld.Get());
}
#undef LOCTEXT_NAMESPACE