Files
UnrealEngineUWP/Engine/Source/Editor/SceneOutliner/Private/SSceneOutliner.cpp
Marc Audy d46179b265 Don't rename selection set in to the PIE world as that has a number of knock on problems
Selection set will not be added to the transaction buffer if it contains any PIE objects
Transaction will not actually be created when clicking on a PIE object in the scene outliner
#codereview Matt.Kuhlenschmidt, Richard.TalbotWatkin

[CL 2606122 by Marc Audy in Main branch]
2015-06-30 14:05:38 -04:00

2368 lines
68 KiB
C++

// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
#include "SceneOutlinerPrivatePCH.h"
#include "SSceneOutliner.h"
#include "ScopedTransaction.h"
#include "ClassIconFinder.h"
#include "SSocketChooser.h"
#include "SceneOutlinerFilters.h"
#include "SceneOutlinerDragDrop.h"
#include "SortHelper.h"
#include "ActorEditorUtils.h"
#include "LevelUtils.h"
#include "EditorActorFolders.h"
#include "SSearchBox.h"
#include "SInlineEditableTextBlock.h"
#include "SNotificationList.h"
#include "NotificationManager.h"
#include "Engine/Selection.h"
#include "EngineUtils.h"
DEFINE_LOG_CATEGORY_STATIC(LogSceneOutliner, Log, All);
#define LOCTEXT_NAMESPACE "SSceneOutliner"
// The amount of time that must pass before the Scene Outliner will attempt a sort when in PIE/SIE.
#define SCENE_OUTLINER_RESORT_TIMER 1.0f
namespace SceneOutliner
{
TSharedPtr< FOutlinerFilter > CreateSelectedActorFilter()
{
auto* Filter = new FOutlinerPredicateFilter(FActorFilterPredicate::CreateStatic([](const AActor* InActor){ return InActor->IsSelected(); }), EDefaultFilterBehaviour::Fail);
// If anything fails this filter, make it non interactive. We don't want to allow selection of implicitly included parents which might nuke the actor selection.
Filter->FailedItemState = EFailedFilterState::NonInteractive;
return MakeShareable( Filter );
}
TSharedPtr< FOutlinerFilter > CreateHideTemporaryActorsFilter()
{
return MakeShareable( new FOutlinerPredicateFilter( FActorFilterPredicate::CreateStatic( []( const AActor* InActor ){
return InActor->GetWorld()->WorldType != EWorldType::PIE || GEditor->ObjectsThatExistInEditorWorld.Get(InActor);
} ), EDefaultFilterBehaviour::Pass ) );
}
TSharedPtr< FOutlinerFilter > CreateIsInCurrentLevelFilter()
{
struct FOnlyCurrentLevelFilter : FOutlinerFilter
{
FOnlyCurrentLevelFilter() : FOutlinerFilter(EDefaultFilterBehaviour::Fail, EFailedFilterState::Interactive) {}
virtual bool PassesFilter(const AActor* InActor) const override
{
return InActor->GetLevel() == InActor->GetWorld()->GetCurrentLevel();
}
};
return MakeShareable( new FOnlyCurrentLevelFilter() );
}
struct FItemSelection : IMutableTreeItemVisitor
{
mutable TArray<FActorTreeItem*> Actors;
mutable TArray<FWorldTreeItem*> Worlds;
mutable TArray<FFolderTreeItem*> Folders;
FItemSelection()
{}
FItemSelection(SOutlinerTreeView& Tree)
{
for (auto& Item : Tree.GetSelectedItems())
{
Item->Visit(*this);
}
}
TArray<TWeakObjectPtr<AActor>> GetWeakActors() const
{
TArray<TWeakObjectPtr<AActor>> ActorArray;
for (const auto* ActorItem : Actors)
{
if (ActorItem->Actor.IsValid())
{
ActorArray.Add(ActorItem->Actor);
}
}
return ActorArray;
}
TArray<AActor*> GetActorPtrs() const
{
TArray<AActor*> ActorArray;
for (const auto* ActorItem : Actors)
{
if (AActor* Actor = ActorItem->Actor.Get())
{
ActorArray.Add(Actor);
}
}
return ActorArray;
}
private:
virtual void Visit(FActorTreeItem& ActorItem) const override
{
Actors.Add(&ActorItem);
}
virtual void Visit(FWorldTreeItem& WorldItem) const override
{
Worlds.Add(&WorldItem);
}
virtual void Visit(FFolderTreeItem& FolderItem) const override
{
Folders.Add(&FolderItem);
}
};
void SSceneOutliner::Construct( const FArguments& InArgs, const FInitializationOptions& InInitOptions )
{
// Copy over the shared data from the initialization options
static_cast<FSharedDataBase&>(*SharedData) = static_cast<const FSharedDataBase&>(InInitOptions);
OnItemPicked = InArgs._OnItemPickedDelegate;
if (InInitOptions.OnSelectionChanged.IsBound())
{
SelectionChanged.Add(InInitOptions.OnSelectionChanged);
}
bFullRefresh = true;
bNeedsRefresh = true;
bIsReentrant = false;
bSortDirty = true;
bActorSelectionDirty = SharedData->Mode == ESceneOutlinerMode::ActorBrowsing;
FilteredActorCount = 0;
SortOutlinerTimer = 0.0f;
bPendingFocusNextFrame = InInitOptions.bFocusSearchBoxWhenOpened;
SortByColumn = FBuiltInColumnTypes::Label();
SortMode = EColumnSortMode::Ascending;
// @todo outliner: Should probably save this in layout!
// @todo outliner: Should save spacing for list view in layout
NoBorder = FEditorStyle::GetBrush( "LevelViewport.NoViewportBorder" );
PlayInEditorBorder = FEditorStyle::GetBrush( "LevelViewport.StartingPlayInEditorBorder" );
SimulateBorder = FEditorStyle::GetBrush( "LevelViewport.StartingSimulateBorder" );
//Setup the SearchBox filter
{
auto Delegate = TreeItemTextFilter::FItemToStringArray::CreateSP( this, &SSceneOutliner::PopulateSearchStrings );
SearchBoxFilter = MakeShareable( new TreeItemTextFilter( Delegate ) );
}
TSharedRef<SVerticalBox> VerticalBox = SNew(SVerticalBox);
// We use the filter collection provided, otherwise we create our own
Filters = InInitOptions.Filters.IsValid() ? InInitOptions.Filters : MakeShareable(new FOutlinerFilters);
SearchBoxFilter->OnChanged().AddSP( this, &SSceneOutliner::FullRefresh );
Filters->OnChanged().AddSP( this, &SSceneOutliner::FullRefresh );
//Apply custom filters based on global preferences
if (SharedData->Mode == ESceneOutlinerMode::ActorBrowsing)
{
ApplyShowOnlySelectedFilter( IsShowingOnlySelected() );
ApplyHideTemporaryActorsFilter( IsHidingTemporaryActors() );
ApplyShowOnlyCurrentLevelFilter( IsShowingOnlyCurrentLevel() );
}
TSharedRef< SHeaderRow > HeaderRowWidget =
SNew( SHeaderRow )
// Only show the list header if the user configured the outliner for that
.Visibility(InInitOptions.bShowHeaderRow ? EVisibility::Visible : EVisibility::Collapsed);
SetupColumns(*HeaderRowWidget);
ChildSlot
[
SNew( SBorder )
.BorderImage( this, &SSceneOutliner::OnGetBorderBrush )
.BorderBackgroundColor( this, &SSceneOutliner::OnGetBorderColorAndOpacity )
.ShowEffectWhenDisabled( false )
[
VerticalBox
]
];
auto Toolbar = SNew(SHorizontalBox);
Toolbar->AddSlot()
.VAlign(VAlign_Center)
[
SAssignNew( FilterTextBoxWidget, SSearchBox )
.Visibility( InInitOptions.bShowSearchBox ? EVisibility::Visible : EVisibility::Collapsed )
.HintText( LOCTEXT( "FilterSearch", "Search..." ) )
.ToolTipText( LOCTEXT("FilterSearchHint", "Type here to search (pressing enter selects the results)") )
.OnTextChanged( this, &SSceneOutliner::OnFilterTextChanged )
.OnTextCommitted( this, &SSceneOutliner::OnFilterTextCommitted )
];
if (SharedData->Mode == ESceneOutlinerMode::ActorBrowsing && InInitOptions.bShowCreateNewFolder)
{
Toolbar->AddSlot()
.VAlign(VAlign_Center)
.AutoWidth()
.Padding(4.f, 0.f, 0.f, 0.f)
[
SNew(SButton)
.ButtonStyle(FEditorStyle::Get(), "HoverHintOnly")
.ToolTipText(LOCTEXT("CreateFolderToolTip", "Create a new folder containing the current actor selection"))
.OnClicked(this, &SSceneOutliner::OnCreateFolderClicked)
[
SNew(SImage)
.Image(FEditorStyle::GetBrush("SceneOutliner.NewFolderIcon"))
]
];
}
VerticalBox->AddSlot()
.AutoHeight()
.Padding( 0.0f, 0.0f, 0.0f, 4.0f )
[
Toolbar
];
VerticalBox->AddSlot()
.FillHeight( 1.0f )
[
SNew( SOverlay )
+SOverlay::Slot()
.HAlign( HAlign_Center )
[
SNew( STextBlock )
.Visibility( this, &SSceneOutliner::GetEmptyLabelVisibility )
.Text( LOCTEXT( "EmptyLabel", "Empty" ) )
.ColorAndOpacity( FLinearColor( 0.4f, 1.0f, 0.4f ) )
]
+SOverlay::Slot()
[
SAssignNew( OutlinerTreeView, SOutlinerTreeView, StaticCastSharedRef<SSceneOutliner>(AsShared()) )
// multi-select if we're in browsing mode,
// single-select if we're in picking mode,
.SelectionMode( this, &SSceneOutliner::GetSelectionMode )
// Point the tree to our array of root-level items. Whenever this changes, we'll call RequestTreeRefresh()
.TreeItemsSource( &RootTreeItems )
// Find out when the user selects something in the tree
.OnSelectionChanged( this, &SSceneOutliner::OnOutlinerTreeSelectionChanged )
// Called when the user double-clicks with LMB on an item in the list
.OnMouseButtonDoubleClick( this, &SSceneOutliner::OnOutlinerTreeDoubleClick )
// Called when an item is scrolled into view
.OnItemScrolledIntoView( this, &SSceneOutliner::OnOutlinerTreeItemScrolledIntoView )
// Called when an item is expanded or collapsed
.OnExpansionChanged(this, &SSceneOutliner::OnItemExpansionChanged)
// Called to child items for any given parent item
.OnGetChildren( this, &SSceneOutliner::OnGetChildrenForOutlinerTree )
// Generates the actual widget for a tree item
.OnGenerateRow( this, &SSceneOutliner::OnGenerateRowForOutlinerTree )
// Use the level viewport context menu as the right click menu for tree items
.OnContextMenuOpening(this, &SSceneOutliner::OnOpenContextMenu)
// Header for the tree
.HeaderRow( HeaderRowWidget )
]
];
if (SharedData->Mode == ESceneOutlinerMode::ActorBrowsing)
{
// Separator
VerticalBox->AddSlot()
.AutoHeight()
.Padding(0, 0, 0, 1)
[
SNew(SSeparator)
];
// Bottom panel
VerticalBox->AddSlot()
.AutoHeight()
[
SNew(SHorizontalBox)
// Asset count
+SHorizontalBox::Slot()
.FillWidth(1.f)
.VAlign(VAlign_Center)
.Padding(8, 0)
[
SNew( STextBlock )
.Text( this, &SSceneOutliner::GetFilterStatusText )
.ColorAndOpacity( this, &SSceneOutliner::GetFilterStatusTextColor )
]
// View mode combo button
+SHorizontalBox::Slot()
.AutoWidth()
[
SAssignNew( ViewOptionsComboButton, SComboButton )
.ContentPadding(0)
.ForegroundColor( this, &SSceneOutliner::GetViewButtonForegroundColor )
.ButtonStyle( FEditorStyle::Get(), "ToggleButton" ) // Use the tool bar item style for this button
.OnGetMenuContent( this, &SSceneOutliner::GetViewButtonContent )
.ButtonContent()
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
[
SNew(SImage).Image( FEditorStyle::GetBrush("GenericViewButton") )
]
+SHorizontalBox::Slot()
.AutoWidth()
.Padding(2, 0, 0, 0)
.VAlign(VAlign_Center)
[
SNew(STextBlock).Text( LOCTEXT("ViewButton", "View Options") )
]
]
]
];
} //end if (SharedData->Mode == ESceneOutlinerMode::ActorBrowsing)
// Don't allow tool-tips over the header
HeaderRowWidget->EnableToolTipForceField( true );
// Populate our data set
Populate();
// We only synchronize selection when in actor browsing mode
if (SharedData->Mode == ESceneOutlinerMode::ActorBrowsing)
{
// Populate and register to find out when the level's selection changes
OnLevelSelectionChanged( NULL );
USelection::SelectionChangedEvent.AddRaw(this, &SSceneOutliner::OnLevelSelectionChanged);
USelection::SelectObjectEvent.AddRaw(this, &SSceneOutliner::OnLevelSelectionChanged);
}
// Register to find out when actors are added or removed
// @todo outliner: Might not catch some cases (see: CALLBACK_ActorPropertiesChange, CALLBACK_LayerChange, CALLBACK_LevelDirtied, CALLBACK_OnActorMoved, CALLBACK_UpdateLevelsForAllActors)
FEditorDelegates::MapChange.AddSP( this, &SSceneOutliner::OnMapChange );
FEditorDelegates::NewCurrentLevel.AddSP( this, &SSceneOutliner::OnNewCurrentLevel );
GEngine->OnLevelActorListChanged().AddSP( this, &SSceneOutliner::FullRefresh );
FWorldDelegates::LevelAddedToWorld.AddSP( this, &SSceneOutliner::OnLevelAdded );
FWorldDelegates::LevelRemovedFromWorld.AddSP( this, &SSceneOutliner::OnLevelRemoved );
GEngine->OnLevelActorAdded().AddSP( this, &SSceneOutliner::OnLevelActorsAdded );
GEngine->OnLevelActorDetached().AddSP( this, &SSceneOutliner::OnLevelActorsDetached );
GEngine->OnLevelActorFolderChanged().AddSP( this, &SSceneOutliner::OnLevelActorFolderChanged );
GEngine->OnLevelActorDeleted().AddSP( this, &SSceneOutliner::OnLevelActorsRemoved );
GEngine->OnLevelActorAttached().AddSP( this, &SSceneOutliner::OnLevelActorsAttached );
GEngine->OnLevelActorRequestRename().AddSP( this, &SSceneOutliner::OnLevelActorsRequestRename );
// Register to update when an undo/redo operation has been called to update our list of actors
GEditor->RegisterForUndo( this );
// Register to be notified when properties are edited
FCoreDelegates::OnActorLabelChanged.AddRaw(this, &SSceneOutliner::OnActorLabelChanged);
auto& Folders = FActorFolders::Get();
Folders.OnFolderCreate.AddSP(this, &SSceneOutliner::OnBroadcastFolderCreate);
Folders.OnFolderMove.AddSP(this, &SSceneOutliner::OnBroadcastFolderMove);
Folders.OnFolderDelete.AddSP(this, &SSceneOutliner::OnBroadcastFolderDelete);
}
void SSceneOutliner::SetupColumns(SHeaderRow& HeaderRow)
{
FSceneOutlinerModule& SceneOutlinerModule = FModuleManager::LoadModuleChecked<FSceneOutlinerModule>("SceneOutliner");
if (SharedData->ColumnMap.Num() == 0)
{
SharedData->UseDefaultColumns();
}
Columns.Empty(SharedData->ColumnMap.Num());
HeaderRow.ClearColumns();
// Get a list of sorted columns IDs to create
TArray<FName> SortedIDs;
SortedIDs.Reserve(SharedData->ColumnMap.Num());
SharedData->ColumnMap.GenerateKeyArray(SortedIDs);
SortedIDs.Sort([&](const FName& A, const FName& B){
return SharedData->ColumnMap[A].PriorityIndex < SharedData->ColumnMap[B].PriorityIndex;
});
for (const FName& ID : SortedIDs)
{
if (SharedData->ColumnMap[ID].Visibility == EColumnVisibility::Invisible)
{
continue;
}
TSharedPtr<ISceneOutlinerColumn> Column;
if (SharedData->ColumnMap[ID].Factory.IsBound())
{
Column = SharedData->ColumnMap[ID].Factory.Execute(*this);
}
else
{
Column = SceneOutlinerModule.FactoryColumn(ID, *this);
}
if (ensure(Column.IsValid()))
{
check(Column->GetColumnID() == ID);
Columns.Add(Column->GetColumnID(), Column);
auto ColumnArgs = Column->ConstructHeaderRowColumn();
if (Column->SupportsSorting())
{
ColumnArgs
.SortMode(this, &SSceneOutliner::GetColumnSortMode, Column->GetColumnID())
.OnSort(this, &SSceneOutliner::OnColumnSortModeChanged);
}
HeaderRow.AddColumn(ColumnArgs);
}
}
Columns.Shrink();
}
SSceneOutliner::~SSceneOutliner()
{
// We only synchronize selection when in actor browsing mode
if( SharedData->Mode == ESceneOutlinerMode::ActorBrowsing )
{
USelection::SelectionChangedEvent.RemoveAll(this);
USelection::SelectObjectEvent.RemoveAll(this);
}
FEditorDelegates::MapChange.RemoveAll( this );
FEditorDelegates::NewCurrentLevel.RemoveAll( this );
GEngine->OnLevelActorListChanged().RemoveAll( this );
GEditor->UnregisterForUndo( this );
SearchBoxFilter->OnChanged().RemoveAll( this );
Filters->OnChanged().RemoveAll( this );
FWorldDelegates::LevelAddedToWorld.RemoveAll( this );
FWorldDelegates::LevelRemovedFromWorld.RemoveAll( this );
FCoreDelegates::OnActorLabelChanged.RemoveAll(this);
if (FActorFolders::IsAvailable())
{
auto& Folders = FActorFolders::Get();
Folders.OnFolderCreate.RemoveAll(this);
Folders.OnFolderMove.RemoveAll(this);
Folders.OnFolderDelete.RemoveAll(this);
}
}
void SSceneOutliner::OnItemAdded(const FTreeItemID& ItemID, uint8 ActionMask)
{
NewItemActions.Add(ItemID, ActionMask);
}
FSlateColor SSceneOutliner::GetViewButtonForegroundColor() const
{
static const FName InvertedForegroundName("InvertedForeground");
static const FName DefaultForegroundName("DefaultForeground");
return ViewOptionsComboButton->IsHovered() ? FEditorStyle::GetSlateColor(InvertedForegroundName) : FEditorStyle::GetSlateColor(DefaultForegroundName);
}
TSharedRef<SWidget> SSceneOutliner::GetViewButtonContent()
{
FMenuBuilder MenuBuilder(true, NULL);
MenuBuilder.BeginSection("AssetThumbnails", LOCTEXT("ShowHeading", "Show"));
{
MenuBuilder.AddMenuEntry(
LOCTEXT("ToggleShowOnlySelected", "Only Selected"),
LOCTEXT("ToggleShowOnlySelectedToolTip", "When enabled, only displays actors that are currently selected."),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateSP( this, &SSceneOutliner::ToggleShowOnlySelected ),
FCanExecuteAction(),
FIsActionChecked::CreateSP( this, &SSceneOutliner::IsShowingOnlySelected )
),
NAME_None,
EUserInterfaceActionType::ToggleButton
);
MenuBuilder.AddMenuEntry(
LOCTEXT("ToggleHideTemporaryActors", "Hide Temporary Actors"),
LOCTEXT("ToggleHideTemporaryActorsToolTip", "When enabled, hides temporary/run-time Actors."),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateSP( this, &SSceneOutliner::ToggleHideTemporaryActors ),
FCanExecuteAction(),
FIsActionChecked::CreateSP( this, &SSceneOutliner::IsHidingTemporaryActors )
),
NAME_None,
EUserInterfaceActionType::ToggleButton
);
MenuBuilder.AddMenuEntry(
LOCTEXT("ToggleShowOnlyCurrentLevel", "Only in Current Level"),
LOCTEXT("ToggleShowOnlyCurrentLevelToolTip", "When enabled, only shows Actors that are in the Current Level."),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateSP( this, &SSceneOutliner::ToggleShowOnlyCurrentLevel ),
FCanExecuteAction(),
FIsActionChecked::CreateSP( this, &SSceneOutliner::IsShowingOnlyCurrentLevel )
),
NAME_None,
EUserInterfaceActionType::ToggleButton
);
}
MenuBuilder.EndSection();
return MenuBuilder.MakeWidget();
}
/** FILTERS */
// Show Only Selected
void SSceneOutliner::ToggleShowOnlySelected()
{
const bool bEnableFlag = !IsShowingOnlySelected();
USceneOutlinerSettings* Settings = GetMutableDefault<USceneOutlinerSettings>();
Settings->bShowOnlySelectedActors = bEnableFlag;
Settings->PostEditChange();
ApplyShowOnlySelectedFilter(bEnableFlag);
}
void SSceneOutliner::ApplyShowOnlySelectedFilter(bool bShowOnlySelected)
{
if ( !SelectedActorFilter.IsValid() )
{
SelectedActorFilter = CreateSelectedActorFilter();
}
if ( bShowOnlySelected )
{
Filters->Add( SelectedActorFilter );
}
else
{
Filters->Remove( SelectedActorFilter );
}
}
bool SSceneOutliner::IsShowingOnlySelected() const
{
return GetDefault<USceneOutlinerSettings>()->bShowOnlySelectedActors;
}
// Hide Temporary Actors
void SSceneOutliner::ToggleHideTemporaryActors()
{
const bool bEnableFlag = !IsHidingTemporaryActors();
USceneOutlinerSettings* Settings = GetMutableDefault<USceneOutlinerSettings>();
Settings->bHideTemporaryActors = bEnableFlag;
Settings->PostEditChange();
ApplyHideTemporaryActorsFilter(bEnableFlag);
}
void SSceneOutliner::ApplyHideTemporaryActorsFilter(bool bHideTemporaryActors)
{
if ( !HideTemporaryActorsFilter.IsValid() )
{
HideTemporaryActorsFilter = CreateHideTemporaryActorsFilter();
}
if ( bHideTemporaryActors )
{
Filters->Add( HideTemporaryActorsFilter );
}
else
{
Filters->Remove( HideTemporaryActorsFilter );
}
}
bool SSceneOutliner::IsHidingTemporaryActors() const
{
return GetDefault<USceneOutlinerSettings>()->bHideTemporaryActors;
}
// Show Only Actors In Current Level
void SSceneOutliner::ToggleShowOnlyCurrentLevel()
{
const bool bEnableFlag = !IsShowingOnlyCurrentLevel();
USceneOutlinerSettings* Settings = GetMutableDefault<USceneOutlinerSettings>();
Settings->bShowOnlyActorsInCurrentLevel = bEnableFlag;
Settings->PostEditChange();
ApplyShowOnlyCurrentLevelFilter(bEnableFlag);
}
void SSceneOutliner::ApplyShowOnlyCurrentLevelFilter(bool bShowOnlyActorsInCurrentLevel)
{
if ( !ShowOnlyActorsInCurrentLevelFilter.IsValid() )
{
ShowOnlyActorsInCurrentLevelFilter = CreateIsInCurrentLevelFilter();
}
if ( bShowOnlyActorsInCurrentLevel )
{
Filters->Add( ShowOnlyActorsInCurrentLevelFilter );
}
else
{
Filters->Remove( ShowOnlyActorsInCurrentLevelFilter );
}
}
bool SSceneOutliner::IsShowingOnlyCurrentLevel() const
{
return GetDefault<USceneOutlinerSettings>()->bShowOnlyActorsInCurrentLevel;
}
/** END FILTERS */
const FSlateBrush* SSceneOutliner::OnGetBorderBrush() const
{
if (SharedData->bRepresentingPlayWorld)
{
return GEditor->bIsSimulatingInEditor ? SimulateBorder : PlayInEditorBorder;
}
else
{
return NoBorder;
}
}
FSlateColor SSceneOutliner::OnGetBorderColorAndOpacity() const
{
return SharedData->bRepresentingPlayWorld ? FLinearColor(1.0f, 1.0f, 1.0f, 0.6f)
: FLinearColor::Transparent;
}
ESelectionMode::Type SSceneOutliner::GetSelectionMode() const
{
return (SharedData->Mode == ESceneOutlinerMode::ActorBrowsing)?
ESelectionMode::Multi : ESelectionMode::Single;
}
void SSceneOutliner::Refresh()
{
bNeedsRefresh = true;
}
void SSceneOutliner::FullRefresh()
{
bFullRefresh = true;
Refresh();
}
void SSceneOutliner::Populate()
{
// Block events while we clear out the list. We don't want actors in the level to become deselected
// while we doing this
TGuardValue<bool> ReentrantGuard(bIsReentrant, true);
for (const FWorldContext& Context : GEngine->GetWorldContexts())
{
if (Context.WorldType == EWorldType::PIE)
{
SharedData->RepresentingWorld = Context.World();
break;
}
else if (Context.WorldType == EWorldType::Editor)
{
SharedData->RepresentingWorld = Context.World();
}
}
if (!CheckWorld())
{
return;
}
SharedData->bRepresentingPlayWorld = SharedData->RepresentingWorld->WorldType == EWorldType::PIE;
// Get a collection of items and folders which were formerly collapsed
const FParentsExpansionState ExpansionStateInfo = GetParentsExpansionState();
bool bMadeAnySignificantChanges = false;
if(bFullRefresh)
{
// Clear the selection here - RepopulateEntireTree will reconstruct it.
OutlinerTreeView->ClearSelection();
RepopulateEntireTree();
bMadeAnySignificantChanges = true;
bFullRefresh = false;
}
// Only deal with 500 at a time
const int32 End = FMath::Min(PendingOperations.Num(), 500);
for (int32 Index = 0; Index < End; ++Index)
{
auto& PendingOp = PendingOperations[Index];
switch (PendingOp.Type)
{
case FPendingTreeOperation::Added:
bMadeAnySignificantChanges = AddItemToTree(PendingOp.Item) || bMadeAnySignificantChanges;
break;
case FPendingTreeOperation::Moved:
bMadeAnySignificantChanges = true;
OnItemMoved(PendingOp.Item);
break;
case FPendingTreeOperation::Removed:
bMadeAnySignificantChanges = true;
RemoveItemFromTree(PendingOp.Item);
break;
default:
check(false);
break;
}
}
PendingOperations.RemoveAt(0, End);
SetParentsExpansionState(ExpansionStateInfo);
if (bMadeAnySignificantChanges)
{
RequestSort();
}
if (PendingOperations.Num() == 0)
{
// We're fully refreshed now.
NewItemActions.Empty();
bNeedsRefresh = false;
}
}
bool SSceneOutliner::ShouldShowFolders() const
{
return SharedData->Mode == ESceneOutlinerMode::ActorBrowsing || SharedData->bOnlyShowFolders;
}
void SSceneOutliner::EmptyTreeItems()
{
FilteredActorCount = 0;
ApplicableActors.Empty();
PendingOperations.Empty();
TreeItemMap.Reset();
RootTreeItems.Empty();
}
void SSceneOutliner::RepopulateEntireTree()
{
EmptyTreeItems();
ConstructItemFor<FWorldTreeItem>(SharedData->RepresentingWorld);
if (!SharedData->bOnlyShowFolders)
{
// Iterate over every actor in memory. WARNING: This is potentially very expensive!
for( FActorIterator ActorIt(SharedData->RepresentingWorld); ActorIt; ++ActorIt )
{
AActor* Actor = *ActorIt;
if (Actor && IsActorDisplayable(Actor))
{
if (Filters->PassesAllFilters(FActorTreeItem(Actor)))
{
ApplicableActors.Emplace(Actor);
}
ConstructItemFor<FActorTreeItem>(Actor);
}
}
}
if (!IsShowingOnlySelected() && ShouldShowFolders())
{
// Add any folders which might match the current search terms
for (const auto& Pair : FActorFolders::Get().GetFolderPropertiesForWorld(*SharedData->RepresentingWorld))
{
if (!TreeItemMap.Contains(Pair.Key))
{
ConstructItemFor<FFolderTreeItem>(Pair.Key);
}
}
}
}
void SSceneOutliner::OnChildRemovedFromParent(ITreeItem& Parent)
{
if (Parent.Flags.bIsFilteredOut && !Parent.GetChildren().Num())
{
// The parent no longer has any children that match the current search terms. Remove it.
RemoveItemFromTree(Parent.AsShared());
}
}
void SSceneOutliner::OnItemMoved(const FTreeItemRef& Item)
{
// Just remove the item if it no longer matches the filters
if (!Item->Flags.bIsFilteredOut && !SearchBoxFilter->PassesFilter(*Item))
{
// This will potentially remove any non-matching, empty parents as well
RemoveItemFromTree(Item);
}
else
{
// The item still matches the filters (or has children that do)
// When an item has been asked to move, it will still reside under its old parent
FTreeItemPtr Parent = Item->GetParent();
if (Parent.IsValid())
{
Parent->RemoveChild(Item);
OnChildRemovedFromParent(*Parent);
}
else
{
RootTreeItems.Remove(Item);
}
Parent = EnsureParentForItem(Item);
if (Parent.IsValid())
{
Parent->AddChild(Item);
OutlinerTreeView->SetItemExpansion(Parent, true);
}
else
{
RootTreeItems.Add(Item);
}
}
}
void SSceneOutliner::RemoveItemFromTree(FTreeItemRef InItem)
{
if (TreeItemMap.Contains(InItem->GetID()))
{
auto Parent = InItem->GetParent();
if (Parent.IsValid())
{
Parent->RemoveChild(InItem);
OnChildRemovedFromParent(*Parent);
}
else
{
RootTreeItems.Remove(InItem);
}
InItem->Visit(FFunctionalVisitor().Actor([&](const FActorTreeItem& ActorItem){
if (!ActorItem.Flags.bIsFilteredOut)
{
--FilteredActorCount;
}
}));
TreeItemMap.Remove(InItem->GetID());
}
}
FTreeItemPtr SSceneOutliner::EnsureParentForItem(FTreeItemRef Item)
{
if (SharedData->bShowParentTree)
{
FTreeItemPtr Parent = Item->FindParent(TreeItemMap);
if (Parent.IsValid())
{
return Parent;
}
else
{
auto NewParent = Item->CreateParent();
if (NewParent.IsValid())
{
NewParent->Flags.bIsFilteredOut = !Filters->TestAndSetInteractiveState(*NewParent) || !SearchBoxFilter->PassesFilter(*NewParent);
AddUnfilteredItemToTree(NewParent.ToSharedRef());
return NewParent;
}
}
}
return nullptr;
}
bool SSceneOutliner::AddItemToTree(FTreeItemRef Item)
{
const auto ItemID = Item->GetID();
// If a tree item already exists that represents the same data, bail
if (TreeItemMap.Find(ItemID))
{
return false;
}
// Set the filtered out flag
Item->Flags.bIsFilteredOut = !SearchBoxFilter->PassesFilter(*Item);
if (!Item->Flags.bIsFilteredOut)
{
AddUnfilteredItemToTree(Item);
// Check if we need to do anything with this new item
if (uint8* ActionMask = NewItemActions.Find(ItemID))
{
if (*ActionMask & ENewItemAction::Select)
{
OutlinerTreeView->ClearSelection();
OutlinerTreeView->SetItemSelection(Item, true);
}
if (*ActionMask & ENewItemAction::Rename)
{
PendingRenameItem = Item;
}
if (*ActionMask & (ENewItemAction::ScrollIntoView | ENewItemAction::Rename))
{
ScrollItemIntoView(Item);
}
}
}
return true;
}
void SSceneOutliner::AddUnfilteredItemToTree(FTreeItemRef Item)
{
Item->SharedData = SharedData;
auto Parent = EnsureParentForItem(Item);
const auto ItemID = Item->GetID();
check(!TreeItemMap.Contains(ItemID));
TreeItemMap.Add(ItemID, Item);
if (Parent.IsValid())
{
Parent->AddChild(Item);
}
else
{
RootTreeItems.Add(Item);
}
Item->Visit(FOnItemAddedToTree(*this));
}
SSceneOutliner::FParentsExpansionState SSceneOutliner::GetParentsExpansionState() const
{
FParentsExpansionState States;
for (const auto& Pair : TreeItemMap)
{
if (Pair.Value->GetChildren().Num())
{
States.Add(Pair.Key, Pair.Value->Flags.bIsExpanded);
}
}
return States;
}
void SSceneOutliner::SetParentsExpansionState(const FParentsExpansionState& ExpansionStateInfo) const
{
for (const auto& Pair : TreeItemMap)
{
auto& Item = Pair.Value;
if (Item->GetChildren().Num())
{
const bool* bIsExpanded = ExpansionStateInfo.Find(Pair.Key);
if (bIsExpanded)
{
OutlinerTreeView->SetItemExpansion(Item, *bIsExpanded);
}
else
{
OutlinerTreeView->SetItemExpansion(Item, Item->Flags.bIsExpanded);
}
}
}
}
void SSceneOutliner::PopulateSearchStrings(const ITreeItem& Item, TArray< FString >& OutSearchStrings) const
{
for (const auto& Pair : Columns)
{
Pair.Value->PopulateSearchStrings(Item, OutSearchStrings);
}
}
TArray<FFolderTreeItem*> SSceneOutliner::GetSelectedFolders() const
{
return FItemSelection(*OutlinerTreeView).Folders;
}
TSharedPtr<SWidget> SSceneOutliner::OnOpenContextMenu()
{
TArray<AActor*> SelectedActors;
GEditor->GetSelectedActors()->GetSelectedObjects<AActor>( SelectedActors );
/** Legacy context menu override only supports actors */
if (SelectedActors.Num() && SharedData->ContextMenuOverride.IsBound())
{
return SharedData->ContextMenuOverride.Execute();
}
if (SharedData->Mode == ESceneOutlinerMode::ActorBrowsing)
{
// 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 BuildDefaultContextMenu();
}
return TSharedPtr<SWidget>();
}
TSharedPtr<SWidget> SSceneOutliner::BuildDefaultContextMenu()
{
if (!CheckWorld())
{
return nullptr;
}
// Build up the menu for a selection
const bool bCloseAfterSelection = true;
FMenuBuilder MenuBuilder(bCloseAfterSelection, TSharedPtr<FUICommandList>(), SharedData->DefaultMenuExtender);
const auto NumSelectedItems = OutlinerTreeView->GetNumItemsSelected();
if (NumSelectedItems == 1)
{
OutlinerTreeView->GetSelectedItems()[0]->GenerateContextMenu(MenuBuilder, *this);
}
// We always create a section here, even if there is no parent so that clients can still extend the menu
MenuBuilder.BeginSection("MainSection");
{
FItemSelection ItemSelection(*OutlinerTreeView);
// Don't add any of these menu items if we're not showing the parent tree
if (SharedData->bShowParentTree)
{
if (NumSelectedItems == 0)
{
const FSlateIcon NewFolderIcon(FEditorStyle::GetStyleSetName(), "SceneOutliner.NewFolderIcon");
MenuBuilder.AddMenuEntry(LOCTEXT("CreateFolder", "Create Folder"), FText(), NewFolderIcon, FUIAction(FExecuteAction::CreateSP(this, &SSceneOutliner::CreateFolder)));
}
else
{
// Can't move worlds or level blueprints
const bool bCanMoveSelection = ItemSelection.Worlds.Num() == 0;
if (bCanMoveSelection)
{
MenuBuilder.AddSubMenu(
LOCTEXT("MoveActorsTo", "Move To"),
LOCTEXT("MoveActorsTo_Tooltip", "Move selection to another folder"),
FNewMenuDelegate::CreateSP(this, &SSceneOutliner::FillFoldersSubMenu));
}
// If we've only got folders selected, show the selection sub menu
if (ItemSelection.Folders.Num() == NumSelectedItems)
{
MenuBuilder.AddSubMenu(
LOCTEXT("SelectSubmenu", "Select"),
LOCTEXT("SelectSubmenu_Tooltip", "Select the contents of the current selection"),
FNewMenuDelegate::CreateSP(this, &SSceneOutliner::FillSelectionSubMenu));
}
}
}
}
MenuBuilder.EndSection();
return MenuBuilder.MakeWidget();
}
void SSceneOutliner::FillFoldersSubMenu(FMenuBuilder& MenuBuilder) const
{
MenuBuilder.AddMenuEntry(LOCTEXT( "CreateNew", "Create New Folder" ), LOCTEXT( "CreateNew_ToolTip", "Move to a new folder" ),
FSlateIcon(FEditorStyle::GetStyleSetName(), "SceneOutliner.NewFolderIcon"), FExecuteAction::CreateSP(this, &SSceneOutliner::CreateFolder));
AddMoveToFolderOutliner(MenuBuilder);
}
TSharedRef<TSet<FName>> SSceneOutliner::GatherInvalidMoveToDestinations() const
{
// We use a pointer here to save copying the whole array for every invocation of the filter delegate
TSharedRef<TSet<FName>> ExcludedParents(new TSet<FName>());
struct FFindInvalidFolders : ITreeItemVisitor
{
TSet<FName>& ExcludedParents;
const TMap<FTreeItemID, FTreeItemPtr>& TreeItemMap;
FFindInvalidFolders(TSet<FName>& InExcludedParents, const TMap<FTreeItemID, FTreeItemPtr>& InTreeItemMap)
: ExcludedParents(InExcludedParents), TreeItemMap(InTreeItemMap)
{}
static bool ItemHasSubFolders(const TWeakPtr<ITreeItem>& WeakItem)
{
bool bHasSubFolder = false;
WeakItem.Pin()->Visit(FFunctionalVisitor().Folder([&](const FFolderTreeItem&){
bHasSubFolder = true;
}));
return bHasSubFolder;
}
virtual void Visit(const FActorTreeItem& ActorItem) const override
{
if (AActor* Actor = ActorItem.Actor.Get())
{
// We exclude actor parent folders if they don't have any sub folders
const FName& Folder = Actor->GetFolderPath();
if (!Folder.IsNone() && !ExcludedParents.Contains(Folder))
{
auto FolderItem = TreeItemMap.FindRef(Folder);
if (FolderItem.IsValid() && !FolderItem->GetChildren().ContainsByPredicate(&ItemHasSubFolders))
{
ExcludedParents.Add(Folder);
}
}
}
}
virtual void Visit(const FFolderTreeItem& Folder) const override
{
// Cannot move into its parent
const FName ParentPath = GetParentPath(Folder.Path);
if (!ParentPath.IsNone())
{
ExcludedParents.Add(ParentPath);
}
else
{
// Failing that, cannot move into itself, or any child
ExcludedParents.Add(Folder.Path);
}
}
};
auto Visitor = FFindInvalidFolders(*ExcludedParents, TreeItemMap);
for (const auto& Item : OutlinerTreeView->GetSelectedItems())
{
Item->Visit(Visitor);
}
return ExcludedParents;
}
void SSceneOutliner::AddMoveToFolderOutliner(FMenuBuilder& MenuBuilder) const
{
// We don't show this if there aren't any folders in the world
if (!FActorFolders::Get().GetFolderPropertiesForWorld(*SharedData->RepresentingWorld).Num())
{
return;
}
// Add a mini scene outliner for choosing an existing folder
FInitializationOptions MiniSceneOutlinerInitOptions;
MiniSceneOutlinerInitOptions.bShowHeaderRow = false;
MiniSceneOutlinerInitOptions.bFocusSearchBoxWhenOpened = true;
MiniSceneOutlinerInitOptions.bOnlyShowFolders = true;
// Don't show any folders that are a child of any of the selected folders
auto ExcludedParents = GatherInvalidMoveToDestinations();
if (ExcludedParents->Num())
{
// Add a filter if necessary
auto FilterOutChildFolders = [](FName Path, TSharedRef<TSet<FName>> InExcludedParents){
for (const auto& Parent : *InExcludedParents)
{
if (Path == Parent || FActorFolders::PathIsChildOf(Path.ToString(), Parent.ToString()))
{
return false;
}
}
return true;
};
MiniSceneOutlinerInitOptions.Filters->AddFilterPredicate(FFolderFilterPredicate::CreateStatic(FilterOutChildFolders, ExcludedParents), EDefaultFilterBehaviour::Pass);
}
{
// Filter in/out the world according to whether it is valid to move to/from the root
FDragDropPayload DraggedObjects(OutlinerTreeView->GetSelectedItems());
const bool bMoveToRootValid = FFolderDropTarget(FName()).ValidateDrop(DraggedObjects, *SharedData->RepresentingWorld).IsValid();
MiniSceneOutlinerInitOptions.Filters->AddFilterPredicate(FWorldFilterPredicate::CreateStatic([](const UWorld*, bool bInMoveToRootValid){
return bInMoveToRootValid;
}, bMoveToRootValid), EDefaultFilterBehaviour::Pass);
}
// Don't show the actor info column
MiniSceneOutlinerInitOptions.UseDefaultColumns();
MiniSceneOutlinerInitOptions.ColumnMap.Remove(FBuiltInColumnTypes::ActorInfo());
// Actor selector to allow the user to choose a folder
FSceneOutlinerModule& SceneOutlinerModule = FModuleManager::LoadModuleChecked<FSceneOutlinerModule>("SceneOutliner");
TSharedRef< SWidget > MiniSceneOutliner =
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.MaxHeight(400.0f)
[
SNew(SSceneOutliner, MiniSceneOutlinerInitOptions)
.IsEnabled(FSlateApplication::Get().GetNormalExecutionAttribute())
.OnItemPickedDelegate(FOnSceneOutlinerItemPicked::CreateSP(this, &SSceneOutliner::MoveSelectionTo))
];
MenuBuilder.BeginSection(FName(), LOCTEXT("ExistingFolders", "Existing:"));
MenuBuilder.AddWidget(MiniSceneOutliner, FText::GetEmpty(), false);
MenuBuilder.EndSection();
}
void SSceneOutliner::FillSelectionSubMenu(FMenuBuilder& MenuBuilder) const
{
MenuBuilder.AddMenuEntry(
LOCTEXT( "AddChildrenToSelection", "Immediate Children" ),
LOCTEXT( "AddChildrenToSelection_ToolTip", "Select all immediate children of the selected folders" ),
FSlateIcon(),
FExecuteAction::CreateSP(this, &SSceneOutliner::SelectFoldersImmediateChildren));
MenuBuilder.AddMenuEntry(
LOCTEXT( "AddDescendantsToSelection", "All Descendants" ),
LOCTEXT( "AddDescendantsToSelection_ToolTip", "Select all descendants of the selected folders" ),
FSlateIcon(),
FExecuteAction::CreateSP(this, &SSceneOutliner::SelectFoldersDescendants));
}
struct FSelectActors : ITreeItemVisitor
{
virtual void Visit(const FActorTreeItem& ActorItem) const override
{
if (AActor* Actor = ActorItem.Actor.Get())
{
GEditor->SelectActor(Actor, true, /*bNotify=*/false);
}
}
};
struct FSelectActorsRecursive : FSelectActors
{
using FSelectActors::Visit;
virtual void Visit(const FFolderTreeItem& FolderItem) const override
{
for (auto& Child : FolderItem.GetChildren())
{
Child.Pin()->Visit(FSelectActorsRecursive());
}
}
};
void SSceneOutliner::SelectFoldersImmediateChildren() const
{
auto SelectedFolders = GetSelectedFolders();
if (SelectedFolders.Num())
{
// We'll batch selection changes instead by using BeginBatchSelectOperation()
GEditor->GetSelectedActors()->BeginBatchSelectOperation();
OutlinerTreeView->ClearSelection();
for (const auto& Folder : SelectedFolders)
{
for (auto& Child : Folder->GetChildren())
{
Child.Pin()->Visit(FSelectActors());
}
}
GEditor->GetSelectedActors()->EndBatchSelectOperation();
GEditor->NoteSelectionChange();
}
}
void SSceneOutliner::SelectFoldersDescendants() const
{
auto SelectedFolders = GetSelectedFolders();
if (SelectedFolders.Num())
{
// We'll batch selection changes instead by using BeginBatchSelectOperation()
GEditor->GetSelectedActors()->BeginBatchSelectOperation();
OutlinerTreeView->ClearSelection();
for (const auto& Folder : SelectedFolders)
{
Folder->Visit(FSelectActorsRecursive());
}
GEditor->GetSelectedActors()->EndBatchSelectOperation();
GEditor->NoteSelectionChange();
}
}
void SSceneOutliner::MoveSelectionTo(FTreeItemRef NewParent)
{
struct FMoveToFolder : ITreeItemVisitor
{
SSceneOutliner& Outliner;
FMoveToFolder(SSceneOutliner& InOutliner) : Outliner(InOutliner) {}
virtual void Visit(const FFolderTreeItem& Folder) const override
{
Outliner.MoveSelectionTo(Folder.Path);
}
virtual void Visit(const FWorldTreeItem&) const override
{
Outliner.MoveSelectionTo(FName());
}
};
NewParent->Visit(FMoveToFolder(*this));
}
void SSceneOutliner::MoveSelectionTo(FName NewParent)
{
if (!CheckWorld())
{
return;
}
FSlateApplication::Get().DismissAllMenus();
FFolderDropTarget DropTarget(NewParent);
FDragDropPayload DraggedObjects(OutlinerTreeView->GetSelectedItems());
FDragValidationInfo Validation = DropTarget.ValidateDrop(DraggedObjects, *SharedData->RepresentingWorld);
if (!Validation.IsValid())
{
FNotificationInfo Info(Validation.ValidationText);
Info.ExpireDuration = 3.0f;
Info.bUseLargeFont = false;
Info.bFireAndForget = true;
Info.bUseSuccessFailIcons = true;
FSlateNotificationManager::Get().AddNotification(Info)->SetCompletionState(SNotificationItem::CS_Fail);
return;
}
const FScopedTransaction Transaction( LOCTEXT("MoveOutlinerItems", "Move World Outliner Items") );
DropTarget.OnDrop(DraggedObjects, *SharedData->RepresentingWorld, Validation, SNullWidget::NullWidget);
}
FReply SSceneOutliner::OnCreateFolderClicked()
{
CreateFolder();
return FReply::Handled();
}
void SSceneOutliner::CreateFolder()
{
if (!CheckWorld())
{
return;
}
UWorld& World = *SharedData->RepresentingWorld;
const FScopedTransaction Transaction(LOCTEXT("UndoAction_CreateFolder", "Create Folder"));
const FName NewFolderName = FActorFolders::Get().GetDefaultFolderNameForSelection(World);
FActorFolders::Get().CreateFolderContainingSelection(World, NewFolderName);
auto PreviouslySelectedItems = OutlinerTreeView->GetSelectedItems();
auto Visit = [&](const FFolderTreeItem& Folder)
{
MoveFolderTo(Folder.Path, NewFolderName, World);
};
auto Visitor = FFunctionalVisitor().Folder(Visit);
// Move any selected folders into the new folder name
for (const auto& Item : PreviouslySelectedItems)
{
Item->Visit(Visitor);
}
// At this point the new folder will be in our newly added list, so select it and open a rename when it gets refreshed
NewItemActions.Add(NewFolderName, ENewItemAction::Select | ENewItemAction::Rename);
}
void SSceneOutliner::OnBroadcastFolderCreate(UWorld& InWorld, FName NewPath)
{
if (!ShouldShowFolders() || &InWorld != SharedData->RepresentingWorld)
{
return;
}
if (!TreeItemMap.Contains(NewPath))
{
ConstructItemFor<FFolderTreeItem>(NewPath);
}
}
void SSceneOutliner::OnBroadcastFolderMove(UWorld& InWorld, FName OldPath, FName NewPath)
{
if (!ShouldShowFolders() || &InWorld != SharedData->RepresentingWorld)
{
return;
}
auto Item = TreeItemMap.FindRef(OldPath);
if (Item.IsValid())
{
// Remove it from the map under the old ID (which is derived from the folder path)
TreeItemMap.Remove(Item->GetID());
// Now change the path and put it back in the map with its new ID
auto Folder = StaticCastSharedPtr<FFolderTreeItem>(Item);
Folder->Path = NewPath;
Folder->LeafName = GetFolderLeafName(NewPath);
TreeItemMap.Add(Item->GetID(), Item);
// Add an operation to move the item in the hierarchy
PendingOperations.Emplace(FPendingTreeOperation::Moved, Item.ToSharedRef());
Refresh();
}
}
void SSceneOutliner::OnBroadcastFolderDelete(UWorld& InWorld, FName Path)
{
if (&InWorld != SharedData->RepresentingWorld)
{
return;
}
auto* Folder = TreeItemMap.Find(Path);
if (Folder)
{
PendingOperations.Emplace(FPendingTreeOperation::Removed, Folder->ToSharedRef());
Refresh();
}
}
void SSceneOutliner::ScrollItemIntoView(FTreeItemPtr Item)
{
auto Parent = Item->GetParent();
while(Parent.IsValid())
{
OutlinerTreeView->SetItemExpansion(Parent->AsShared(), true);
Parent = Parent->GetParent();
}
OutlinerTreeView->RequestScrollIntoView(Item);
}
void SSceneOutliner::InitiateRename(TSharedRef<ITreeItem> Item)
{
if (Item->CanInteract())
{
PendingRenameItem = Item;
ScrollItemIntoView(Item);
}
}
TSharedRef< ITableRow > SSceneOutliner::OnGenerateRowForOutlinerTree( FTreeItemPtr Item, const TSharedRef< STableViewBase >& OwnerTable )
{
return SNew( SSceneOutlinerTreeRow, OutlinerTreeView.ToSharedRef(), SharedThis(this) ).Item( Item );
}
void SSceneOutliner::OnGetChildrenForOutlinerTree( FTreeItemPtr InParent, TArray< FTreeItemPtr >& OutChildren )
{
if( SharedData->bShowParentTree )
{
for (auto& WeakChild : InParent->GetChildren())
{
auto Child = WeakChild.Pin();
// Should never have bogus entries in this list
check(Child.IsValid());
OutChildren.Add(Child);
}
// If the item needs it's children sorting, do that now
if (OutChildren.Num() && InParent->Flags.bChildrenRequireSort)
{
// Sort the children we returned
SortItems(OutChildren);
// Empty out the children and repopulate them in the correct order
InParent->Children.Empty();
for (auto& Child : OutChildren)
{
InParent->Children.Emplace(Child);
}
// They no longer need sorting
InParent->Flags.bChildrenRequireSort = false;
}
}
}
bool SSceneOutliner::IsActorDisplayable( const AActor* Actor ) const
{
return !SharedData->bOnlyShowFolders && // Don't show actors if we're only showing folders
Actor->IsEditable() && // Only show actors that are allowed to be selected and drawn in editor
Actor->IsListedInSceneOutliner() &&
(SharedData->bRepresentingPlayWorld || !Actor->HasAnyFlags(RF_Transient)) && // Don't show transient actors in non-play worlds
!Actor->IsTemplate() && // Should never happen, but we never want CDOs displayed
!FActorEditorUtils::IsABuilderBrush(Actor) && // Don't show the builder brush
!Actor->IsA( AWorldSettings::StaticClass() ) && // Don't show the WorldSettings actor, even though it is technically editable
!Actor->IsPendingKill() && // We don't want to show actors that are about to go away
FLevelUtils::IsLevelVisible( Actor->GetLevel() ); // Only show Actors whose level is visible
}
void SSceneOutliner::OnOutlinerTreeSelectionChanged( FTreeItemPtr TreeItem, ESelectInfo::Type SelectInfo )
{
if (SelectInfo == ESelectInfo::Direct)
{
return;
}
if( SharedData->Mode == ESceneOutlinerMode::ActorPicker )
{
// In actor picking mode, we fire off the notification to whoever is listening.
// This may often cause the widget itself to be enqueued for destruction
if( OutlinerTreeView->GetNumItemsSelected() > 0 )
{
auto FirstItem = OutlinerTreeView->GetSelectedItems()[0];
if (FirstItem->CanInteract())
{
OnItemPicked.ExecuteIfBound( FirstItem->AsShared() );
}
}
}
// We only synchronize selection when in actor browsing mode
else if( SharedData->Mode == ESceneOutlinerMode::ActorBrowsing )
{
if( !bIsReentrant )
{
TGuardValue<bool> ReentrantGuard(bIsReentrant,true);
// @todo outliner: Can be called from non-interactive selection
// The tree let us know that selection has changed, but wasn't able to tell us
// what changed. So we'll perform a full difference check and update the editor's
// selected actors to match the control's selection set.
// Make a list of all the actors that should now be selected in the world.
FItemSelection Selection(*OutlinerTreeView);
auto SelectedActors = TSet<AActor*>(Selection.GetActorPtrs());
bool bChanged = false;
bool bAnyInPIE = false;
for (auto* Actor : SelectedActors)
{
if (!bAnyInPIE && Actor && (Actor->GetOutermost()->PackageFlags & PKG_PlayInEditor) != 0)
{
bAnyInPIE = true;
}
if (!GEditor->GetSelectedActors()->IsSelected(Actor))
{
bChanged = true;
break;
}
}
for (FSelectionIterator SelectionIt( *GEditor->GetSelectedActors() ); SelectionIt && !bChanged; ++SelectionIt)
{
AActor* Actor = CastChecked< AActor >( *SelectionIt );
if (!bAnyInPIE && (Actor->GetOutermost()->PackageFlags & PKG_PlayInEditor) != 0)
{
bAnyInPIE = true;
}
if (!SelectedActors.Contains(Actor))
{
// Actor has been deselected
bChanged = true;
// If actor was a group actor, remove its members from the ActorsToSelect list
AGroupActor* DeselectedGroupActor = Cast<AGroupActor>(Actor);
if ( DeselectedGroupActor )
{
TArray<AActor*> GroupActors;
DeselectedGroupActor->GetGroupActors( GroupActors );
for (auto* GroupActor : GroupActors)
{
SelectedActors.Remove(GroupActor);
}
}
}
}
// If there's a discrepancy, update the selected actors to reflect this list.
if ( bChanged )
{
const FScopedTransaction Transaction( NSLOCTEXT("UnrealEd", "ClickingOnActors", "Clicking on Actors"), !bAnyInPIE );
GEditor->GetSelectedActors()->Modify();
// Clear the selection.
GEditor->SelectNone(false, true, true);
// We'll batch selection changes instead by using BeginBatchSelectOperation()
GEditor->GetSelectedActors()->BeginBatchSelectOperation();
const bool bShouldSelect = true;
const bool bNotifyAfterSelect = false;
const bool bSelectEvenIfHidden = true; // @todo outliner: Is this actually OK?
for (auto* Actor : SelectedActors)
{
UE_LOG(LogSceneOutliner, Verbose, TEXT("Clicking on Actor (world outliner): %s (%s)"), *Actor->GetClass()->GetName(), *Actor->GetActorLabel());
GEditor->SelectActor( Actor, bShouldSelect, bNotifyAfterSelect, bSelectEvenIfHidden );
}
// Commit selection changes
GEditor->GetSelectedActors()->EndBatchSelectOperation();
// Fire selection changed event
GEditor->NoteSelectionChange();
}
bActorSelectionDirty = true;
}
}
}
void SSceneOutliner::OnLevelSelectionChanged(UObject* Obj)
{
// We only synchronize selection when in actor browsing mode
if( SharedData->Mode == ESceneOutlinerMode::ActorBrowsing )
{
// @todo outliner: Because we are not notified of which items are being added/removed from selection, we have
// no immediate means to incrementally update the tree when selection changes.
// Ideally, we can improve the filtering paradigm to better support incremental updates in cases such as these
if ( IsShowingOnlySelected() )
{
FullRefresh();
}
else if (!bIsReentrant)
{
OutlinerTreeView->ClearSelection();
bActorSelectionDirty = true;
// Scroll last item into view - this means if we are multi-selecting, we show newest selection. @TODO Not perfect though
if (AActor* LastSelectedActor = GEditor->GetSelectedActors()->GetBottom<AActor>())
{
auto TreeItem = TreeItemMap.FindRef(LastSelectedActor);
if (TreeItem.IsValid())
{
if (!OutlinerTreeView->IsItemVisible(TreeItem))
{
ScrollItemIntoView(TreeItem);
}
}
else
{
OnItemAdded(LastSelectedActor, ENewItemAction::ScrollIntoView);
}
}
}
}
}
void SSceneOutliner::OnOutlinerTreeDoubleClick( FTreeItemPtr TreeItem )
{
// We only deal with double clicks when in actor browsing mode
if( SharedData->Mode == ESceneOutlinerMode::ActorBrowsing )
{
auto ExpandCollapseFolder = [&](const FFolderTreeItem& Folder){
auto Shared = const_cast<FFolderTreeItem&>(Folder).AsShared();
OutlinerTreeView->SetItemExpansion(Shared, !OutlinerTreeView->IsItemExpanded(Shared));
};
if (TreeItem->CanInteract())
{
TreeItem->Visit(FFunctionalVisitor()
.Actor([&](const FActorTreeItem&){
// Move all actors into view
FItemSelection Selection(*OutlinerTreeView);
if( Selection.Actors.Num() > 0 )
{
const bool bActiveViewportOnly = false;
GEditor->MoveViewportCamerasToActor( Selection.GetActorPtrs(), bActiveViewportOnly );
}
})
.Folder(ExpandCollapseFolder)
.World([](const FWorldTreeItem& WorldItem){
WorldItem.OpenWorldSettings();
})
);
}
else
{
TreeItem->Visit(FFunctionalVisitor()
.Folder(ExpandCollapseFolder)
.Actor([&](const FActorTreeItem& Item){
// Move just this actor into view
if (AActor* Actor = Item.Actor.Get())
{
const bool bActiveViewportOnly = false;
GEditor->MoveViewportCamerasToActor( *Actor, bActiveViewportOnly );
}
})
);
}
}
}
void SSceneOutliner::OnOutlinerTreeItemScrolledIntoView( FTreeItemPtr TreeItem, const TSharedPtr<ITableRow>& Widget )
{
if (TreeItem == PendingRenameItem.Pin())
{
PendingRenameItem = nullptr;
TreeItem->RenameRequestEvent.ExecuteIfBound();
}
}
void SSceneOutliner::OnItemExpansionChanged(FTreeItemPtr TreeItem, bool bIsExpanded) const
{
TreeItem->Flags.bIsExpanded = bIsExpanded;
TreeItem->OnExpansionChanged();
// Expand any children that are also expanded
for (auto WeakChild : TreeItem->GetChildren())
{
auto Child = WeakChild.Pin();
if (Child->Flags.bIsExpanded)
{
OutlinerTreeView->SetItemExpansion(Child, true);
}
}
}
void SSceneOutliner::OnLevelAdded(ULevel* InLevel, UWorld* InWorld)
{
FullRefresh();
}
void SSceneOutliner::OnLevelRemoved(ULevel* InLevel, UWorld* InWorld)
{
FullRefresh();
}
void SSceneOutliner::OnLevelActorsAdded(AActor* InActor)
{
if( !bIsReentrant )
{
if( InActor && SharedData->RepresentingWorld == InActor->GetWorld() && IsActorDisplayable(InActor) )
{
if (!TreeItemMap.Find(InActor))
{
// Update the total actor count that match the filters
if (Filters->PassesAllFilters(FActorTreeItem(InActor)))
{
ApplicableActors.Emplace(InActor);
}
ConstructItemFor<FActorTreeItem>(InActor);
}
}
}
}
void SSceneOutliner::OnLevelActorsRemoved(AActor* InActor)
{
if( !bIsReentrant )
{
if( InActor && SharedData->RepresentingWorld == InActor->GetWorld() )
{
ApplicableActors.Remove(InActor);
if (auto* ItemPtr = TreeItemMap.Find(InActor))
{
PendingOperations.Emplace(FPendingTreeOperation::Removed, ItemPtr->ToSharedRef());
Refresh();
}
}
}
}
void SSceneOutliner::OnLevelActorsAttached(AActor* InActor, const AActor* InParent)
{
// InActor can be equal to InParent in cases of components being attached internally. The Scene Outliner does not need to do anything in this case.
if( !bIsReentrant && InActor != InParent )
{
if( InActor && SharedData->RepresentingWorld == InActor->GetWorld() )
{
if (auto* ItemPtr = TreeItemMap.Find(InActor))
{
PendingOperations.Emplace(FPendingTreeOperation::Moved, ItemPtr->ToSharedRef());
Refresh();
}
}
}
}
void SSceneOutliner::OnLevelActorsDetached(AActor* InActor, const AActor* InParent)
{
// InActor can be equal to InParent in cases of components being attached internally. The Scene Outliner does not need to do anything in this case.
if( !bIsReentrant && InActor != InParent)
{
if( InActor && SharedData->RepresentingWorld == InActor->GetWorld() )
{
if (auto* ItemPtr = TreeItemMap.Find(InActor))
{
PendingOperations.Emplace(FPendingTreeOperation::Moved, ItemPtr->ToSharedRef());
Refresh();
}
else
{
// We should find the item, but if we don't, do an add.
OnLevelActorsAdded(InActor);
}
}
}
}
/** Called by the engine when an actor's folder is changed */
void SSceneOutliner::OnLevelActorFolderChanged(const AActor* InActor, FName OldPath)
{
auto* ActorTreeItem = TreeItemMap.Find(InActor);
if (!ShouldShowFolders() || !InActor || !ActorTreeItem)
{
return;
}
PendingOperations.Emplace(FPendingTreeOperation::Moved, ActorTreeItem->ToSharedRef());
Refresh();
}
void SSceneOutliner::OnLevelActorsRequestRename(const AActor* InActor)
{
auto SelectedItems = OutlinerTreeView->GetSelectedItems();
if( SelectedItems.Num() > 0)
{
// Ensure that the item we want to rename is visible in the tree
FTreeItemPtr ItemToRename = SelectedItems[SelectedItems.Num() - 1];
if (ItemToRename->CanInteract())
{
PendingRenameItem = ItemToRename->AsShared();
ScrollItemIntoView(ItemToRename);
}
}
}
void SSceneOutliner::OnMapChange(uint32 MapFlags)
{
FullRefresh();
}
void SSceneOutliner::OnNewCurrentLevel()
{
if (IsShowingOnlyCurrentLevel())
{
FullRefresh();
}
}
void SSceneOutliner::PostUndo(bool bSuccess)
{
// Refresh our tree in case any changes have been made to the scene that might effect our actor list
if( !bIsReentrant )
{
FullRefresh();
}
}
void SSceneOutliner::OnActorLabelChanged(AActor* ChangedActor)
{
if ( !ensure(ChangedActor) )
{
return;
}
auto TreeItem = TreeItemMap.FindRef(ChangedActor);
if (TreeItem.IsValid())
{
if (SearchBoxFilter->PassesFilter(*TreeItem))
{
OutlinerTreeView->FlashHighlightOnItem(TreeItem);
RequestSort();
}
else
{
// Do longer matches the filters, remove it
PendingOperations.Emplace(FPendingTreeOperation::Removed, TreeItem.ToSharedRef());
Refresh();
}
}
else if (IsActorDisplayable(ChangedActor))
{
// Attempt to add the item if we didn't find it - perhaps it now matches the filter?
ConstructItemFor<FActorTreeItem>(ChangedActor);
}
}
void SSceneOutliner::OnFilterTextChanged( const FText& InFilterText )
{
SearchBoxFilter->SetRawFilterText( InFilterText );
FilterTextBoxWidget->SetError( SearchBoxFilter->GetFilterErrorText() );
}
void SSceneOutliner::OnFilterTextCommitted( const FText& InFilterText, ETextCommit::Type CommitInfo )
{
const FString CurrentFilterText = InFilterText.ToString();
// We'll only select actors if the user actually pressed the enter key. We don't want to change
// selection just because focus was lost from the search text field.
if( CommitInfo == ETextCommit::OnEnter )
{
// Any text in the filter? If not, we won't bother doing anything
if( !CurrentFilterText.IsEmpty() )
{
FItemSelection Selection;
// Gather all of the actors that match the filter text
for (auto& Pair : TreeItemMap)
{
if (!Pair.Value->Flags.bIsFilteredOut)
{
Pair.Value->Visit(Selection);
}
}
// We only select level actors when in actor browsing mode
if( SharedData->Mode == ESceneOutlinerMode::ActorBrowsing )
{
// 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 (auto* Actor : Selection.GetActorPtrs())
{
const bool bShouldSelect = true;
const bool bSelectEvenIfHidden = false;
GEditor->SelectActor( Actor, bShouldSelect, bNoteSelectionChange, bSelectEvenIfHidden );
}
// Commit selection changes
GEditor->GetSelectedActors()->EndBatchSelectOperation();
// 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.)
SetKeyboardFocus();
}
// In 'actor picking' mode, we allow the user to commit their selection by pressing enter
// in the search window when a single actor is available
else if( SharedData->Mode == ESceneOutlinerMode::ActorPicker )
{
// In actor picking mode, we check to see if we have a selected actor, and if so, fire
// off the notification to whoever is listening. This may often cause the widget itself
// to be enqueued for destruction
if( Selection.Actors.Num() == 1 )
{
// Signal that an actor was selected. We assume it is valid as it won't have been added to ActorsToSelect if not.
OnItemPicked.ExecuteIfBound( Selection.Actors[0]->AsShared() );
}
}
}
}
}
EVisibility SSceneOutliner::GetFilterStatusVisibility() const
{
return IsFilterActive() ? EVisibility::Visible : EVisibility::Collapsed;
}
EVisibility SSceneOutliner::GetEmptyLabelVisibility() const
{
return ( IsFilterActive() || RootTreeItems.Num() > 0 ) ? EVisibility::Collapsed : EVisibility::Visible;
}
FText SSceneOutliner::GetFilterStatusText() const
{
const int32 TotalActorCount = ApplicableActors.Num();
int32 SelectedActorCount = 0;
auto Count = [&](const FActorTreeItem&) { ++SelectedActorCount; };
for (const auto& Item : OutlinerTreeView->GetSelectedItems())
{
Item->Visit(FFunctionalVisitor().Actor(Count));
}
if ( !IsFilterActive() )
{
if (SelectedActorCount == 0)
{
return FText::Format( LOCTEXT("ShowingAllActorsFmt", "{0} actors"), FText::AsNumber( TotalActorCount ) );
}
else
{
return FText::Format( LOCTEXT("ShowingAllActorsSelectedFmt", "{0} actors ({1} selected)"), FText::AsNumber( TotalActorCount ), FText::AsNumber( SelectedActorCount ) );
}
}
else if( IsFilterActive() && FilteredActorCount == 0 )
{
return FText::Format( LOCTEXT("ShowingNoActorsFmt", "No matching actors ({0} total)"), FText::AsNumber( TotalActorCount ) );
}
else if (SelectedActorCount != 0)
{
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 SSceneOutliner::GetFilterStatusTextColor() const
{
if ( !IsFilterActive() )
{
// White = no text filter
return FLinearColor( 1.0f, 1.0f, 1.0f );
}
else if( FilteredActorCount == 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 );
}
}
bool SSceneOutliner::IsFilterActive() const
{
return FilterTextBoxWidget->GetText().ToString().Len() > 0 && ApplicableActors.Num() != FilteredActorCount;
}
const FSlateBrush* SSceneOutliner::GetFilterButtonGlyph() const
{
if( IsFilterActive() )
{
return FEditorStyle::GetBrush(TEXT("SceneOutliner.FilterCancel"));
}
else
{
return FEditorStyle::GetBrush(TEXT("SceneOutliner.FilterSearch"));
}
}
FString SSceneOutliner::GetFilterButtonToolTip() const
{
return IsFilterActive() ? LOCTEXT("ClearSearchFilter", "Clear search filter").ToString() : LOCTEXT("StartSearching", "Search").ToString();
}
TAttribute<FText> SSceneOutliner::GetFilterHighlightText() const
{
return TAttribute<FText>::Create(TAttribute<FText>::FGetter::CreateStatic([](TWeakPtr<TreeItemTextFilter> Filter){
auto FilterPtr = Filter.Pin();
return FilterPtr.IsValid() ? FilterPtr->GetRawFilterText() : FText();
}, TWeakPtr<TreeItemTextFilter>(SearchBoxFilter)));
}
void SSceneOutliner::SetKeyboardFocus()
{
if (SupportsKeyboardFocus())
{
FWidgetPath OutlinerTreeViewWidgetPath;
// NOTE: Careful, GeneratePathToWidget can be reentrant in that it can call visibility delegates and such
FSlateApplication::Get().GeneratePathToWidgetUnchecked( OutlinerTreeView.ToSharedRef(), OutlinerTreeViewWidgetPath );
FSlateApplication::Get().SetKeyboardFocus( OutlinerTreeViewWidgetPath, EFocusCause::SetDirectly );
}
}
bool SSceneOutliner::SupportsKeyboardFocus() const
{
// We only need to support keyboard focus if we're in actor browsing mode
if( SharedData->Mode == ESceneOutlinerMode::ActorBrowsing )
{
// Scene outliner needs keyboard focus so the user can press keys to activate commands, such as the Delete
// key to delete selected actors
return true;
}
return false;
}
FReply SSceneOutliner::OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent )
{
// @todo outliner: Use command system for these for discoverability? (allow bindings?)
// We only allow these operations in actor browsing mode
if( SharedData->Mode == ESceneOutlinerMode::ActorBrowsing )
{
// Rename key: Rename selected actors (not rebindable, because it doesn't make much sense to bind.)
if( InKeyEvent.GetKey() == EKeys::F2 )
{
if (OutlinerTreeView->GetNumItemsSelected() == 1)
{
FTreeItemPtr ItemToRename = OutlinerTreeView->GetSelectedItems()[0];
if (ItemToRename->CanInteract())
{
PendingRenameItem = ItemToRename->AsShared();
ScrollItemIntoView(ItemToRename);
}
return FReply::Handled();
}
}
// F5 forces a full refresh
else if ( InKeyEvent.GetKey() == EKeys::F5 )
{
FullRefresh();
return FReply::Handled();
}
// Delete key: Delete selected actors (not rebindable, because it doesn't make much sense to bind.)
else if ( InKeyEvent.GetKey() == EKeys::Platform_Delete )
{
const FItemSelection Selection(*OutlinerTreeView);
if( SharedData->CustomDelete.IsBound() )
{
SharedData->CustomDelete.Execute( Selection.GetWeakActors() );
}
else if (CheckWorld())
{
const FScopedTransaction Transaction( LOCTEXT("UndoAction_DeleteSelection", "Delete selection") );
// Delete selected folders too
auto SelectedItems = OutlinerTreeView->GetSelectedItems();
GEditor->SelectNone(true, true);
for (auto* Folder : Selection.Folders)
{
Folder->Delete();
}
for (auto* Actor : Selection.GetActorPtrs())
{
GEditor->SelectActor(Actor, true, false);
}
// Code from FLevelEditorActionCallbacks::Delete_CanExecute()
// Should this be just return FReply::Unhandled()?
TArray<FEdMode*> ActiveModes;
GLevelEditorModeTools().GetActiveModes( ActiveModes );
for( int32 ModeIndex = 0; ModeIndex < ActiveModes.Num(); ++ModeIndex )
{
const EEditAction::Type CanProcess = ActiveModes[ModeIndex]->GetActionEditDelete();
if (CanProcess == EEditAction::Process)
{
return FReply::Handled();
}
else if (CanProcess == EEditAction::Halt)
{
return FReply::Unhandled();
}
}
if (GUnrealEd->CanDeleteSelectedActors( SharedData->RepresentingWorld, true, false ))
{
GEditor->edactDeleteSelected( SharedData->RepresentingWorld );
}
}
return FReply::Handled();
}
}
return FReply::Unhandled();
}
void SSceneOutliner::SynchronizeActorSelection()
{
TGuardValue<bool> ReentrantGuard(bIsReentrant, true);
auto* SelectedActors = GEditor->GetSelectedActors();
// Deselect actors in the tree that are no longer selected in the world
FItemSelection Selection(*OutlinerTreeView);
for (auto* ActorItem : Selection.Actors)
{
if (!SelectedActors->IsSelected(ActorItem->Actor.Get()))
{
OutlinerTreeView->SetItemSelection(ActorItem->AsShared(), false);
}
}
// Ensure that all selected actors in the world are selected in the tree
for (FSelectionIterator SelectionIt( *SelectedActors ); SelectionIt; ++SelectionIt)
{
AActor* Actor = CastChecked< AActor >(*SelectionIt);
if (auto* ActorItem = TreeItemMap.Find(Actor))
{
OutlinerTreeView->SetItemSelection(*ActorItem, true);
}
}
// Broadcast selection changed delegate
SelectionChanged.Broadcast();
}
void SSceneOutliner::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime )
{
for (auto& Pair : Columns)
{
Pair.Value->Tick(InCurrentTime, InDeltaTime);
}
if ( bPendingFocusNextFrame && FilterTextBoxWidget->GetVisibility() == EVisibility::Visible )
{
FWidgetPath WidgetToFocusPath;
FSlateApplication::Get().GeneratePathToWidgetUnchecked( FilterTextBoxWidget.ToSharedRef(), WidgetToFocusPath );
FSlateApplication::Get().SetKeyboardFocus( WidgetToFocusPath, EFocusCause::SetDirectly );
bPendingFocusNextFrame = false;
}
if( bNeedsRefresh )
{
if( !bIsReentrant )
{
Populate();
}
}
SortOutlinerTimer -= InDeltaTime;
if (bSortDirty && (!SharedData->bRepresentingPlayWorld || SortOutlinerTimer <= 0))
{
SortItems(RootTreeItems);
for (const auto& Pair : TreeItemMap)
{
Pair.Value->Flags.bChildrenRequireSort = true;
}
OutlinerTreeView->RequestTreeRefresh();
bSortDirty = false;
}
if (SortOutlinerTimer <= 0)
{
SortOutlinerTimer = SCENE_OUTLINER_RESORT_TIMER;
}
if (bActorSelectionDirty)
{
SynchronizeActorSelection();
bActorSelectionDirty = false;
}
}
EColumnSortMode::Type SSceneOutliner::GetColumnSortMode( const FName ColumnId ) const
{
if (SortByColumn == ColumnId)
{
auto Column = Columns.FindRef(ColumnId);
if (Column.IsValid() && Column->SupportsSorting())
{
return SortMode;
}
}
return EColumnSortMode::None;
}
void SSceneOutliner::OnColumnSortModeChanged( const EColumnSortPriority::Type SortPriority, const FName& ColumnId, const EColumnSortMode::Type InSortMode )
{
auto Column = Columns.FindRef(ColumnId);
if (!Column.IsValid() || !Column->SupportsSorting())
{
return;
}
SortByColumn = ColumnId;
SortMode = InSortMode;
RequestSort();
}
void SSceneOutliner::RequestSort()
{
bSortDirty = true;
}
void SSceneOutliner::SortItems(TArray<FTreeItemPtr>& Items) const
{
auto Column = Columns.FindRef(SortByColumn);
if (Column.IsValid())
{
Column->SortItems(Items, SortMode);
}
}
void SSceneOutliner::FOnItemAddedToTree::Visit(FActorTreeItem& ActorItem) const
{
Outliner.FilteredActorCount += ActorItem.Flags.bIsFilteredOut ? 0 : 1;
// Synchronize selection
if (Outliner.SharedData->Mode == ESceneOutlinerMode::ActorBrowsing && GEditor->GetSelectedActors()->IsSelected(ActorItem.Actor.Get()))
{
// Have to const cast here as the tree view is templated on non-const ptrs
Outliner.OutlinerTreeView->SetItemSelection(ActorItem.AsShared(), true);
}
}
void SSceneOutliner::FOnItemAddedToTree::Visit(FFolderTreeItem& Folder) const
{
if (!Outliner.SharedData->RepresentingWorld)
{
return;
}
if (FActorFolderProps* Props = FActorFolders::Get().GetFolderProperties(*Outliner.SharedData->RepresentingWorld, Folder.Path))
{
Folder.Flags.bIsExpanded = Props->bIsExpanded;
}
}
} // namespace SceneOutliner
#undef LOCTEXT_NAMESPACE