Files
UnrealEngine/Engine/Source/Editor/WorldBrowser/Private/SWorldHierarchyImpl.cpp
2025-05-18 13:04:45 +08:00

1779 lines
50 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SWorldHierarchyImpl.h"
#include "SLevelsTreeWidget.h"
#include "SWorldHierarchyItem.h"
#include "SWorldHierarchy.h"
#include "WorldBrowserModule.h"
#include "Modules/ModuleManager.h"
#include "Framework/Application/SlateApplication.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "ToolMenus.h"
#include "UObject/ObjectSaveContext.h"
#include "Widgets/Layout/SSeparator.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Input/SComboButton.h"
#include "Widgets/Input/SSearchBox.h"
#include "Widgets/Input/SButton.h"
#include "SSimpleComboButton.h"
#include "SSimpleButton.h"
#include "WorldTreeItemTypes.h"
#include "LevelFolders.h"
#include "ScopedTransaction.h"
#include "Templates/UnrealTemplate.h"
#include "Editor.h"
#include "WorldBrowserConfig.h"
#include "WorldBrowserStyle.h"
#include "WorldHierarchyColumns.h"
#include "RevisionControlStyle/RevisionControlStyle.h"
#define LOCTEXT_NAMESPACE "WorldBrowser"
DEFINE_LOG_CATEGORY_STATIC(LogWorldHierarchy, Log, All);
SWorldHierarchyImpl::SWorldHierarchyImpl()
: bUpdatingSelection(false)
, bIsReentrant(false)
, bFullRefresh(true)
, bNeedsRefresh(true)
, bRebuildFolders(false)
, bSortDirty(false)
, bFoldersOnlyMode(false)
{
}
SWorldHierarchyImpl::~SWorldHierarchyImpl()
{
GEditor->UnregisterForUndo(this);
WorldModel->SelectionChanged.RemoveAll(this);
WorldModel->HierarchyChanged.RemoveAll(this);
WorldModel->CollectionChanged.RemoveAll(this);
WorldModel->PreLevelsUnloaded.RemoveAll(this);
if (SearchBoxLevelFilter.IsValid())
{
WorldModel->RemoveFilter(SearchBoxLevelFilter.ToSharedRef());
}
if (FLevelFolders::IsAvailable())
{
FLevelFolders& LevelFolders = FLevelFolders::Get();
LevelFolders.OnFolderCreate.RemoveAll(this);
LevelFolders.OnFolderMove.RemoveAll(this);
LevelFolders.OnFolderDelete.RemoveAll(this);
}
FEditorDelegates::PostSaveWorldWithContext.RemoveAll(this);
}
void SWorldHierarchyImpl::Construct(const FArguments& InArgs)
{
WorldModel = InArgs._InWorldModel;
check(WorldModel.IsValid());
WorldModel->SelectionChanged.AddSP(this, &SWorldHierarchyImpl::OnUpdateSelection);
WorldModel->HierarchyChanged.AddSP(this, &SWorldHierarchyImpl::RebuildFoldersAndFullRefresh);
WorldModel->CollectionChanged.AddSP(this, &SWorldHierarchyImpl::RebuildFoldersAndFullRefresh);
WorldModel->PreLevelsUnloaded.AddSP(this, &SWorldHierarchyImpl::OnBroadcastLevelsUnloaded);
bFoldersOnlyMode = InArgs._ShowFoldersOnly;
ExcludedFolders = InArgs._InExcludedFolders;
OnItemPicked = InArgs._OnItemPickedDelegate;
if (!bFoldersOnlyMode)
{
SearchBoxLevelFilter = MakeShareable(new LevelTextFilter(
LevelTextFilter::FItemToStringArray::CreateSP(this, &SWorldHierarchyImpl::TransformLevelToString)
));
}
SearchBoxHierarchyFilter = MakeShareable(new HierarchyFilter(
HierarchyFilter::FItemToStringArray::CreateSP(this, &SWorldHierarchyImpl::TransformItemToString)
));
// Might be overkill to have both filters call full refresh on change, but this should just request a full refresh
// twice instead of actually performing the refresh itself.
if (SearchBoxLevelFilter.IsValid())
{
SearchBoxLevelFilter->OnChanged().AddSP(this, &SWorldHierarchyImpl::FullRefresh);
}
SearchBoxHierarchyFilter->OnChanged().AddSP(this, &SWorldHierarchyImpl::FullRefresh);
FOnContextMenuOpening ContextMenuEvent;
if (!bFoldersOnlyMode)
{
ContextMenuEvent = FOnContextMenuOpening::CreateSP(this, &SWorldHierarchyImpl::ConstructLevelContextMenu);
}
TSharedRef<SWidget> CreateNewFolderButton = SNullWidget::NullWidget;
if (!bFoldersOnlyMode)
{
CreateNewFolderButton =
SNew(SSimpleButton)
.ToolTipText(LOCTEXT("CreateFolderTooltip", "Create a new folder containing the current selection"))
.OnClicked(this, &SWorldHierarchyImpl::OnCreateFolderClicked)
.Visibility(WorldModel->HasFolderSupport() ? EVisibility::Visible : EVisibility::Collapsed)
.Icon(FAppStyle::Get().GetBrush("WorldBrowser.NewFolderIcon"));
}
ChildSlot
.Padding(8.f, 0.f, 8.f, 0.f)
[
SNew(SVerticalBox)
// Hierarchy Toolbar
+SVerticalBox::Slot()
.AutoHeight()
[
SNew(SHorizontalBox)
// Filter box
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
[
SNew(SSearchBox)
.ToolTipText(LOCTEXT("FilterSearchToolTip", "Type here to search Levels"))
.HintText(LOCTEXT("FilterSearchHint", "Search Levels"))
.OnTextChanged(this, &SWorldHierarchyImpl::SetFilterText)
]
// Create New Folder icon
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(2.0f, 0.0f, 0.0f, 0.0f)
[
CreateNewFolderButton
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(2.0f, 0.0f, 0.0f, 0.0f)
[
SNew(SSimpleComboButton)
.OnGetMenuContent( this, &SWorldHierarchyImpl::GetViewButtonContent )
.Icon(FAppStyle::Get().GetBrush("Icons.Settings"))
]
]
// Empty Label
+SVerticalBox::Slot()
.HAlign(HAlign_Center)
[
SNew(STextBlock)
.Visibility(this, &SWorldHierarchyImpl::GetEmptyLabelVisibility)
.Text(LOCTEXT("EmptyLabel", "Empty"))
.ColorAndOpacity(FLinearColor(0.4f, 1.0f, 0.4f))
]
// Hierarchy
+SVerticalBox::Slot()
.FillHeight(1.f)
.Padding(0.f, 4.f, 0.f, 0.f)
[
SNew(SBorder)
.BorderImage(FAppStyle::Get().GetBrush("Brushes.Recessed"))
.Padding(FMargin(0.f, 4.f, 0.f, 0.f))
[
SAssignNew(TreeWidget, SLevelsTreeWidget, WorldModel, SharedThis(this))
.TreeItemsSource(&RootTreeItems)
.SelectionMode(ESelectionMode::Multi)
.OnGenerateRow(this, &SWorldHierarchyImpl::GenerateTreeRow)
.OnGetChildren(this, &SWorldHierarchyImpl::GetChildrenForTree)
.OnSelectionChanged(this, &SWorldHierarchyImpl::OnSelectionChanged)
.OnExpansionChanged(this, &SWorldHierarchyImpl::OnExpansionChanged)
.OnMouseButtonDoubleClick(this, &SWorldHierarchyImpl::OnTreeViewMouseButtonDoubleClick)
.OnContextMenuOpening(ContextMenuEvent)
.OnItemScrolledIntoView(this, &SWorldHierarchyImpl::OnTreeItemScrolledIntoView)
.HeaderRow(CreateHeaderRow())
]
]
// Separator
+SVerticalBox::Slot()
.AutoHeight()
.Padding(0, 0, 0, 1)
[
SNew(SSeparator)
.Visibility(bFoldersOnlyMode ? EVisibility::Collapsed : EVisibility::Visible)
]
// View options
+SVerticalBox::Slot()
.AutoHeight()
[
SNew(SHorizontalBox)
.Visibility(bFoldersOnlyMode ? EVisibility::Collapsed : EVisibility::Visible)
// Asset count
+SHorizontalBox::Slot()
.FillWidth(1.f)
.VAlign(VAlign_Center)
.Padding(8.f)
[
SNew( STextBlock )
.Text( this, &SWorldHierarchyImpl::GetFilterStatusText )
.ColorAndOpacity( this, &SWorldHierarchyImpl::GetFilterStatusTextColor )
]
]
];
if (FLevelFolders::IsAvailable())
{
FLevelFolders& LevelFolders = FLevelFolders::Get();
LevelFolders.OnFolderCreate.AddSP(this, &SWorldHierarchyImpl::OnBroadcastFolderCreate);
LevelFolders.OnFolderMove.AddSP(this, &SWorldHierarchyImpl::OnBroadcastFolderMove);
LevelFolders.OnFolderDelete.AddSP(this, &SWorldHierarchyImpl::OnBroadcastFolderDelete);
if (!bFoldersOnlyMode)
{
FEditorDelegates::PostSaveWorldWithContext.AddSP(this, &SWorldHierarchyImpl::OnWorldSaved);
}
}
if (SearchBoxLevelFilter.IsValid())
{
WorldModel->AddFilter(SearchBoxLevelFilter.ToSharedRef());
}
OnUpdateSelection();
GEditor->RegisterForUndo(this);
}
void SWorldHierarchyImpl::Tick( const FGeometry& AllotedGeometry, const double InCurrentTime, const float InDeltaTime )
{
SCompoundWidget::Tick(AllotedGeometry, InCurrentTime, InDeltaTime);
if (bNeedsRefresh)
{
if (!bIsReentrant)
{
Populate();
}
}
if (bSortDirty)
{
SortItems(RootTreeItems);
for (const auto& Pair : TreeItemMap)
{
Pair.Value->Flags.bChildrenRequiresSort = true;
}
bSortDirty = false;
}
}
void SWorldHierarchyImpl::OnWorldSaved(UWorld* World, FObjectPostSaveContext ObjectSaveContext)
{
if (FLevelFolders::IsAvailable())
{
for (TSharedPtr<FLevelModel> RootLevel : WorldModel->GetRootLevelList())
{
FLevelFolders::Get().SaveLevel(RootLevel.ToSharedRef());
}
}
}
void SWorldHierarchyImpl::RefreshView()
{
bNeedsRefresh = true;
}
TSharedRef<SHeaderRow> SWorldHierarchyImpl::CreateHeaderRow()
{
using namespace UE::WorldHierarchy;
constexpr float IconWidth = 24.f;
TSharedRef<SHeaderRow> HeaderRow = SAssignNew(HeaderRowWidget, SHeaderRow)
.CanSelectGeneratedColumn(true) // Lets the user choose which columns should be shown
.OnHiddenColumnsListChanged(this, &SWorldHierarchyImpl::SaveColumnVisibilitiesIntoConfig)
/** Level visibility column */
+ SHeaderRow::Column(HierarchyColumns::ColumnID_EditorVisibility)
.Visibility(bFoldersOnlyMode ? EVisibility::Collapsed : EVisibility::Visible)
.DefaultLabel(LOCTEXT("Column.Visibility.DefaultLabel", "Visibility"))
.FixedWidth(IconWidth)
.HeaderContent()
[
SNew(SImage)
.Image(FAppStyle::Get().GetBrush("Level.VisibleIcon16x"))
.ColorAndOpacity(FSlateColor::UseForeground())
.ToolTipText(LOCTEXT("Column.Visibility.Tooltip", "Toggles the editor visibility"))
]
/** Level game visibility column */
+ SHeaderRow::Column(HierarchyColumns::ColumnID_GameVisibility)
.Visibility(bFoldersOnlyMode ? EVisibility::Collapsed : EVisibility::Visible)
.DefaultLabel(LOCTEXT("Column.GameVisibility.DefaultLabel", "Game Visibility"))
.FixedWidth(IconWidth)
.HeaderContent()
[
SNew(SImage)
.Image(WorldBrowser::FWorldBrowserStyle::Get().GetBrush( "WorldBrowser.VisibleInGame" ))
.ColorAndOpacity(FSlateColor::UseForeground())
.ToolTipText(LOCTEXT("Column.GameVisibility.Tooltip", "Toggles the visibility in games"))
]
/** LevelName label column */
+ SHeaderRow::Column( HierarchyColumns::ColumnID_LevelLabel )
.FillWidth(1.f)
.DefaultLabel(LOCTEXT("Column.Level.DefaultLabel", "Level"))
.ShouldGenerateWidget(true) // This column cannot be toggled off by the user
.HeaderContent()
[
SNew(STextBlock)
.Text(LOCTEXT("Column.Level.Label", "Level"))
.ToolTipText(LOCTEXT("Column.Level.Tooltip", "Level"))
]
/** Lighting Scenario column */
+ SHeaderRow::Column(HierarchyColumns::ColumnID_LightingScenario)
.Visibility(bFoldersOnlyMode ? EVisibility::Collapsed : EVisibility::Visible)
.DefaultLabel(LOCTEXT("Column.LightingScenario.DefaultLabel", "Lighting Scenario"))
.FixedWidth(IconWidth)
.HeaderContent()
[
SNew(SImage)
.Image(FAppStyle::Get().GetBrush("Level.LightingScenarioIconSolid16x"))
.ColorAndOpacity(FSlateColor::UseForeground())
.ToolTipText(LOCTEXT("Column.LightingScenario.Tooltip", "Lighting Scenario"))
]
/** Level lock column */
+SHeaderRow::Column(HierarchyColumns::ColumnID_Lock)
.Visibility(bFoldersOnlyMode ? EVisibility::Collapsed : EVisibility::Visible)
.DefaultLabel(LOCTEXT("Column.LevelLock.DefaultLabel", "Lock"))
.FixedWidth(IconWidth)
.HeaderContent()
[
SNew(SImage)
.Image(FAppStyle::Get().GetBrush("Icons.Lock"))
.ColorAndOpacity(FSlateColor::UseForeground())
.ToolTipText(LOCTEXT("Column.LevelLock.Tooltip", "Lock"))
]
/** Level kismet column */
+ SHeaderRow::Column(HierarchyColumns::ColumnID_Kismet)
.Visibility(bFoldersOnlyMode ? EVisibility::Collapsed : EVisibility::Visible)
.DefaultLabel(LOCTEXT("Column.Kismet.DefaultLabel", "Open Blueprint"))
.FixedWidth(IconWidth)
.HeaderContent()
[
SNew(SImage)
.ColorAndOpacity(FSlateColor::UseForeground())
.Image(FAppStyle::Get().GetBrush("Icons.Blueprints"))
.ToolTipText(LOCTEXT("Column.Kismet.Tooltip", "Open the level blueprint for this Level"))
]
/** Level SCC status column */
+ SHeaderRow::Column(HierarchyColumns::ColumnID_SCCStatus)
.Visibility(bFoldersOnlyMode ? EVisibility::Collapsed : EVisibility::Visible)
.DefaultLabel(LOCTEXT("Column.SCC.DefaultLabel", "Revision Control"))
.FixedWidth(IconWidth)
.HeaderContent()
[
SNew(SImage)
.ColorAndOpacity(FSlateColor::UseForeground())
.Image(FRevisionControlStyleManager::Get().GetBrush("RevisionControl.Icon"))
.ToolTipText(LOCTEXT("Column.SCC.Tooltip", "Status in Revision Control"))
]
/** Level save column */
+ SHeaderRow::Column(HierarchyColumns::ColumnID_Save)
.Visibility(bFoldersOnlyMode ? EVisibility::Collapsed : EVisibility::Visible)
.DefaultLabel(LOCTEXT("Column.Save.DefaultLabel", "Save"))
.FixedWidth(IconWidth)
.HeaderContent()
[
SNew(SImage)
.ColorAndOpacity(FSlateColor::UseForeground())
.Image(FAppStyle::Get().GetBrush("Icons.SaveModified"))
.ToolTipText(LOCTEXT("Column.Save.Tooltip", "Save this Level"))
]
/** Level color column */
+ SHeaderRow::Column(HierarchyColumns::ColumnID_Color)
.Visibility(bFoldersOnlyMode ? EVisibility::Collapsed : EVisibility::Visible)
.DefaultLabel(LOCTEXT("Column.Color.DefaultLabel", "Color"))
.FixedWidth(IconWidth)
.HeaderContent()
[
SNew(SImage)
.ColorAndOpacity(FSlateColor(FColor::White))
.Image(FAppStyle::Get().GetBrush("Level.ColorIcon"))
.ToolTipText(LOCTEXT("Column.Color.Tooltip", "Color used for visualization of Level"))
];
UWorldBrowserConfig::Initialize();
UWorldBrowserConfig* Config = UWorldBrowserConfig::Get();
for (const TPair<FName, bool>& VisibilityPair : Config->ColumnConfig.ColumnVisibilities)
{
const FName ColumnId = VisibilityPair.Key;
if (!IsRequiredColumn(ColumnId))
{
SetColumnVisible(ColumnId, VisibilityPair.Value);
}
}
return HeaderRow;
}
void SWorldHierarchyImpl::SaveColumnVisibilitiesIntoConfig()
{
if (bIsProgrammaticallyChangingColumnVisibility)
{
return;
}
UWorldBrowserConfig::Initialize();
UWorldBrowserConfig* Config = UWorldBrowserConfig::Get();
TMap<FName, bool>& Visibilities = Config->ColumnConfig.ColumnVisibilities;
Visibilities.Empty();
for (const SHeaderRow::FColumn& Column : HeaderRowWidget->GetColumns())
{
const FName ColumnId = Column.ColumnId;
if (!IsRequiredColumn(ColumnId))
{
Visibilities.Add(ColumnId, IsColumnVisible(ColumnId));
}
}
Config->SaveEditorConfig();
}
bool SWorldHierarchyImpl::IsRequiredColumn(FName ColumnId)
{
return ColumnId == UE::WorldHierarchy::HierarchyColumns::ColumnID_LevelLabel;
}
bool SWorldHierarchyImpl::IsKnownColumn(FName ColumnId)
{
using namespace UE::WorldHierarchy;
return ColumnId == HierarchyColumns::ColumnID_EditorVisibility
|| ColumnId == HierarchyColumns::ColumnID_GameVisibility
|| ColumnId == HierarchyColumns::ColumnID_LevelLabel
|| ColumnId == HierarchyColumns::ColumnID_LightingScenario
|| ColumnId == HierarchyColumns::ColumnID_Lock
|| ColumnId == HierarchyColumns::ColumnID_SCCStatus
|| ColumnId == HierarchyColumns::ColumnID_Save
|| ColumnId == HierarchyColumns::ColumnID_Color
|| ColumnId == HierarchyColumns::ColumnID_Kismet;
}
TSharedRef<ITableRow> SWorldHierarchyImpl::GenerateTreeRow(WorldHierarchy::FWorldTreeItemPtr Item, const TSharedRef<STableViewBase>& OwnerTable)
{
check(Item.IsValid());
return SNew(SWorldHierarchyItem, OwnerTable)
.InWorldModel(WorldModel)
.InHierarchy(SharedThis(this))
.InItemModel(Item)
.IsItemExpanded(Item->Flags.bExpanded)
.HighlightText(this, &SWorldHierarchyImpl::GetSearchBoxText)
.FoldersOnlyMode(bFoldersOnlyMode)
;
}
void SWorldHierarchyImpl::GetChildrenForTree(WorldHierarchy::FWorldTreeItemPtr Item, TArray<WorldHierarchy::FWorldTreeItemPtr>& OutChildren)
{
OutChildren = Item->GetChildren();
if (Item->Flags.bChildrenRequiresSort)
{
if (OutChildren.Num() > 0)
{
SortItems(OutChildren);
// Empty out the children and repopulate them in the correct order
Item->RemoveAllChildren();
for (WorldHierarchy::FWorldTreeItemPtr Child : OutChildren)
{
Item->AddChild(Child.ToSharedRef());
}
}
Item->Flags.bChildrenRequiresSort = false;
}
}
bool SWorldHierarchyImpl::PassesFilter(const WorldHierarchy::IWorldTreeItem& Item)
{
bool bPassesFilter = true;
WorldHierarchy::FFolderTreeItem* Folder = Item.GetAsFolderTreeItem();
if (bFoldersOnlyMode && Folder == nullptr)
{
// Level items should fail to pass the filter if we only want to display folders
bPassesFilter = false;
}
else
{
bPassesFilter = SearchBoxHierarchyFilter->PassesFilter(Item);
}
if (bPassesFilter && ExcludedFolders.Num() > 0)
{
if (Folder != nullptr)
{
FName CheckPath = Folder->GetFullPath();
// Folders should not be shown if it or its parent have been excluded
while (!CheckPath.IsNone())
{
if (ExcludedFolders.Contains(CheckPath))
{
bPassesFilter = false;
break;
}
CheckPath = WorldHierarchy::GetParentPath(CheckPath);
}
}
}
return bPassesFilter;
}
TSharedPtr<SWidget> SWorldHierarchyImpl::ConstructLevelContextMenu() const
{
TSharedRef<SWidget> MenuWidget = SNullWidget::NullWidget;
if (!WorldModel->IsReadOnly())
{
UToolMenus* ToolMenus = UToolMenus::Get();
static const FName MenuName = "WorldBrowser.WorldHierarchy.LevelContextMenu";
if (!ToolMenus->IsMenuRegistered(MenuName))
{
ToolMenus->RegisterMenu(MenuName);
}
FToolMenuContext Context(WorldModel->GetCommandList(), TSharedPtr<FExtender>());
UToolMenu* Menu = ToolMenus->GenerateMenu(MenuName, Context);
TArray<WorldHierarchy::FWorldTreeItemPtr> SelectedItems = GetSelectedTreeItems();
if (SelectedItems.Num() == 1)
{
// If exactly one item is selected, allow it to generate its own context menu
SelectedItems[0]->GenerateContextMenu(Menu, *this);
}
else if (SelectedItems.Num() == 0)
{
// If no items are selected, allow the first root level item to create a context menu
RootTreeItems[0]->GenerateContextMenu(Menu, *this);
}
Menu->AddDynamicSection("HierarchyDynamicSection", FNewToolMenuDelegateLegacy::CreateLambda([this](FMenuBuilder& MenuBuilder, UToolMenu* Menu)
{
const bool bIsGameVisibilityColumnVisible = IsColumnVisible(UE::WorldHierarchy::HierarchyColumns::ColumnID_GameVisibility);
WorldModel->BuildHierarchyMenu(
MenuBuilder,
bIsGameVisibilityColumnVisible ? EBuildHierarchyMenuFlags::ShowGameVisibility : EBuildHierarchyMenuFlags::None
);
}));
// Generate the "Move To" and "Select" submenus based on the current selection
if (WorldModel->HasFolderSupport())
{
bool bOnlyFoldersSelected = SelectedItems.Num() > 0;
bool bAllSelectedItemsCanMove = SelectedItems.Num() > 0;
for (WorldHierarchy::FWorldTreeItemPtr Item : SelectedItems)
{
bOnlyFoldersSelected &= Item->GetAsFolderTreeItem() != nullptr;
bAllSelectedItemsCanMove &= Item->CanChangeParents();
if (!bOnlyFoldersSelected && !bAllSelectedItemsCanMove)
{
// Neither submenu can be built, kill the check
break;
}
}
if (bAllSelectedItemsCanMove && FLevelFolders::IsAvailable())
{
FToolMenuSection& Section = Menu->AddSection("Section");
Section.AddSubMenu(
"MoveSelectionTo",
LOCTEXT("MoveSelectionTo", "Move To"),
LOCTEXT("MoveSelectionTo_Tooltip", "Move selection to another folder"),
FNewMenuDelegate::CreateSP(const_cast<SWorldHierarchyImpl*>(this), &SWorldHierarchyImpl::FillFoldersSubmenu)
);
}
if (bOnlyFoldersSelected)
{
FToolMenuSection& Section = Menu->AddSection("Section");
Section.AddSubMenu(
"SelectSubmenu",
LOCTEXT("SelectSubmenu", "Select"),
LOCTEXT("SelectSubmenu_Tooltip", "Select child items of the current selection"),
FNewMenuDelegate::CreateSP(const_cast<SWorldHierarchyImpl*>(this), &SWorldHierarchyImpl::FillSelectionSubmenu)
);
}
}
MenuWidget = ToolMenus->GenerateWidget(Menu);
}
return MenuWidget;
}
void SWorldHierarchyImpl::FillFoldersSubmenu(FMenuBuilder& MenuBuilder)
{
TArray<WorldHierarchy::FWorldTreeItemPtr> SelectedItems = GetSelectedTreeItems();
check(SelectedItems.Num() > 0);
// Assume that the root item of the first selected item is the root for all of them
TSharedPtr<FLevelModel> RootItem = SelectedItems[0]->GetRootItem();
FName RootPath = NAME_None;
MenuBuilder.AddMenuEntry(
LOCTEXT("CreateNewFolder", "Create New Folder"),
LOCTEXT("CreateNewFolder_Tooltip", "Move the selection to a new folder"),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "WorldBrowser.NewFolderIcon"),
FExecuteAction::CreateSP(this, &SWorldHierarchyImpl::CreateFolder, RootItem, RootPath, /*bMoveSelected*/ true)
);
AddMoveToFolderOutliner(MenuBuilder, SelectedItems, RootItem.ToSharedRef());
}
void SWorldHierarchyImpl::AddMoveToFolderOutliner(FMenuBuilder& MenuBuilder, const TArray<WorldHierarchy::FWorldTreeItemPtr>& SelectedItems, TSharedRef<FLevelModel> RootItem)
{
FLevelFolders& LevelFolders = FLevelFolders::Get();
if (LevelFolders.GetFolderProperties(RootItem).Num() > 0)
{
TSet<FName> ExcludedFolderPaths;
// Exclude selected folders
for (WorldHierarchy::FWorldTreeItemPtr Item : SelectedItems)
{
if (WorldHierarchy::FFolderTreeItem* Folder = Item->GetAsFolderTreeItem())
{
ExcludedFolderPaths.Add(Folder->GetFullPath());
}
}
// Copy the world model to ensure that any delegates fired for the mini hierarchy doesn't affect the main hierarchy
FWorldBrowserModule& WorldBrowserModule = FModuleManager::LoadModuleChecked<FWorldBrowserModule>("WorldBrowser");
TSharedPtr<FLevelCollectionModel> WorldModelCopy = WorldBrowserModule.SharedWorldModel(WorldModel->GetWorld());
TSharedRef<SWidget> MiniHierarchy =
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.MaxHeight(400.0f)
[
SNew(SWorldHierarchyImpl)
.InWorldModel(WorldModelCopy)
.ShowFoldersOnly(true)
.InExcludedFolders(ExcludedFolderPaths)
.OnItemPickedDelegate(FOnWorldHierarchyItemPicked::CreateSP(this, &SWorldHierarchyImpl::MoveSelectionTo))
];
MenuBuilder.BeginSection(FName(), LOCTEXT("ExistingFolders", "Existing:"));
MenuBuilder.AddWidget(MiniHierarchy, FText::GetEmpty(), false);
MenuBuilder.EndSection();
}
}
void SWorldHierarchyImpl::MoveSelectionTo(WorldHierarchy::FWorldTreeItemRef Item)
{
FSlateApplication::Get().DismissAllMenus();
TSharedPtr<FLevelModel> RootLevel = Item->GetRootItem();
FName Path = NAME_None;
if (WorldHierarchy::FFolderTreeItem* Folder = Item->GetAsFolderTreeItem())
{
Path = Folder->GetFullPath();
}
MoveItemsTo(RootLevel, Path);
RefreshView();
}
void SWorldHierarchyImpl::FillSelectionSubmenu(FMenuBuilder& MenuBuilder)
{
const bool bSelectAllDescendants = true;
MenuBuilder.AddMenuEntry(
LOCTEXT("SelectImmediateChildren", "Immediate Children"),
LOCTEXT("SelectImmediateChildren_Tooltip", "Select all immediate children of the selected folders"),
FSlateIcon(),
FExecuteAction::CreateSP(this, &SWorldHierarchyImpl::SelectFolderDescendants, !bSelectAllDescendants)
);
MenuBuilder.AddMenuEntry(
LOCTEXT("SelectAllDescendants", "All Descendants"),
LOCTEXT("SelectAllDescendants_Tooltip", "Selects all descendants of the selected folders"),
FSlateIcon(),
FExecuteAction::CreateSP(this, &SWorldHierarchyImpl::SelectFolderDescendants, bSelectAllDescendants)
);
}
void SWorldHierarchyImpl::SelectFolderDescendants(bool bSelectAllDescendants)
{
TArray<WorldHierarchy::FWorldTreeItemPtr> OldSelection = GetSelectedTreeItems();
FLevelModelList SelectedLevels;
TreeWidget->ClearSelection();
for (WorldHierarchy::FWorldTreeItemPtr Item : OldSelection)
{
for (WorldHierarchy::FWorldTreeItemPtr Child : Item->GetChildren())
{
if (bSelectAllDescendants)
{
SelectedLevels.Append(Child->GetLevelModels());
}
else
{
SelectedLevels.Append(Child->GetModel());
}
}
}
if (SelectedLevels.Num() > 0)
{
WorldModel->SetSelectedLevels(SelectedLevels);
}
}
void SWorldHierarchyImpl::MoveDroppedItems(const TArray<WorldHierarchy::FWorldTreeItemPtr>& DraggedItems, FName FolderPath)
{
if (DraggedItems.Num() > 0)
{
// Ensure that the dragged items are selected in the tree
TreeWidget->ClearSelection();
for (WorldHierarchy::FWorldTreeItemPtr Item : DraggedItems)
{
TreeWidget->SetItemSelection(Item, true);
}
// Assume that the root of the first is the root of all the items
const FScopedTransaction Transaction(LOCTEXT("ItemsMoved", "Move World Hierarchy Items"));
MoveItemsTo(DraggedItems[0]->GetRootItem(), FolderPath);
RefreshView();
}
}
void SWorldHierarchyImpl::AddDroppedLevelsToFolder(const TArray<FAssetData>& WorldAssetList, FName FolderPath)
{
if (WorldAssetList.Num() > 0)
{
// Populate the set of existing levels in the world
TSet<FName> ExistingLevels;
for (TSharedPtr<FLevelModel> Level : WorldModel->GetAllLevels())
{
ExistingLevels.Add(Level->GetLongPackageName());
}
WorldModel->AddExistingLevelsFromAssetData(WorldAssetList);
// Set the folder path of any newly added levels
for (TSharedPtr<FLevelModel> Level : WorldModel->GetAllLevels())
{
if (!ExistingLevels.Contains(Level->GetLongPackageName()))
{
Level->SetFolderPath(FolderPath);
}
}
RefreshView();
}
}
bool SWorldHierarchyImpl::IsColumnVisible(FName ColumnId) const
{
return HeaderRowWidget->IsColumnVisible(ColumnId);
}
void SWorldHierarchyImpl::SetColumnVisible(FName ColumnId, bool bVisible)
{
if (IsKnownColumn(ColumnId))
{
TGuardValue Guard(bIsProgrammaticallyChangingColumnVisibility, true);
HeaderRowWidget->SetShowGeneratedColumn(ColumnId, bVisible);
}
}
bool SWorldHierarchyImpl::IsVisibleInConfig(FName ColumnId)
{
UWorldBrowserConfig::Initialize();
UWorldBrowserConfig* Config = UWorldBrowserConfig::Get();
const bool* bIsVisible = Config->ColumnConfig.ColumnVisibilities.Find(ColumnId);
return IsRequiredColumn(ColumnId) || !bIsVisible || *bIsVisible;
}
void SWorldHierarchyImpl::SetWillBeVisibleInConfigTransient(FName ColumnId, bool bIsVisible)
{
if (!IsRequiredColumn(ColumnId) && IsKnownColumn(ColumnId))
{
UWorldBrowserConfig::Initialize();
UWorldBrowserConfig* Config = UWorldBrowserConfig::Get();
Config->ColumnConfig.ColumnVisibilities.Add(ColumnId, bIsVisible);
}
}
void SWorldHierarchyImpl::OnTreeItemScrolledIntoView( WorldHierarchy::FWorldTreeItemPtr Item, const TSharedPtr<ITableRow>& Widget )
{
if (Item == ItemPendingRename)
{
ItemPendingRename = nullptr;
Item->RenameRequestEvent.ExecuteIfBound();
}
}
void SWorldHierarchyImpl::OnExpansionChanged(WorldHierarchy::FWorldTreeItemPtr Item, bool bIsItemExpanded)
{
Item->SetExpansion(bIsItemExpanded);
WorldHierarchy::FFolderTreeItem* Folder = Item->GetAsFolderTreeItem();
if (FLevelFolders::IsAvailable() && Folder != nullptr)
{
if (FLevelFolderProps* Props = FLevelFolders::Get().GetFolderProperties(Item->GetRootItem().ToSharedRef(), Folder->GetFullPath()))
{
Props->bExpanded = Item->Flags.bExpanded;
}
}
RefreshView();
}
void SWorldHierarchyImpl::OnSelectionChanged(const WorldHierarchy::FWorldTreeItemPtr Item, ESelectInfo::Type SelectInfo)
{
if (bUpdatingSelection)
{
return;
}
bUpdatingSelection = true;
TArray<WorldHierarchy::FWorldTreeItemPtr> SelectedItems = GetSelectedTreeItems();
FLevelModelList SelectedLevels;
for (const WorldHierarchy::FWorldTreeItemPtr& TreeItem : SelectedItems)
{
// Folder items should return all child models, but anything else should only return the model for that item
if (TreeItem->GetAsFolderTreeItem() != nullptr)
{
SelectedLevels.Append(TreeItem->GetLevelModels());
}
else
{
SelectedLevels.Append(TreeItem->GetModel());
}
}
if (!bFoldersOnlyMode)
{
WorldModel->SetSelectedLevels(SelectedLevels);
}
bUpdatingSelection = false;
if (TreeWidget->GetNumItemsSelected() > 0)
{
OnItemPicked.ExecuteIfBound(GetSelectedTreeItems()[0].ToSharedRef());
}
}
void SWorldHierarchyImpl::OnUpdateSelection()
{
if (bUpdatingSelection)
{
return;
}
bUpdatingSelection = true;
ItemsSelectedAfterRefresh.Empty();
const FLevelModelList& SelectedItems = WorldModel->GetSelectedLevels();
TreeWidget->ClearSelection();
// To get the list of items that should be displayed as selected we need to find the level tree items belonging to the selected level models.
if (SelectedItems.Num() > 0)
{
for (auto It = TreeItemMap.CreateConstIterator(); It; ++It)
{
WorldHierarchy::FWorldTreeItemPtr TreeItemPtr = It->Value;
if (TreeItemPtr.IsValid())
{
for (auto SelectedItemIt = SelectedItems.CreateConstIterator(); SelectedItemIt; ++SelectedItemIt)
{
if (TreeItemPtr->HasModel(*SelectedItemIt))
{
ItemsSelectedAfterRefresh.Add(It->Key);
break;
}
}
}
}
}
RefreshView();
bUpdatingSelection = false;
}
void SWorldHierarchyImpl::OnTreeViewMouseButtonDoubleClick(WorldHierarchy::FWorldTreeItemPtr Item)
{
if (Item->CanBeCurrent())
{
Item->MakeCurrent();
}
else
{
Item->SetExpansion(!Item->Flags.bExpanded);
TreeWidget->SetItemExpansion(Item, Item->Flags.bExpanded);
}
}
FReply SWorldHierarchyImpl::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent)
{
if (WorldModel->GetCommandList()->ProcessCommandBindings(InKeyEvent))
{
return FReply::Handled();
}
else if ( InKeyEvent.GetKey() == EKeys::F2 )
{
// If a single folder is selected, F2 should attempt to rename it
if (TreeWidget->GetNumItemsSelected() == 1)
{
WorldHierarchy::FWorldTreeItemPtr ItemToRename = GetSelectedTreeItems()[0];
if (ItemToRename->GetAsFolderTreeItem() != nullptr)
{
ItemPendingRename = ItemToRename;
ScrollItemIntoView(ItemToRename);
return FReply::Handled();
}
}
}
else if ( InKeyEvent.GetKey() == EKeys::Platform_Delete )
{
// Delete was pressed, but no levels were unloaded. Any selected folders should be removed transactionally
const bool bTransactional = true;
DeleteFolders(GetSelectedTreeItems(), bTransactional);
}
// F5 (Refresh) should be handled by the world model
return SCompoundWidget::OnKeyDown(MyGeometry, InKeyEvent);
}
void SWorldHierarchyImpl::OnBroadcastLevelsUnloaded()
{
// We deleted levels from the hierarchy, so do not record the folder delete transaction either
const bool bTransactional = false;
DeleteFolders(GetSelectedTreeItems(), bTransactional);
}
void SWorldHierarchyImpl::InitiateRename(WorldHierarchy::FWorldTreeItemRef InItem)
{
// Only folders items are valid for rename in this view
if (InItem->GetAsFolderTreeItem() != nullptr)
{
ItemPendingRename = InItem;
ScrollItemIntoView(InItem);
}
}
void SWorldHierarchyImpl::EmptyTreeItems()
{
for (auto& Pair : TreeItemMap)
{
Pair.Value->RemoveAllChildren();
}
PendingOperations.Empty();
TreeItemMap.Reset();
PendingTreeItemMap.Reset();
RootTreeItems.Empty();
NewItemActions.Empty();
ItemPendingRename.Reset();
}
void SWorldHierarchyImpl::RepopulateEntireTree()
{
EmptyTreeItems();
for (const TSharedPtr<FLevelModel>& Level : WorldModel->GetFilteredLevels())
{
if (Level.IsValid())
{
ConstructItemFor<WorldHierarchy::FLevelModelTreeItem>(Level.ToSharedRef());
}
}
if (FLevelFolders::IsAvailable() && WorldModel->HasFolderSupport())
{
FLevelFolders& LevelFolders = FLevelFolders::Get();
// Add any folders which might match the search terms for each root level
for (TSharedPtr<FLevelModel> RootLevel : WorldModel->GetRootLevelList())
{
for (const auto& Pair : LevelFolders.GetFolderProperties(RootLevel.ToSharedRef()))
{
if (!TreeItemMap.Contains(Pair.Key))
{
ConstructItemFor<WorldHierarchy::FFolderTreeItem>(Pair.Key);
}
}
}
}
}
TMap<WorldHierarchy::FWorldTreeItemID, bool> SWorldHierarchyImpl::GetParentsExpansionState() const
{
TMap<WorldHierarchy::FWorldTreeItemID, bool> ExpansionStates;
for (const auto& Pair : TreeItemMap)
{
if (Pair.Value->GetChildren().Num() > 0)
{
ExpansionStates.Add(Pair.Key, Pair.Value->Flags.bExpanded);
}
}
return ExpansionStates;
}
void SWorldHierarchyImpl::SetParentsExpansionState(const TMap<WorldHierarchy::FWorldTreeItemID, bool>& ExpansionInfo)
{
for (const auto& Pair : TreeItemMap)
{
auto& Item = Pair.Value;
if (Item->GetChildren().Num() > 0)
{
const bool* bExpandedPtr = ExpansionInfo.Find(Pair.Key);
bool bExpanded = bExpandedPtr != nullptr ? *bExpandedPtr : Item->Flags.bExpanded;
TreeWidget->SetItemExpansion(Item, bExpanded);
}
}
}
void SWorldHierarchyImpl::OnBroadcastFolderCreate(TSharedPtr<FLevelModel> LevelModel, FName NewPath)
{
if (!TreeItemMap.Contains(NewPath))
{
ConstructItemFor<WorldHierarchy::FFolderTreeItem>(NewPath);
}
}
void SWorldHierarchyImpl::OnBroadcastFolderDelete(TSharedPtr<FLevelModel> LevelModel, FName Path)
{
WorldHierarchy::FWorldTreeItemPtr* Folder = TreeItemMap.Find(Path);
if (Folder != nullptr)
{
PendingOperations.Emplace(WorldHierarchy::FPendingWorldTreeOperation::Removed, Folder->ToSharedRef());
RefreshView();
}
}
void SWorldHierarchyImpl::OnBroadcastFolderMove(TSharedPtr<FLevelModel> LevelModel, FName OldPath, FName NewPath)
{
WorldHierarchy::FWorldTreeItemPtr Folder = TreeItemMap.FindRef(OldPath);
if (Folder.IsValid())
{
// Remove the item with the old ID
TreeItemMap.Remove(Folder->GetID());
// Get all items that were moved
TArray<WorldHierarchy::FWorldTreeItemPtr> AllSelectedItems = GetSelectedTreeItems();
// Change the path, and place it back in the tree with the new ID
{
WorldHierarchy::FFolderTreeItem* FolderItem = Folder->GetAsFolderTreeItem();
FolderItem->SetNewPath(NewPath);
}
for (WorldHierarchy::FWorldTreeItemPtr Child : Folder->GetChildren())
{
// Any level model children that were not explicitly moved will need to be moved here to remain in
// sync with their parent folders
if (!AllSelectedItems.Contains(Child) && Child->GetAsLevelModelTreeItem() != nullptr)
{
Child->SetParentPath(NewPath);
}
}
TreeItemMap.Add(Folder->GetID(), Folder);
PendingOperations.Emplace(WorldHierarchy::FPendingWorldTreeOperation::Moved, Folder.ToSharedRef());
RefreshView();
}
}
void SWorldHierarchyImpl::FullRefresh()
{
bFullRefresh = true;
RefreshView();
}
void SWorldHierarchyImpl::RebuildFoldersAndFullRefresh()
{
bRebuildFolders = true;
FullRefresh();
}
void SWorldHierarchyImpl::RequestSort()
{
bSortDirty = true;
}
void SWorldHierarchyImpl::Populate()
{
TGuardValue<bool> ReentrantGuard(bIsReentrant, true);
bool bMadeSignificantChanges = false;
const TMap<WorldHierarchy::FWorldTreeItemID, bool> ExpansionStateInfo = GetParentsExpansionState();
if (bRebuildFolders)
{
if (FLevelFolders::IsAvailable())
{
FLevelFolders& LevelFolders = FLevelFolders::Get();
for (TSharedPtr<FLevelModel> LevelModel : WorldModel->GetRootLevelList())
{
LevelFolders.RebuildFolderList(LevelModel.ToSharedRef());
}
}
bRebuildFolders = false;
}
if (bFullRefresh)
{
RepopulateEntireTree();
bFullRefresh = false;
bMadeSignificantChanges = true;
}
if (PendingOperations.Num() > 0)
{
const int32 End = FMath::Min(PendingOperations.Num(), MaxPendingOperations);
for (int32 Index = 0; Index < End; ++Index)
{
const WorldHierarchy::FPendingWorldTreeOperation& PendingOp = PendingOperations[Index];
switch (PendingOp.Operation)
{
case WorldHierarchy::FPendingWorldTreeOperation::Added:
bMadeSignificantChanges = AddItemToTree(PendingOp.Item);
break;
case WorldHierarchy::FPendingWorldTreeOperation::Moved:
bMadeSignificantChanges = true;
OnItemMoved(PendingOp.Item);
break;
case WorldHierarchy::FPendingWorldTreeOperation::Removed:
bMadeSignificantChanges = true;
RemoveItemFromTree(PendingOp.Item);
break;
default:
check(false);
break;
}
}
PendingOperations.RemoveAt(0, End);
}
SetParentsExpansionState(ExpansionStateInfo);
if (ItemsSelectedAfterRefresh.Num() > 0)
{
bool bScrolledIntoView = false;
for (const WorldHierarchy::FWorldTreeItemID& ID : ItemsSelectedAfterRefresh)
{
if (TreeItemMap.Contains(ID))
{
WorldHierarchy::FWorldTreeItemPtr Item = TreeItemMap[ID];
for (WorldHierarchy::FWorldTreeItemPtr ItemParent = Item->GetParent(); ItemParent.IsValid(); ItemParent = ItemParent->GetParent())
{
TreeWidget->SetItemExpansion(ItemParent, true);
}
TreeWidget->SetItemSelection(Item, true);
if (!bScrolledIntoView)
{
bScrolledIntoView = true;
TreeWidget->RequestScrollIntoView(Item);
}
}
}
ItemsSelectedAfterRefresh.Empty();
}
if (bMadeSignificantChanges)
{
RequestSort();
}
TreeWidget->RequestTreeRefresh();
if (PendingOperations.Num() == 0)
{
NewItemActions.Empty();
bNeedsRefresh = false;
}
}
bool SWorldHierarchyImpl::AddItemToTree(WorldHierarchy::FWorldTreeItemRef InItem)
{
const WorldHierarchy::FWorldTreeItemID ItemID = InItem->GetID();
bool bItemAdded = false;
PendingTreeItemMap.Remove(ItemID);
if (!TreeItemMap.Find(ItemID))
{
// Not currently in the tree, check if the item passes the current filter
bool bFilteredOut = !PassesFilter(*InItem);
InItem->Flags.bFilteredOut = bFilteredOut;
if (!bFilteredOut)
{
AddUnfilteredItemToTree(InItem);
bItemAdded = true;
if (WorldHierarchy::ENewItemAction* ActionPtr = NewItemActions.Find(ItemID))
{
WorldHierarchy::ENewItemAction Actions = *ActionPtr;
if ((Actions & WorldHierarchy::ENewItemAction::Select) != WorldHierarchy::ENewItemAction::None)
{
TreeWidget->ClearSelection();
TreeWidget->SetItemSelection(InItem, true);
}
if ((Actions & WorldHierarchy::ENewItemAction::Rename) != WorldHierarchy::ENewItemAction::None)
{
ItemPendingRename = InItem;
}
WorldHierarchy::ENewItemAction ScrollIntoView = WorldHierarchy::ENewItemAction::ScrollIntoView | WorldHierarchy::ENewItemAction::Rename;
if ((Actions & ScrollIntoView) != WorldHierarchy::ENewItemAction::None)
{
ScrollItemIntoView(InItem);
}
}
}
}
return bItemAdded;
}
void SWorldHierarchyImpl::AddUnfilteredItemToTree(WorldHierarchy::FWorldTreeItemRef InItem)
{
WorldHierarchy::FWorldTreeItemPtr Parent = EnsureParentForItem(InItem);
const WorldHierarchy::FWorldTreeItemID ItemID = InItem->GetID();
if (TreeItemMap.Contains(ItemID))
{
UE_LOG(LogWorldHierarchy, Error, TEXT("(%d | %s) already exists in the World Hierarchy. Dumping map..."), GetTypeHash(ItemID), *InItem->GetDisplayString());
for (const auto& Entry : TreeItemMap)
{
UE_LOG(LogWorldHierarchy, Log, TEXT("(%d | %s)"), GetTypeHash(Entry.Key), *Entry.Value->GetDisplayString());
}
// Treat this as a fatal error
check(false);
}
TreeItemMap.Add(ItemID, InItem);
if (Parent.IsValid())
{
Parent->AddChild(InItem);
}
else
{
RootTreeItems.Add(InItem);
}
if (FLevelFolders::IsAvailable())
{
WorldHierarchy::FFolderTreeItem* Folder = InItem->GetAsFolderTreeItem();
if (Folder != nullptr)
{
if (const FLevelFolderProps* Props = FLevelFolders::Get().GetFolderProperties(InItem->GetRootItem().ToSharedRef(), Folder->GetFullPath()))
{
InItem->SetExpansion(Props->bExpanded);
}
}
}
}
void SWorldHierarchyImpl::RemoveItemFromTree(WorldHierarchy::FWorldTreeItemRef InItem)
{
if (TreeItemMap.Contains(InItem->GetID()))
{
WorldHierarchy::FWorldTreeItemPtr Parent = InItem->GetParent();
if (Parent.IsValid())
{
Parent->RemoveChild(InItem);
OnChildRemovedFromParent(Parent.ToSharedRef());
}
else
{
RootTreeItems.Remove(InItem);
}
TreeItemMap.Remove(InItem->GetID());
}
}
void SWorldHierarchyImpl::OnItemMoved(WorldHierarchy::FWorldTreeItemRef InItem)
{
// If the item no longer matches the filter, remove it from the tree
if (!InItem->Flags.bFilteredOut && !PassesFilter(*InItem))
{
RemoveItemFromTree(InItem);
}
else
{
WorldHierarchy::FWorldTreeItemPtr Parent = InItem->GetParent();
if (Parent.IsValid())
{
Parent->RemoveChild(InItem);
OnChildRemovedFromParent(Parent.ToSharedRef());
}
else
{
RootTreeItems.Remove(InItem);
}
Parent = EnsureParentForItem(InItem);
if (Parent.IsValid())
{
Parent->AddChild(InItem);
Parent->SetExpansion(true);
TreeWidget->SetItemExpansion(Parent, true);
}
else
{
RootTreeItems.Add(InItem);
}
}
}
void SWorldHierarchyImpl::ScrollItemIntoView(WorldHierarchy::FWorldTreeItemPtr Item)
{
WorldHierarchy::FWorldTreeItemPtr Parent = Item->GetParent();
while (Parent.IsValid())
{
TreeWidget->SetItemExpansion(Parent, true);
Parent = Parent->GetParent();
}
TreeWidget->RequestScrollIntoView(Item);
}
void SWorldHierarchyImpl::OnChildRemovedFromParent(WorldHierarchy::FWorldTreeItemRef InParent)
{
if (InParent->Flags.bFilteredOut && InParent->GetChildren().Num() == 0)
{
// Parent does not match the search terms nor does it have any children that matches the search terms
RemoveItemFromTree(InParent);
}
}
WorldHierarchy::FWorldTreeItemPtr SWorldHierarchyImpl::EnsureParentForItem(WorldHierarchy::FWorldTreeItemRef Item)
{
WorldHierarchy::FWorldTreeItemID ParentID = Item->GetParentID();
WorldHierarchy::FWorldTreeItemPtr ParentPtr;
if (TreeItemMap.Contains(ParentID))
{
ParentPtr = TreeItemMap[ParentID];
}
else
{
ParentPtr = Item->CreateParent();
if (ParentPtr.IsValid())
{
AddUnfilteredItemToTree(ParentPtr.ToSharedRef());
}
}
return ParentPtr;
}
bool SWorldHierarchyImpl::IsTreeItemExpanded(WorldHierarchy::FWorldTreeItemPtr Item) const
{
return Item->Flags.bExpanded;
}
void SWorldHierarchyImpl::SortItems(TArray<WorldHierarchy::FWorldTreeItemPtr>& Items)
{
if (Items.Num() > 1)
{
Items.Sort([](WorldHierarchy::FWorldTreeItemPtr Item1, WorldHierarchy::FWorldTreeItemPtr Item2) {
const int32 Priority1 = Item1->GetSortPriority();
const int32 Priority2 = Item2->GetSortPriority();
if (Priority1 == Priority2)
{
return Item1->GetDisplayString() < Item2->GetDisplayString();
}
return Priority1 > Priority2;
});
}
}
void SWorldHierarchyImpl::TransformLevelToString(const FLevelModel* Level, TArray<FString>& OutSearchStrings) const
{
if (Level != nullptr && Level->HasValidPackage())
{
OutSearchStrings.Add(FPackageName::GetShortName(Level->GetLongPackageName()));
}
}
void SWorldHierarchyImpl::TransformItemToString(const WorldHierarchy::IWorldTreeItem& Item, TArray<FString>& OutSearchStrings) const
{
OutSearchStrings.Add(Item.GetDisplayString());
}
void SWorldHierarchyImpl::SetFilterText(const FText& InFilterText)
{
// Ensure that the level and hierarchy filters remain in sync
if (SearchBoxLevelFilter.IsValid())
{
SearchBoxLevelFilter->SetRawFilterText(InFilterText);
}
SearchBoxHierarchyFilter->SetRawFilterText(InFilterText);
}
FText SWorldHierarchyImpl::GetSearchBoxText() const
{
return SearchBoxHierarchyFilter->GetRawFilterText();
}
FText SWorldHierarchyImpl::GetFilterStatusText() const
{
const int32 SelectedLevelsCount = WorldModel->GetSelectedLevels().Num();
const int32 TotalLevelsCount = WorldModel->GetAllLevels().Num();
const int32 FilteredLevelsCount = WorldModel->GetFilteredLevels().Num();
if (!WorldModel->IsFilterActive())
{
if (SelectedLevelsCount == 0)
{
return FText::Format(LOCTEXT("ShowingAllLevelsFmt", "{0} levels"), FText::AsNumber(TotalLevelsCount));
}
else
{
return FText::Format(LOCTEXT("ShowingAllLevelsSelectedFmt", "{0} levels ({1} selected)"), FText::AsNumber(TotalLevelsCount), FText::AsNumber(SelectedLevelsCount));
}
}
else if (WorldModel->IsFilterActive() && FilteredLevelsCount == 0)
{
return FText::Format(LOCTEXT("ShowingNoLevelsFmt", "No matching levels ({0} total)"), FText::AsNumber(TotalLevelsCount));
}
else if (SelectedLevelsCount != 0)
{
return FText::Format(LOCTEXT("ShowingOnlySomeLevelsSelectedFmt", "Showing {0} of {1} levels ({2} selected)"), FText::AsNumber(FilteredLevelsCount), FText::AsNumber(TotalLevelsCount), FText::AsNumber(SelectedLevelsCount));
}
else
{
return FText::Format(LOCTEXT("ShowingOnlySomeLevelsFmt", "Showing {0} of {1} levels"), FText::AsNumber(FilteredLevelsCount), FText::AsNumber(TotalLevelsCount));
}
}
FReply SWorldHierarchyImpl::OnCreateFolderClicked()
{
// Assume that the folder will be created for the first persistent level
TSharedPtr<FLevelModel> PersistentLevel = WorldModel->GetRootLevelList()[0];
CreateFolder(PersistentLevel);
return FReply::Handled();
}
EVisibility SWorldHierarchyImpl::GetEmptyLabelVisibility() const
{
return ( !bFoldersOnlyMode || RootTreeItems.Num() > 0 ) ? EVisibility::Collapsed : EVisibility::Visible;
}
void SWorldHierarchyImpl::CreateFolder(TSharedPtr<FLevelModel> InModel, FName ParentPath /* = NAME_None */, const bool bMoveSelected)
{
if (FLevelFolders::IsAvailable())
{
TSharedPtr<FLevelModel> PersistentLevelModel = InModel;
if (!InModel.IsValid())
{
// We're not making this for any specific level...assume it's the first persistent level in the world
PersistentLevelModel = WorldModel->GetRootLevelList()[0];
}
const FScopedTransaction Transaction(LOCTEXT("UndoAction_CreateFolder", "Create Folder"));
FLevelFolders& LevelFolders = FLevelFolders::Get();
FName NewFolderName = ParentPath;
// Get the folder name for the selected level items
if (NewFolderName.IsNone())
{
// Attempt to find the most relevant shared folder for all selected items
TArray<WorldHierarchy::FWorldTreeItemPtr> SelectedItems = GetSelectedTreeItems();
TSet<FName> SharedAncestorPaths = SelectedItems.Num() > 0 ? SelectedItems[0]->GetAncestorPaths() : TSet<FName>();
for (int32 Index = 1; Index < SelectedItems.Num(); ++Index)
{
SharedAncestorPaths = SharedAncestorPaths.Intersect(SelectedItems[Index]->GetAncestorPaths());
if (SharedAncestorPaths.Num() == 0)
{
// No common ancestor path found, put them at the root
break;
}
}
// Find the longest name in the shared ancestor paths, because that's the most local "root" folder
for (FName Ancestor : SharedAncestorPaths)
{
if (Ancestor.ToString().Len() > NewFolderName.ToString().Len())
{
NewFolderName = Ancestor;
}
}
}
NewFolderName = LevelFolders.GetDefaultFolderName(PersistentLevelModel.ToSharedRef(), NewFolderName);
if (bMoveSelected)
{
MoveItemsTo(PersistentLevelModel, NewFolderName);
}
else if (FLevelFolders::IsAvailable())
{
LevelFolders.CreateFolder(PersistentLevelModel.ToSharedRef(), NewFolderName);
NewItemActions.Add(NewFolderName, WorldHierarchy::ENewItemAction::Select | WorldHierarchy::ENewItemAction::Rename);
}
}
}
void SWorldHierarchyImpl::MoveItemsTo(TSharedPtr<FLevelModel> InModel, FName InPath)
{
if (FLevelFolders::IsAvailable())
{
FLevelFolders& LevelFolders = FLevelFolders::Get();
// Get the selected folders first before any items move
TArray<WorldHierarchy::FWorldTreeItemPtr> PreviouslySelectedItems = GetSelectedTreeItems();
TArray<WorldHierarchy::FFolderTreeItem*> SelectedFolders;
for (WorldHierarchy::FWorldTreeItemPtr Item : PreviouslySelectedItems)
{
if (WorldHierarchy::FFolderTreeItem* Folder = Item->GetAsFolderTreeItem())
{
SelectedFolders.Add(Folder);
}
}
// Move the levels first
LevelFolders.CreateFolderContainingSelectedLevels(WorldModel.ToSharedRef(), InModel.ToSharedRef(), InPath);
// Ensure that any moved levels will have their hierarchy items updated
for (TSharedPtr<FLevelModel> SelectedLevel : WorldModel->GetSelectedLevels())
{
WorldHierarchy::FWorldTreeItemID LevelID(SelectedLevel->GetLevelObject());
if (TreeItemMap.Contains(LevelID))
{
PendingOperations.Emplace(WorldHierarchy::FPendingWorldTreeOperation::Moved, TreeItemMap[LevelID].ToSharedRef());
}
}
// Move any of the previously selected folders
for (WorldHierarchy::FFolderTreeItem* Folder : SelectedFolders)
{
FName OldPath = Folder->GetFullPath();
FName NewPath = FName(*(InPath.ToString() / Folder->GetLeafName().ToString()));
LevelFolders.RenameFolder(Folder->GetRootItem().ToSharedRef(), OldPath, NewPath);
}
NewItemActions.Add(InPath, WorldHierarchy::ENewItemAction::Select | WorldHierarchy::ENewItemAction::Rename);
}
}
void SWorldHierarchyImpl::DeleteFolders(TArray<WorldHierarchy::FWorldTreeItemPtr> SelectedItems, bool bTransactional/* = true*/)
{
TArray<WorldHierarchy::FWorldTreeItemPtr> FolderItems;
TSet<FName> DeletedPaths;
for (WorldHierarchy::FWorldTreeItemPtr Item : SelectedItems)
{
// Only take folder items
if (WorldHierarchy::FFolderTreeItem* Folder = Item->GetAsFolderTreeItem())
{
FolderItems.Add(Item);
DeletedPaths.Add(Folder->GetFullPath());
}
}
FScopedTransaction Transaction(LOCTEXT("DeleteFolderTransaction", "Delete Folder"));
FLevelFolders& LevelFolders = FLevelFolders::Get();
// Folders are deleted one at a time
for (WorldHierarchy::FWorldTreeItemPtr Item : FolderItems)
{
TSharedRef<FLevelModel> LevelModel = Item->GetRootItem().ToSharedRef();
// First, move the folder's children up to the ancestor that will not be deleted
FName ItemPath = Item->GetAsFolderTreeItem()->GetFullPath();
FName ParentPath = ItemPath;
do
{
ParentPath = WorldHierarchy::GetParentPath(ParentPath);
} while (DeletedPaths.Contains(ParentPath) && !ParentPath.IsNone());
TArray<WorldHierarchy::FWorldTreeItemPtr> Children = Item->GetChildren();
for (WorldHierarchy::FWorldTreeItemPtr Child : Children)
{
if (!SelectedItems.Contains(Child))
{
if (WorldHierarchy::FFolderTreeItem* ChildFolder = Child->GetAsFolderTreeItem())
{
FName NewChildPath = ChildFolder->GetLeafName();
if (!ParentPath.IsNone())
{
NewChildPath = FName(*(ParentPath.ToString() / NewChildPath.ToString()));
}
LevelFolders.RenameFolder(LevelModel, ChildFolder->GetFullPath(), NewChildPath);
}
else
{
Child->SetParentPath(ParentPath);
OnItemMoved(Child.ToSharedRef());
}
}
}
// Then delete the folder
LevelFolders.DeleteFolder(LevelModel, ItemPath);
}
if (!bTransactional || FolderItems.Num() == 0)
{
Transaction.Cancel();
}
}
FSlateColor SWorldHierarchyImpl::GetFilterStatusTextColor() const
{
if (!WorldModel->IsFilterActive())
{
// White = no text filter
return FLinearColor(1.0f, 1.0f, 1.0f);
}
else if (WorldModel->GetFilteredLevels().Num() == 0)
{
// Red = no matching actors
return FLinearColor(1.0f, 0.4f, 0.4f);
}
else
{
// Green = found at least one match!
return FLinearColor(0.4f, 1.0f, 0.4f);
}
}
TSharedRef<SWidget> SWorldHierarchyImpl::GetViewButtonContent()
{
FMenuBuilder MenuBuilder(true, NULL);
MenuBuilder.BeginSection("SubLevelsViewMenu", LOCTEXT("ShowHeading", "Show"));
{
MenuBuilder.AddMenuEntry(LOCTEXT("ToggleDisplayPaths", "Display Paths"),
LOCTEXT("ToggleDisplayPaths_Tooltip", "If enabled, displays the path for each level"),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateSP(this, &SWorldHierarchyImpl::ToggleDisplayPaths_Executed),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SWorldHierarchyImpl::GetDisplayPathsState)),
NAME_None,
EUserInterfaceActionType::ToggleButton
);
MenuBuilder.AddMenuEntry(LOCTEXT("ToggleDisplayActorsCount", "Display Actors Count"),
LOCTEXT("ToggleDisplayActorsCount_Tooltip", "If enabled, displays actors count for each level"),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateSP(this, &SWorldHierarchyImpl::ToggleDisplayActorsCount_Executed),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SWorldHierarchyImpl::GetDisplayActorsCountState)),
NAME_None,
EUserInterfaceActionType::ToggleButton
);
}
MenuBuilder.EndSection();
return MenuBuilder.MakeWidget();
}
bool SWorldHierarchyImpl::GetDisplayPathsState() const
{
return WorldModel->GetDisplayPathsState();
}
void SWorldHierarchyImpl::ToggleDisplayActorsCount_Executed()
{
WorldModel->SetDisplayActorsCountState(!WorldModel->GetDisplayActorsCountState());
}
bool SWorldHierarchyImpl::GetDisplayActorsCountState() const
{
return WorldModel->GetDisplayActorsCountState();
}
void SWorldHierarchyImpl::PostUndo(bool bSuccess)
{
if (!bIsReentrant)
{
FullRefresh();
}
}
#undef LOCTEXT_NAMESPACE