Files
UnrealEngineUWP/Engine/Source/Editor/SceneOutliner/Private/ActorMode.cpp
Richard Malo 223c790fef ActorFolder Improvements :
- Optimized FFolder resolving to UActorFolder : now uses and maintains an acceleration table
- Favor creation of FFolder using ActorFolderGuid when available
- FFolder creation now always passes a Root Object to facilitate Root Object ptr resolving (even when it's the main world)
- Fixed Duplicate Hierarchy when using Actor Folders and target level is different
- Fixed copy/paste actor from Persistent to LevelInstance not loosing actor folder
- Fixed mark for delete of an Actor Folder that could generate duplicates
- Modified fix of duplicate Actor Folders in a level : instead of renaming duplicates, mark for delete all duplicates except one and redirect children to the one we keep

#jira UE-150566
#rb patrick.enfedaque, jeanfrancois.dube
#preflight 6284101f486700b561a555ff

[CL 20346124 by Richard Malo in ue5-main branch]
2022-05-24 06:58:06 -04:00

417 lines
14 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 "WorldTreeItem.h"
#include "LevelInstance/LevelInstanceInterface.h"
#include "LevelInstance/LevelInstanceSubsystem.h"
#include "LevelInstance/LevelInstanceEditorInstanceActor.h"
#include "LevelEditor.h"
#include "Modules/ModuleManager.h"
#define LOCTEXT_NAMESPACE "SceneOutliner_ActorMode"
using FActorFilter = TSceneOutlinerPredicateFilter<FActorTreeItem>;
using FFolderFilter = TSceneOutlinerPredicateFilter<FFolderTreeItem>;
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;
}
}
FActorMode::FActorMode(const FActorModeParams& Params)
: ISceneOutlinerMode(Params.SceneOutliner)
, SpecifiedWorldToDisplay(Params.SpecifiedWorldToDisplay)
, bHideComponents(Params.bHideComponents)
, bHideActorWithNoComponent(Params.bHideActorWithNoComponent)
, bHideLevelInstanceHierarchy(Params.bHideLevelInstanceHierarchy)
, bHideUnloadedActors(Params.bHideUnloadedActors)
{
SceneOutliner->AddFilter(MakeShared<FActorFilter>(FActorTreeItem::FFilterPredicate::CreateLambda([this](const AActor* Actor)
{
return IsActorDisplayable(Actor);
}), 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);
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
int32 LowestClientInstanceSeen = 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() == NM_Client) && (Context.PIEInstance < LowestClientInstanceSeen))
{
RepresentingWorld = World;
LowestClientInstanceSeen = Context.PIEInstance;
}
}
}
}
if (RepresentingWorld == nullptr)
{
// still no world so fallback to old logic where we just prefer PIE over Editor
for (const FWorldContext& Context : GEngine->GetWorldContexts())
{
if (Context.WorldType == EWorldType::PIE)
{
RepresentingWorld = Context.World();
break;
}
else if (Context.WorldType == EWorldType::Editor)
{
RepresentingWorld = Context.World();
}
}
}
}
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);
}
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)
{
static const FName SequencerActorTag(TEXT("SequencerActor"));
return 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() &&
(((Actor->GetWorld() && Actor->GetWorld()->IsPlayInEditor()) || !Actor->HasAnyFlags(RF_Transient)) ||
(SceneOutliner->GetSharedData().bShowTransient && Actor->HasAnyFlags(RF_Transient)) || // Don't show transient actors in non-play worlds
(Actor->ActorHasTag(SequencerActorTag))) &&
!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
}
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>())
{
return EItemSortOrder::Actor;
}
else if (Item.IsA<FActorDescTreeItem>())
{
return EItemSortOrder::Unloaded;
}
// Warning: using actor mode with an unsupported item type!
check(false);
return -1;
}
#undef LOCTEXT_NAMESPACE