// Copyright Epic Games, Inc. All Rights Reserved. #include "ActorBrowsingMode.h" #include "ActorBrowsingModeCommands.h" #include "Engine/Blueprint.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 "Engine/World.h" #include "Editor/UnrealEdEngine.h" #include "HAL/PlatformApplicationMisc.h" #include "HAL/FileManager.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 "WorldPartition/WorldPartition.h" #include "WorldPartition/WorldPartitionSubsystem.h" #include "WorldPartition/DataLayer/DataLayerManager.h" #include "WorldPartition/IWorldPartitionEditorModule.h" #include "WorldPartition/ActorDescContainerInstance.h" #include "WorldPartition/WorldPartitionActorDescInstance.h" #include "Subsystems/ActorEditorContextSubsystem.h" #include "EditorLevelUtils.h" #include "EditorViewportCommands.h" #include "Misc/ScopedSlowTask.h" #include "Elements/Framework/EngineElementsLibrary.h" #include "Elements/Framework/TypedElementHandle.h" #include "Framework/Commands/GenericCommands.h" DEFINE_LOG_CATEGORY_STATIC(LogActorBrowser, Log, All); #define LOCTEXT_NAMESPACE "SceneOutliner_ActorBrowsingMode" using FActorFilter = TSceneOutlinerPredicateFilter; using FActorDescFilter = TSceneOutlinerPredicateFilter; using FFolderFilter = TSceneOutlinerPredicateFilter; FActorBrowsingMode::FActorBrowsingMode(SSceneOutliner* InSceneOutliner, TWeakObjectPtr InSpecifiedWorldToDisplay) : FActorModeInteractive(FActorModeParams(InSceneOutliner, InSpecifiedWorldToDisplay, /* bHideComponents */ true, /* bHideLevelInstanceHierarchy */ false, /* bHideUnloadedActors */ false, /* bHideEmptyFolders */ false)) , FilteredActorCount(0) { WorldPartitionEditorModule = FModuleManager::GetModulePtr("WorldPartitionEditor"); // 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); UActorEditorContextSubsystem::Get()->OnActorEditorContextSubsystemChanged().AddRaw(this, &FActorBrowsingMode::OnActorEditorContextSubsystemChanged); 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; } bHideLevelInstanceHierarchy = LocalSettings.bHideLevelInstanceHierarchy; InSceneOutliner->SetShowTransient(!LocalSettings.bHideTemporaryActors); bShouldUpdateContentWhileInPIEFocused = LocalSettings.bShouldUpdateContentWhileInPIEFocused; // 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 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); FSceneOutlinerFilterInfo OnlyCurrentDataLayersInfo(LOCTEXT("ToggleShowOnlyCurrentDataLayers", "Only in any Current Data Layers"), LOCTEXT("ToggleShowOnlyCurrentDataLayersToolTip", "When enabled, only shows Actors that are in any Current Data Layers."), LocalSettings.bShowOnlyActorsInCurrentDataLayers, FCreateSceneOutlinerFilter::CreateStatic(&FActorBrowsingMode::CreateIsInCurrentDataLayersFilter)); OnlyCurrentDataLayersInfo.OnToggle().AddLambda([this](bool bIsActive) { FActorBrowsingModeConfig* Settings = GetMutableConfig(); if (Settings) { Settings->bShowOnlyActorsInCurrentDataLayers = bIsActive; SaveConfig(); } }); FilterInfoMap.Add(TEXT("ShowOnlyCurrentDataLayers"), OnlyCurrentDataLayersInfo); // Add a filter for unloaded actors to properly reflect the bShowOnlyActorsInCurrentDataLayers flag. SceneOutliner->AddFilter(MakeShared(FActorDescTreeItem::FFilterPredicate::CreateLambda([this](const FWorldPartitionActorDescInstance* ActorDescInstance) { FActorBrowsingModeConfig* Settings = GetMutableConfig(); if (Settings && Settings->bShowOnlyActorsInCurrentDataLayers) { const UDataLayerManager* DataLayerManager = UDataLayerManager::GetDataLayerManager(RepresentingWorld.Get()); if (!DataLayerManager || DataLayerManager->GetActorEditorContextDataLayers().IsEmpty()) { return true; } for(const UDataLayerInstance* const DataLayerInstance : DataLayerManager->GetDataLayerInstances(ActorDescInstance->GetDataLayerInstanceNames().ToArray())) { if (DataLayerInstance->IsInActorEditorContext()) { return true; } } return false; } return true; }), FSceneOutlinerFilter::EDefaultBehaviour::Pass)); FSceneOutlinerFilterInfo OnlyCurrentContentBundleInfo(LOCTEXT("ToggleShowOnlyCurrentContentBundle", "Only in Current Content Bundle"), LOCTEXT("ToggleShowOnlyCurrentContentBundleToolTip", "When enabled, only shows Actors that are in the Current Content Bundle."), LocalSettings.bShowOnlyActorsInCurrentContentBundle, FCreateSceneOutlinerFilter::CreateRaw(this, &FActorBrowsingMode::CreateIsInCurrentContentBundleFilter)); OnlyCurrentContentBundleInfo.OnToggle().AddLambda([this](bool bIsActive) { FActorBrowsingModeConfig* Settings = GetMutableConfig(); if (Settings) { Settings->bShowOnlyActorsInCurrentContentBundle = bIsActive; SaveConfig(); } }); FilterInfoMap.Add(TEXT("ShowOnlyCurrentContentBundle"), OnlyCurrentContentBundleInfo); // Add a filter for unloaded actors to properly reflect the bShowOnlyActorsInCurrentContentBundle flag. SceneOutliner->AddFilter(MakeShared(FActorDescTreeItem::FFilterPredicate::CreateLambda([this](const FWorldPartitionActorDescInstance* ActorDescInstance) { FActorBrowsingModeConfig* Settings = GetMutableConfig(); if (Settings && Settings->bShowOnlyActorsInCurrentContentBundle) { if (!WorldPartitionEditorModule || !WorldPartitionEditorModule->IsEditingContentBundle()) { return true; } return ActorDescInstance->GetContentBundleGuid().IsValid() && WorldPartitionEditorModule->IsEditingContentBundle(ActorDescInstance->GetContentBundleGuid()); } return true; }), FSceneOutlinerFilter::EDefaultBehaviour::Pass)); 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(Hierarchy.Get())) { ActorHierarchy->SetShowingComponents(!bIsActive); } }); FilterInfoMap.Add(TEXT("HideComponentsFilter"), HideComponentsInfo); bHideUnloadedActors = LocalSettings.bHideUnloadedActors; 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(Hierarchy.Get())) { ActorHierarchy->SetShowingUnloadedActors(!bIsActive); } }); FilterInfoMap.Add(TEXT("HideUnloadedActorsFilter"), HideUnloadedActorsInfo); bHideEmptyFolders = LocalSettings.bHideEmptyFolders; FSceneOutlinerFilterInfo HideEmptyFoldersInfo(LOCTEXT("ToggleHideEmptyFolders", "Hide Empty Folders"), LOCTEXT("ToggleHideEmptyFoldersToolTip", "When enabled, hides all empty folders."), LocalSettings.bHideEmptyFolders, FCreateSceneOutlinerFilter::CreateStatic(&FActorBrowsingMode::CreateHideEmptyFoldersFilter)); HideEmptyFoldersInfo.OnToggle().AddLambda([this](bool bIsActive) { FActorBrowsingModeConfig* Settings = GetMutableConfig(); if (Settings) { Settings->bHideEmptyFolders = bHideEmptyFolders = bIsActive; SaveConfig(); } if (auto ActorHierarchy = StaticCast(Hierarchy.Get())) { ActorHierarchy->SetShowingEmptyFolders(!bIsActive); } }); FilterInfoMap.Add(TEXT("HideEmptyFoldersFilter"), HideEmptyFoldersInfo); // Add a filter which sets the interactive mode of LevelInstance items and their children SceneOutliner->AddInteractiveFilter(MakeShared(FActorTreeItem::FFilterPredicate::CreateLambda([this](const AActor* Actor) { if (Actor->SupportsSubRootSelection()) { return true; } if (!bHideLevelInstanceHierarchy) { if (const ULevelInstanceSubsystem* LevelInstanceSubsystem = RepresentingWorld->GetSubsystem()) { // 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) && !LevelInstanceSubsystem->IsEditingLevelInstancePropertyOverrides(ParentLevelInstance)) { return false; } } } } return true; }), FSceneOutlinerFilter::EDefaultBehaviour::Pass)); bAlwaysFrameSelection = LocalSettings.bAlwaysFrameSelection; bCollapseOutlinerTreeOnNewSelection = LocalSettings.bCollapseOutlinerTreeOnNewSelection; Rebuild(); } FActorBrowsingMode::~FActorBrowsingMode() { if (RepresentingWorld.IsValid()) { if (UWorldPartition* const WorldPartition = RepresentingWorld->GetWorldPartition()) { WorldPartition->OnActorDescInstanceRemovedEvent.RemoveAll(this); } } FSceneOutlinerDelegates::Get().OnComponentsUpdated.RemoveAll(this); GEngine->OnLevelActorDeleted().RemoveAll(this); GEditor->OnSelectUnloadedActorsEvent().RemoveAll(this); if (UActorEditorContextSubsystem* ActorEditorContextSubsystem = UActorEditorContextSubsystem::Get()) { ActorEditorContextSubsystem->OnActorEditorContextSubsystemChanged().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->OnActorDescInstanceRemovedEvent.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->OnActorDescInstanceRemovedEvent.AddRaw(this, &FActorBrowsingMode::OnActorDescInstanceRemoved); } } 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(); 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::OnActorEditorContextSubsystemChanged() { // For performance reasons avoid doing full refresh if we don't have an active filter relying on the current Actor Editor Context if (const FActorBrowsingModeConfig* Settings = GetConstConfig(); Settings && (Settings->bShowOnlyActorsInCurrentDataLayers || Settings->bShowOnlyActorsInCurrentContentBundle)) { SceneOutliner->FullRefresh(); } } void FActorBrowsingMode::OnToggleAlwaysFrameSelection() { bAlwaysFrameSelection = !bAlwaysFrameSelection; FActorBrowsingModeConfig* Settings = GetMutableConfig(); if(Settings) { Settings->bAlwaysFrameSelection = bAlwaysFrameSelection; SaveConfig(); } } bool FActorBrowsingMode::ShouldAlwaysFrameSelection() const { return bAlwaysFrameSelection; } void FActorBrowsingMode::OnToggleHideTemporaryActors() { FActorBrowsingModeConfig* Settings = GetMutableConfig(); if (Settings) { Settings->bHideTemporaryActors = !Settings->bHideTemporaryActors; SaveConfig(); SceneOutliner->SetShowTransient(!Settings->bHideTemporaryActors); SceneOutliner->FullRefresh(); } } bool FActorBrowsingMode::ShouldHideTemporaryActors() const { const FActorBrowsingModeConfig* Settings = GetConstConfig(); if (Settings) { return Settings->bHideTemporaryActors; } return false; } void FActorBrowsingMode::OnToggleHideLevelInstanceHierarchy() { bHideLevelInstanceHierarchy = !bHideLevelInstanceHierarchy; FActorBrowsingModeConfig* Settings = GetMutableConfig(); if (Settings) { Settings->bHideLevelInstanceHierarchy = bHideLevelInstanceHierarchy; SaveConfig(); } if (auto ActorHierarchy = StaticCast(Hierarchy.Get())) { ActorHierarchy->SetShowingLevelInstances(!bHideLevelInstanceHierarchy); } SceneOutliner->FullRefresh(); } bool FActorBrowsingMode::ShouldHideLevelInstanceHierarchy() const { const FActorBrowsingModeConfig* Settings = GetConstConfig(); if (Settings) { return Settings->bHideLevelInstanceHierarchy; } return false; } void FActorBrowsingMode::InitializeViewMenuExtender(TSharedPtr Extender) { FActorModeInteractive::InitializeViewMenuExtender(Extender); Extender->AddMenuExtension(SceneOutliner::ExtensionHooks::Show, EExtensionHook::First, nullptr, FMenuExtensionDelegate::CreateLambda([this](FMenuBuilder& MenuBuilder) { MenuBuilder.AddMenuEntry( LOCTEXT("ToggleHideTemporaryActors", "Hide Temporary Actors"), LOCTEXT("ToggleHideTemporaryActorsToolTip", "When enabled, hides temporary/run-time Actors."), FSlateIcon(), FUIAction( FExecuteAction::CreateRaw(this, &FActorBrowsingMode::OnToggleHideTemporaryActors), FCanExecuteAction(), FIsActionChecked::CreateRaw(this, &FActorBrowsingMode::ShouldHideTemporaryActors) ), NAME_None, EUserInterfaceActionType::ToggleButton ); MenuBuilder.AddMenuEntry( LOCTEXT("ToggleHideLevelInstanceContent", "Hide Level Instance Content"), LOCTEXT("ToggleHideLevelInstancesToolTip", "When enabled, hides all level instance content."), FSlateIcon(), FUIAction( FExecuteAction::CreateRaw(this, &FActorBrowsingMode::OnToggleHideLevelInstanceHierarchy), FCanExecuteAction(), FIsActionChecked::CreateRaw(this, &FActorBrowsingMode::ShouldHideLevelInstanceHierarchy) ), NAME_None, EUserInterfaceActionType::ToggleButton ); })); Extender->AddMenuExtension(SceneOutliner::ExtensionHooks::Show, EExtensionHook::After, nullptr, FMenuExtensionDelegate::CreateLambda([this](FMenuBuilder& MenuBuilder) { MenuBuilder.BeginSection("OutlinerSelectionOptions", LOCTEXT("OptionsHeading", "Options")); MenuBuilder.AddMenuEntry( LOCTEXT("AlwaysFrameSelectionLabel", "Always Frame Selection"), LOCTEXT("AlwaysFrameSelectionTooltip", "When enabled, selecting an Actor in the Viewport also scrolls to that Actor in the Outliner."), FSlateIcon(), FUIAction( FExecuteAction::CreateRaw(this, &FActorBrowsingMode::OnToggleAlwaysFrameSelection), FCanExecuteAction(), FIsActionChecked::CreateRaw(this, &FActorBrowsingMode::ShouldAlwaysFrameSelection) ), NAME_None, EUserInterfaceActionType::ToggleButton ); MenuBuilder.AddMenuEntry( LOCTEXT("FolderDoubleClickToggleCurrentFolderLabel", "Double Click toggles Current Folder"), LOCTEXT( "FolderDoubleClickToggleCurrentFolderTooltip", "When enabled, double clicking on a folder will result in it toggling its Current Folder state" "instead of its expansion." ), FSlateIcon(), FUIAction( FExecuteAction::CreateRaw(this, &FActorBrowsingMode::OnToggleFolderDoubleClickMarkCurrentFolder), FCanExecuteAction(), FIsActionChecked::CreateRaw(this, &FActorBrowsingMode::DoesFolderDoubleClickMarkCurrentFolder) ), NAME_None, EUserInterfaceActionType::ToggleButton ); MenuBuilder.AddMenuEntry( LOCTEXT("ShouldUpdateContentWhileInPIEFocusedLabel", "Update In PIE"), LOCTEXT("ShouldUpdateContentWhileInPIEFocusedLabelTooltip", "When enabled, the Outliner will update in PIE."), FSlateIcon(), FUIAction( FExecuteAction::CreateRaw(this, &FActorBrowsingMode::OnToggleShouldUpdateContentWhileInPIEFocused), FCanExecuteAction(), FIsActionChecked::CreateRaw(this, &FActorBrowsingMode::ShouldUpdateContentWhileInPIEFocused), FIsActionButtonVisible::CreateLambda([this]() { return (WorldPartitionEditorModule ? !WorldPartitionEditorModule->GetDisablePIE() : true); }) ), NAME_None, EUserInterfaceActionType::ToggleButton ); MenuBuilder.AddMenuEntry( LOCTEXT("CollapseOutlinerTreeOnNewSelectionLabel", "Collapse Hierarchy on Viewport Selection"), LOCTEXT("CollapseOutlinerTreeOnNewSelectionTooltip", "When enabled, all items except the one that was just selected are collapsed."), FSlateIcon(), FUIAction( FExecuteAction::CreateRaw(this, &FActorBrowsingMode::OnToggleCollapseOutlinerTreeOnNewSelection), FCanExecuteAction(), FIsActionChecked::CreateRaw(this, &FActorBrowsingMode::CollapseOutlinerTreeOnNewSelection) ), NAME_None, EUserInterfaceActionType::ToggleButton ); MenuBuilder.EndSection(); MenuBuilder.BeginSection("World", 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 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 FActorBrowsingMode::CreateIsInCurrentLevelFilter() { return MakeShareable(new TSceneOutlinerPredicateFilter(ISceneOutlinerTreeItem::FFilterPredicate::CreateStatic([](const ISceneOutlinerTreeItem& InItem) { if (const FActorTreeItem* ActorItem = InItem.CastTo()) { if (const AActor* Actor = ActorItem->Actor.Get(); Actor && Actor->GetWorld()) { if (const ILevelInstanceInterface* LevelInstance = Cast(Actor)) { if (LevelInstance->IsEditing()) { return Actor->GetWorld()->GetCurrentLevel() == LevelInstance->GetLoadedLevel(); } } return Actor->GetLevel()->IsCurrentLevel(); } } else if (const FActorFolderTreeItem* FolderItem = InItem.CastTo()) { if (const UWorld* FolderWorld = FolderItem->World.Get()) { if (ULevel* FolderLevel = FolderItem->GetFolder().GetRootObjectAssociatedLevel()) { return FolderLevel->IsCurrentLevel(); } } } else if (const FComponentTreeItem* ComponentItem = InItem.CastTo()) { if (const UActorComponent* Component = ComponentItem->Component.Get(); Component && Component->GetComponentLevel()) { return Component->GetComponentLevel()->IsCurrentLevel(); } } return false; }), FSceneOutlinerFilter::EDefaultBehaviour::Pass)); } TSharedRef FActorBrowsingMode::CreateIsInCurrentDataLayersFilter() { return MakeShareable(new FActorFilter(FActorTreeItem::FFilterPredicate::CreateStatic([](const AActor* InActor) { const UDataLayerManager* DataLayerManager = UDataLayerManager::GetDataLayerManager(InActor->GetWorld()); if (!DataLayerManager || DataLayerManager->GetActorEditorContextDataLayers().IsEmpty()) { return true; } for (const UDataLayerInstance* DataLayerInstance : InActor->GetDataLayerInstances()) { if (DataLayerInstance->IsInActorEditorContext()) { return true; } } return false; }), FSceneOutlinerFilter::EDefaultBehaviour::Pass)); } TSharedRef FActorBrowsingMode::CreateIsInCurrentContentBundleFilter() { return MakeShareable(new FActorFilter(FActorTreeItem::FFilterPredicate::CreateLambda([this](const AActor* InActor) { if (!WorldPartitionEditorModule || !WorldPartitionEditorModule->IsEditingContentBundle()) { return true; } return InActor->GetContentBundleGuid().IsValid() && WorldPartitionEditorModule->IsEditingContentBundle(InActor->GetContentBundleGuid()); }), FSceneOutlinerFilter::EDefaultBehaviour::Pass)); } TSharedRef FActorBrowsingMode::CreateHideComponentsFilter() { return MakeShared>(TSceneOutlinerPredicateFilter( FComponentTreeItem::FFilterPredicate::CreateStatic([](const UActorComponent*) { return false; }), FSceneOutlinerFilter::EDefaultBehaviour::Pass)); } TSharedRef FActorBrowsingMode::CreateHideUnloadedActorsFilter() { return MakeShareable(new FActorDescFilter(FActorDescTreeItem::FFilterPredicate::CreateStatic( [](const FWorldPartitionActorDescInstance* ActorDescInstance) { return false; }), FSceneOutlinerFilter::EDefaultBehaviour::Pass)); } TSharedRef FActorBrowsingMode::CreateHideEmptyFoldersFilter() { return MakeShareable(new FFolderFilter(FFolderTreeItem::FFilterPredicate::CreateStatic( [](const FFolder& Folder) { return true; }), 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::CreateStatic(&FActorBrowsingMode::FillDefaultContextBaseMenu)); } if (!ToolMenus->IsMenuRegistered(DefaultContextMenuName)) { ToolMenus->RegisterMenu(DefaultContextMenuName, DefaultContextBaseMenuName); } } void FActorBrowsingMode::FillDefaultContextBaseMenu(UToolMenu* InMenu) { USceneOutlinerMenuContext* Context = InMenu->FindContext(); if (!Context) { return; } TSharedPtr SharedOutliner = Context->SceneOutliner.Pin(); SSceneOutliner* SceneOutliner = SharedOutliner.Get(); if (!SceneOutliner) { return; } { // NOTE: the name "Section" is used in many other places FToolMenuSection& Section = InMenu->FindOrAddSection("Section"); Section.Label = LOCTEXT("HierarchySectionName", "Hierarchy"); if (Context->bShowParentTree) { if (Context->NumSelectedItems == 0) { FSceneOutlinerMenuHelper::AddMenuEntryCreateFolder(Section, *SceneOutliner); } 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)); } } } } { // We always create a section here, even if there is no parent so that clients can still extend the menu FToolMenuSection& MainSection = InMenu->AddSection("MainSection", LOCTEXT("OutlinerSectionName", "Outliner")); // 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) { MainSection.AddSubMenu( "MoveActorsTo", LOCTEXT("MoveActorsTo", "Move To"), LOCTEXT("MoveActorsTo_Tooltip", "Move selection to another folder"), FNewToolMenuDelegate::CreateSP(SceneOutliner, &SSceneOutliner::FillFoldersSubMenu)); } if (Context->bShowParentTree && Context->NumSelectedItems > 0) { // Only add the menu option to wp levels if (!Context->bRepresentingGameWorld && Context->bRepresentingPartitionedWorld) { MainSection.AddMenuEntry( "ForceLoadItems", LOCTEXT("ForceLoad", "Force Load"), LOCTEXT("ForceLoadTooltip", "Keep the selected items loaded in the editor even when they don't overlap a loaded World Partition region"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(SceneOutliner, &SSceneOutliner::PinSelectedItems), FCanExecuteAction::CreateLambda([SceneOutliner, Context]() { if (Context->NumPinnedItems != Context->NumSelectedItems || Context->NumSelectedFolders > 0) { return SceneOutliner->CanPinSelectedItems(); } return false; }))); MainSection.AddMenuEntry( "ReleaseForceLoadItems", LOCTEXT("ReleaseForceLoad", "Release Force Load"), LOCTEXT("ReleaseForceLoadTooltip", "Allow the World Partition system to load and unload the selected items automatically"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(SceneOutliner, &SSceneOutliner::UnpinSelectedItems), FCanExecuteAction::CreateLambda([SceneOutliner, Context]() { if (Context->NumPinnedItems != 0 || Context->NumSelectedFolders > 0) { return SceneOutliner->CanUnpinSelectedItems(); } return false; }))); } } if (Context->NumSelectedItems > 0) { SceneOutliner->AddSourceControlMenuOptions(InMenu); } } if (Context->bShowParentTree) { FToolMenuSection& ActorEditorContextSection = InMenu->AddSection("ActorEditorContextSection", LOCTEXT("ActorEditorContextSectionName", "Actor Editor Context")); if ((Context->NumSelectedItems == 1) && (Context->NumSelectedFolders == 1)) { ActorEditorContextSection.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()) { 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()) { return FolderItem->World.IsValid() && (FolderItem->World->GetCurrentLevel() == FolderItem->GetFolder().GetRootObjectAssociatedLevel()) && (FActorFolders::Get().GetActorEditorContextFolder(*FolderItem->World) != FolderItem->GetFolder()); } } return false; }) ) ); } const FActorBrowsingMode* Mode = static_cast(SceneOutliner->GetMode()); check(Mode); ActorEditorContextSection.AddMenuEntry( "ClearCurrentFolder", LOCTEXT("ClearCurrentFolder", "Clear Current Folder"), FText(), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([Mode]() { if (Mode->RepresentingWorld.IsValid()) { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "ClearCurrentActorFolder", "Clear Current Actor Folder")); FActorFolders::Get().SetActorEditorContextFolder(*Mode->RepresentingWorld.Get(), FFolder::GetWorldRootFolder(Mode->RepresentingWorld.Get())); } }), FCanExecuteAction::CreateLambda([Mode] { return Mode->RepresentingWorld.IsValid() && !FActorFolders::Get().GetActorEditorContextFolder(*Mode->RepresentingWorld.Get()).IsNone(); }) ) ); } } TSharedPtr FActorBrowsingMode::BuildContextMenu() { FActorBrowsingMode::RegisterContextMenu(); const FSceneOutlinerItemSelection ItemSelection(SceneOutliner->GetSelection()); USceneOutlinerMenuContext* ContextObject = NewObject(); ContextObject->SceneOutliner = StaticCastSharedRef(SceneOutliner->AsShared()); ContextObject->bShowParentTree = SceneOutliner->GetSharedData().bShowParentTree; ContextObject->NumSelectedItems = ItemSelection.Num(); ContextObject->NumSelectedFolders = ItemSelection.Num(); ContextObject->NumWorldsSelected = ItemSelection.Num(); ContextObject->bRepresentingGameWorld = bRepresentingWorldGameWorld; ContextObject->bRepresentingPartitionedWorld = bRepresentingWorldPartitionedWorld; int32 NumPinnedItems = 0; if (RepresentingWorld.IsValid()) { if (const UWorldPartition* const WorldPartition = RepresentingWorld->GetWorldPartition()) { ItemSelection.ForEachItem([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 FActorBrowsingMode::CreateContextMenu() { TArray SelectedActors; GEditor->GetSelectedActors()->GetSelectedObjects(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()) { // We incremented the count regardless of Flags.bIsFilteredOut because the count should match what the user sees, which includes things like // actors which don't pass the filter themselves but are force shown by children being visible. ++FilteredActorCount; if (!Item->Flags.bIsFilteredOut) { // Synchronize selection if (GEditor->GetSelectedActors()->IsSelected(ActorItem->Actor.Get())) { SceneOutliner->SetItemSelection(Item, true); } } } else if (Item->IsA()) { ++FilteredActorCount; ++FilteredUnloadedActorCount; } } void FActorBrowsingMode::OnItemRemoved(FSceneOutlinerTreeItemPtr Item) { if (Item->IsA()) { --FilteredActorCount; } else if (Item->IsA()) { --FilteredActorCount; --FilteredUnloadedActorCount; } } void FActorBrowsingMode::OnComponentsUpdated() { UE_LOG(LogSceneOutliner, VeryVerbose, TEXT("FullRefresh requested by FActorBrowsingMode::OnComponentsUpdated")); SceneOutliner->FullRefresh(); } void FActorBrowsingMode::OnLevelActorDeleted(AActor* Actor) { ApplicableActors.Remove(Actor); } void FActorBrowsingMode::OnSelectUnloadedActors(const TArray& ActorGuids) { if (RepresentingWorld.IsValid()) { if (UWorldPartition* WorldPartition = RepresentingWorld->GetWorldPartition()) { TArray ItemsToSelect; ItemsToSelect.Reserve(ActorGuids.Num()); for (const FGuid& ActorGuid : ActorGuids) { if (FWorldPartitionActorDescInstance* ActorDescInstance = WorldPartition->GetActorDescInstance(ActorGuid)) { if (FSceneOutlinerTreeItemPtr ItemPtr = SceneOutliner->GetTreeItem(FActorDescTreeItem::ComputeTreeItemID(ActorDescInstance->GetGuid(), ActorDescInstance->GetContainerInstance()))) { ItemsToSelect.Add(ItemPtr); } } } if (ItemsToSelect.Num()) { SceneOutliner->SetItemSelection(ItemsToSelect, true); SceneOutliner->ScrollItemIntoView(ItemsToSelect.Last()); if (const FActorDescTreeItem* ActorDescItem = ItemsToSelect.Last()->CastTo()) { ActorDescItem->FocusActorBounds(); } } } } } void FActorBrowsingMode::OnActorDescInstanceRemoved(FWorldPartitionActorDescInstance* InActorDescInstance) { ApplicableUnloadedActors.Remove(InActorDescInstance); } void FActorBrowsingMode::OnItemSelectionChanged(FSceneOutlinerTreeItemPtr TreeItem, ESelectInfo::Type SelectionType, const FSceneOutlinerItemSelection& Selection) { TSet OutlinerSelectedActors(Selection.GetData(SceneOutliner::FActorSelector())); TSet ActorsToSelect; ActorsToSelect.Reserve(OutlinerSelectedActors.Num()); SynchronizeSelectedActorDescs(); USelection* ActorSelection = GEditor->GetSelectedActors(); if (UTypedElementSelectionSet* SelectionSet = ActorSelection->GetElementSelectionSet()) { TSet EditorSelectedActors(SelectionSet->GetSelectedObjects()); bool bChanged = false; bool bAnyInPIE = false; for (AActor* Actor : OutlinerSelectedActors) { if (!bAnyInPIE && Actor && Actor->GetPackage()->HasAnyPackageFlags(PKG_PlayInEditor)) { bAnyInPIE = true; } if (!EditorSelectedActors.Contains(Actor)) { bChanged = true; } // Allow selection of Sub Roots only if Roots aren't in the outliners selection AActor* RootSelectionParent = Actor ? Actor->GetRootSelectionParent() : nullptr; if (!RootSelectionParent || !OutlinerSelectedActors.Contains(RootSelectionParent)) { ActorsToSelect.Add(Actor); } } for (const AActor* SelectedActor : EditorSelectedActors) { if (!bAnyInPIE && SelectedActor && SelectedActor->GetPackage()->HasAnyPackageFlags(PKG_PlayInEditor)) { bAnyInPIE = true; } if (!ActorsToSelect.Contains(SelectedActor)) { // Actor has been deselected bChanged = true; // If actor was a group actor, remove its members from the ActorsToSelect list if (const AGroupActor* DeselectedGroupActor = Cast(SelectedActor)) { TArray GroupActors; DeselectedGroupActor->GetGroupActors(GroupActors); for (AActor* GroupActor : GroupActors) { ActorsToSelect.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); TArray ElementsToSelect; for (auto* Actor : ActorsToSelect) { UE_LOG(LogActorBrowser, Verbose, TEXT("Clicking on Actor (world outliner): %s (%s)"), *Actor->GetClass()->GetName(), *Actor->GetActorLabel()); ElementsToSelect.Add(UEngineElementsLibrary::AcquireEditorActorElementHandle(Actor)); } { const FTypedElementSelectionOptions SelectionOptions = FTypedElementSelectionOptions() .SetAllowHidden(true) .SetWarnIfLocked(false) .SetAllowLegacyNotifications(false) .SetAllowSubRootSelection(true); // Avoid senting out notification via typed element and call NoteSelectionChange to preserve previous behavior FTypedElementList::FScopedClearNewPendingChange ClearNewPendingChange = SelectionSet->GetScopedClearNewPendingChange(); SelectionSet->SetSelection(ElementsToSelect, SelectionOptions); SelectionSet->NotifyPendingChanges(); } // 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()) { if (AActor* Actor = ActorItem->Actor.Get()) { ILevelInstanceInterface* LevelInstance = Cast(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()) { const bool bActiveViewportOnly = false; GEditor->MoveViewportCamerasToActor(Selection.GetData(SceneOutliner::FActorSelector()), bActiveViewportOnly); } } else { const bool bActiveViewportOnly = false; GEditor->MoveViewportCamerasToActor(*Actor, bActiveViewportOnly); } } } else if (const FActorDescTreeItem* ActorDescItem = Item->CastTo()) { ActorDescItem->FocusActorBounds(); } else if (const FActorFolderTreeItem* FolderItem = Item->CastTo()) { if (DoesFolderDoubleClickMarkCurrentFolder()) { if (UWorld* World = FolderItem->World.Get()) { const FScopedTransaction Transaction( LOCTEXT("ToggleCurrentActorFolder", "Toggle Current Actor Folder") ); const FFolder CurrentContextFolder = FActorFolders::Get().GetActorEditorContextFolder(*World); if (CurrentContextFolder == FolderItem->GetFolder()) { FActorFolders::Get().SetActorEditorContextFolder(*World, FFolder::GetWorldRootFolder(World)); } else { FActorFolders::Get().SetActorEditorContextFolder(*World, FolderItem->GetFolder()); } } } } } bool FActorBrowsingMode::HasCustomFolderDoubleClick() const { return DoesFolderDoubleClickMarkCurrentFolder(); } void FActorBrowsingMode::OnToggleFolderDoubleClickMarkCurrentFolder() { if (FActorBrowsingModeConfig* Settings = GetMutableConfig()) { if (Settings->FolderDoubleClickMethod == EActorBrowsingFolderDoubleClickMethod::ToggleCurrentFolder) { Settings->FolderDoubleClickMethod = EActorBrowsingFolderDoubleClickMethod::ToggleExpansion; } else { Settings->FolderDoubleClickMethod = EActorBrowsingFolderDoubleClickMethod::ToggleCurrentFolder; } SaveConfig(); } } bool FActorBrowsingMode::DoesFolderDoubleClickMarkCurrentFolder() const { if (const FActorBrowsingModeConfig* Settings = GetConstConfig()) { return Settings->FolderDoubleClickMethod == EActorBrowsingFolderDoubleClickMethod::ToggleCurrentFolder; } return false; } void FActorBrowsingMode::OnToggleShouldUpdateContentWhileInPIEFocused() { if (FActorBrowsingModeConfig* Settings = GetMutableConfig()) { Settings->bShouldUpdateContentWhileInPIEFocused = !Settings->bShouldUpdateContentWhileInPIEFocused; bShouldUpdateContentWhileInPIEFocused = Settings->bShouldUpdateContentWhileInPIEFocused; SaveConfig(); } } bool FActorBrowsingMode::ShouldUpdateContentWhileInPIEFocused() const { return bShouldUpdateContentWhileInPIEFocused; } void FActorBrowsingMode::OnToggleCollapseOutlinerTreeOnNewSelection() { if (FActorBrowsingModeConfig* Settings = GetMutableConfig()) { Settings->bCollapseOutlinerTreeOnNewSelection = !Settings->bCollapseOutlinerTreeOnNewSelection; bCollapseOutlinerTreeOnNewSelection = Settings->bCollapseOutlinerTreeOnNewSelection; SaveConfig(); } } bool FActorBrowsingMode::CollapseOutlinerTreeOnNewSelection() const { return bCollapseOutlinerTreeOnNewSelection; } 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(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()) { ApplicableActors.Add(ActorItem->Actor); } else if (const FActorDescTreeItem* const ActorDescItem = Item.CastTo(); ActorDescItem && ActorDescItem->IsValid()) { ApplicableUnloadedActors.Add(ActorDescItem->ActorDescHandle); } } FReply FActorBrowsingMode::OnKeyDown(const FKeyEvent& InKeyEvent) { const FSceneOutlinerItemSelection& Selection = SceneOutliner->GetSelection(); const FModifierKeysState ModifierKeys = FSlateApplication::Get().GetModifierKeys(); const FInputChord CheckChord( InKeyEvent.GetKey(), EModifierKey::FromBools(ModifierKeys.IsControlDown(), ModifierKeys.IsAltDown(), ModifierKeys.IsShiftDown(), ModifierKeys.IsCommandDown()) ); // Use the keyboard shortcut bound to 'Focus Viewport To Selection' if (FEditorViewportCommands::Get().FocusViewportToSelection->HasActiveChord(CheckChord)) { if (Selection.Num() == 1) { FSceneOutlinerTreeItemPtr ItemToFocus = Selection.SelectedItems[0].Pin(); if (ItemToFocus.IsValid()) { SceneOutliner->ScrollItemIntoView(ItemToFocus); } } } // Always return Unhandled here even if it entered the previous if so that the level editor viewport can handle the FocusViewport command as well return FReply::Unhandled(); } bool FActorBrowsingMode::CanDelete() const { const FSceneOutlinerItemSelection ItemSelection = SceneOutliner->GetSelection(); const uint32 NumberOfFolders = ItemSelection.Num(); return (NumberOfFolders > 0 && NumberOfFolders == ItemSelection.Num()); } bool FActorBrowsingMode::CanRename() const { const FSceneOutlinerItemSelection ItemSelection = SceneOutliner->GetSelection(); const uint32 NumberOfFolders = ItemSelection.Num(); 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() || Item.IsA())); } bool FActorBrowsingMode::CanCut() const { const FSceneOutlinerItemSelection ItemSelection = SceneOutliner->GetSelection(); const uint32 NumberOfFolders = ItemSelection.Num(); return (NumberOfFolders > 0 && NumberOfFolders == ItemSelection.Num()); } bool FActorBrowsingMode::CanCopy() const { const FSceneOutlinerItemSelection ItemSelection = SceneOutliner->GetSelection(); const uint32 NumberOfFolders = ItemSelection.Num(); return (NumberOfFolders > 0 && NumberOfFolders == ItemSelection.Num()); } bool FActorBrowsingMode::CanPaste() const { return CanPasteFoldersOnlyFromClipboard(); } bool FActorBrowsingMode::HasErrors() const { if (RepresentingWorld.IsValid() && !bRepresentingWorldGameWorld && bRepresentingWorldPartitionedWorld && WorldPartitionEditorModule) { return WorldPartitionEditorModule->HasErrors(RepresentingWorld.Get()); } return false; } FText FActorBrowsingMode::GetErrorsText() const { return LOCTEXT("WorldHasInvalidActorFiles", "The world contains invalid actor files. Click the Repair button to repair them."); } void FActorBrowsingMode::RepairErrors() const { if (RepresentingWorld.IsValid() && !bRepresentingWorldGameWorld && bRepresentingWorldPartitionedWorld && WorldPartitionEditorModule) { WorldPartitionEditorModule->RepairErrors(RepresentingWorld.Get()); } } void FActorBrowsingMode::BindCommands(const TSharedRef& OutCommandList) { OutCommandList->MapAction( FGenericCommands::Get().Rename, FExecuteAction::CreateRaw(this, &FActorBrowsingMode::OnExecuteRename ), FCanExecuteAction::CreateRaw(this, &FActorBrowsingMode::CanExecuteRename)); OutCommandList->MapAction( FGenericCommands::Get().Delete, FExecuteAction::CreateRaw(this, &FActorBrowsingMode::OnExecuteDelete), FCanExecuteAction::CreateRaw(this, &FActorBrowsingMode::CanDelete)); OutCommandList->MapAction( FActorBrowsingModeCommands::Get().Refresh, FExecuteAction::CreateRaw(this, &FActorBrowsingMode::OnExecuteRefresh)); FInputBindingManager::Get().RegisterCommandList(FActorBrowsingModeCommands::Get().GetContextName(), OutCommandList); } 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"); } void FActorBrowsingMode::OnExecuteDelete() { const FSceneOutlinerItemSelection& Selection = SceneOutliner->GetSelection(); if (SceneOutliner->GetSharedData().CustomDelete.IsBound()) { SceneOutliner->GetSharedData().CustomDelete.Execute(Selection.SelectedItems); } else { if (RepresentingWorld.IsValid()) { GUnrealEd->Exec(RepresentingWorld.Get(), TEXT("DELETE")); } } } void FActorBrowsingMode::OnExecuteRefresh() { SceneOutliner->FullRefresh(); } void FActorBrowsingMode::OnExecuteRename() { const FSceneOutlinerItemSelection& Selection = SceneOutliner->GetSelection(); if (Selection.Num() == 1) { FSceneOutlinerTreeItemPtr ItemToRename = Selection.SelectedItems[0].Pin(); if (ItemToRename.IsValid() && CanRenameItem(*ItemToRename) && ItemToRename->CanInteract()) { SceneOutliner->SetPendingRenameItem(ItemToRename); SceneOutliner->ScrollItemIntoView(ItemToRename); } } } bool FActorBrowsingMode::CanExecuteRename() { const FSceneOutlinerItemSelection& Selection = SceneOutliner->GetSelection(); if (Selection.Num() == 1) { FSceneOutlinerTreeItemPtr ItemToRename = Selection.SelectedItems[0].Pin(); return ItemToRename.IsValid() && CanRenameItem(*ItemToRename) && ItemToRename->CanInteract(); } return false; } void FActorBrowsingMode::SynchronizeSelectedActorDescs() { if (UWorldPartitionSubsystem* WorldPartitionSubsystem = UWorld::GetSubsystem(RepresentingWorld.Get())) { const FSceneOutlinerItemSelection Selection = SceneOutliner->GetSelection(); TArray SelectedActorHandles = Selection.GetData(SceneOutliner::FActorHandleSelector()); WorldPartitionSubsystem->SelectedActorHandles.Empty(); for (const FWorldPartitionHandle& ActorHandle : SelectedActorHandles) { WorldPartitionSubsystem->SelectedActorHandles.Add(ActorHandle); } } } FFolder FActorBrowsingMode::CreateNewFolder() { if (TStrongObjectPtr World = RepresentingWorld.Pin()) { const FScopedTransaction Transaction(LOCTEXT("UndoAction_CreateFolder", "Create Folder")); TArray SelectedFolders = SceneOutliner->GetSelection().GetData(SceneOutliner::FFolderPathSelector()); const FFolder NewFolderName = FActorFolders::Get().GetDefaultFolderForSelection(*World, &SelectedFolders); FActorFolders::Get().CreateFolderContainingSelection(*World, NewFolderName); return NewFolderName; } return FFolder(); } 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 if (TStrongObjectPtr World = RepresentingWorld.Pin()) { return FActorFolders::Get().GetFolderName(*World, ParentPath, LeafName); } return FFolder(); } bool FActorBrowsingMode::CreateFolder(const FFolder& NewPath) { if (TStrongObjectPtr World = RepresentingWorld.Pin()) { return FActorFolders::Get().CreateFolder(*World, NewPath); } return false; } bool FActorBrowsingMode::ReparentItemToFolder(const FFolder& FolderPath, const FSceneOutlinerTreeItemPtr& Item) { if (FActorTreeItem* ActorItem = Item->CastTo()) { // 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& 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()) { if (AActor* Actor = ActorItem->Actor.Get()) { GEditor->SelectActor(Actor, true, false); } } // Select all children for (const TWeakPtr& Child : Item->GetChildren()) { FSceneOutlinerTreeItemPtr ChildPtr = Child.Pin(); if (ChildPtr.IsValid()) { if (FActorTreeItem* ActorItem = ChildPtr->CastTo()) { if (AActor* Actor = ActorItem->Actor.Get()) { GEditor->SelectActor(Actor, true, false); } } else if (FFolderTreeItem* FolderItem = ChildPtr->CastTo()) { SceneOutliner->SetItemSelection(FolderItem->AsShared(), true); } if (!bSelectImmediateChildrenOnly) { for (const TWeakPtr& Grandchild : ChildPtr->GetChildren()) { RecursiveActorSelect(SceneOutliner, Grandchild.Pin(), bSelectImmediateChildrenOnly); } } } } } } static void RecursiveAddItemsToActorGuidList(const TArray& Items, TArray& List, bool bSearchForHiddenUnloadedActors) { // In the case where we want the list of unloaded actors under a folder and the option to hide unloaded actors is enabled, we need to find them through FActorFolders TMap> UnloadedActorsFolderPaths; for (const FSceneOutlinerTreeItemPtr& Item : Items) { if (const FActorDescTreeItem* const ActorDescTreeItem = Item->CastTo()) { List.Add(ActorDescTreeItem->GetGuid()); } else if (const FActorTreeItem* const ActorTreeItem = Item->CastTo()) { if (ActorTreeItem->Actor.IsValid()) { List.Add(ActorTreeItem->Actor->GetActorGuid()); } } else if (const FActorFolderTreeItem* const ActorFolderTreeItem = Item->CastTo(); ActorFolderTreeItem && bSearchForHiddenUnloadedActors) { if (UWorld* FolderWorld = ActorFolderTreeItem->World.Get()) { UnloadedActorsFolderPaths.FindOrAdd(FolderWorld).Add(ActorFolderTreeItem->GetPath()); } } TArray ChildrenItems; for (const auto& Child : Item->GetChildren()) { if (Child.IsValid()) { ChildrenItems.Add(Child.Pin()); } } if (ChildrenItems.Num()) { RecursiveAddItemsToActorGuidList(ChildrenItems, List, bSearchForHiddenUnloadedActors); } } for (const TTuple>& Pair : UnloadedActorsFolderPaths) { FActorFolders::ForEachActorDescInstanceInFolders(*Pair.Key, Pair.Value, [&List](const FWorldPartitionActorDescInstance* ActorDescInstance) { if (!ActorDescInstance->IsLoaded()) { List.Add(ActorDescInstance->GetGuid()); } return true; }); } }; bool CanChangePinnedStates(const TArray& InItems) { for (const FSceneOutlinerTreeItemPtr& Item : InItems) { if (Item->ShouldShowPinnedState()) { return true; } else if (const FActorFolderTreeItem* FolderItem = Item->CastTo(); FolderItem && FolderItem->CanChangeChildrenPinnedState()) { return true; } } return false; } } void FActorBrowsingMode::SelectFoldersDescendants(const TArray& 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(); } bool FActorBrowsingMode::CanPinItems(const TArray& InItems) const { return ActorBrowsingModeUtils::CanChangePinnedStates(InItems); } void FActorBrowsingMode::PinItems(const TArray& InItems) { UWorldPartition* const WorldPartition = RepresentingWorld->GetWorldPartition(); if (!WorldPartition) { return; } TArray ActorsToPin; // If Unloaded actors are hidden and we are pinning folders we need to find them through FActorFolders const bool bSearchForHiddenUnloadedActors = bHideUnloadedActors; ActorBrowsingModeUtils::RecursiveAddItemsToActorGuidList(InItems, ActorsToPin, bSearchForHiddenUnloadedActors); 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); } } } bool FActorBrowsingMode::CanUnpinItems(const TArray& InItems) const { return ActorBrowsingModeUtils::CanChangePinnedStates(InItems); } void FActorBrowsingMode::UnpinItems(const TArray& InItems) { UWorldPartition* const WorldPartition = RepresentingWorld->GetWorldPartition(); if (!WorldPartition) { return; } TArray ActorsToUnpin; // No need to search for hidden unloaded actors when unloading const bool bSearchForHiddenUnloadedActors = false; ActorBrowsingModeUtils::RecursiveAddItemsToActorGuidList(InItems, ActorsToUnpin, bSearchForHiddenUnloadedActors); if (ActorsToUnpin.Num()) { WorldPartition->UnpinActors(ActorsToUnpin); } } void FActorBrowsingMode::SynchronizeSelection() { FActorModeInteractive::SynchronizeSelection(); SynchronizeSelectedActorDescs(); } FCreateSceneOutlinerMode FActorBrowsingMode::CreateFolderPickerMode(const FFolder::FRootObject& InRootObject) const { auto MoveSelectionTo = [this, InRootObject](const FSceneOutlinerTreeItemRef& NewParent) { if (FWorldTreeItem* WorldItem = NewParent->CastTo()) { SceneOutliner->MoveSelectionTo(GetWorldDefaultRootFolder()); } else if (FFolderTreeItem* FolderItem = NewParent->CastTo()) { SceneOutliner->MoveSelectionTo(FolderItem->GetFolder()); } else if (FActorTreeItem* ActorItem = NewParent->CastTo()) { if (FFolder::IsRootObjectValid(InRootObject)) { SceneOutliner->MoveSelectionTo(FFolder(InRootObject)); } } else if (FLevelTreeItem* LevelItem = NewParent->CastTo()) { 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 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 SelectedFolderPaths; FFolder::GetFolderPathsAndCommonRootObject(SceneOutliner->GetSelection().GetData(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(); } bool FActorBrowsingMode::CompareItemWithClassName(SceneOutliner::FilterBarType InItem, const TSet& AssetClassPaths) const { // Type filtering only supported for Actors (and unloaded actors) currently if (const FActorTreeItem* ActorItem = InItem.CastTo()) { AActor* Actor = ActorItem->Actor.Get(); if(!Actor) { return false; } FTopLevelAssetPath AssetClassPath = Actor->GetClass()->GetClassPathName(); // For Blueprints, we check both the parent type (e.g Pawn/Actor etc) and the Blueprint class itself if (UBlueprint* ClassBP = UBlueprint::GetBlueprintFromClass(Actor->GetClass())) { return AssetClassPaths.Contains(ClassBP->GetClass()->GetClassPathName()) || AssetClassPaths.Contains(AssetClassPath); } return AssetClassPaths.Contains(AssetClassPath); } else if (const FActorDescTreeItem* ActorDescItem = InItem.CastTo()) { if (const FWorldPartitionActorDescInstance* ActorDescInstance = *ActorDescItem->ActorDescHandle) { // For Unloaded Actors, grab the native class FTopLevelAssetPath ClassPath = ActorDescInstance->GetNativeClass(); return AssetClassPaths.Contains(ClassPath); } } return FActorModeInteractive::CompareItemWithClassName(InItem, AssetClassPaths); } #undef LOCTEXT_NAMESPACE