// Copyright Epic Games, Inc. All Rights Reserved. #include "SSceneOutliner.h" #include "Editor.h" #include "Styling/AppStyle.h" #include "Engine/GameViewportClient.h" #include "Framework/Application/SlateApplication.h" #include "Framework/Commands/UIAction.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "HAL/PlatformApplicationMisc.h" #include "ISceneOutlinerColumn.h" #include "Layout/WidgetPath.h" #include "Modules/ModuleManager.h" #include "SceneOutlinerFilters.h" #include "SceneOutlinerModule.h" #include "ScopedTransaction.h" #include "Textures/SlateIcon.h" #include "ToolMenus.h" #include "UObject/PackageReload.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/SOverlay.h" #include "ISceneOutlinerMode.h" #include "FolderTreeItem.h" #include "EditorFolderUtils.h" #include "SceneOutlinerConfig.h" #include "SceneOutlinerFilterBar.h" #include "Framework/Notifications/NotificationManager.h" #include "Widgets/Notifications/SNotificationList.h" #include "Widgets/Input/SMultiLineEditableTextBox.h" #include "DetailLayoutBuilder.h" DEFINE_LOG_CATEGORY(LogSceneOutliner); static float GSceneOutlinerProcessingBudgetPerFrame = 5.0f; static FAutoConsoleVariableRef CVarGuardBandMultiplier( TEXT("SceneOutliner.ProcessingBudgetPerFrame"), GSceneOutlinerProcessingBudgetPerFrame, TEXT("Maximum time in mZilliseconds to spend processing operations per frame")); #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 void SSceneOutliner::Construct(const FArguments& InArgs, const FSceneOutlinerInitializationOptions& InInitOptions) { // Copy over the shared data from the initialization options static_cast(*SharedData) = static_cast(InInitOptions); // We use the filter collection provided, otherwise we create our own Filters = InInitOptions.Filters.IsValid() ? InInitOptions.Filters : MakeShareable(new FSceneOutlinerFilters); OutlinerIdentifier = InInitOptions.OutlinerIdentifier; // Setup the SearchBox // Modes can add filters on init so we do the widget creation before initing the mode { SearchBoxFilter = CreateTextFilter(); FilterTextBoxWidget = SNew(SFilterSearchBox) .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 ); } CreateFilterBar(InInitOptions.FilterBarOptions); check(InInitOptions.ModeFactory.IsBound()); Mode = InInitOptions.ModeFactory.Execute(this); check(Mode); bProcessingFullRefresh = false; bFullRefresh = true; bNeedsRefresh = true; bNeedsColumRefresh = true; bShouldCacheColumnVisibility = true; bForceParentItemsExpanded = false; bIsReentrant = false; bSortDirty = true; bSelectionDirty = true; SortOutlinerTimer = 0.0f; bPendingFocusNextFrame = InInitOptions.bFocusSearchBoxWhenOpened; SortByColumn = FSceneOutlinerBuiltInColumnTypes::Label(); SortMode = EColumnSortMode::Ascending; UOutlinerConfig::Initialize(); UOutlinerConfig::Get()->LoadEditorConfig(); const FSceneOutlinerConfig* SceneOutlinerConfig = GetConstConfig(); // Load the pinned items visibility from the config file if (SceneOutlinerConfig) { bShouldStackHierarchyHeaders = SceneOutlinerConfig->bShouldStackHierarchyHeaders; } // @todo outliner: Should probably save this in layout! // @todo outliner: Should save spacing for list view in layout // Setup Commands BindCommands(); FSceneOutlinerModule& SceneOutlinerModule = FModuleManager::LoadModuleChecked("SceneOutliner"); SceneOutlinerModule.OnColumnPermissionListChanged().AddSP(this, &SSceneOutliner::OnColumnPermissionListChanged); TSharedRef VerticalBox = SNew(SVerticalBox); for (auto& ModeFilterInfo : Mode->GetFilterInfos()) { ModeFilterInfo.Value.InitFilter(Filters); } SearchBoxFilter->OnChanged().AddSP( this, &SSceneOutliner::FullRefresh ); Filters->OnChanged().AddSP(this, &SSceneOutliner::FullRefresh); HeaderRowWidget = SNew( SHeaderRow ) // Only show the list header if the user configured the outliner for that .Visibility(InInitOptions.bShowHeaderRow ? EVisibility::Visible : EVisibility::Collapsed) .CanSelectGeneratedColumn(InInitOptions.bCanSelectGeneratedColumns) .OnHiddenColumnsListChanged(this, &SSceneOutliner::HandleHiddenColumnsChanged); SetupColumns(); CacheHiddenColumns = TSet(HeaderRowWidget->GetHiddenColumnIds()); ChildSlot [ VerticalBox ]; VerticalBox->AddSlot() .AutoHeight() [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() [ SNew(SMultiLineEditableTextBox) .IsReadOnly(true) .Visibility_Lambda([this]() { return Mode->HasErrors() ? EVisibility::Visible : EVisibility::Collapsed; }) .Font(IDetailLayoutBuilder::GetDetailFontBold()) .BackgroundColor(FAppStyle::GetColor("ErrorReporting.WarningBackgroundColor")) .Text(Mode->GetErrorsText()) .AutoWrapText(true) ] + SVerticalBox::Slot() .AutoHeight() [ SNew(SButton) .OnClicked_Lambda([this] { Mode->RepairErrors(); return FReply::Handled(); }) .Visibility_Lambda([this]() { return Mode->HasErrors() ? EVisibility::Visible : EVisibility::Collapsed; }) .HAlign(HAlign_Center) .Text(LOCTEXT("SceneOutlinerRepairErrors", "Repair Errors")) ] ]; auto Toolbar = SNew(SHorizontalBox); Toolbar->AddSlot() .VAlign(VAlign_Center) [ FilterTextBoxWidget.ToSharedRef() ]; if (Mode->CanCustomizeToolbar()) { CustomAddToToolbar(Toolbar); } if (Mode->SupportsCreateNewFolder() && InInitOptions.bShowCreateNewFolder) { Toolbar->AddSlot() .VAlign(VAlign_Center) .AutoWidth() .Padding(4.f, 0.f, 0.f, 0.f) [ SNew(SButton) .ButtonStyle(FAppStyle::Get(), "SimpleButton") .ToolTipText(LOCTEXT("CreateFolderToolTip", "Create a new folder containing the current selection")) .OnClicked(this, &SSceneOutliner::OnCreateFolderClicked) [ SNew(SImage) .ColorAndOpacity(FSlateColor::UseForeground()) .Image(FAppStyle::Get().GetBrush("SceneOutliner.NewFolderIcon")) ] ]; } if (Mode->ShowViewButton()) { // View mode combo button Toolbar->AddSlot() .VAlign(VAlign_Center) .AutoWidth() [ SAssignNew( ViewOptionsComboButton, SComboButton ) .ComboButtonStyle( FAppStyle::Get(), "SimpleComboButtonWithIcon" ) // Use the tool bar item style for this button .OnGetMenuContent( this, &SSceneOutliner::GetViewButtonContent, Mode->ShowFilterOptions()) .HasDownArrow(false) .ButtonContent() [ SNew(SImage) .ColorAndOpacity(FSlateColor::UseForeground()) .Image( FAppStyle::Get().GetBrush("Icons.Settings") ) ] ]; } VerticalBox->AddSlot() .AutoHeight() .Padding( 8.0f, 8.0f, 8.0f, 4.0f ) [ Toolbar ]; // Add the FilterBar and the Add Filter button if it exists if(FilterBar) { // Add Filter Menu Toolbar->InsertSlot(0) .VAlign(VAlign_Center) .Padding(0.0f, 0.0f, 2.0f, 0.0f) .AutoWidth() [ SSceneOutlinerFilterBar::MakeAddFilterButton(FilterBar.ToSharedRef()) ]; VerticalBox->AddSlot() .AutoHeight() .Padding( 0.0f, 0.0f, 0.0f, 4.0f ) [ FilterBar.ToSharedRef() ]; } VerticalBox->AddSlot() .FillHeight(1.0) [ 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() [ SNew(SBorder).BorderImage( FAppStyle::Get().GetBrush("Brushes.Recessed") ) ] +SOverlay::Slot() [ SAssignNew( OutlinerTreeView, SSceneOutlinerTreeView, StaticCastSharedRef(AsShared()) ) // Determined by the 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 the user single-clicks with LMB on an item in the list .OnMouseButtonClick( this, &SSceneOutliner::OnOutlinerTreeSingleClick ) // 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 ) // Generates the actual widget for a pinned tree item .OnGeneratePinnedRow(this, &SSceneOutliner::OnGeneratePinnedRowForOutlinerTree) // Use the level viewport context menu as the right click menu for tree items .OnContextMenuOpening(this, &SSceneOutliner::OnOpenContextMenu) // Header for the tree .HeaderRow( HeaderRowWidget ) // Called when an item is expanded or collapsed with the shift-key pressed down .OnSetExpansionRecursive(this, &SSceneOutliner::SetItemExpansionRecursive) // Make it easier to see hierarchies when there are a lot of items .HighlightParentNodesForSelection(true) // Show the Hierarchy of actors pinned at the top of the tree view .ShouldStackHierarchyHeaders(this, &SSceneOutliner::ShouldStackHierarchyHeaders) // Preserve the selection when the selected item is hidden due to a parent collapsing .AllowInvisibleItemSelection(true) ] ]; // Bottom panel status bar, if enabled by the mode if (Mode->ShowStatusBar()) { VerticalBox->AddSlot() .AutoHeight() [ SNew(SBorder) .BorderImage( FAppStyle::Get().GetBrush("Brushes.Header") ) .VAlign(VAlign_Center) .HAlign(HAlign_Left) .Padding(FMargin(14, 9)) [ SNew( STextBlock ) .Text(this, &SSceneOutliner::GetFilterStatusText) .ColorAndOpacity(this, &SSceneOutliner::GetFilterStatusTextColor) ] ]; } // Don't allow tool-tips over the header HeaderRowWidget->EnableToolTipForceField( true ); // Populate our data set Populate(); // Register to update when an undo/redo operation has been called to update our list of items GEditor->RegisterForUndo( this ); // Register to be notified when properties are edited FCoreUObjectDelegates::OnPackageReloaded.AddRaw(this, &SSceneOutliner::OnAssetReloaded); SourceControlHandler = TSharedPtr(new FSceneOutlinerSCCHandler()); } void SSceneOutliner::HandleHiddenColumnsChanged() { if (!bShouldCacheColumnVisibility) { return; } TSet HiddenColumns = TSet(HeaderRowWidget->GetHiddenColumnIds()); FSceneOutlinerConfig* OutlinerConfig = GetMutableConfig(); if (OutlinerConfig != nullptr) { TMap ColumnVisibilities = OutlinerConfig->ColumnVisibilities; bool bAnyColumnVisibilityChanged = false; for (const TPair>& Pair : Columns) { const bool bWasColumnVisible = CacheHiddenColumns.Find(Pair.Key) == nullptr; const bool bIsColumnVisible = HiddenColumns.Find(Pair.Key) == nullptr; // Only update column visibility if it changed if (bWasColumnVisible != bIsColumnVisible) { ColumnVisibilities.FindOrAdd(Pair.Key) = bIsColumnVisible; bAnyColumnVisibilityChanged = true; } } // Only call SaveConfig if something actually changed if(bAnyColumnVisibilityChanged) { OutlinerConfig->ColumnVisibilities = ColumnVisibilities; SaveConfig(); } } CacheHiddenColumns = MoveTemp(HiddenColumns); } void SSceneOutliner::GetSortedColumnIDs(TArray& OutColumnIDs) const { FSceneOutlinerModule& SceneOutlinerModule = FModuleManager::LoadModuleChecked("SceneOutliner"); TMap FilteredColumnMap; for(auto It(SharedData->ColumnMap.CreateIterator()); It; ++It) { if (SceneOutlinerModule.GetColumnPermissionList()->PassesFilter(It.Key())) { FilteredColumnMap.Add(It.Key(), It.Value()); } } // Get a list of sorted columns IDs to create OutColumnIDs.Empty(); OutColumnIDs.Reserve(FilteredColumnMap.Num()); FilteredColumnMap.GenerateKeyArray(OutColumnIDs); OutColumnIDs.Sort([&](const FName& A, const FName& B) { return FilteredColumnMap[A].PriorityIndex < FilteredColumnMap[B].PriorityIndex; }); } void SSceneOutliner::AddColumn_Internal(const FName& ColumnId, const FSceneOutlinerColumnInfo& ColumnInfo, const TMap& ColumnVisibilities, int32 InsertPosition) { if(!HeaderRowWidget) { return; } SHeaderRow& HeaderRow = *HeaderRowWidget; FSceneOutlinerModule& SceneOutlinerModule = FModuleManager::LoadModuleChecked("SceneOutliner"); // Avoid caching column visibilities while building the columns bool const bPreviousShouldCacheColumnVisibility = bShouldCacheColumnVisibility; bShouldCacheColumnVisibility = false; bool bIsVisible = true; // If there is a config saved for this column, ignore the default visibility if (const bool *ColumnVisibility = ColumnVisibilities.Find(ColumnId)) { bIsVisible = *ColumnVisibility; } else if (ColumnInfo.Visibility == ESceneOutlinerColumnVisibility::Invisible) { bIsVisible = false; } TSharedPtr Column; if (ColumnInfo.Factory.IsBound()) { Column = ColumnInfo.Factory.Execute(*this); } else { Column = SceneOutlinerModule.FactoryColumn(ColumnId, *this); } if (ensure(Column.IsValid())) { Columns.Add(ColumnId, Column); auto ColumnArgs = Column->ConstructHeaderRowColumn(); if (Column->SupportsSorting()) { ColumnArgs .SortMode(this, &SSceneOutliner::GetColumnSortMode, ColumnId) .OnSort(this, &SSceneOutliner::OnColumnSortModeChanged); } if (ColumnInfo.ColumnLabel.IsSet()) { ColumnArgs.DefaultLabel(ColumnInfo.ColumnLabel); } else { if (HeaderRow.GetVisibility() == EVisibility::Visible) { UE_LOG(LogSceneOutliner, Log, TEXT("Outliner Column %s does not have a localizable name, please specify one to FSceneOutlinerColumnInfo"), *ColumnId.ToString()); } ColumnArgs.DefaultLabel(FText::FromName(ColumnId)); } if (!ColumnInfo.bCanBeHidden) { ColumnArgs.ShouldGenerateWidget(true); } if (ColumnInfo.FillSize.IsSet()) { ColumnArgs.FillWidth(ColumnInfo.FillSize.GetValue()); } if (ColumnInfo.OnGetHeaderContextMenuContent.IsBound()) { ColumnArgs.MenuContent() [ ColumnInfo.OnGetHeaderContextMenuContent.Execute() ]; } ColumnArgs.HeaderComboVisibility(ColumnInfo.HeaderComboVisibility); if(InsertPosition == INDEX_NONE) { HeaderRow.AddColumn(ColumnArgs); } else { HeaderRow.InsertColumn(ColumnArgs, InsertPosition); } HeaderRow.SetShowGeneratedColumn(ColumnId, bIsVisible); } bShouldCacheColumnVisibility = bPreviousShouldCacheColumnVisibility; } void SSceneOutliner::RemoveColumn_Internal(const FName& ColumnId) { Columns.Remove(ColumnId); HeaderRowWidget->RemoveColumn(ColumnId); } void SSceneOutliner::SetupColumns() { if(!HeaderRowWidget) { return; } FSceneOutlinerModule& SceneOutlinerModule = FModuleManager::LoadModuleChecked("SceneOutliner"); if (SharedData->ColumnMap.Num() == 0) { SharedData->UseDefaultColumns(); } TMap FilteredColumnMap; for (auto It(SharedData->ColumnMap.CreateIterator()); It; ++It) { if (SceneOutlinerModule.GetColumnPermissionList()->PassesFilter(It.Key())) { FilteredColumnMap.Add(It.Key(), It.Value()); } } Columns.Empty(FilteredColumnMap.Num()); HeaderRowWidget->ClearColumns(); TArray SortedIDs; GetSortedColumnIDs(SortedIDs); TMap ColumnVisibilities; const FSceneOutlinerConfig* OutlinerConfig = GetConstConfig(); // Try to load visibility of columns from the config file if (OutlinerConfig) { ColumnVisibilities = OutlinerConfig->ColumnVisibilities; } for (const FName& ID : SortedIDs) { AddColumn_Internal(ID, FilteredColumnMap[ID], ColumnVisibilities); } Columns.Shrink(); bNeedsColumRefresh = false; } void SSceneOutliner::RefreshColumns() { bNeedsColumRefresh = true; } void SSceneOutliner::OnColumnPermissionListChanged() { RefreshColumns(); FullRefresh(); } SSceneOutliner::~SSceneOutliner() { if (FSceneOutlinerModule* SceneOutlinerModule = FModuleManager::GetModulePtr("SceneOutliner")) { SceneOutlinerModule->OnColumnPermissionListChanged().RemoveAll(this); } Mode->GetHierarchy()->OnHierarchyChanged().RemoveAll(this); delete Mode; if(GEngine) { GEditor->UnregisterForUndo(this); } SearchBoxFilter->OnChanged().RemoveAll( this ); Filters->OnChanged().RemoveAll(this); FCoreUObjectDelegates::OnPackageReloaded.RemoveAll(this); } void SSceneOutliner::OnItemAdded(const FSceneOutlinerTreeItemID& ItemID, uint8 ActionMask) { NewItemActions.Add(ItemID, ActionMask); } FSlateColor SSceneOutliner::GetViewButtonForegroundColor() const { static const FName InvertedForegroundName("InvertedForeground"); static const FName DefaultForegroundName("DefaultForeground"); return ViewOptionsComboButton->IsHovered() ? FAppStyle::GetSlateColor(InvertedForegroundName) : FAppStyle::GetSlateColor(DefaultForegroundName); } TSharedRef SSceneOutliner::GetViewButtonContent(bool bShowFilters) { // Menu should stay open on selection if filters are not being shown TSharedPtr MenuExtender = MakeShared(); Mode->InitializeViewMenuExtender(MenuExtender); FMenuBuilder MenuBuilder(bShowFilters, nullptr, MenuExtender); MenuBuilder.BeginSection(SceneOutliner::ExtensionHooks::Hierarchy, LOCTEXT("HierarchyHeading", "Hierarchy")); { MenuBuilder.AddMenuEntry( LOCTEXT("ExpandAll", "Expand All"), LOCTEXT("ExpandAllToolTip", "Expand All Items in the Hierarchy"), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &SSceneOutliner::ExpandAll))); MenuBuilder.AddMenuEntry( LOCTEXT("CollapseAll", "Collapse All"), LOCTEXT("CollapseAllToolTip", "Collapse All Items in the Hierarchy"), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &SSceneOutliner::CollapseAll))); MenuBuilder.AddMenuEntry( LOCTEXT("ShowHierarchy", "Stack Hierarchy Headers"), LOCTEXT("ShowHierarchyToolTip", "Toggle pinning of the hierarchy of items at the top of the outliner"), FSlateIcon(), FUIAction( FExecuteAction::CreateRaw(this, &SSceneOutliner::ToggleStackHierarchyHeaders), FCanExecuteAction(), FIsActionChecked::CreateRaw(this, &SSceneOutliner::ShouldStackHierarchyHeaders) ), NAME_None, EUserInterfaceActionType::ToggleButton ); } MenuBuilder.EndSection(); if (bShowFilters) { MenuBuilder.BeginSection(SceneOutliner::ExtensionHooks::Show, LOCTEXT("ShowHeading", "Show")); { // Add mode filters for (auto& ModeFilterInfo : Mode->GetFilterInfos()) { ModeFilterInfo.Value.AddMenu(MenuBuilder); } } MenuBuilder.EndSection(); } Mode->CreateViewContent(MenuBuilder); return MenuBuilder.MakeWidget(); } ESelectionMode::Type SSceneOutliner::GetSelectionMode() const { return Mode->GetSelectionMode(); } void SSceneOutliner::Refresh() { UE_LOG(LogSceneOutliner, VeryVerbose, TEXT("Refresh requested, current refresh delay: %f SceneOutliner = %p"), UIRefreshDelay, this); bNeedsRefresh = true; } void SSceneOutliner::FullRefresh() { UE_LOG(LogSceneOutliner, Verbose, TEXT("Full Refresh")); bDisableIntermediateSorting = true; bFullRefresh = true; RefreshSelection(); Refresh(); } void SSceneOutliner::RefreshSelection() { bSelectionDirty = true; } void SSceneOutliner::Populate() { TRACE_CPUPROFILER_EVENT_SCOPE(SSceneOutliner::Populate); // Block events while we clear out the list TGuardValue ReentrantGuard(bIsReentrant, true); bool bMadeAnySignificantChanges = false; if (bFullRefresh) { // Remember the selected folders TArray> SelectedItems = OutlinerTreeView->GetSelectedItems(); for (const TSharedPtr& SelectedItem : SelectedItems) { if (const FFolderTreeItem* FolderItem = SelectedItem->CastTo()) { PendingFoldersSelect.Add(FolderItem->GetFolder()); } } // Clear the selection here - RepopulateEntireTree will reconstruct it. OutlinerTreeView->ClearSelection(); RepopulateEntireTree(); bMadeAnySignificantChanges = true; bFullRefresh = false; } const double StartTime = FPlatformTime::Seconds(); // To avoid checking the time budget for every item. const int32 CheckBudgetEveryNthItem = 100; int32 Index = 0; while (Index < PendingOperations.Num()) { auto& PendingOp = PendingOperations[Index]; switch (PendingOp.Type) { case SceneOutliner::FPendingTreeOperation::Added: bMadeAnySignificantChanges = AddItemToTree(PendingOp.Item) || bMadeAnySignificantChanges; break; case SceneOutliner::FPendingTreeOperation::Moved: bMadeAnySignificantChanges = true; OnItemMoved(PendingOp.Item); break; case SceneOutliner::FPendingTreeOperation::Removed: bMadeAnySignificantChanges = true; RemoveItemFromTree(PendingOp.Item); break; default: check(false); break; } ++Index; if ((Index % CheckBudgetEveryNthItem) == 0) { const double TimeSpentInMs = (FPlatformTime::Seconds() - StartTime) * 1000.0; if (TimeSpentInMs > GSceneOutlinerProcessingBudgetPerFrame) { UE_LOG(LogSceneOutliner, Verbose, TEXT("Processing out of budget (%.2f ms) : %.2f ms"), GSceneOutlinerProcessingBudgetPerFrame, (float)TimeSpentInMs); break; } } } UE_LOG(LogSceneOutliner, Verbose, TEXT("%d Items Processed"), Index); PendingOperations.RemoveAt(0, Index); for (const FFolder& Folder : PendingFoldersSelect) { if (FSceneOutlinerTreeItemPtr* Item = TreeItemMap.Find(Folder)) { OutlinerTreeView->SetItemSelection(*Item, true); } } PendingFoldersSelect.Empty(); // Check if we need to sort because we are finished with the populating operations bool bFinalSort = false; if (PendingOperations.Num() == 0) { // When done processing a FullRefresh Scroll to First item in selection as it may have been // scrolled out of view by the Refresh if (bProcessingFullRefresh) { FSceneOutlinerItemSelection ItemSelection(*OutlinerTreeView); if (ItemSelection.Num() > 0) { FSceneOutlinerTreeItemPtr ItemToScroll = ItemSelection.SelectedItems[0].Pin(); if (ItemToScroll) { ScrollItemIntoView(ItemToScroll); } } } bProcessingFullRefresh = false; // We're fully refreshed now. NewItemActions.Empty(); bNeedsRefresh = false; if (bDisableIntermediateSorting) { bDisableIntermediateSorting = false; bFinalSort = true; } } // If we are allowing intermediate sorts and met the conditions, or this is the final sort after all ops are complete if ((bMadeAnySignificantChanges && !bDisableIntermediateSorting) || bFinalSort) { RequestSort(); } } bool SSceneOutliner::ShouldShowFolders() const { return Mode->ShouldShowFolders(); } void SSceneOutliner::EmptyTreeItems() { PendingOperations.Empty(); TreeItemMap.Reset(); PendingTreeItemMap.Empty(); RootTreeItems.Empty(); } void SSceneOutliner::AddPendingItem(FSceneOutlinerTreeItemPtr Item) { PendingTreeItemMap.Add(Item->GetID(), Item); PendingOperations.Emplace(SceneOutliner::FPendingTreeOperation::Added, Item.ToSharedRef()); } void SSceneOutliner::AddPendingItemAndChildren(FSceneOutlinerTreeItemPtr Item) { if (!Item.IsValid()) { return; } // Verify that there isn't already a pending operation for this item: if (PendingTreeItemMap.Contains(Item->GetID())) { return; } AddPendingItem(Item); TArray Children; Mode->GetHierarchy()->CreateChildren(Item, Children); for (auto& Child : Children) { AddPendingItem(Child); } Refresh(); } void SSceneOutliner::RepopulateEntireTree() { TRACE_CPUPROFILER_EVENT_SCOPE(SSceneOutliner::RepopulateEntireTree); EmptyTreeItems(); // Rebuild the hierarchy Mode->Rebuild(); Mode->GetHierarchy()->OnHierarchyChanged().AddSP(this, &SSceneOutliner::OnHierarchyChangedEvent); // Create all the items which match the filters, parent-child relationships are handled when each item is actually added to the tree TArray Items; Mode->GetHierarchy()->CreateItems(Items); for (FSceneOutlinerTreeItemPtr& Item : Items) { AddPendingItem(Item); } bProcessingFullRefresh = PendingOperations.Num() > 0; Refresh(); } void SSceneOutliner::OnChildRemovedFromParent(ISceneOutlinerTreeItem& Parent) { if (!Parent.GetChildren().Num()) { if (Parent.ShouldRemoveOnceLastChildRemoved()) { // The parent no longer has any children that match the current search terms. Remove it. RemoveItemFromTree(Parent.AsShared()); } } } void SSceneOutliner::OnItemMoved(const FSceneOutlinerTreeItemRef& ReferenceItem) { // Just remove the item if it no longer matches the filters if (!ReferenceItem->Flags.bIsFilteredOut && !PassesAllFilters(ReferenceItem)) { // This will potentially remove any non-matching, empty parents as well RemoveItemFromTree(ReferenceItem); } else if (const FSceneOutlinerTreeItemPtr* ItemInTree = TreeItemMap.Find(ReferenceItem->GetID())) { FSceneOutlinerTreeItemRef Item = ItemInTree->ToSharedRef(); // 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 FSceneOutlinerTreeItemPtr 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); } } } FSceneOutlinerTreeItemPtr SSceneOutliner::GetTreeItem(FSceneOutlinerTreeItemID ItemID, bool bIncludePending) { FSceneOutlinerTreeItemPtr Result = TreeItemMap.FindRef(ItemID); if (bIncludePending && !Result.IsValid()) { Result = PendingTreeItemMap.FindRef(ItemID); } return Result; } void SSceneOutliner::SetNextUIRefreshDelay(float InDelay) { UE_LOG(LogSceneOutliner, VeryVerbose, TEXT("UI refresh delay set to %f, previously %f Scene Outliner = %p"), InDelay, UIRefreshDelay, this); UIRefreshDelay = InDelay; } void SSceneOutliner::RemoveItemFromTree(FSceneOutlinerTreeItemRef ReferenceItem) { if (const FSceneOutlinerTreeItemPtr* ItemInTree = TreeItemMap.Find(ReferenceItem->GetID())) { FSceneOutlinerTreeItemRef Item = ItemInTree->ToSharedRef(); // If the item we are removing is selected, refresh the selection to remove it from there as well if(OutlinerTreeView->GetSelectedItems().Contains(Item)) { RefreshSelection(); } auto Parent = Item->GetParent(); if (Parent.IsValid()) { Parent->RemoveChild(Item); OnChildRemovedFromParent(*Parent); } else { RootTreeItems.Remove(Item); } PendingTreeItemMap_Removal.Remove(Item->GetID()); TreeItemMap.Remove(Item->GetID()); Mode->OnItemRemoved(Item); } } FSceneOutlinerTreeItemPtr SSceneOutliner::EnsureParentForItem(FSceneOutlinerTreeItemRef Item) { if (SharedData->bShowParentTree) { FSceneOutlinerTreeItemPtr Parent = Mode->GetHierarchy()->FindOrCreateParentItem(*Item, TreeItemMap, /*bCreate=*/false); if (Parent.IsValid()) { return Parent; } else { // Try to find the parent in the pending items Parent = Mode->GetHierarchy()->FindOrCreateParentItem(*Item, PendingTreeItemMap, /*bCreate=*/true); if (Parent.IsValid()) { AddUnfilteredItemToTree(Parent.ToSharedRef()); return Parent; } } } return nullptr; } bool SSceneOutliner::AddItemToTree(FSceneOutlinerTreeItemRef Item) { const auto ItemID = Item->GetID(); PendingTreeItemMap.Remove(ItemID); // If a tree item already exists that represents the same data or if the item represents invalid data, bail if (TreeItemMap.Find(ItemID) || !Item->IsValid()) { return false; } // Set the filtered out flag Item->Flags.bIsFilteredOut = !PassesAllFilters(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 & SceneOutliner::ENewItemAction::Select) { OutlinerTreeView->ClearSelection(); OutlinerTreeView->SetItemSelection(Item, true); } if (*ActionMask & SceneOutliner::ENewItemAction::Rename && CanExecuteRenameRequest(*Item)) { PendingRenameItem = Item; } if (*ActionMask & (SceneOutliner::ENewItemAction::ScrollIntoView | SceneOutliner::ENewItemAction::Rename)) { ScrollItemIntoView(Item); } } } return true; } void SSceneOutliner::AddUnfilteredItemToTree(FSceneOutlinerTreeItemRef Item) { FSceneOutlinerTreeItemPtr Parent = EnsureParentForItem(Item); const FSceneOutlinerTreeItemID ItemID = Item->GetID(); if(TreeItemMap.Contains(ItemID)) { UE_LOG(LogSceneOutliner, Error, TEXT("(%d | %s) already exists in tree. Dumping map..."), GetTypeHash(ItemID), *Item->GetDisplayString() ); for(TPair& Entry : TreeItemMap) { UE_LOG(LogSceneOutliner, Log, TEXT("(%d | %s)"), GetTypeHash(Entry.Key), *Entry.Value->GetDisplayString()); } // this is a fatal error check(false); } TreeItemMap.Add(ItemID, Item); if (Parent.IsValid()) { Parent->AddChild(Item); } else { RootTreeItems.Add(Item); } Item->Flags.bIsExpanded = CachedExpansionStateInfo.FindOrAdd(Item->GetID(), Item->Flags.bIsExpanded); Mode->OnItemAdded(Item); } void SSceneOutliner::PopulateSearchStrings(const ISceneOutlinerTreeItem& Item, TArray< FString >& OutSearchStrings) const { for (const auto& Pair : Columns) { Pair.Value->PopulateSearchStrings(Item, OutSearchStrings); } } /** Creates a TextFilter for ISceneOutlinerTreeItem */ TSharedPtr< SceneOutliner::TreeItemTextFilter > SSceneOutliner::CreateTextFilter() const { auto Delegate = SceneOutliner::TreeItemTextFilter::FItemToStringArray::CreateSP( this, &SSceneOutliner::PopulateSearchStrings ); return MakeShareable( new SceneOutliner::TreeItemTextFilter( Delegate ) ); } void SSceneOutliner::GetSelectedFolders(TArray& OutFolders) const { return FSceneOutlinerItemSelection(*OutlinerTreeView).Get(OutFolders); } TSharedPtr SSceneOutliner::OnOpenContextMenu() { return Mode->CreateContextMenu(); } bool SSceneOutliner::Delete_CanExecute() { return Mode->CanDelete(); } bool SSceneOutliner::Rename_CanExecute() { return Mode->CanRename(); } void SSceneOutliner::Rename_Execute() { FSceneOutlinerItemSelection ItemSelection(*OutlinerTreeView); FSceneOutlinerTreeItemPtr ItemToRename; if (Mode->CanRename()) { ItemToRename = OutlinerTreeView->GetSelectedItems()[0]; } if (ItemToRename.IsValid() && CanExecuteRenameRequest(*ItemToRename) && ItemToRename->CanInteract()) { PendingRenameItem = ItemToRename->AsShared(); ScrollItemIntoView(ItemToRename); } } bool SSceneOutliner::Cut_CanExecute() { return Mode->CanCut(); } bool SSceneOutliner::Copy_CanExecute() { return Mode->CanCopy(); } bool SSceneOutliner::Paste_CanExecute() { return Mode->CanPaste(); } bool SSceneOutliner::CanSupportDragAndDrop() const { return Mode->CanSupportDragAndDrop(); } bool SSceneOutliner::CanExecuteRenameRequest(const ISceneOutlinerTreeItem& ItemPtr) const { return Mode->CanRenameItem(ItemPtr); } int32 SSceneOutliner::AddFilter(const TSharedRef& Filter) { return Filters->Add(Filter); } void SSceneOutliner::AddFilterToFilterBar(const TSharedRef>& InFilter) { if(FilterBar) { FilterBar->AddFilter(InFilter); } } void SSceneOutliner::DisableAllFilterBarFilters(bool bRemove) { if(FilterBar) { bRemove ? FilterBar->RemoveAllFilters() : FilterBar->DisableAllFilters(); } } bool SSceneOutliner::RemoveFilter(const TSharedRef& Filter) { return Filters->Remove(Filter) > 0; } int32 SSceneOutliner::AddInteractiveFilter(const TSharedRef& Filter) { return InteractiveFilters->Add(Filter); } bool SSceneOutliner::RemoveInteractiveFilter(const TSharedRef& Filter) { return InteractiveFilters->Remove(Filter) > 0; } TSharedPtr SSceneOutliner::GetFilterAtIndex(int32 Index) { return StaticCastSharedPtr(Filters->GetFilterAtIndex(Index)); } int32 SSceneOutliner::GetFilterCount() const { return Filters->Num(); } void SSceneOutliner::AddColumn(FName ColumnId, const FSceneOutlinerColumnInfo& ColumnInfo) { if (!SharedData->ColumnMap.Contains(ColumnId)) { SharedData->ColumnMap.Add(ColumnId, ColumnInfo); // Get the new sorted list of columns to make sure this is added in the right position TArray SortedColumnIDs; GetSortedColumnIDs(SortedColumnIDs); TMap ColumnVisibilities; const FSceneOutlinerConfig* OutlinerConfig = GetConstConfig(); if(OutlinerConfig) { ColumnVisibilities = OutlinerConfig->ColumnVisibilities; } AddColumn_Internal(ColumnId, ColumnInfo, ColumnVisibilities, SortedColumnIDs.Find(ColumnId)); } } void SSceneOutliner::RemoveColumn(FName ColumnId) { if (SharedData->ColumnMap.Contains(ColumnId)) { SharedData->ColumnMap.Remove(ColumnId); RemoveColumn_Internal(ColumnId); } } void SSceneOutliner::SetColumnVisibility(FName ColumnId, bool bIsVisible) { if (Columns.Contains(ColumnId)) { HeaderRowWidget->SetShowGeneratedColumn(ColumnId, bIsVisible); } } TArray SSceneOutliner::GetColumnIds() const { TArray ColumnsName; SharedData->ColumnMap.GenerateKeyArray(ColumnsName); return ColumnsName; } void SSceneOutliner::SetSelection(const TFunctionRef Selector) { TArray ItemsToAdd; for (const auto& Pair : TreeItemMap) { FSceneOutlinerTreeItemPtr ItemPtr = Pair.Value; if (ISceneOutlinerTreeItem* Item = ItemPtr.Get()) { if (Selector(*Item)) { ItemsToAdd.Add(ItemPtr); } } } SetItemSelection(ItemsToAdd, true); } void SSceneOutliner::SetItemSelection(const TArray& InItems, bool bSelected, ESelectInfo::Type SelectInfo) { OutlinerTreeView->ClearSelection(); OutlinerTreeView->SetItemSelection(InItems, bSelected, SelectInfo); } void SSceneOutliner::SetItemSelection(const FSceneOutlinerTreeItemPtr& InItem, bool bSelected, ESelectInfo::Type SelectInfo) { OutlinerTreeView->ClearSelection(); OutlinerTreeView->SetItemSelection(InItem, bSelected, SelectInfo); } void SSceneOutliner::AddToSelection(const TArray& InItems, ESelectInfo::Type SelectInfo) { OutlinerTreeView->SetItemSelection(InItems, true, SelectInfo); } void SSceneOutliner::AddToSelection(const FSceneOutlinerTreeItemPtr& InItem, ESelectInfo::Type SelectInfo) { OutlinerTreeView->SetItemSelection(InItem, true, SelectInfo); } void SSceneOutliner::RemoveFromSelection(const TArray& InItems, ESelectInfo::Type SelectInfo) { OutlinerTreeView->SetItemSelection(InItems, false, SelectInfo); } void SSceneOutliner::AddFolderToSelection(const FName& FolderName) { // Not used (but public) : For backward compatibility, we use Mode->GetRootObject() FFolder::FRootObject RootObject = Mode->GetRootObject(); if (FFolder::IsRootObjectValid(RootObject)) { if (FSceneOutlinerTreeItemPtr* ItemPtr = TreeItemMap.Find(FFolder(RootObject, FolderName))) { OutlinerTreeView->SetItemSelection(*ItemPtr, true); } } } void SSceneOutliner::RemoveFolderFromSelection(const FName& FolderName) { // Not used (but public) : For backward compatibility, we use Mode->GetRootObject() FFolder::FRootObject RootObject = Mode->GetRootObject(); if (FFolder::IsRootObjectValid(RootObject)) { if (FSceneOutlinerTreeItemPtr* ItemPtr = TreeItemMap.Find(FFolder(RootObject, FolderName))) { OutlinerTreeView->SetItemSelection(*ItemPtr, false); } } } void SSceneOutliner::ClearSelection() { if (!bIsReentrant) { OutlinerTreeView->ClearSelection(); } } void SSceneOutliner::FillFoldersSubMenu(UToolMenu* Menu) const { FFolder::FRootObject TargetRootObject; if (!GetCommonRootObjectFromSelection(TargetRootObject)) { return; } FToolMenuSection& Section = Menu->AddSection("Section"); Section.AddMenuEntry("CreateNew", LOCTEXT( "CreateNew", "Create New Folder" ), LOCTEXT( "CreateNew_ToolTip", "Move to a new folder" ), FSlateIcon(FAppStyle::GetAppStyleSetName(), "SceneOutliner.NewFolderIcon"), FExecuteAction::CreateSP(const_cast(this), &SSceneOutliner::CreateFolder)); AddMoveToFolderOutliner(Menu); } TSharedRef> SSceneOutliner::GatherInvalidMoveToDestinations() const { // We use a pointer here to save copying the whole array for every invocation of the filter delegate TSharedRef> ExcludedParents(new TSet()); for (const auto& Item : OutlinerTreeView->GetSelectedItems()) { const FSceneOutlinerTreeItemPtr Parent = Item->GetParent(); const FFolderTreeItem* ParentFolderItem = Parent.IsValid() ? Parent->CastTo() : nullptr; if (ParentFolderItem) { auto FolderHasOtherSubFolders = [&Item](const TWeakPtr& WeakItem) { if (WeakItem.Pin()->IsA() && WeakItem.Pin() != Item) { return true; } return false; }; // Exclude this items direct parent if it is a folder and has no other subfolders we can move to bool bFolderHasSubFolders = false; for (const TWeakPtr& ItemPtr : ParentFolderItem->GetChildren()) { if (FolderHasOtherSubFolders(ItemPtr)) { bFolderHasSubFolders = true; break; } } if (!bFolderHasSubFolders) { ExcludedParents->Add(ParentFolderItem->GetFolder()); } } if (FFolderTreeItem* FolderItem = Item->CastTo()) { // Cannot move into itself, or any child ExcludedParents->Add(FolderItem->GetFolder()); } } return ExcludedParents; } void SSceneOutliner::AddMoveToFolderOutliner(UToolMenu* Menu) const { // We don't show this if the mode does not show folders if (!Mode->ShouldShowFolders()) { return; } FFolder::FRootObject TargetRootObject; if (!GetCommonRootObjectFromSelection(TargetRootObject)) { return; } // Add a mini scene outliner for choosing an existing folder FSceneOutlinerInitializationOptions MiniSceneOutlinerInitOptions; MiniSceneOutlinerInitOptions.bShowHeaderRow = false; MiniSceneOutlinerInitOptions.bFocusSearchBoxWhenOpened = 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 = [](const FFolder& Folder, TSharedRef> ExcludedParents) { for (const auto& Parent : *ExcludedParents) { if (Folder == Parent || Folder.IsChildOf(Parent)) { return false; } } return true; }; MiniSceneOutlinerInitOptions.Filters->AddFilterPredicate(FFolderTreeItem::FFilterPredicate::CreateStatic(FilterOutChildFolders, ExcludedParents), FSceneOutlinerFilter::EDefaultBehaviour::Pass); } { struct FFilterRoot : public FSceneOutlinerFilter { FFilterRoot(const SSceneOutliner& InSceneOutliner) : FSceneOutlinerFilter(FSceneOutlinerFilter::EDefaultBehaviour::Pass) , SceneOutliner(InSceneOutliner) {} virtual bool PassesFilter(const ISceneOutlinerTreeItem& Item) const override { FSceneOutlinerTreeItemPtr Parent = SceneOutliner.FindParent(Item); // if item has no parent, it is a root item and should be filtered out if (!Parent.IsValid()) { return false; } return DefaultBehaviour == FSceneOutlinerFilter::EDefaultBehaviour::Pass; } const SSceneOutliner& SceneOutliner; }; // Filter in/out root items according to whether it is valid to move to/from the root FSceneOutlinerDragDropPayload DraggedObjects(OutlinerTreeView->GetSelectedItems()); const bool bMoveToRootValid = Mode->ValidateDrop(FFolderTreeItem(FFolder(TargetRootObject, FFolder::GetEmptyPath())), DraggedObjects).IsValid(); if (!bMoveToRootValid) { MiniSceneOutlinerInitOptions.Filters->Add(MakeShared(*this)); } } //Let the mode decide how folder selection is handled MiniSceneOutlinerInitOptions.ModeFactory = Mode->CreateFolderPickerMode(TargetRootObject); FSceneOutlinerModule& SceneOutlinerModule = FModuleManager::LoadModuleChecked("SceneOutliner"); TSharedRef< SWidget > MiniSceneOutliner = SNew(SVerticalBox) + SVerticalBox::Slot() .MaxHeight(400.0f) [ SNew(SSceneOutliner, MiniSceneOutlinerInitOptions) .IsEnabled(FSlateApplication::Get().GetNormalExecutionAttribute()) ]; FToolMenuSection& Section = Menu->AddSection(FName(), LOCTEXT("ExistingFolders", "Existing:")); Section.AddEntry(FToolMenuEntry::InitWidget( "MiniSceneOutliner", MiniSceneOutliner, FText::GetEmpty(), false)); } void SSceneOutliner::FillSelectionSubMenu(UToolMenu* Menu) const { FToolMenuSection& Section = Menu->AddSection("Section"); Section.AddMenuEntry( "AddChildrenToSelection", LOCTEXT( "AddChildrenToSelection", "Immediate Children" ), LOCTEXT( "AddChildrenToSelection_ToolTip", "Select all immediate children of the selected folders" ), FSlateIcon(), FExecuteAction::CreateSP(const_cast(this), &SSceneOutliner::SelectFoldersDescendants, /*bSelectImmediateChildrenOnly=*/ true)); Section.AddMenuEntry( "AddDescendantsToSelection", LOCTEXT( "AddDescendantsToSelection", "All Descendants" ), LOCTEXT( "AddDescendantsToSelection_ToolTip", "Select all descendants of the selected folders" ), FSlateIcon(), FExecuteAction::CreateSP(const_cast(this), &SSceneOutliner::SelectFoldersDescendants, /*bSelectImmediateChildrenOnly=*/ false)); } void SSceneOutliner::SelectFoldersDescendants(bool bSelectImmediateChildrenOnly) { TArray SelectedFolders; GetSelectedFolders(SelectedFolders); OutlinerTreeView->ClearSelection(); if (SelectedFolders.Num()) { Mode->SelectFoldersDescendants(SelectedFolders, bSelectImmediateChildrenOnly); } Refresh(); } void SSceneOutliner::MoveSelectionTo(const FFolder& NewParent) { FSlateApplication::Get().DismissAllMenus(); FFolderTreeItem DropTarget(NewParent); FSceneOutlinerDragDropPayload DraggedObjects(OutlinerTreeView->GetSelectedItems()); FSceneOutlinerDragValidationInfo Validation = Mode->ValidateDrop(DropTarget, DraggedObjects); 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 Outliner Items")); Mode->OnDrop(DropTarget, DraggedObjects, Validation); } FReply SSceneOutliner::OnCreateFolderClicked() { CreateFolder(); return FReply::Handled(); } void SSceneOutliner::CreateFolder() { const FFolder& NewFolderName = Mode->CreateNewFolder(); if (!NewFolderName.IsNone()) { // Move any selected folders into the new folder auto PreviouslySelectedItems = OutlinerTreeView->GetSelectedItems(); for (const auto& Item : PreviouslySelectedItems) { if (FFolderTreeItem* FolderItem = Item->CastTo()) { // New folder root object will be identical if whole selection had a common root object. // If not, new folder won't have a root object (world), so this check is needed to skip folders with a different root object. if (FolderItem->GetRootObject() == NewFolderName.GetRootObject()) { FolderItem->MoveTo(NewFolderName); } } } // 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, SceneOutliner::ENewItemAction::Select | SceneOutliner::ENewItemAction::Rename); RequestSort(); } } void SSceneOutliner::CopyFoldersBegin() { CacheFoldersEdit.Reset(); CacheFoldersEditRootObject = FFolder::GetInvalidRootObject(); TArray SelectedFolders = GetSelection().GetData(SceneOutliner::FFolderPathSelector()); FFolder::GetFolderPathsAndCommonRootObject(SelectedFolders, CacheFoldersEdit, CacheFoldersEditRootObject); FPlatformApplicationMisc::ClipboardPaste(CacheClipboardContents); } void SSceneOutliner::CopyFoldersEnd() { if (CacheFoldersEdit.Num() > 0) { CopyFoldersToClipboard(CacheFoldersEdit, CacheClipboardContents); CacheFoldersEdit.Reset(); CacheFoldersEditRootObject = FFolder::GetInvalidRootObject(); } } void SSceneOutliner::CopyFoldersToClipboard(const TArray& InFolders, const FString& InPrevClipboardContents) { if (InFolders.Num() > 0) { // If clipboard paste has changed since we cached it, items must have been cut // so folders need to appended to clipboard contents rather than replacing them. FString CurrClipboardContents; FPlatformApplicationMisc::ClipboardPaste(CurrClipboardContents); FString Buffer = ExportFolderList(InFolders); FString* SourceData = (CurrClipboardContents != InPrevClipboardContents ? &CurrClipboardContents : nullptr); if (SourceData) { SourceData->Append(Buffer); } else { SourceData = &Buffer; } // Replace clipboard contents with original plus folders appended FPlatformApplicationMisc::ClipboardCopy(**SourceData); } } bool SSceneOutliner::GetCommonRootObjectFromSelection(FFolder::FRootObject& OutCommonRootObject) const { TOptional CommonRootObject; for (const TWeakPtr& Item : GetSelection().SelectedItems) { if (auto TreeItem = Item.Pin()) { if (!CommonRootObject.IsSet()) { CommonRootObject = TreeItem->GetRootObject(); } else if (CommonRootObject.GetValue() != TreeItem->GetRootObject()) { OutCommonRootObject = FFolder::GetInvalidRootObject(); break; } } } OutCommonRootObject = CommonRootObject.Get(FFolder::GetInvalidRootObject()); return FFolder::IsRootObjectValid(OutCommonRootObject); } void SSceneOutliner::PasteFoldersBegin(TArray InFolders) { auto CacheExistingChildrenAction = [this](const FSceneOutlinerTreeItemPtr& Item, const FFolder::FRootObject& InTargetRootObject) { if (FFolderTreeItem* FolderItem = Item->CastTo()) { TArray ExistingChildren; for (const TWeakPtr& Child : FolderItem->GetChildren()) { if (Child.IsValid()) { ExistingChildren.Add(Child.Pin()->GetID()); } } check(FolderItem->GetRootObject() == InTargetRootObject); CachePasteFolderExistingChildrenMap.Add(FolderItem->GetFolder(), ExistingChildren); } }; CacheFoldersEdit.Reset(); CacheFoldersEditRootObject = Mode->GetPasteTargetRootObject(); CachePasteFolderExistingChildrenMap.Reset(); PendingFoldersSelect.Reset(); CacheFoldersEdit = InFolders; // Sort folder names so parents appear before children CacheFoldersEdit.Sort(FNameLexicalLess()); // Cache existing children for (FName Folder : CacheFoldersEdit) { if (FSceneOutlinerTreeItemPtr* TreeItem = TreeItemMap.Find(FFolder(CacheFoldersEditRootObject, Folder))) { CacheExistingChildrenAction(*TreeItem, CacheFoldersEditRootObject); } } // Prepare CacheFolderMap which maps old to new/duplicate folder names CacheFolderMap.Reset(); for (FName Folder : CacheFoldersEdit) { FName ParentPath = FEditorFolderUtils::GetParentPath(Folder); FName LeafName = FEditorFolderUtils::GetLeafName(Folder); if (LeafName != TEXT("")) { if (FName* NewParentPath = CacheFolderMap.Find(ParentPath)) { ParentPath = *NewParentPath; } FFolder NewFolderPath = Mode->GetFolder(FFolder(CacheFoldersEditRootObject, ParentPath), LeafName); CacheFolderMap.Add(Folder, NewFolderPath.GetPath()); } } } void SSceneOutliner::PasteFoldersEnd() { ON_SCOPE_EXIT { CacheFoldersEdit.Reset(); CacheFoldersEditRootObject = FFolder::GetInvalidRootObject(); CacheFolderMap.Reset(); CachePasteFolderExistingChildrenMap.Reset(); }; if (CacheFoldersEdit.IsEmpty()) { FullRefresh(); return; } const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "PasteItems", "Paste Items")); // Create new folder TMap CreatedFolders; for (FName Folder : CacheFoldersEdit) { if (FName* NewFolder = CacheFolderMap.Find(Folder)) { // When using Actor Folder, duplicated actors might already have created the actor folder (when destination rootobject is different) if (Mode->CreateFolder(FFolder(CacheFoldersEditRootObject, *NewFolder))) { CreatedFolders.Add(Folder, *NewFolder); } } } // Populate our data set Populate(); // Reparent duplicated items if the folder has been pasted/duplicated for (FName OldFolderName : CacheFoldersEdit) { // Get the new folder that was created from this name if (const FName* NewFolderName = CreatedFolders.Find(OldFolderName)) { FFolder NewFolder(CacheFoldersEditRootObject, *NewFolderName); FFolder OldFolder(CacheFoldersEditRootObject, OldFolderName); if (FSceneOutlinerTreeItemPtr* OldFolderItem = TreeItemMap.Find(OldFolder)) { for (const TWeakPtr& Child : (*OldFolderItem)->GetChildren()) { // If this child did not exist in the folder before the paste operation, it should be moved to the new folder TArray* ExistingChildren = CachePasteFolderExistingChildrenMap.Find(OldFolder); if (ExistingChildren && !ExistingChildren->Contains(Child.Pin()->GetID())) { Mode->ReparentItemToFolder(NewFolder, Child.Pin()); } } } PendingFoldersSelect.Add(NewFolder); } } FullRefresh(); } void SSceneOutliner::DuplicateFoldersHierarchy() { TFunction RecursiveFolderSelect = [&](const FSceneOutlinerTreeItemPtr& Item) { if (Item->IsA()) { OutlinerTreeView->SetItemSelection(Item, true); } for (const TWeakPtr& Child : Item->GetChildren()) { RecursiveFolderSelect(Child.Pin()); } }; const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "DuplicateFoldersHierarchy", "Duplicate Folders Hierarchy")); TArray SelectedFolders; GetSelectedFolders(SelectedFolders); if (SelectedFolders.Num() > 0) { // Select folder descendants SelectFoldersDescendants(); // Select all sub-folders for (FFolderTreeItem* Folder : SelectedFolders) { RecursiveFolderSelect(Folder->AsShared()); } // Duplicate selected Mode->OnDuplicateSelected(); } } void SSceneOutliner::DeleteFoldersBegin() { CacheFoldersDelete.Empty(); GetSelectedFolders(CacheFoldersDelete); } void SSceneOutliner::DeleteFoldersEnd() { struct FMatchFolder { FMatchFolder(const FFolder& InFolder) : Folder(InFolder) {} const FFolder Folder; bool operator()(const FFolderTreeItem *Entry) { return Folder == Entry->GetFolder(); } }; if (CacheFoldersDelete.Num() > 0) { // Sort in descending order so children will be deleted before parents CacheFoldersDelete.Sort([](const FFolderTreeItem& FolderA, const FFolderTreeItem& FolderB) { return FolderB.GetPath().LexicalLess(FolderA.GetPath()); }); for (FFolderTreeItem* FolderItem : CacheFoldersDelete) { if (FolderItem) { // Find lowest parent not being deleted, for reparenting children of current folder FFolder NewParentPath = FolderItem->GetFolder().GetParent(); while (!NewParentPath.IsNone() && CacheFoldersDelete.FindByPredicate(FMatchFolder(NewParentPath))) { NewParentPath = NewParentPath.GetParent(); } FolderItem->Delete(NewParentPath); } } CacheFoldersDelete.Reset(); FullRefresh(); } } TArray SSceneOutliner::GetClipboardPasteFolders() const { FString PasteString; FPlatformApplicationMisc::ClipboardPaste(PasteString); return ImportFolderList(*PasteString); } FString SSceneOutliner::ExportFolderList(TArray InFolders) const { FString Buffer = FString(TEXT("Begin FolderList\n")); for (auto& FolderName : InFolders) { Buffer.Append(FString(TEXT("\tFolder=")) + FolderName.ToString() + FString(TEXT("\n"))); } Buffer += FString(TEXT("End FolderList\n")); return Buffer; } TArray SSceneOutliner::ImportFolderList(const FString& InStrBuffer) const { TArray Folders; int32 Index = InStrBuffer.Find(TEXT("Begin FolderList")); if (Index != INDEX_NONE) { FString TmpStr = InStrBuffer.RightChop(Index); const TCHAR* Buffer = *TmpStr; FString StrLine; while (FParse::Line(&Buffer, StrLine)) { const TCHAR* Str = *StrLine; FString FolderName; if (FParse::Command(&Str, TEXT("Begin")) && FParse::Command(&Str, TEXT("FolderList"))) { continue; } else if (FParse::Command(&Str, TEXT("End")) && FParse::Command(&Str, TEXT("FolderList"))) { break; } else if (FParse::Value(Str, TEXT("Folder="), FolderName)) { Folders.Add(FName(*FolderName)); } } } return Folders; } void SSceneOutliner::ScrollItemIntoView(const FSceneOutlinerTreeItemPtr& Item) { auto Parent = Item->GetParent(); while (Parent.IsValid()) { OutlinerTreeView->SetItemExpansion(Parent->AsShared(), true); Parent = Parent->GetParent(); } OutlinerTreeView->RequestScrollIntoView(Item); } void SSceneOutliner::SetItemExpansion(const FSceneOutlinerTreeItemPtr& Item, bool bIsExpanded) { OutlinerTreeView->SetItemExpansion(Item, bIsExpanded); } bool SSceneOutliner::IsItemExpanded(const FSceneOutlinerTreeItemPtr& Item) const { return OutlinerTreeView->IsItemExpanded(Item); } TSharedRef< ITableRow > SSceneOutliner::OnGenerateRowForOutlinerTree( FSceneOutlinerTreeItemPtr Item, const TSharedRef< STableViewBase >& OwnerTable ) { return SNew( SSceneOutlinerTreeRow, OutlinerTreeView.ToSharedRef(), SharedThis(this) ).Item( Item ); } TSharedRef< ITableRow > SSceneOutliner::OnGeneratePinnedRowForOutlinerTree(FSceneOutlinerTreeItemPtr Item, const TSharedRef< STableViewBase >& OwnerTable) { return SNew(SSceneOutlinerPinnedTreeRow, OutlinerTreeView.ToSharedRef(), SharedThis(this)).Item(Item); } void SSceneOutliner::OnGetChildrenForOutlinerTree( FSceneOutlinerTreeItemPtr InParent, TArray< FSceneOutlinerTreeItemPtr >& 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(); InParent->Children.Reserve(OutChildren.Num()); for (auto& Child : OutChildren) { InParent->Children.Emplace(Child); } // They no longer need sorting InParent->Flags.bChildrenRequireSort = false; } } } void SSceneOutliner::OnOutlinerTreeSelectionChanged( FSceneOutlinerTreeItemPtr TreeItem, ESelectInfo::Type SelectInfo ) { if (SelectInfo == ESelectInfo::Direct) { return; } if (!bIsReentrant) { TGuardValue ReentrantGuard(bIsReentrant, true); Mode->OnItemSelectionChanged(TreeItem, SelectInfo, FSceneOutlinerItemSelection(*OutlinerTreeView)); OnItemSelectionChanged.Broadcast(TreeItem, SelectInfo); } } void SSceneOutliner::OnOutlinerTreeDoubleClick( FSceneOutlinerTreeItemPtr TreeItem ) { if (!Mode->HasCustomFolderDoubleClick()) { if (TreeItem->IsA()) { SetItemExpansion(TreeItem, !IsItemExpanded(TreeItem)); } } Mode->OnItemDoubleClick(TreeItem); OnDoubleClickOnTreeEvent.Broadcast(TreeItem); } void SSceneOutliner::OnOutlinerTreeSingleClick(FSceneOutlinerTreeItemPtr TreeItem) const { Mode->OnItemClicked(TreeItem); } void SSceneOutliner::OnOutlinerTreeItemScrolledIntoView( FSceneOutlinerTreeItemPtr TreeItem, const TSharedPtr& Widget ) { if (TreeItem == PendingRenameItem.Pin()) { PendingRenameItem = nullptr; TreeItem->RenameRequestEvent.ExecuteIfBound(); } } void SSceneOutliner::OnItemExpansionChanged(FSceneOutlinerTreeItemPtr TreeItem, bool bIsExpanded) const { if (bForceParentItemsExpanded) { return; } TreeItem->Flags.bIsExpanded = bIsExpanded; TreeItem->OnExpansionChanged(); // Expand any children that are also expanded for (auto WeakChild : TreeItem->GetChildren()) { auto Child = WeakChild.Pin(); if (Child.IsValid() && Child->Flags.bIsExpanded) { OutlinerTreeView->SetItemExpansion(Child, true); } } // Notify Mode CachedExpansionStateInfo.Add(TreeItem->GetID(), TreeItem->Flags.bIsExpanded); } void SSceneOutliner::OnHierarchyChangedEvent(FSceneOutlinerHierarchyChangedData Event) { if (Event.Type == FSceneOutlinerHierarchyChangedData::Added) { for (const auto& TreeItemPtr : Event.Items) { if(!TreeItemPtr.IsValid()) { continue; } // If the item doesn't exist in the tree, or is being removed and re-added in the same frame - it is not a duplicate and can be added if(!TreeItemMap.Find(TreeItemPtr->GetID()) || PendingTreeItemMap_Removal.Find(TreeItemPtr->GetID())) { AddPendingItemAndChildren(TreeItemPtr); if (Event.ItemActions) { NewItemActions.Add(TreeItemPtr->GetID(), Event.ItemActions); } } } } else if (Event.Type == FSceneOutlinerHierarchyChangedData::Removed) { for (const auto& TreeItemID : Event.ItemIDs) { FSceneOutlinerTreeItemPtr* Item = TreeItemMap.Find(TreeItemID); if (!Item) { Item = PendingTreeItemMap.Find(TreeItemID); } if (Item) { PendingOperations.Emplace(SceneOutliner::FPendingTreeOperation::Removed, Item->ToSharedRef()); PendingTreeItemMap_Removal.Add(TreeItemID, Item->ToSharedRef()); } } UE_LOG(LogSceneOutliner, VeryVerbose, TEXT("Refresh requested by FSceneOutlinerHierarchyChangedData::Removed")); Refresh(); } else if (Event.Type == FSceneOutlinerHierarchyChangedData::Moved) { for (const auto& TreeItemID : Event.ItemIDs) { FSceneOutlinerTreeItemPtr* Item = TreeItemMap.Find(TreeItemID); if (!Item) { Item = PendingTreeItemMap.Find(TreeItemID); } if (Item) { PendingOperations.Emplace(SceneOutliner::FPendingTreeOperation::Moved, Item->ToSharedRef()); } } for (const auto& TreeItemPtr : Event.Items) { if (TreeItemPtr.IsValid()) { PendingOperations.Emplace(SceneOutliner::FPendingTreeOperation::Moved, TreeItemPtr.ToSharedRef()); } } UE_LOG(LogSceneOutliner, VeryVerbose, TEXT("Refresh requested by FSceneOutlinerHierarchyChangedData::Moved")); Refresh(); } else if (Event.Type == FSceneOutlinerHierarchyChangedData::FolderMoved) { check (Event.ItemIDs.Num() == Event.NewPaths.Num()); for (int32 i = 0; i < Event.ItemIDs.Num(); ++i) { FSceneOutlinerTreeItemPtr Item = TreeItemMap.FindRef(Event.ItemIDs[i]); 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(Item); check(Event.NewPaths[i].GetRootObject() == Folder->GetRootObject()); Folder->SetPath(Event.NewPaths[i].GetPath()); TreeItemMap.Add(Item->GetID(), Item); // Add an operation to move the item in the hierarchy PendingOperations.Emplace(SceneOutliner::FPendingTreeOperation::Moved, Item.ToSharedRef()); } } UE_LOG(LogSceneOutliner, VeryVerbose, TEXT("Refresh requested by FSceneOutlinerHierarchyChangedData::FolderMoved")); Refresh(); } else if (Event.Type == FSceneOutlinerHierarchyChangedData::FullRefresh) { UE_LOG(LogSceneOutliner, VeryVerbose, TEXT("Full Refresh requested by FSceneOutlinerHierarchyChangedData::FullRefresh")); FullRefresh(); } } void SSceneOutliner::PostUndo(bool bSuccess) { // Refresh our tree in case any changes have been made to the scene that might effect our list if( !bIsReentrant ) { UE_LOG(LogSceneOutliner, VeryVerbose, TEXT("FullRefresh requested by SSceneOutliner::PostUndo")); FullRefresh(); } } void SSceneOutliner::OnItemLabelChanged(FSceneOutlinerTreeItemPtr ChangedItem, bool bFlashHighlight) { // If the item already exists if (FSceneOutlinerTreeItemPtr* ExistingItem = TreeItemMap.Find(ChangedItem->GetID())) { (*ExistingItem)->OnLabelChanged(); // The changed item flags will have been set already if (!ChangedItem->Flags.bIsFilteredOut) { if (bFlashHighlight) { OutlinerTreeView->FlashHighlightOnItem(*ExistingItem); } RequestSort(); } else { // No longer matches the filters, remove it PendingOperations.Emplace(SceneOutliner::FPendingTreeOperation::Removed, ExistingItem->ToSharedRef()); PendingTreeItemMap_Removal.Add(ChangedItem->GetID(), ExistingItem->ToSharedRef()); Refresh(); } } else if (FSceneOutlinerTreeItemPtr* PendingItem = PendingTreeItemMap.Find(ChangedItem->GetID())) { (*PendingItem)->OnLabelChanged(); } else { // Attempt to add the item if we didn't find it - perhaps it now matches the filter? if (ChangedItem && !ChangedItem->Flags.bIsFilteredOut) { AddPendingItemAndChildren(ChangedItem); } } } void SSceneOutliner::BindCommands() { CommandList = MakeShared(); // Bind Mode Commands Mode->BindCommands(CommandList.ToSharedRef()); } void SSceneOutliner::OnAssetReloaded(const EPackageReloadPhase InPackageReloadPhase, FPackageReloadedEvent* InPackageReloadedEvent) { if (InPackageReloadPhase == EPackageReloadPhase::PostBatchPostGC) { // perhaps overkill but a simple Refresh() doesn't appear to work. UE_LOG(LogSceneOutliner, VeryVerbose, TEXT("FullRefresh requested by SSceneOutliner::OnAssetReloaded")); FullRefresh(); } } void SSceneOutliner::OnFilterTextChanged( const FText& InFilterText ) { SearchBoxFilter->SetRawFilterText( InFilterText ); FilterTextBoxWidget->SetError( SearchBoxFilter->GetFilterErrorText() ); Mode->OnFilterTextChanged(InFilterText); } void SSceneOutliner::OnFilterTextCommitted( const FText& InFilterText, ETextCommit::Type CommitInfo ) { const FString CurrentFilterText = InFilterText.ToString(); // We'll only select items 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() ) { FSceneOutlinerItemSelection Selection; // Gather all of the items that match the filter text for (auto& Pair : TreeItemMap) { Pair.Value->Flags.bIsFilteredOut = !PassesAllFilters(Pair.Value); if (!Pair.Value->Flags.bIsFilteredOut) { Selection.Add(Pair.Value); } } Mode->OnFilterTextCommited(Selection, CommitInfo); } } else if (CommitInfo == ETextCommit::OnCleared) { OnFilterTextChanged(InFilterText); } } EVisibility SSceneOutliner::GetFilterStatusVisibility() const { return IsTextFilterActive() ? EVisibility::Visible : EVisibility::Collapsed; } EVisibility SSceneOutliner::GetEmptyLabelVisibility() const { return ( IsTextFilterActive() || RootTreeItems.Num() > 0 ) ? EVisibility::Collapsed : EVisibility::Visible; } FText SSceneOutliner::GetFilterStatusText() const { return Mode->GetStatusText(); } FSlateColor SSceneOutliner::GetFilterStatusTextColor() const { return Mode->GetStatusTextColor(); } bool SSceneOutliner::IsTextFilterActive() const { return FilterTextBoxWidget->GetText().ToString().Len() > 0; } const FSlateBrush* SSceneOutliner::GetFilterButtonGlyph() const { if( IsTextFilterActive() ) { return FAppStyle::GetBrush(TEXT("SceneOutliner.FilterCancel")); } else { return FAppStyle::GetBrush(TEXT("SceneOutliner.FilterSearch")); } } FString SSceneOutliner::GetFilterButtonToolTip() const { return IsTextFilterActive() ? LOCTEXT("ClearSearchFilter", "Clear search filter").ToString() : LOCTEXT("StartSearching", "Search").ToString(); } TAttribute SSceneOutliner::GetFilterHighlightText() const { return TAttribute::Create(TAttribute::FGetter::CreateStatic([](TWeakPtr Filter){ auto FilterPtr = Filter.Pin(); return FilterPtr.IsValid() ? FilterPtr->GetRawFilterText() : FText(); }, TWeakPtr(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 ); } } const FSlateBrush* SSceneOutliner::GetCachedIconForClass(FName InClassName) const { if (CachedIcons.Find(InClassName)) { return *CachedIcons.Find(InClassName); } else { return nullptr; } } void SSceneOutliner::CacheIconForClass(FName InClassName, const FSlateBrush* InSlateBrush) { CachedIcons.Emplace(InClassName, InSlateBrush); } bool SSceneOutliner::SupportsKeyboardFocus() const { return Mode->SupportsKeyboardFocus(); } FReply SSceneOutliner::OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent ) { if (CommandList.IsValid()) { if (CommandList->ProcessCommandBindings(InKeyEvent)) { return FReply::Handled(); } } // Fallback to the Mode OnKeyDown to check if it's handled there return Mode->OnKeyDown(InKeyEvent); } void SSceneOutliner::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { TRACE_CPUPROFILER_EVENT_SCOPE(SSceneOutliner::Tick); 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 ( bNeedsColumRefresh ) { SetupColumns(); } if( bNeedsRefresh ) { if( !bIsReentrant ) { if (Mode->CanPopulate()) { Populate(); } } } // If we are pending a sort if (bSortDirty) { bool bExecuteSort = false; SortOutlinerTimer -= InDeltaTime; UIRefreshDelay -= InDeltaTime; // If we are in PIE, we will use SortOutlinerTimer as the min delay between sorts if (GEditor->PlayWorld) { if (SortOutlinerTimer <= 0) { bExecuteSort = true; } } // In editor worlds, we will check if there is any user specific UIRefreshDelay else { if (UIRefreshDelay <= 0) { bExecuteSort = true; } } if (bExecuteSort) { UE_LOG(LogSceneOutliner, VeryVerbose, TEXT("Sort Executed")); SortItems(RootTreeItems); // Also update expansion state based on item states // This is done here because updating expansion state causes a refresh, so we want to make sure that does not ignore any UIRefreshDelay bForceParentItemsExpanded = !SearchBoxFilter->GetRawFilterText().IsEmpty(); for (const auto& Pair : TreeItemMap) { Pair.Value->Flags.bChildrenRequireSort = true; if (Pair.Value->GetChildren().Num()) { OutlinerTreeView->SetItemExpansion(Pair.Value, bForceParentItemsExpanded || Pair.Value->Flags.bIsExpanded); } } OutlinerTreeView->RequestTreeRefresh(); bSortDirty = false; // Reset both timers SortOutlinerTimer = SCENE_OUTLINER_RESORT_TIMER; UIRefreshDelay = 0.0f; } } if (bSelectionDirty) { Mode->SynchronizeSelection(); bSelectionDirty = 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& Items) const { auto Column = Columns.FindRef(SortByColumn); if (Column.IsValid()) { Column->SortItems(Items, SortMode); } } uint32 SSceneOutliner::GetTypeSortPriority(const ISceneOutlinerTreeItem& Item) const { return Mode->GetTypeSortPriority(Item); } void SSceneOutliner::ExpandAll() { for (FSceneOutlinerTreeItemPtr& Item : RootTreeItems) { SetItemExpansionRecursive(Item, true); } } void SSceneOutliner::CollapseAll() { for (FSceneOutlinerTreeItemPtr& Item : RootTreeItems) { SetItemExpansionRecursive(Item, false); } } void SSceneOutliner::SetItemExpansionRecursive(FSceneOutlinerTreeItemPtr Model, bool bInExpansionState) { if (Model.IsValid()) { OutlinerTreeView->SetItemExpansion(Model, bInExpansionState); for (auto& Child : Model->Children) { if (Child.IsValid()) { SetItemExpansionRecursive(Child.Pin(), bInExpansionState); } } } } TSharedPtr SSceneOutliner::CreateDragDropOperation(const FPointerEvent& MouseEvent, const TArray& InTreeItems) const { return Mode->CreateDragDropOperation(MouseEvent, InTreeItems); } /** Parse a drag drop operation into a payload */ bool SSceneOutliner::ParseDragDrop(FSceneOutlinerDragDropPayload& OutPayload, const FDragDropOperation& Operation) const { return Mode->ParseDragDrop(OutPayload, Operation); } /** Validate a drag drop operation on a drop target */ FSceneOutlinerDragValidationInfo SSceneOutliner::ValidateDrop(const ISceneOutlinerTreeItem& DropTarget, const FSceneOutlinerDragDropPayload& Payload) const { return Mode->ValidateDrop(DropTarget, Payload); } /** Called when a payload is dropped onto a target */ void SSceneOutliner::OnDropPayload(ISceneOutlinerTreeItem& DropTarget, const FSceneOutlinerDragDropPayload& Payload, const FSceneOutlinerDragValidationInfo& ValidationInfo) const { return Mode->OnDrop(DropTarget, Payload, ValidationInfo); } /** Called when a payload is dragged over an item */ FReply SSceneOutliner::OnDragOverItem(const FDragDropEvent& Event, const ISceneOutlinerTreeItem& Item) const { return Mode->OnDragOverItem(Event, Item); } void SSceneOutliner::PinItems(const TArray& InItems) { Mode->PinItems(InItems); } void SSceneOutliner::UnpinItems(const TArray& InItems) { Mode->UnpinItems(InItems); } bool SSceneOutliner::CanPinItems(const TArray& InItems) const { return Mode->CanPinItems(InItems); } bool SSceneOutliner::CanUnpinItems(const TArray& InItems) const { return Mode->CanUnpinItems(InItems); } void SSceneOutliner::PinSelectedItems() { TArray SelectedItems; GetSelection().Get(SelectedItems); PinItems(SelectedItems); } void SSceneOutliner::UnpinSelectedItems() { TArray SelectedItems; GetSelection().Get(SelectedItems); UnpinItems(SelectedItems); } bool SSceneOutliner::CanPinSelectedItems() const { TArray SelectedItems; GetSelection().Get(SelectedItems); return CanPinItems(SelectedItems); } bool SSceneOutliner::CanUnpinSelectedItems() const { TArray SelectedItems; GetSelection().Get(SelectedItems); return CanUnpinItems(SelectedItems); } void SSceneOutliner::FrameSelectedItems() { TArray> SelectedItems = GetSelectedItems(); if (!SelectedItems.IsEmpty()) { ScrollItemIntoView(SelectedItems.Last()); } } void SSceneOutliner::FrameItem(const FSceneOutlinerTreeItemID& Item) { if (const TSharedPtr* TreeItem = TreeItemMap.Find(Item)) { ScrollItemIntoView(*TreeItem); } } FSceneOutlinerTreeItemPtr SSceneOutliner::FindParent(const ISceneOutlinerTreeItem& InItem) const { FSceneOutlinerTreeItemPtr Parent = Mode->GetHierarchy()->FindOrCreateParentItem(InItem, TreeItemMap, /*bCreate=*/false); if (!Parent.IsValid()) { Parent = Mode->GetHierarchy()->FindOrCreateParentItem(InItem, PendingTreeItemMap, /*bCreate=*/false); } return Parent; } void SSceneOutliner::ToggleStackHierarchyHeaders() { bShouldStackHierarchyHeaders = !bShouldStackHierarchyHeaders; FSceneOutlinerConfig* OutlinerConfig = GetMutableConfig(); if (OutlinerConfig != nullptr) { OutlinerConfig->bShouldStackHierarchyHeaders = bShouldStackHierarchyHeaders; SaveConfig(); } FullRefresh(); } bool SSceneOutliner::ShouldStackHierarchyHeaders() const { return bShouldStackHierarchyHeaders; } struct FSceneOutlinerConfig* SSceneOutliner::GetMutableConfig() { if (OutlinerIdentifier.IsNone()) { return nullptr; } return &UOutlinerConfig::Get()->Outliners.FindOrAdd(OutlinerIdentifier); } const FSceneOutlinerConfig* SSceneOutliner::GetConstConfig() const { if (OutlinerIdentifier.IsNone()) { return nullptr; } return UOutlinerConfig::Get()->Outliners.Find(OutlinerIdentifier); } void SSceneOutliner::SaveConfig() { UOutlinerConfig::Get()->SaveEditorConfig(); } void FSceneOutlinerMenuHelper::AddMenuEntryCreateFolder(FToolMenuSection& InSection, SSceneOutliner& InOutliner) { const FSlateIcon NewFolderIcon(FAppStyle::GetAppStyleSetName(), "SceneOutliner.NewFolderIcon"); InSection.AddMenuEntry("CreateFolder", LOCTEXT("CreateFolder", "Create Folder"), LOCTEXT("CreateFolderTooltip", "Create Folder"), NewFolderIcon, FUIAction(FExecuteAction::CreateSP(&InOutliner, &SSceneOutliner::CreateFolder))); } void FSceneOutlinerMenuHelper::AddMenuEntryCleanupFolders(FToolMenuSection& InSection, ULevel* InLevel) { if (InLevel && InLevel->IsUsingActorFolders()) { const FSlateIcon CleanupFoldersIcon(FAppStyle::GetAppStyleSetName(), "SceneOutliner.CleanupActorFoldersIcon"); InSection.AddMenuEntry("CleanupFolders", LOCTEXT("CleanupFolders", "Cleanup Folders"), LOCTEXT("CleanupFoldersTooltip", "Cleanup unreferenced and deleted actor folders"), CleanupFoldersIcon, FExecuteAction::CreateLambda([InLevel]() { const FScopedTransaction Transaction(LOCTEXT("CleanupFolders", "Cleanup Folders")); InLevel->CleanupDeletedAndUnreferencedActorFolders(); })); } } TSharedPtr SSceneOutliner::GetItemSourceControl(const FSceneOutlinerTreeItemPtr& InItem) { return SourceControlHandler->GetItemSourceControl(InItem); } void SSceneOutliner::AddSourceControlMenuOptions(UToolMenu* Menu) { TArray SelectedItems = GetTree().GetSelectedItems(); SourceControlHandler->AddSourceControlMenuOptions(Menu, SelectedItems); } void SSceneOutliner::FilterByType(TSharedPtr CustomClassFilterData) { if (FilterBar) { FilterBar->FilterByType(CustomClassFilterData); } } void SSceneOutliner::FilterByTypeCategory(TSharedPtr TypeCategory, TArray> Classes) { if (FilterBar) { FilterBar->FilterByTypeCategory(TypeCategory, Classes); } } bool SSceneOutliner::NeedsRefresh() const { return bNeedsRefresh || bFullRefresh; } void SSceneOutliner::CreateCustomTextFilter(const FCustomTextFilterData& InFilterData, bool bApplyFilter) { if (FilterBar) { if (TSharedPtr OutlinerFilterBar = StaticCastSharedPtr(FilterBar)) { OutlinerFilterBar->OnCreateCustomTextFilter(InFilterData, bApplyFilter); } } } void SSceneOutliner::ModifyCustomTextFilter(const FCustomTextFilterData& InFilterData, const TSharedPtr>& InFilter) { if (FilterBar) { if (TSharedPtr OutlinerFilterBar = StaticCastSharedPtr(FilterBar)) { OutlinerFilterBar->OnModifyCustomTextFilter(InFilterData, InFilter); } } } void SSceneOutliner::DeleteCustomTextFilter(const TSharedPtr>& InFilter) { if (FilterBar) { if (TSharedPtr OutlinerFilterBar = StaticCastSharedPtr(FilterBar)) { OutlinerFilterBar->OnDeleteCustomTextFilter(InFilter); } } } void SSceneOutliner::OnFilterBarFilterChanged() { FilterCollection = FilterBar->GetAllActiveFilters(); FilterBar->SaveSettings(); FullRefresh(); } bool SSceneOutliner::CompareItemWithClassName(SceneOutliner::FilterBarType InItem, const TSet& AssetClassPaths) const { return Mode->CompareItemWithClassName(InItem, AssetClassPaths); } void SSceneOutliner::CreateFilterBar(const FSceneOutlinerFilterBarOptions& FilterBarOptions) { if (!FilterBarOptions.bHasFilterBar) { return; } FName FilterBarIdentifier = OutlinerIdentifier; SAssignNew(FilterBar, SSceneOutlinerFilterBar) .OnCompareItemWithClassNames(this, &SSceneOutliner::CompareItemWithClassName) .OnFilterChanged(this, &SSceneOutliner::OnFilterBarFilterChanged) .CustomClassFilters(FilterBarOptions.CustomClassFilters) .CustomFilters(FilterBarOptions.CustomFilters) .FilterSearchBox(FilterTextBoxWidget) .FilterBarIdentifier(FilterBarIdentifier) .UseSharedSettings(FilterBarOptions.bUseSharedSettings) .CategoryToExpand(FilterBarOptions.CategoryToExpand) .CreateTextFilter(SSceneOutlinerFilterBar::FCreateTextFilter::CreateLambda([this]() { TSharedPtr< SceneOutliner::TreeItemTextFilter > Filter = this->CreateTextFilter(); return MakeShareable(new FCustomTextFilter(Filter)); })); FilterBar->LoadSettings(); } bool SSceneOutliner::IsFilterActive(const FString& FilterName) const { if (FilterBar.IsValid()) { if (TSharedPtr> FoundFilter = FilterBar->GetFilter(FilterName)) { return FilterBar->IsFilterActive(FoundFilter); } } return false; } #undef LOCTEXT_NAMESPACE