Files
UnrealEngineUWP/Engine/Source/Editor/SceneOutliner/Private/ActorBrowsingMode.cpp
JeanFrancois Dube ebba4df748 Scene Outliner: refactored actor pin API to be batched, so we can pin actors all at once instead of doing the operation one by one.
#rb patrick.enfedaque, richard.malo
#preflight 628faefee17a02240dd5dfe3
#rnx

[CL 20391677 by JeanFrancois Dube in ue5-main branch]
2022-05-27 07:18:07 -04:00

2047 lines
74 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ActorBrowsingMode.h"
#include "SceneOutlinerFilters.h"
#include "SceneOutlinerModule.h"
#include "SceneOutlinerMenuContext.h"
#include "ActorHierarchy.h"
#include "SceneOutlinerDelegates.h"
#include "Editor.h"
#include "Editor/GroupActor.h"
#include "UnrealEdGlobals.h"
#include "ToolMenus.h"
#include "Engine/Selection.h"
#include "Editor/UnrealEdEngine.h"
#include "HAL/PlatformApplicationMisc.h"
#include "SceneOutlinerDragDrop.h"
#include "EditorActorFolders.h"
#include "EditorFolderUtils.h"
#include "ActorEditorUtils.h"
#include "DragAndDrop/ActorDragDropOp.h"
#include "DragAndDrop/ActorDragDropGraphEdOp.h"
#include "DragAndDrop/FolderDragDropOp.h"
#include "Logging/MessageLog.h"
#include "SSocketChooser.h"
#include "ActorFolderPickingMode.h"
#include "ActorTreeItem.h"
#include "LevelTreeItem.h"
#include "ActorFolderTreeItem.h"
#include "WorldTreeItem.h"
#include "ComponentTreeItem.h"
#include "ActorBrowsingModeSettings.h"
#include "ActorDescTreeItem.h"
#include "ScopedTransaction.h"
#include "LevelInstance/LevelInstanceInterface.h"
#include "LevelInstance/LevelInstanceSubsystem.h"
#include "LevelInstance/LevelInstanceEditorInstanceActor.h"
#include "EditorLevelUtils.h"
DEFINE_LOG_CATEGORY_STATIC(LogActorBrowser, Log, All);
#define LOCTEXT_NAMESPACE "SceneOutliner_ActorBrowsingMode"
using FActorFilter = TSceneOutlinerPredicateFilter<FActorTreeItem>;
using FActorDescFilter = TSceneOutlinerPredicateFilter<FActorDescTreeItem>;
FActorBrowsingMode::FActorBrowsingMode(SSceneOutliner* InSceneOutliner, TWeakObjectPtr<UWorld> InSpecifiedWorldToDisplay)
: FActorModeInteractive(FActorModeParams(InSceneOutliner, InSpecifiedWorldToDisplay, /* bHideComponents */ true, /* bHideLevelInstanceHierarchy */ false, /* bHideUnloadedActors */ false))
, FilteredActorCount(0)
{
// Capture selection changes of bones from mesh selection in fracture tools
FSceneOutlinerDelegates::Get().OnComponentsUpdated.AddRaw(this, &FActorBrowsingMode::OnComponentsUpdated);
GEngine->OnLevelActorDeleted().AddRaw(this, &FActorBrowsingMode::OnLevelActorDeleted);
GEditor->OnSelectUnloadedActorsEvent().AddRaw(this, &FActorBrowsingMode::OnSelectUnloadedActors);
FEditorDelegates::OnEditCutActorsBegin.AddRaw(this, &FActorBrowsingMode::OnEditCutActorsBegin);
FEditorDelegates::OnEditCutActorsEnd.AddRaw(this, &FActorBrowsingMode::OnEditCutActorsEnd);
FEditorDelegates::OnEditCopyActorsBegin.AddRaw(this, &FActorBrowsingMode::OnEditCopyActorsBegin);
FEditorDelegates::OnEditCopyActorsEnd.AddRaw(this, &FActorBrowsingMode::OnEditCopyActorsEnd);
FEditorDelegates::OnEditPasteActorsBegin.AddRaw(this, &FActorBrowsingMode::OnEditPasteActorsBegin);
FEditorDelegates::OnEditPasteActorsEnd.AddRaw(this, &FActorBrowsingMode::OnEditPasteActorsEnd);
FEditorDelegates::OnDuplicateActorsBegin.AddRaw(this, &FActorBrowsingMode::OnDuplicateActorsBegin);
FEditorDelegates::OnDuplicateActorsEnd.AddRaw(this, &FActorBrowsingMode::OnDuplicateActorsEnd);
FEditorDelegates::OnDeleteActorsBegin.AddRaw(this, &FActorBrowsingMode::OnDeleteActorsBegin);
FEditorDelegates::OnDeleteActorsEnd.AddRaw(this, &FActorBrowsingMode::OnDeleteActorsEnd);
UActorBrowserConfig::Initialize();
UActorBrowserConfig::Get()->LoadEditorConfig();
// Get a MutableConfig here to force create a config for the current outliner if it doesn't exist
const FActorBrowsingModeConfig* SavedSettings = GetMutableConfig();
// Create a local struct to use the default values if this outliner doesn't want to save configs
FActorBrowsingModeConfig LocalSettings;
// If this outliner doesn't want to save config (OutlinerIdentifier is empty, use the defaults)
if (SavedSettings)
{
LocalSettings = *SavedSettings;
}
// Get the OutlinerModule to register FilterInfos with the FilterInfoMap
FSceneOutlinerFilterInfo ShowOnlySelectedActorsInfo(LOCTEXT("ToggleShowOnlySelected", "Only Selected"), LOCTEXT("ToggleShowOnlySelectedToolTip", "When enabled, only displays actors that are currently selected."), LocalSettings.bShowOnlySelectedActors, FCreateSceneOutlinerFilter::CreateStatic(&FActorBrowsingMode::CreateShowOnlySelectedActorsFilter));
ShowOnlySelectedActorsInfo.OnToggle().AddLambda([this](bool bIsActive)
{
FActorBrowsingModeConfig* Settings = GetMutableConfig();
if(Settings)
{
Settings->bShowOnlySelectedActors = bIsActive;
SaveConfig();
}
});
FilterInfoMap.Add(TEXT("ShowOnlySelectedActors"), ShowOnlySelectedActorsInfo);
FSceneOutlinerFilterInfo HideTemporaryActorsInfo(LOCTEXT("ToggleHideTemporaryActors", "Hide Temporary Actors"), LOCTEXT("ToggleHideTemporaryActorsToolTip", "When enabled, hides temporary/run-time Actors."), LocalSettings.bHideTemporaryActors, FCreateSceneOutlinerFilter::CreateStatic(&FActorBrowsingMode::CreateHideTemporaryActorsFilter));
HideTemporaryActorsInfo.OnToggle().AddLambda([this](bool bIsActive)
{
FActorBrowsingModeConfig* Settings = GetMutableConfig();
if(Settings)
{
Settings->bHideTemporaryActors = bIsActive;
SaveConfig();
}
});
FilterInfoMap.Add(TEXT("HideTemporaryActors"), HideTemporaryActorsInfo);
FSceneOutlinerFilterInfo OnlyCurrentLevelInfo(LOCTEXT("ToggleShowOnlyCurrentLevel", "Only in Current Level"), LOCTEXT("ToggleShowOnlyCurrentLevelToolTip", "When enabled, only shows Actors that are in the Current Level."), LocalSettings.bShowOnlyActorsInCurrentLevel, FCreateSceneOutlinerFilter::CreateStatic(&FActorBrowsingMode::CreateIsInCurrentLevelFilter));
OnlyCurrentLevelInfo.OnToggle().AddLambda([this](bool bIsActive)
{
FActorBrowsingModeConfig* Settings = GetMutableConfig();
if(Settings)
{
Settings->bShowOnlyActorsInCurrentLevel = bIsActive;
SaveConfig();
}
});
FilterInfoMap.Add(TEXT("ShowOnlyCurrentLevel"), OnlyCurrentLevelInfo);
bHideComponents = LocalSettings.bHideActorComponents;
FSceneOutlinerFilterInfo HideComponentsInfo(LOCTEXT("ToggleHideActorComponents", "Hide Actor Components"), LOCTEXT("ToggleHideActorComponentsToolTip", "When enabled, hides components belonging to actors."), LocalSettings.bHideActorComponents, FCreateSceneOutlinerFilter::CreateStatic(&FActorBrowsingMode::CreateHideComponentsFilter));
HideComponentsInfo.OnToggle().AddLambda([this](bool bIsActive)
{
FActorBrowsingModeConfig* Settings = GetMutableConfig();
if(Settings)
{
Settings->bHideActorComponents = bHideComponents = bIsActive;
SaveConfig();
}
if (auto ActorHierarchy = StaticCast<FActorHierarchy*>(Hierarchy.Get()))
{
ActorHierarchy->SetShowingComponents(!bIsActive);
}
});
FilterInfoMap.Add(TEXT("HideComponentsFilter"), HideComponentsInfo);
FSceneOutlinerFilterInfo HideLevelInstancesInfo(LOCTEXT("ToggleHideLevelInstanceContent", "Hide Level Instance Content"), LOCTEXT("ToggleHideLevelInstancesToolTip", "When enabled, hides all level instance content."), LocalSettings.bHideLevelInstanceHierarchy, FCreateSceneOutlinerFilter::CreateStatic(&FActorBrowsingMode::CreateHideLevelInstancesFilter));
HideLevelInstancesInfo.OnToggle().AddLambda([this](bool bIsActive)
{
FActorBrowsingModeConfig* Settings = GetMutableConfig();
if(Settings)
{
Settings->bHideLevelInstanceHierarchy = bHideLevelInstanceHierarchy = bIsActive;
SaveConfig();
}
if (auto ActorHierarchy = StaticCast<FActorHierarchy*>(Hierarchy.Get()))
{
ActorHierarchy->SetShowingLevelInstances(!bIsActive);
}
});
FilterInfoMap.Add(TEXT("HideLevelInstancesFilter"), HideLevelInstancesInfo);
FSceneOutlinerFilterInfo HideUnloadedActorsInfo(LOCTEXT("ToggleHideUnloadedActors", "Hide Unloaded Actors"), LOCTEXT("ToggleHideUnloadedActorsToolTip", "When enabled, hides all unloaded world partition actors."), LocalSettings.bHideUnloadedActors, FCreateSceneOutlinerFilter::CreateStatic(&FActorBrowsingMode::CreateHideUnloadedActorsFilter));
HideUnloadedActorsInfo.OnToggle().AddLambda([this] (bool bIsActive)
{
FActorBrowsingModeConfig* Settings = GetMutableConfig();
if(Settings)
{
Settings->bHideUnloadedActors = bHideUnloadedActors = bIsActive;
SaveConfig();
}
if (auto ActorHierarchy = StaticCast<FActorHierarchy*>(Hierarchy.Get()))
{
ActorHierarchy->SetShowingUnloadedActors(!bIsActive);
}
});
FilterInfoMap.Add(TEXT("HideUnloadedActorsFilter"), HideUnloadedActorsInfo);
// Add a filter which sets the interactive mode of LevelInstance items and their children
SceneOutliner->AddFilter(MakeShared<FActorFilter>(FActorTreeItem::FFilterPredicate::CreateStatic([](const AActor* Actor) {return true; }), FSceneOutlinerFilter::EDefaultBehaviour::Pass, FActorTreeItem::FFilterPredicate::CreateLambda([this](const AActor* Actor)
{
if (!bHideLevelInstanceHierarchy)
{
if (const ULevelInstanceSubsystem* LevelInstanceSubsystem = RepresentingWorld->GetSubsystem<ULevelInstanceSubsystem>())
{
// if actor has a valid parent and the parent is not being edited,
// then the actor should not be selectable.
if (const ILevelInstanceInterface* ParentLevelInstance = LevelInstanceSubsystem->GetParentLevelInstance(Actor))
{
if (!LevelInstanceSubsystem->IsEditingLevelInstance(ParentLevelInstance))
{
return false;
}
}
}
}
return true;
})));
bAlwaysFrameSelection = LocalSettings.bAlwaysFrameSelection;
Rebuild();
}
FActorBrowsingMode::~FActorBrowsingMode()
{
if (RepresentingWorld.IsValid())
{
if (UWorldPartition* const WorldPartition = RepresentingWorld->GetWorldPartition())
{
WorldPartition->OnActorDescRemovedEvent.RemoveAll(this);
}
}
FSceneOutlinerDelegates::Get().OnComponentsUpdated.RemoveAll(this);
GEngine->OnLevelActorDeleted().RemoveAll(this);
GEditor->OnSelectUnloadedActorsEvent().RemoveAll(this);
FEditorDelegates::OnEditCutActorsBegin.RemoveAll(this);
FEditorDelegates::OnEditCutActorsEnd.RemoveAll(this);
FEditorDelegates::OnEditCopyActorsBegin.RemoveAll(this);
FEditorDelegates::OnEditCopyActorsEnd.RemoveAll(this);
FEditorDelegates::OnEditPasteActorsBegin.RemoveAll(this);
FEditorDelegates::OnEditPasteActorsEnd.RemoveAll(this);
FEditorDelegates::OnDuplicateActorsBegin.RemoveAll(this);
FEditorDelegates::OnDuplicateActorsEnd.RemoveAll(this);
FEditorDelegates::OnDeleteActorsBegin.RemoveAll(this);
FEditorDelegates::OnDeleteActorsEnd.RemoveAll(this);
}
void FActorBrowsingMode::Rebuild()
{
// If we used to be representing a wp world, unbind delegates before rebuilding begins
if (RepresentingWorld.IsValid())
{
if (UWorldPartition* const WorldPartition = RepresentingWorld->GetWorldPartition())
{
WorldPartition->OnActorDescRemovedEvent.RemoveAll(this);
}
}
FActorMode::Rebuild();
FilteredActorCount = 0;
FilteredUnloadedActorCount = 0;
ApplicableUnloadedActors.Empty();
ApplicableActors.Empty();
bRepresentingWorldGameWorld = RepresentingWorld.IsValid() && RepresentingWorld->IsGameWorld();
bRepresentingWorldPartitionedWorld = RepresentingWorld.IsValid() && RepresentingWorld->IsPartitionedWorld();
if (bRepresentingWorldPartitionedWorld)
{
UWorldPartition* const WorldPartition = RepresentingWorld->GetWorldPartition();
WorldPartition->OnActorDescRemovedEvent.AddRaw(this, &FActorBrowsingMode::OnActorDescRemoved);
}
}
FText FActorBrowsingMode::GetStatusText() const
{
if (!RepresentingWorld.IsValid())
{
return FText();
}
// The number of actors in the outliner before applying the text filter
const int32 TotalActorCount = ApplicableActors.Num() + ApplicableUnloadedActors.Num();
const int32 SelectedActorCount = SceneOutliner->GetSelection().Num<FActorTreeItem, FActorDescTreeItem>();
if (!SceneOutliner->IsTextFilterActive())
{
if (SelectedActorCount == 0) //-V547
{
if (bRepresentingWorldPartitionedWorld)
{
return FText::Format(LOCTEXT("ShowingAllLoadedActorsFmt", "{0} actors ({1} loaded)"), FText::AsNumber(FilteredActorCount), FText::AsNumber(FilteredActorCount - FilteredUnloadedActorCount));
}
else
{
return FText::Format(LOCTEXT("ShowingAllActorsFmt", "{0} actors"), FText::AsNumber(FilteredActorCount));
}
}
else
{
return FText::Format(LOCTEXT("ShowingAllActorsSelectedFmt", "{0} actors ({1} selected)"), FText::AsNumber(FilteredActorCount), FText::AsNumber(SelectedActorCount));
}
}
else if (SceneOutliner->IsTextFilterActive() && FilteredActorCount == 0)
{
return FText::Format(LOCTEXT("ShowingNoActorsFmt", "No matching actors ({0} total)"), FText::AsNumber(TotalActorCount));
}
else if (SelectedActorCount != 0) //-V547
{
return FText::Format(LOCTEXT("ShowingOnlySomeActorsSelectedFmt", "Showing {0} of {1} actors ({2} selected)"), FText::AsNumber(FilteredActorCount), FText::AsNumber(TotalActorCount), FText::AsNumber(SelectedActorCount));
}
else
{
return FText::Format(LOCTEXT("ShowingOnlySomeActorsFmt", "Showing {0} of {1} actors"), FText::AsNumber(FilteredActorCount), FText::AsNumber(TotalActorCount));
}
}
FSlateColor FActorBrowsingMode::GetStatusTextColor() const
{
if (!SceneOutliner->IsTextFilterActive())
{
return FSlateColor::UseForeground();
}
else if (FilteredActorCount == 0)
{
return FAppStyle::Get().GetSlateColor("Colors.AccentRed");
}
else
{
return FAppStyle::Get().GetSlateColor("Colors.AccentGreen");
}
}
void FActorBrowsingMode::OnToggleAlwaysFrameSelection()
{
bAlwaysFrameSelection = !bAlwaysFrameSelection;
FActorBrowsingModeConfig* Settings = GetMutableConfig();
if(Settings)
{
Settings->bAlwaysFrameSelection = bAlwaysFrameSelection;
SaveConfig();
}
}
bool FActorBrowsingMode::ShouldAlwaysFrameSelection()
{
return bAlwaysFrameSelection;
}
void FActorBrowsingMode::CreateViewContent(FMenuBuilder& MenuBuilder)
{
MenuBuilder.BeginSection("OutlinerSelectionOptions", LOCTEXT("OptionsHeading", "Options"));
MenuBuilder.AddMenuEntry(
LOCTEXT("AlwaysFrameSelection", "Always Frame Selection"),
LOCTEXT("AlwaysFrameSelectionTooltip", "Select an item in the outliner when it is selected through the level editor viewport."),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateRaw(this, &FActorBrowsingMode::OnToggleAlwaysFrameSelection),
FCanExecuteAction(),
FIsActionChecked::CreateRaw(this, &FActorBrowsingMode::ShouldAlwaysFrameSelection)
),
NAME_None,
EUserInterfaceActionType::ToggleButton
);
MenuBuilder.EndSection();
MenuBuilder.BeginSection("AssetThumbnails", LOCTEXT("ShowWorldHeading", "World"));
{
MenuBuilder.AddSubMenu(
LOCTEXT("ChooseWorldSubMenu", "Choose World"),
LOCTEXT("ChooseWorldSubMenuToolTip", "Choose the world to display in the outliner."),
FNewMenuDelegate::CreateRaw(this, &FActorMode::BuildWorldPickerMenu)
);
}
MenuBuilder.EndSection();
}
TSharedRef<FSceneOutlinerFilter> FActorBrowsingMode::CreateShowOnlySelectedActorsFilter()
{
auto IsActorSelected = [](const AActor* InActor)
{
return InActor && InActor->IsSelected();
};
return MakeShareable(new FActorFilter(FActorTreeItem::FFilterPredicate::CreateStatic(IsActorSelected), FSceneOutlinerFilter::EDefaultBehaviour::Fail, FActorTreeItem::FFilterPredicate::CreateStatic(IsActorSelected)));
}
TSharedRef<FSceneOutlinerFilter> FActorBrowsingMode::CreateHideTemporaryActorsFilter()
{
return MakeShareable(new FActorFilter(FActorTreeItem::FFilterPredicate::CreateStatic([](const AActor* InActor)
{
return ((InActor->GetWorld() && InActor->GetWorld()->WorldType != EWorldType::PIE) || GEditor->ObjectsThatExistInEditorWorld.Get(InActor)) && !InActor->HasAnyFlags(EObjectFlags::RF_Transient);
}), FSceneOutlinerFilter::EDefaultBehaviour::Pass));
}
TSharedRef<FSceneOutlinerFilter> FActorBrowsingMode::CreateIsInCurrentLevelFilter()
{
return MakeShareable(new FActorFilter(FActorTreeItem::FFilterPredicate::CreateStatic([](const AActor* InActor)
{
if (InActor->GetWorld())
{
return InActor->GetLevel() == InActor->GetWorld()->GetCurrentLevel();
}
return false;
}), FSceneOutlinerFilter::EDefaultBehaviour::Pass));
}
TSharedRef<FSceneOutlinerFilter> FActorBrowsingMode::CreateHideComponentsFilter()
{
return MakeShared<TSceneOutlinerPredicateFilter<FComponentTreeItem>>(TSceneOutlinerPredicateFilter<FComponentTreeItem>(
FComponentTreeItem::FFilterPredicate::CreateStatic([](const UActorComponent*) { return false; }),
FSceneOutlinerFilter::EDefaultBehaviour::Pass));
}
TSharedRef<FSceneOutlinerFilter> FActorBrowsingMode::CreateHideLevelInstancesFilter()
{
return MakeShareable(new FActorFilter(FActorTreeItem::FFilterPredicate::CreateStatic([](const AActor* Actor)
{
// Check if actor belongs to a LevelInstance
if (const ULevelInstanceSubsystem* LevelInstanceSubsystem = Actor->GetWorld()->GetSubsystem<ULevelInstanceSubsystem>())
{
if (const ILevelInstanceInterface* ParentLevelInstance = LevelInstanceSubsystem->GetParentLevelInstance(Actor))
{
if (!LevelInstanceSubsystem->IsEditingLevelInstance(ParentLevelInstance))
{
return false;
}
}
}
// Or if the actor itself is a LevelInstance editor instance
return Cast<ALevelInstanceEditorInstanceActor>(Actor) == nullptr;
}), FSceneOutlinerFilter::EDefaultBehaviour::Pass));
}
TSharedRef<FSceneOutlinerFilter> FActorBrowsingMode::CreateHideUnloadedActorsFilter()
{
return MakeShareable(new FActorDescFilter(FActorDescTreeItem::FFilterPredicate::CreateStatic(
[](const FWorldPartitionActorDesc* ActorDesc) { return false; }), FSceneOutlinerFilter::EDefaultBehaviour::Pass));
}
static const FName DefaultContextBaseMenuName("SceneOutliner.DefaultContextMenuBase");
static const FName DefaultContextMenuName("SceneOutliner.DefaultContextMenu");
void FActorBrowsingMode::RegisterContextMenu()
{
UToolMenus* ToolMenus = UToolMenus::Get();
if (!ToolMenus->IsMenuRegistered(DefaultContextBaseMenuName))
{
UToolMenu* Menu = ToolMenus->RegisterMenu(DefaultContextBaseMenuName);
Menu->AddDynamicSection("DynamicHierarchySection", FNewToolMenuDelegate::CreateLambda([](UToolMenu* InMenu)
{
USceneOutlinerMenuContext* Context = InMenu->FindContext<USceneOutlinerMenuContext>();
if (!Context || !Context->SceneOutliner.IsValid())
{
return;
}
// NOTE: the name "Section" is used in many other places
FToolMenuSection& Section = InMenu->FindOrAddSection("Section");
Section.Label = LOCTEXT("HierarchySectionName", "Hierarchy");
SSceneOutliner* SceneOutliner = Context->SceneOutliner.Pin().Get();
if (Context->bShowParentTree)
{
if (Context->NumSelectedItems == 0)
{
Section.AddMenuEntry(
"CreateFolder",
LOCTEXT("CreateFolder", "Create Folder"),
FText(),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "SceneOutliner.NewFolderIcon"),
FUIAction(FExecuteAction::CreateSP(SceneOutliner, &SSceneOutliner::CreateFolder)));
}
else
{
if (Context->NumSelectedItems == 1)
{
SceneOutliner->GetTree().GetSelectedItems()[0]->GenerateContextMenu(InMenu, *SceneOutliner);
}
// If we've only got folders selected, show the selection and edit sub menus
if (Context->NumSelectedItems > 0 && Context->NumSelectedFolders == Context->NumSelectedItems)
{
Section.AddSubMenu(
"SelectSubMenu",
LOCTEXT("SelectSubmenu", "Select"),
LOCTEXT("SelectSubmenu_Tooltip", "Select the contents of the current selection"),
FNewToolMenuDelegate::CreateSP(SceneOutliner, &SSceneOutliner::FillSelectionSubMenu));
}
}
}
}));
Menu->AddDynamicSection("DynamicMainSection", FNewToolMenuDelegate::CreateLambda([](UToolMenu* InMenu)
{
// We always create a section here, even if there is no parent so that clients can still extend the menu
FToolMenuSection& Section = InMenu->AddSection("MainSection", LOCTEXT("OutlinerSectionName", "Outliner"));
if (USceneOutlinerMenuContext* Context = InMenu->FindContext<USceneOutlinerMenuContext>())
{
// Don't add any of these menu items if we're not showing the parent tree
// Can't move worlds or level blueprints
if (Context->bShowParentTree && Context->NumSelectedItems > 0 && Context->NumWorldsSelected == 0 && Context->SceneOutliner.IsValid())
{
Section.AddSubMenu(
"MoveActorsTo",
LOCTEXT("MoveActorsTo", "Move To"),
LOCTEXT("MoveActorsTo_Tooltip", "Move selection to another folder"),
FNewToolMenuDelegate::CreateSP(Context->SceneOutliner.Pin().Get(), &SSceneOutliner::FillFoldersSubMenu));
}
if (Context->bShowParentTree && Context->NumSelectedItems > 0 && Context->SceneOutliner.IsValid())
{
// Only add the menu option to wp levels
if (!Context->bRepresentingGameWorld && Context->bRepresentingPartitionedWorld)
{
// If selection contains some unpinned items, show the pin option
// If the selection contains folders, always show the pin option
if (Context->NumPinnedItems != Context->NumSelectedItems || Context->NumSelectedFolders > 0)
{
Section.AddMenuEntry(
"PinItems",
LOCTEXT("Pin", "Pin"),
FText(),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(Context->SceneOutliner.Pin().Get(), &SSceneOutliner::PinSelectedItems)));
}
// If the selection contains some pinned items, show the unpin option
// If the selection contains folders, always show the unpin option
if (Context->NumPinnedItems != 0 || Context->NumSelectedFolders > 0)
{
Section.AddMenuEntry(
"UnpinItems",
LOCTEXT("Unpin", "Unpin"),
FText(),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(Context->SceneOutliner.Pin().Get(), &SSceneOutliner::UnpinSelectedItems)));
}
}
}
}
}));
Menu->AddDynamicSection("DynamicActorEditorContext", FNewToolMenuDelegate::CreateLambda([this](UToolMenu* InMenu)
{
USceneOutlinerMenuContext* Context = InMenu->FindContext<USceneOutlinerMenuContext>();
if (Context && Context->SceneOutliner.IsValid() && Context->bShowParentTree)
{
FToolMenuSection& Section = InMenu->AddSection("ActorEditorContextSection", LOCTEXT("ActorEditorContextSectionName", "Actor Editor Context"));
SSceneOutliner* SceneOutliner = Context->SceneOutliner.Pin().Get();
if ((Context->NumSelectedItems == 1) && (Context->NumSelectedFolders == 1))
{
SceneOutliner->GetTree().GetSelectedItems()[0]->GenerateContextMenu(InMenu, *SceneOutliner);
Section.AddMenuEntry(
"MakeCurrentFolder",
LOCTEXT("MakeCurrentFolder", "Make Current Folder"),
FText(),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateLambda([SceneOutliner]()
{
const FSceneOutlinerItemSelection& Selection = SceneOutliner->GetSelection();
if (Selection.SelectedItems.Num() == 1)
{
FSceneOutlinerTreeItemPtr Item = Selection.SelectedItems[0].Pin();
if (FActorFolderTreeItem* FolderItem = Item->CastTo<FActorFolderTreeItem>())
{
if (FolderItem->World.IsValid())
{
const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "MakeCurrentActorFolder", "Make Current Actor Folder"));
FActorFolders::Get().SetActorEditorContextFolder(*FolderItem->World, FolderItem->GetFolder());
}
}
}
}),
FCanExecuteAction::CreateLambda([SceneOutliner]
{
const FSceneOutlinerItemSelection& Selection = SceneOutliner->GetSelection();
if (Selection.SelectedItems.Num() == 1)
{
FSceneOutlinerTreeItemPtr Item = Selection.SelectedItems[0].Pin();
if (FActorFolderTreeItem* FolderItem = Item->CastTo<FActorFolderTreeItem>())
{
return FolderItem->World.IsValid() && (FolderItem->World->GetCurrentLevel() == FolderItem->GetFolder().GetRootObjectAssociatedLevel());
}
}
return false;
})
)
);
}
Section.AddMenuEntry(
"ClearCurrentFolder",
LOCTEXT("ClearCurrentFolder", "Clear Current Folder"),
FText(),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateLambda([this, SceneOutliner]()
{
if (RepresentingWorld.IsValid())
{
const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "ClearCurrentActorFolder", "Clear Current Actor Folder"));
FActorFolders::Get().SetActorEditorContextFolder(*RepresentingWorld.Get(), FFolder::GetWorldRootFolder(RepresentingWorld.Get()));
}
}),
FCanExecuteAction::CreateLambda([this]
{
return RepresentingWorld.IsValid() && !FActorFolders::Get().GetActorEditorContextFolder(*RepresentingWorld.Get()).IsNone();
})
)
);
}
}));
}
if (!ToolMenus->IsMenuRegistered(DefaultContextMenuName))
{
ToolMenus->RegisterMenu(DefaultContextMenuName, DefaultContextBaseMenuName);
}
}
TSharedPtr<SWidget> FActorBrowsingMode::BuildContextMenu()
{
RegisterContextMenu();
const FSceneOutlinerItemSelection ItemSelection(SceneOutliner->GetSelection());
USceneOutlinerMenuContext* ContextObject = NewObject<USceneOutlinerMenuContext>();
ContextObject->SceneOutliner = StaticCastSharedRef<SSceneOutliner>(SceneOutliner->AsShared());
ContextObject->bShowParentTree = SceneOutliner->GetSharedData().bShowParentTree;
ContextObject->NumSelectedItems = ItemSelection.Num();
ContextObject->NumSelectedFolders = ItemSelection.Num<FFolderTreeItem>();
ContextObject->NumWorldsSelected = ItemSelection.Num<FWorldTreeItem>();
ContextObject->bRepresentingGameWorld = bRepresentingWorldGameWorld;
ContextObject->bRepresentingPartitionedWorld = bRepresentingWorldPartitionedWorld;
int32 NumPinnedItems = 0;
if (const UWorldPartition* const WorldPartition = RepresentingWorld->GetWorldPartition())
{
ItemSelection.ForEachItem<IActorBaseTreeItem>([WorldPartition, &NumPinnedItems](const IActorBaseTreeItem& ActorItem)
{
if (WorldPartition->IsActorPinned(ActorItem.GetGuid()))
{
++NumPinnedItems;
}
return true;
});
}
ContextObject->NumPinnedItems = NumPinnedItems;
FToolMenuContext Context(ContextObject);
FName MenuName = DefaultContextMenuName;
SceneOutliner->GetSharedData().ModifyContextMenu.ExecuteIfBound(MenuName, Context);
// Build up the menu for a selection
UToolMenus* ToolMenus = UToolMenus::Get();
UToolMenu* Menu = ToolMenus->GenerateMenu(MenuName, Context);
for (const FToolMenuSection& Section : Menu->Sections)
{
if (Section.Blocks.Num() > 0)
{
return ToolMenus->GenerateWidget(Menu);
}
}
return nullptr;
}
TSharedPtr<SWidget> FActorBrowsingMode::CreateContextMenu()
{
TArray<AActor*> SelectedActors;
GEditor->GetSelectedActors()->GetSelectedObjects<AActor>(SelectedActors);
// Make sure that no components are selected
if (GEditor->GetSelectedComponentCount() > 0)
{
// We want to be able to undo to regain the previous component selection
const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "ClickingOnActorsContextMenu", "Clicking on Actors (context menu)"));
USelection* ComponentSelection = GEditor->GetSelectedComponents();
ComponentSelection->Modify(false);
ComponentSelection->DeselectAll();
GUnrealEd->UpdatePivotLocationForSelection();
GEditor->RedrawLevelEditingViewports(false);
}
return BuildContextMenu();
}
void FActorBrowsingMode::OnItemAdded(FSceneOutlinerTreeItemPtr Item)
{
if (const FActorTreeItem* ActorItem = Item->CastTo<FActorTreeItem>())
{
if (!Item->Flags.bIsFilteredOut)
{
++FilteredActorCount;
// Synchronize selection
if (GEditor->GetSelectedActors()->IsSelected(ActorItem->Actor.Get()))
{
SceneOutliner->SetItemSelection(Item, true);
}
}
}
else if (FActorFolderTreeItem* FolderItem = Item->CastTo<FActorFolderTreeItem>())
{
if (FolderItem->World.IsValid())
{
FolderItem->Flags.bIsExpanded = FActorFolders::Get().IsFolderExpanded(*FolderItem->World, FolderItem->GetFolder());
}
}
else if (Item->IsA<FActorDescTreeItem>())
{
if (!Item->Flags.bIsFilteredOut)
{
++FilteredActorCount;
++FilteredUnloadedActorCount;
}
}
}
void FActorBrowsingMode::OnItemRemoved(FSceneOutlinerTreeItemPtr Item)
{
if (Item->IsA<FActorTreeItem>())
{
if (!Item->Flags.bIsFilteredOut)
{
--FilteredActorCount;
}
}
else if (Item->IsA<FActorDescTreeItem>())
{
if (!Item->Flags.bIsFilteredOut)
{
--FilteredActorCount;
--FilteredUnloadedActorCount;
}
}
}
void FActorBrowsingMode::OnComponentsUpdated()
{
SceneOutliner->FullRefresh();
}
void FActorBrowsingMode::OnLevelActorDeleted(AActor* Actor)
{
ApplicableActors.Remove(Actor);
}
void FActorBrowsingMode::OnSelectUnloadedActors(const TArray<FGuid>& ActorGuids)
{
TArray<FSceneOutlinerTreeItemPtr> ItemsToSelect;
ItemsToSelect.Reserve(ActorGuids.Num());
for (const FGuid& ActorGuid : ActorGuids)
{
if (FSceneOutlinerTreeItemPtr ItemPtr = SceneOutliner->GetTreeItem(ActorGuid))
{
ItemsToSelect.Add(ItemPtr);
}
}
if (ItemsToSelect.Num())
{
SceneOutliner->SetItemSelection(ItemsToSelect, true);
}
}
void FActorBrowsingMode::OnActorDescRemoved(FWorldPartitionActorDesc* InActorDesc)
{
ApplicableUnloadedActors.Remove(InActorDesc);
}
void FActorBrowsingMode::OnItemSelectionChanged(FSceneOutlinerTreeItemPtr TreeItem, ESelectInfo::Type SelectionType, const FSceneOutlinerItemSelection& Selection)
{
TArray<AActor*> SelectedActors = Selection.GetData<AActor*>(SceneOutliner::FActorSelector());
bool bChanged = false;
bool bAnyInPIE = false;
for (auto* Actor : SelectedActors)
{
if (!bAnyInPIE && Actor && Actor->GetOutermost()->HasAnyPackageFlags(PKG_PlayInEditor))
{
bAnyInPIE = true;
}
if (!GEditor->GetSelectedActors()->IsSelected(Actor))
{
bChanged = true;
break;
}
}
for (FSelectionIterator SelectionIt(*GEditor->GetSelectedActors()); SelectionIt && !bChanged; ++SelectionIt)
{
const AActor* Actor = CastChecked< AActor >(*SelectionIt);
if (!bAnyInPIE && Actor->GetOutermost()->HasAnyPackageFlags(PKG_PlayInEditor))
{
bAnyInPIE = true;
}
if (!SelectedActors.Contains(Actor))
{
// Actor has been deselected
bChanged = true;
// If actor was a group actor, remove its members from the ActorsToSelect list
const AGroupActor* DeselectedGroupActor = Cast<AGroupActor>(Actor);
if (DeselectedGroupActor)
{
TArray<AActor*> GroupActors;
DeselectedGroupActor->GetGroupActors(GroupActors);
for (auto* GroupActor : GroupActors)
{
SelectedActors.Remove(GroupActor);
}
}
}
}
// If there's a discrepancy, update the selected actors to reflect this list.
if (bChanged)
{
const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "ClickingOnActors", "Clicking on Actors"), !bAnyInPIE);
GEditor->GetSelectedActors()->Modify();
// We'll batch selection changes instead by using BeginBatchSelectOperation()
GEditor->GetSelectedActors()->BeginBatchSelectOperation();
// Clear the selection.
GEditor->SelectNone(false, true, true);
const bool bShouldSelect = true;
const bool bNotifyAfterSelect = false;
const bool bSelectEvenIfHidden = true; // @todo outliner: Is this actually OK?
for (auto* Actor : SelectedActors)
{
UE_LOG(LogActorBrowser, Verbose, TEXT("Clicking on Actor (world outliner): %s (%s)"), *Actor->GetClass()->GetName(), *Actor->GetActorLabel());
GEditor->SelectActor(Actor, bShouldSelect, bNotifyAfterSelect, bSelectEvenIfHidden);
}
// Commit selection changes
GEditor->GetSelectedActors()->EndBatchSelectOperation(/*bNotify*/false);
// Fire selection changed event
GEditor->NoteSelectionChange();
// Set this outliner as the most recently interacted with
SetAsMostRecentOutliner();
}
SceneOutliner->RefreshSelection();
}
void FActorBrowsingMode::OnItemDoubleClick(FSceneOutlinerTreeItemPtr Item)
{
if (const FActorTreeItem* ActorItem = Item->CastTo<FActorTreeItem>())
{
AActor* Actor = ActorItem->Actor.Get();
check(Actor);
ILevelInstanceInterface* LevelInstance = Cast<ILevelInstanceInterface>(Actor);
if (LevelInstance && FSlateApplication::Get().GetModifierKeys().IsAltDown())
{
if (LevelInstance->CanEnterEdit())
{
LevelInstance->EnterEdit();
}
else if (LevelInstance->CanExitEdit())
{
LevelInstance->ExitEdit();
}
}
else if (Item->CanInteract())
{
FSceneOutlinerItemSelection Selection(SceneOutliner->GetSelection());
if (Selection.Has<FActorTreeItem>())
{
const bool bActiveViewportOnly = false;
GEditor->MoveViewportCamerasToActor(Selection.GetData<AActor*>(SceneOutliner::FActorSelector()), bActiveViewportOnly);
}
}
else
{
const bool bActiveViewportOnly = false;
GEditor->MoveViewportCamerasToActor(*Actor, bActiveViewportOnly);
}
}
}
void FActorBrowsingMode::OnFilterTextCommited(FSceneOutlinerItemSelection& Selection, ETextCommit::Type CommitType)
{
// Start batching selection changes
GEditor->GetSelectedActors()->BeginBatchSelectOperation();
// Select actors (and only the actors) that match the filter text
const bool bNoteSelectionChange = false;
const bool bDeselectBSPSurfs = false;
const bool WarnAboutManyActors = true;
GEditor->SelectNone(bNoteSelectionChange, bDeselectBSPSurfs, WarnAboutManyActors);
for (AActor* Actor : Selection.GetData<AActor*>(SceneOutliner::FActorSelector()))
{
const bool bShouldSelect = true;
const bool bSelectEvenIfHidden = false;
GEditor->SelectActor(Actor, bShouldSelect, bNoteSelectionChange, bSelectEvenIfHidden);
}
// Commit selection changes
GEditor->GetSelectedActors()->EndBatchSelectOperation(/*bNotify*/false);
// Fire selection changed event
GEditor->NoteSelectionChange();
// Set keyboard focus to the SceneOutliner, so the user can perform keyboard commands that interact
// with selected actors (such as Delete, to delete selected actors.)
SceneOutliner->SetKeyboardFocus();
SetAsMostRecentOutliner();
}
void FActorBrowsingMode::OnItemPassesFilters(const ISceneOutlinerTreeItem& Item)
{
if (const FActorTreeItem* const ActorItem = Item.CastTo<FActorTreeItem>())
{
ApplicableActors.Add(ActorItem->Actor);
}
else if (const FActorDescTreeItem* const ActorDescItem = Item.CastTo<FActorDescTreeItem>(); ActorDescItem && ActorDescItem->IsValid())
{
ApplicableUnloadedActors.Add(ActorDescItem->ActorDescHandle.Get());
}
}
FReply FActorBrowsingMode::OnKeyDown(const FKeyEvent& InKeyEvent)
{
const FSceneOutlinerItemSelection& Selection = SceneOutliner->GetSelection();
// Rename key: Rename selected actors (not rebindable, because it doesn't make much sense to bind.)
if (InKeyEvent.GetKey() == EKeys::F2)
{
if (Selection.Num() == 1)
{
FSceneOutlinerTreeItemPtr ItemToRename = Selection.SelectedItems[0].Pin();
if (ItemToRename.IsValid() && CanRenameItem(*ItemToRename) && ItemToRename->CanInteract())
{
SceneOutliner->SetPendingRenameItem(ItemToRename);
SceneOutliner->ScrollItemIntoView(ItemToRename);
}
return FReply::Handled();
}
}
// F5 forces a full refresh
else if (InKeyEvent.GetKey() == EKeys::F5)
{
SceneOutliner->FullRefresh();
return FReply::Handled();
}
// Delete key: Delete selected actors (not rebindable, because it doesn't make much sense to bind.)
// Use Delete and Backspace instead of Platform_Delete because the LevelEditor default Edit Delete is bound to both
else if (InKeyEvent.GetKey() == EKeys::Delete || InKeyEvent.GetKey() == EKeys::BackSpace)
{
if (SceneOutliner->GetSharedData().CustomDelete.IsBound())
{
SceneOutliner->GetSharedData().CustomDelete.Execute(Selection.SelectedItems);
}
else
{
if (RepresentingWorld.IsValid())
{
GUnrealEd->Exec(RepresentingWorld.Get(), TEXT("DELETE"));
}
}
return FReply::Handled();
}
return FReply::Unhandled();
}
bool FActorBrowsingMode::CanDelete() const
{
const FSceneOutlinerItemSelection ItemSelection = SceneOutliner->GetSelection();
const uint32 NumberOfFolders = ItemSelection.Num<FFolderTreeItem>();
return (NumberOfFolders > 0 && NumberOfFolders == ItemSelection.Num());
}
bool FActorBrowsingMode::CanRename() const
{
const FSceneOutlinerItemSelection ItemSelection = SceneOutliner->GetSelection();
const uint32 NumberOfFolders = ItemSelection.Num<FFolderTreeItem>();
return (NumberOfFolders == 1 && NumberOfFolders == ItemSelection.Num());
}
bool FActorBrowsingMode::CanRenameItem(const ISceneOutlinerTreeItem& Item) const
{
// Can only rename actor and folder items when in actor browsing mode
return (Item.IsValid() && (Item.IsA<FActorTreeItem>() || Item.IsA<FFolderTreeItem>()));
}
bool FActorBrowsingMode::CanCut() const
{
const FSceneOutlinerItemSelection ItemSelection = SceneOutliner->GetSelection();
const uint32 NumberOfFolders = ItemSelection.Num<FFolderTreeItem>();
return (NumberOfFolders > 0 && NumberOfFolders == ItemSelection.Num());
}
bool FActorBrowsingMode::CanCopy() const
{
const FSceneOutlinerItemSelection ItemSelection = SceneOutliner->GetSelection();
const uint32 NumberOfFolders = ItemSelection.Num<FFolderTreeItem>();
return (NumberOfFolders > 0 && NumberOfFolders == ItemSelection.Num());
}
bool FActorBrowsingMode::CanPaste() const
{
return CanPasteFoldersOnlyFromClipboard();
}
bool FActorBrowsingMode::CanPasteFoldersOnlyFromClipboard() const
{
// Intentionally not checking if the level is locked/hidden here, as it's better feedback for the user if they attempt to paste
// and get the message explaining why it's failed, than just not having the option available to them.
FString PasteString;
FPlatformApplicationMisc::ClipboardPaste(PasteString);
return PasteString.StartsWith("BEGIN FOLDERLIST");
}
bool FActorBrowsingMode::GetFolderNamesFromPayload(const FSceneOutlinerDragDropPayload& InPayload, TArray<FName>& OutFolders, FFolder::FRootObject& OutCommonRootObject) const
{
return FFolder::GetFolderPathsAndCommonRootObject(InPayload.GetData<FFolder>(SceneOutliner::FFolderPathSelector()), OutFolders, OutCommonRootObject);
}
TSharedPtr<FDragDropOperation> FActorBrowsingMode::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 FActorBrowsingMode::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 auto& Actor : ActorOp->Actors)
{
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)
{
OutPayload.DraggedItems.Add(SceneOutliner->GetTreeItem(Actor.Get()));
}
return true;
}
return false;
}
FFolder FActorBrowsingMode::GetWorldDefaultRootFolder() const
{
return FFolder::GetWorldRootFolder(RepresentingWorld.Get());
}
FSceneOutlinerDragValidationInfo FActorBrowsingMode::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)
{
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);
}
}
}
// 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();
}
void FActorBrowsingMode::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 = [](FName SocketName, TWeakObjectPtr<AActor> Parent, const TArray<TWeakObjectPtr<AActor>> NewAttachments)
{
AActor* ParentActor = Parent.Get();
if (ParentActor)
{
// modify parent and child
const FScopedTransaction Transaction(LOCTEXT("UndoAction_PerformAttachment", "Attach actors"));
// Attach each child
bool bAttached = false;
for (auto& Child : NewAttachments)
{
AActor* ChildActor = Child.Get();
if (GEditor->CanParentActors(ParentActor, ChildActor))
{
GEditor->ParentActors(ParentActor, ChildActor, SocketName);
ChildActor->SetFolderPath_Recursively(ParentActor->GetFolderPath());
}
}
}
};
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(LogActorBrowser, 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());
}
}
}
}
}
FFolder FActorBrowsingMode::CreateNewFolder()
{
const FScopedTransaction Transaction(LOCTEXT("UndoAction_CreateFolder", "Create Folder"));
TArray<FFolder> SelectedFolders = SceneOutliner->GetSelection().GetData<FFolder>(SceneOutliner::FFolderPathSelector());
const FFolder NewFolderName = FActorFolders::Get().GetDefaultFolderForSelection(*RepresentingWorld, &SelectedFolders);
FActorFolders::Get().CreateFolderContainingSelection(*RepresentingWorld, NewFolderName);
return NewFolderName;
}
FFolder FActorBrowsingMode::GetFolder(const FFolder& ParentPath, const FName& LeafName)
{
// Return a unique folder under the provided parent path & root object and using the provided leaf name
return FActorFolders::Get().GetFolderName(*RepresentingWorld, ParentPath, LeafName);
}
bool FActorBrowsingMode::CreateFolder(const FFolder& NewPath)
{
return FActorFolders::Get().CreateFolder(*RepresentingWorld, NewPath);
}
bool FActorBrowsingMode::ReparentItemToFolder(const FFolder& FolderPath, const FSceneOutlinerTreeItemPtr& Item)
{
if (FActorTreeItem* ActorItem = Item->CastTo<FActorTreeItem>())
{
// Make sure actor has the same root object before updating path
if (ActorItem->Actor->GetFolderRootObject() == FolderPath.GetRootObject())
{
ActorItem->Actor->SetFolderPath_Recursively(FolderPath.GetPath());
return true;
}
}
return false;
}
namespace ActorBrowsingModeUtils
{
static void RecursiveFolderExpandChildren(SSceneOutliner* SceneOutliner, const FSceneOutlinerTreeItemPtr& Item)
{
if (Item.IsValid())
{
for (const TWeakPtr<ISceneOutlinerTreeItem>& Child : Item->GetChildren())
{
FSceneOutlinerTreeItemPtr ChildPtr = Child.Pin();
SceneOutliner->SetItemExpansion(ChildPtr, true);
RecursiveFolderExpandChildren(SceneOutliner, ChildPtr);
}
}
}
static void RecursiveActorSelect(SSceneOutliner* SceneOutliner, const FSceneOutlinerTreeItemPtr& Item, bool bSelectImmediateChildrenOnly)
{
if (Item.IsValid())
{
// If the current item is an actor, ensure to select it as well
if (FActorTreeItem* ActorItem = Item->CastTo<FActorTreeItem>())
{
if (AActor* Actor = ActorItem->Actor.Get())
{
GEditor->SelectActor(Actor, true, false);
}
}
// Select all children
for (const TWeakPtr<ISceneOutlinerTreeItem>& Child : Item->GetChildren())
{
FSceneOutlinerTreeItemPtr ChildPtr = Child.Pin();
if (ChildPtr.IsValid())
{
if (FActorTreeItem* ActorItem = ChildPtr->CastTo<FActorTreeItem>())
{
if (AActor* Actor = ActorItem->Actor.Get())
{
GEditor->SelectActor(Actor, true, false);
}
}
else if (FFolderTreeItem* FolderItem = ChildPtr->CastTo<FFolderTreeItem>())
{
SceneOutliner->SetItemSelection(FolderItem->AsShared(), true);
}
if (!bSelectImmediateChildrenOnly)
{
for (const TWeakPtr<ISceneOutlinerTreeItem>& Grandchild : ChildPtr->GetChildren())
{
RecursiveActorSelect(SceneOutliner, Grandchild.Pin(), bSelectImmediateChildrenOnly);
}
}
}
}
}
}
static void RecursiveAddItemsToActorGuidList(const TArray<FSceneOutlinerTreeItemPtr>& Items, TArray<FGuid>& List)
{
for (const FSceneOutlinerTreeItemPtr& Item : Items)
{
if (const FActorDescTreeItem* const ActorDescTreeItem = Item->CastTo<FActorDescTreeItem>())
{
List.Add(ActorDescTreeItem->GetGuid());
}
else if (const FActorTreeItem* const ActorTreeItem = Item->CastTo<FActorTreeItem>())
{
if (ActorTreeItem->Actor.IsValid())
{
List.Add(ActorTreeItem->Actor->GetActorGuid());
}
}
TArray<FSceneOutlinerTreeItemPtr> ChildrenItems;
for (const auto& Child : Item->GetChildren())
{
if (Child.IsValid())
{
ChildrenItems.Add(Child.Pin());
}
}
if (ChildrenItems.Num())
{
RecursiveAddItemsToActorGuidList(ChildrenItems, List);
}
}
};
}
void FActorBrowsingMode::SelectFoldersDescendants(const TArray<FFolderTreeItem*>& FolderItems, bool bSelectImmediateChildrenOnly)
{
// Expand everything before beginning selection
for (FFolderTreeItem* Folder : FolderItems)
{
FSceneOutlinerTreeItemPtr FolderPtr = Folder->AsShared();
SceneOutliner->SetItemExpansion(FolderPtr, true);
if (!bSelectImmediateChildrenOnly)
{
ActorBrowsingModeUtils::RecursiveFolderExpandChildren(SceneOutliner, FolderPtr);
}
}
// batch selection
GEditor->GetSelectedActors()->BeginBatchSelectOperation();
for (FFolderTreeItem* Folder : FolderItems)
{
ActorBrowsingModeUtils::RecursiveActorSelect(SceneOutliner, Folder->AsShared(), bSelectImmediateChildrenOnly);
}
GEditor->GetSelectedActors()->EndBatchSelectOperation(/*bNotify*/false);
GEditor->NoteSelectionChange();
}
void FActorBrowsingMode::PinItems(const TArray<FSceneOutlinerTreeItemPtr>& InItems)
{
UWorldPartition* const WorldPartition = RepresentingWorld->GetWorldPartition();
if (!WorldPartition)
{
return;
}
TArray<FGuid> ActorsToPin;
ActorBrowsingModeUtils::RecursiveAddItemsToActorGuidList(InItems, ActorsToPin);
if (ActorsToPin.Num())
{
GEditor->GetSelectedActors()->BeginBatchSelectOperation();
GEditor->SelectNone(/*bNoteSelectionChange=*/false, /*bDeselectBSPSurfs=*/true);
WorldPartition->PinActors(ActorsToPin);
AActor* LastPinnedActor = nullptr;
for (const FGuid& ActorGuid : ActorsToPin)
{
if (FWorldPartitionHandle ActorHandle(WorldPartition, ActorGuid); ActorHandle.IsValid())
{
if (AActor* PinnedActor = ActorHandle->GetActor())
{
GEditor->SelectActor(PinnedActor, /*bInSelected=*/true, /*bNotify=*/false);
LastPinnedActor = PinnedActor;
}
}
}
GEditor->GetSelectedActors()->EndBatchSelectOperation(/*bNotify=*/true);
if (LastPinnedActor)
{
SceneOutliner->OnItemAdded(LastPinnedActor, SceneOutliner::ENewItemAction::ScrollIntoView);
}
}
}
void FActorBrowsingMode::UnpinItems(const TArray<FSceneOutlinerTreeItemPtr>& InItems)
{
UWorldPartition* const WorldPartition = RepresentingWorld->GetWorldPartition();
if (!WorldPartition)
{
return;
}
TArray<FGuid> ActorsToUnpin;
ActorBrowsingModeUtils::RecursiveAddItemsToActorGuidList(InItems, ActorsToUnpin);
if (ActorsToUnpin.Num())
{
GEditor->GetSelectedActors()->BeginBatchSelectOperation();
AActor* LastPinnedActor = nullptr;
for (const FGuid& ActorGuid : ActorsToUnpin)
{
if (FWorldPartitionHandle ActorHandle(WorldPartition, ActorGuid); ActorHandle.IsValid())
{
if (AActor* PinnedActor = ActorHandle->GetActor())
{
GEditor->SelectActor(PinnedActor, /*bInSelected=*/false, /*bNotify=*/false);
}
}
}
WorldPartition->UnpinActors(ActorsToUnpin);
GEditor->GetSelectedActors()->EndBatchSelectOperation(/*bNotify=*/true);
}
}
void FActorBrowsingMode::PinSelectedItems()
{
const FSceneOutlinerItemSelection Selection = SceneOutliner->GetSelection();
if (Selection.Num())
{
TArray<FSceneOutlinerTreeItemPtr> ItemsToPin;
Selection.ForEachItem([this, &ItemsToPin](const FSceneOutlinerTreeItemPtr& TreeItem)
{
ItemsToPin.Add(TreeItem);
return true;
});
PinItems(ItemsToPin);
}
}
void FActorBrowsingMode::UnpinSelectedItems()
{
const FSceneOutlinerItemSelection Selection = SceneOutliner->GetSelection();
if(Selection.Num())
{
TArray<FSceneOutlinerTreeItemPtr> ItemsToUnpin;
Selection.ForEachItem([this, &ItemsToUnpin](const FSceneOutlinerTreeItemPtr& TreeItem)
{
ItemsToUnpin.Add(TreeItem);
return true;
});
UnpinItems(ItemsToUnpin);
}
}
FCreateSceneOutlinerMode FActorBrowsingMode::CreateFolderPickerMode(const FFolder::FRootObject& InRootObject) const
{
auto MoveSelectionTo = [this, InRootObject](const FSceneOutlinerTreeItemRef& NewParent)
{
if (FWorldTreeItem* WorldItem = NewParent->CastTo<FWorldTreeItem>())
{
SceneOutliner->MoveSelectionTo(GetWorldDefaultRootFolder());
}
else if (FFolderTreeItem* FolderItem = NewParent->CastTo<FFolderTreeItem>())
{
SceneOutliner->MoveSelectionTo(FolderItem->GetFolder());
}
else if (FActorTreeItem* ActorItem = NewParent->CastTo<FActorTreeItem>())
{
if (FFolder::IsRootObjectValid(InRootObject))
{
SceneOutliner->MoveSelectionTo(FFolder(InRootObject));
}
}
else if (FLevelTreeItem* LevelItem = NewParent->CastTo<FLevelTreeItem>())
{
if (FFolder::IsRootObjectValid(InRootObject))
{
SceneOutliner->MoveSelectionTo(FFolder(InRootObject));
}
}
};
return FCreateSceneOutlinerMode::CreateLambda([this, MoveSelectionTo, InRootObject](SSceneOutliner* Outliner)
{
return new FActorFolderPickingMode(Outliner, FOnSceneOutlinerItemPicked::CreateLambda(MoveSelectionTo), nullptr, InRootObject);
});
}
void FActorBrowsingMode::OnDuplicateSelected()
{
GUnrealEd->Exec(RepresentingWorld.Get(), TEXT("DUPLICATE"));
}
void FActorBrowsingMode::OnEditCutActorsBegin()
{
// Only a callback in actor browsing mode
SceneOutliner->CopyFoldersBegin();
SceneOutliner->DeleteFoldersBegin();
}
void FActorBrowsingMode::OnEditCutActorsEnd()
{
// Only a callback in actor browsing mode
SceneOutliner->CopyFoldersEnd();
SceneOutliner->DeleteFoldersEnd();
}
void FActorBrowsingMode::OnEditCopyActorsBegin()
{
// Only a callback in actor browsing mode
SceneOutliner->CopyFoldersBegin();
}
void FActorBrowsingMode::OnEditCopyActorsEnd()
{
// Only a callback in actor browsing mode
SceneOutliner->CopyFoldersEnd();
}
void FActorBrowsingMode::OnEditPasteActorsBegin()
{
// Only a callback in actor browsing mode
const TArray<FName> FolderPaths = SceneOutliner->GetClipboardPasteFolders();
SceneOutliner->PasteFoldersBegin(FolderPaths);
}
void FActorBrowsingMode::OnEditPasteActorsEnd()
{
// Only a callback in actor browsing mode
SceneOutliner->PasteFoldersEnd();
}
void FActorBrowsingMode::OnDuplicateActorsBegin()
{
// Only a callback in actor browsing mode
FFolder::FRootObject CommonRootObject;
TArray<FName> SelectedFolderPaths;
FFolder::GetFolderPathsAndCommonRootObject(SceneOutliner->GetSelection().GetData<FFolder>(SceneOutliner::FFolderPathSelector()), SelectedFolderPaths, CommonRootObject);
SceneOutliner->PasteFoldersBegin(SelectedFolderPaths);
}
void FActorBrowsingMode::OnDuplicateActorsEnd()
{
// Only a callback in actor browsing mode
SceneOutliner->PasteFoldersEnd();
}
void FActorBrowsingMode::OnDeleteActorsBegin()
{
SceneOutliner->DeleteFoldersBegin();
}
void FActorBrowsingMode::OnDeleteActorsEnd()
{
SceneOutliner->DeleteFoldersEnd();
}
struct FActorBrowsingModeConfig* FActorBrowsingMode::GetMutableConfig()
{
FName OutlinerIdentifier = SceneOutliner->GetOutlinerIdentifier();
if (OutlinerIdentifier.IsNone())
{
return nullptr;
}
return &UActorBrowserConfig::Get()->ActorBrowsers.FindOrAdd(OutlinerIdentifier);
}
const FActorBrowsingModeConfig* FActorBrowsingMode::GetConstConfig() const
{
FName OutlinerIdentifier = SceneOutliner->GetOutlinerIdentifier();
if (OutlinerIdentifier.IsNone())
{
return nullptr;
}
return UActorBrowserConfig::Get()->ActorBrowsers.Find(OutlinerIdentifier);
}
void FActorBrowsingMode::SaveConfig()
{
UActorBrowserConfig::Get()->SaveEditorConfig();
}
#undef LOCTEXT_NAMESPACE