// Copyright Epic Games, Inc. All Rights Reserved. #include "SPathView.h" #include "Algo/AllOf.h" #include "Algo/AnyOf.h" #include "Algo/Copy.h" #include "AssetRegistry/ARFilter.h" #include "AssetToolsModule.h" #include "Containers/ContainerAllocationPolicies.h" #include "Containers/StringView.h" #include "ContentBrowserConfig.h" #include "ContentBrowserDataDragDropOp.h" #include "ContentBrowserDataSource.h" #include "ContentBrowserDataSubsystem.h" #include "ContentBrowserItemData.h" #include "ContentBrowserLog.h" #include "ContentBrowserMenuUtils.h" #include "ContentBrowserModule.h" #include "ContentBrowserPathViewMenuContexts.h" #include "ContentBrowserPluginFilters.h" #include "ContentBrowserSingleton.h" #include "ContentBrowserStyle.h" #include "ContentBrowserUtils.h" #include "CoreGlobals.h" #include "CoreTypes.h" #include "DragDropHandler.h" #include "Fonts/SlateFontInfo.h" #include "Framework/Application/SlateApplication.h" #include "Framework/Commands/UIAction.h" #include "Framework/Commands/UICommandInfo.h" #include "HAL/PlatformTime.h" #include "HistoryManager.h" #include "IAssetTools.h" #include "IContentBrowserDataModule.h" #include "Input/Events.h" #include "InputCoreTypes.h" #include "Interfaces/IPluginManager.h" #include "Layout/Children.h" #include "Layout/Margin.h" #include "Layout/SlateRect.h" #include "Layout/WidgetPath.h" #include "Logging/LogCategory.h" #include "Logging/LogMacros.h" #include "Misc/CString.h" #include "Misc/ComparisonUtility.h" #include "Misc/ConfigCacheIni.h" #include "Misc/FilterCollection.h" #include "Misc/NamePermissionList.h" #include "Misc/PathViews.h" #include "Misc/Paths.h" #include "Misc/StringBuilder.h" #include "Modules/ModuleManager.h" #include "PathViewTypes.h" #include "ProfilingDebugging/CpuProfilerTrace.h" #include "Settings/ContentBrowserSettings.h" #include "SlotBase.h" #include "SourcesSearch.h" #include "SourcesViewWidgets.h" #include "Styling/AppStyle.h" #include "Styling/ISlateStyle.h" #include "Textures/SlateIcon.h" #include "ToolMenu.h" #include "ToolMenus.h" #include "ToolMenuSection.h" #include "Framework/Commands/GenericCommands.h" #include "Trace/Detail/Channel.h" #include "Types/WidgetActiveTimerDelegate.h" #include "UObject/UObjectGlobals.h" #include "UObject/UnrealNames.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/Input/SSearchBox.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SExpandableArea.h" #include "Widgets/Layout/SSeparator.h" #include "Widgets/Layout/SSplitter.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Views/ITableRow.h" #include "Widgets/Views/STableRow.h" class FDragDropOperation; class SWidget; struct FAssetData; struct FGeometry; DEFINE_LOG_CATEGORY_STATIC(LogPathView, Log, Log); #define LOCTEXT_NAMESPACE "ContentBrowser" namespace UE::PathView { TArray> AllPathViews; FAutoConsoleCommand RepopulateAllPathViewsCommand(TEXT("PathView.Repopulate"), TEXT("Repopulate all path views to expose bugs with caching/data updates"), FConsoleCommandDelegate::CreateLambda([]() { for (TWeakPtr WeakView : AllPathViews) { if (TSharedPtr View = WeakView.Pin()) { View->Populate(); } } })); TSharedRef CreateOrReuseNode(FContentBrowserItemData&& InData, TMap>* OldItemsByInvariantPath) { if (OldItemsByInvariantPath) { TSharedPtr ExistingItem; // Reolve old value so we don't pick it out again when looking at another item from a different source if (OldItemsByInvariantPath->RemoveAndCopyValue(InData.GetInvariantPath(), ExistingItem) && ExistingItem.IsValid()) { ExistingItem->RemoveAllChildren(); ExistingItem->SetItemData(FContentBrowserItem(MoveTemp(InData))); return ExistingItem.ToSharedRef(); } } return MakeShared(MoveTemp(InData)); } void DefaultSort(TArray>& InChildren) { if (InChildren.Num() < 2) { return; } static const FString ClassesPrefix = TEXT("Classes_"); struct FItemSortInfo { // Name to display FString FolderName; float Priority; int32 SpecialDefaultFolderPriority; bool bIsClassesFolder; TSharedPtr TreeItem; // Name to use when comparing "MyPlugin" vs "Classes_MyPlugin", looking up a plugin by name and other situations FName ItemNameWithoutClassesPrefix; }; TArray SortInfoArray; SortInfoArray.Reserve(InChildren.Num()); const TArray& SpecialSortFolders = IContentBrowserDataModule::Get().GetSubsystem()->GetPathViewSpecialSortFolders(); // Generate information needed to perform sort for (TSharedPtr& It : InChildren) { FItemSortInfo& SortInfo = SortInfoArray.AddDefaulted_GetRef(); SortInfo.TreeItem = It; const FName InvariantPathFName = It->GetItem().GetInvariantPath(); FNameBuilder InvariantPathBuilder(InvariantPathFName); const FStringView InvariantPath(InvariantPathBuilder); bool bIsRootInvariantFolder = false; if (InvariantPath.Len() > 1) { FStringView RootInvariantFolder(InvariantPath); RootInvariantFolder.RightChopInline(1); int32 SecondSlashIndex = INDEX_NONE; bIsRootInvariantFolder = !RootInvariantFolder.FindChar(TEXT('/'), SecondSlashIndex); } SortInfo.FolderName = It->GetItem().GetDisplayName().ToString(); SortInfo.bIsClassesFolder = false; if (bIsRootInvariantFolder) { FNameBuilder ItemNameBuilder(It->GetItem().GetItemName()); const FStringView ItemNameView(ItemNameBuilder); if (ItemNameView.StartsWith(ClassesPrefix)) { SortInfo.bIsClassesFolder = true; SortInfo.ItemNameWithoutClassesPrefix = FName(ItemNameView.RightChop(ClassesPrefix.Len())); } if (SortInfo.FolderName.StartsWith(ClassesPrefix)) { SortInfo.bIsClassesFolder = true; SortInfo.FolderName.RightChopInline(ClassesPrefix.Len(), EAllowShrinking::No); } } if (SortInfo.ItemNameWithoutClassesPrefix.IsNone()) { SortInfo.ItemNameWithoutClassesPrefix = It->GetItem().GetItemName(); } if (SortInfo.bIsClassesFolder) { // Sort using a path without "Classes_" prefix FStringView InvariantWithoutClassesPrefix(InvariantPath); InvariantWithoutClassesPrefix.RightChopInline(1); if (InvariantWithoutClassesPrefix.StartsWith(ClassesPrefix)) { InvariantWithoutClassesPrefix.RightChopInline(ClassesPrefix.Len()); FNameBuilder Builder; Builder.Append(TEXT("/")); Builder.Append(InvariantWithoutClassesPrefix); SortInfo.SpecialDefaultFolderPriority = SpecialSortFolders.IndexOfByKey(FName(Builder)); } else { SortInfo.SpecialDefaultFolderPriority = SpecialSortFolders.IndexOfByKey(InvariantPathFName); } } else { SortInfo.SpecialDefaultFolderPriority = SpecialSortFolders.IndexOfByKey(InvariantPathFName); } if (bIsRootInvariantFolder) { if (SortInfo.SpecialDefaultFolderPriority == INDEX_NONE) { SortInfo.Priority = FContentBrowserSingleton::Get() .GetPluginSettings(SortInfo.ItemNameWithoutClassesPrefix) .RootFolderSortPriority; } else { SortInfo.Priority = 1.f; } } else { if (SortInfo.SpecialDefaultFolderPriority != INDEX_NONE) { SortInfo.Priority = 1.f; } else { SortInfo.Priority = 0.f; } } } // Perform sort SortInfoArray.Sort([](const FItemSortInfo& SortInfoA, const FItemSortInfo& SortInfoB) -> bool { if (SortInfoA.Priority != SortInfoB.Priority) { // Not the same priority, use priority to sort return SortInfoA.Priority > SortInfoB.Priority; } else if (SortInfoA.SpecialDefaultFolderPriority != SortInfoB.SpecialDefaultFolderPriority) { // Special folders use the index to sort. Non special folders are all set to 0. return SortInfoA.SpecialDefaultFolderPriority < SortInfoB.SpecialDefaultFolderPriority; } else { // If either is a class folder and names without classes prefix are same if ((SortInfoA.bIsClassesFolder != SortInfoB.bIsClassesFolder) && (SortInfoA.ItemNameWithoutClassesPrefix == SortInfoB.ItemNameWithoutClassesPrefix)) { return !SortInfoA.bIsClassesFolder; } // Two non special folders of the same priority, sort alphabetically const int32 CompareResult = UE::ComparisonUtility::CompareWithNumericSuffix(SortInfoA.FolderName, SortInfoB.FolderName); if (CompareResult != 0) { return CompareResult < 0; } else { // Classes folders have the same name so sort them adjacent but under non-classes return !SortInfoA.bIsClassesFolder; } } }); // Replace with sorted array TArray> NewList; NewList.Reserve(SortInfoArray.Num()); for (const FItemSortInfo& It : SortInfoArray) { NewList.Add(It.TreeItem); } InChildren = MoveTemp(NewList); } } // namespace UE::PathView // Struct to factor out path view data fetching/filtering as a precursor to being able to bind this data to the view // instead of fetching it internally struct FPathViewData { public: FPathViewData(FName InContentBrowserName, bool InFlat) : OwningContentBrowserName(InContentBrowserName) , bFlat(InFlat) , FolderPathTextFilter(decltype(FolderPathTextFilter)::FItemToStringArray::CreateStatic( [](FStringView Input, TArray& Out) { Out.Emplace(FString(Input)); })) { } ~FPathViewData() { } uint64 GetVersion() { return Version; } // Return an array that can be bound to a tree view widget for the current visible set of root items TArray>* GetVisibleRootItems() { return &VisibleRootItems; } TTextFilter& GetFolderPathTextFilter() { return FolderPathTextFilter; } // Fetch all data from the content browser data backend and transform it into the tree data void PopulateFullFolderTree(const FContentBrowserDataCompiledFilter& InFilter); // Fetch favorite folders from config, filter them against the content browser data filter // bFlat parameter adds all items at the root of the tree and not create parents void PopulateWithFavorites(const FContentBrowserDataCompiledFilter& InFilter); // Apply the current text filter to everything in the tree void FilterFullFolderTree(); // Clear the filter state of all items in the tree void ClearItemFilterState(); // Sort the roots of the tree void SortRootItems(); // Add an item to the tree by its virtual path, reusing an old object if possible to ensure persistence of // selection/expansion for the widget the items are bound to TSharedRef AddFolderItem(FContentBrowserItemData&& InItemData); // Remove the given item from the tree whether it's a root or child. void RemoveFolderItem(const TSharedRef& InItem); // Find an item with the exact virtual path TSharedPtr FindTreeItem(FName InVirtualPath, bool bVisibleOnly = false); // Search the tree for the item furthest from the root that matches the given path, if any // Searches all items, not just visible ones according to the current text filter TSharedPtr FindBestItemForPath(FStringView InVirtualPath); // Apply new/modified/removed data notifications to the tree // bFlat parameter adds all items at the root of the tree as in the favorites tree void ProcessDataUpdates(TConstArrayView InUpdatedItems, const FContentBrowserDataCompiledFilter& InFilter); protected: TSharedRef AddFolderItemInternal(FContentBrowserItemData&& InItemData, TMap>* OldItemsByInvariantPath); // Remove the given item data from the tree - if this results in an item having no data from any sources, the tree // item is removed. If the item was removed and had a parent, the parent is returned. TSharedPtr TryRemoveFolderItemInternal(const FContentBrowserItemData& InItem); TSharedPtr TryRemoveFolderItemInternal(const FContentBrowserMinimalItemData& InKey); bool PassesTextFilter(const TSharedPtr& InItem); struct FEmptyFolderFilter { FContentBrowserFolderContentsFilter FolderFilter; EContentBrowserIsFolderVisibleFlags FolderFlags; }; FEmptyFolderFilter GetEmptyFolderFilter(const FContentBrowserDataCompiledFilter& CompiledDataFilter) const; // Incremented to trigger tree rebuild from changes to the tree contents uint64 Version; // Items with no parent TArray> RootItems; // Items with no parent TArray> VisibleRootItems; // Mapping of full virtual path such as '/All/Game/Maps/Arena' to items TMap> VirtualPathToItem; // Mapping of path that doesn't change based on display settings (e.g. '/MyPlugin/MyAsset') to item // Used to reuse node objects when changing path view settings and rebuilding the tree TMap> InvariantPathToItem; // Used for retrieving saved settings per content browser instance FName OwningContentBrowserName; // If true, parent items are not created and all items are added as roots. bool bFlat; TTextFilter FolderPathTextFilter; }; FPathViewData::FEmptyFolderFilter FPathViewData::GetEmptyFolderFilter(const FContentBrowserDataCompiledFilter& CompiledDataFilter) const { const UContentBrowserSettings* ContentBrowserSettings = GetDefault(); bool bDisplayEmpty = ContentBrowserSettings->DisplayEmptyFolders; // check to see if we have an instance config that overrides the default in UContentBrowserSettings if (FContentBrowserInstanceConfig* EditorConfig = ContentBrowserUtils::GetContentBrowserConfig(OwningContentBrowserName)) { bDisplayEmpty = EditorConfig->bShowEmptyFolders; } FContentBrowserFolderContentsFilter FolderFilter; if (bDisplayEmpty) { UContentBrowserDataSubsystem* ContentBrowserData = IContentBrowserDataModule::Get().GetSubsystem(); FolderFilter.HideFolderIfEmptyFilter = ContentBrowserData->CreateHideFolderIfEmptyFilter(); } else { FolderFilter.ItemCategoryFilter = CompiledDataFilter.ItemCategoryFilter; } EContentBrowserIsFolderVisibleFlags FolderFlags = ContentBrowserUtils::GetIsFolderVisibleFlags(bDisplayEmpty); return { MoveTemp(FolderFilter), FolderFlags }; } void FPathViewData::PopulateFullFolderTree(const FContentBrowserDataCompiledFilter& CompiledDataFilter) { TMap> OldItemsByInvariantPath = MoveTemp(InvariantPathToItem); RootItems.Reset(); VisibleRootItems.Reset(); InvariantPathToItem.Reset(); VirtualPathToItem.Reset(); UContentBrowserDataSubsystem* ContentBrowserData = IContentBrowserDataModule::Get().GetSubsystem(); FEmptyFolderFilter EmptyFilter = GetEmptyFolderFilter(CompiledDataFilter); TArray> ItemsCreated; ContentBrowserData->EnumerateItemsMatchingFilter(CompiledDataFilter, [this, CompiledDataFilter, EmptyFilter, ContentBrowserData, &OldItemsByInvariantPath](FContentBrowserItemData&& InItemData) { UContentBrowserDataSource* Source = InItemData.GetOwnerDataSource(); if (Source && !Source->IsFolderVisible(InItemData.GetVirtualPath(), EmptyFilter.FolderFlags, EmptyFilter.FolderFilter)) { UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Populate: skipping folder %s:%s that fails current pre-text filtering"), *WriteToString<256>(OwningContentBrowserName), *WriteToString<256>(Source->GetFName()), *WriteToString<256>(InItemData.GetVirtualPath())); return true; // continue enumerating } UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Populate: adding folder %s:%s"), *WriteToString<256>(OwningContentBrowserName), *WriteToString<256>(Source->GetFName()), *WriteToString<256>(InItemData.GetVirtualPath())); AddFolderItemInternal(MoveTemp(InItemData), &OldItemsByInvariantPath); return true; }); VisibleRootItems = RootItems; ++Version; } void FPathViewData::PopulateWithFavorites(const FContentBrowserDataCompiledFilter& CompiledDataFilter) { // Clear all root items and clear selection TMap> OldItemsByInvariantPath = MoveTemp(InvariantPathToItem); RootItems.Reset(); VisibleRootItems.Reset(); InvariantPathToItem.Reset(); VirtualPathToItem.Reset(); const TArray& FavoritePaths = ContentBrowserUtils::GetFavoriteFolders(); UContentBrowserDataSubsystem* ContentBrowserData = IContentBrowserDataModule::Get().GetSubsystem(); FEmptyFolderFilter EmptyFilter = GetEmptyFolderFilter(CompiledDataFilter); for (const FString& InvariantPath : FavoritePaths) { FName VirtualPath; IContentBrowserDataModule::Get().GetSubsystem()->ConvertInternalPathToVirtual(InvariantPath, VirtualPath); const FString Path = VirtualPath.ToString(); ContentBrowserData->EnumerateItemsAtPath(*Path, CompiledDataFilter.ItemTypeFilter, [this, &CompiledDataFilter, EmptyFilter, &OldItemsByInvariantPath](FContentBrowserItemData&& InItemData) { UContentBrowserDataSource* ItemDataSource = InItemData.GetOwnerDataSource(); if (!ItemDataSource->IsFolderVisible(InItemData.GetVirtualPath(), EmptyFilter.FolderFlags, EmptyFilter.FolderFilter)) { UE_LOG(LogPathView, VeryVerbose, TEXT("Hiding folder %s that fails current pre-text filtering"), *WriteToString<256>(InItemData.GetVirtualPath())); return true; // continue enumerating } ItemDataSource->ConvertItemForFilter(InItemData, CompiledDataFilter); if (ItemDataSource->DoesItemPassFilter(InItemData, CompiledDataFilter)) { AddFolderItemInternal(MoveTemp(InItemData), &OldItemsByInvariantPath); } return true; }); } ++Version; } void FPathViewData::ProcessDataUpdates(TConstArrayView InUpdatedItems, const FContentBrowserDataCompiledFilter& CompiledDataFilter) { UContentBrowserDataSubsystem* ContentBrowserData = IContentBrowserDataModule::Get().GetSubsystem(); FEmptyFolderFilter EmptyFilter = GetEmptyFolderFilter(CompiledDataFilter); auto DoesItemPassFilter = [this, EmptyFilter, ContentBrowserData, &CompiledDataFilter]( const FContentBrowserItemData& InItemData) { UContentBrowserDataSource* ItemDataSource = InItemData.GetOwnerDataSource(); if (!ItemDataSource->DoesItemPassFilter(InItemData, CompiledDataFilter)) { UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Fails compiled data filter"), *WriteToString<256>(OwningContentBrowserName)); return false; } if (!ContentBrowserData->IsFolderVisible(InItemData.GetVirtualPath(), EmptyFilter.FolderFlags, EmptyFilter.FolderFilter)) { UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Fails folder visibility filter"), *WriteToString<256>(OwningContentBrowserName)); return false; } return true; }; TArray> NewItems; TArray> ModifiedParents; // Parents who need their bHasVisibleDescendants updated for (const FContentBrowserItemDataUpdate& ItemDataUpdate : InUpdatedItems) { const FContentBrowserItemData& ItemDataRef = ItemDataUpdate.GetItemData(); if (!ItemDataRef.IsFolder()) { continue; } FContentBrowserItemData ItemData = ItemDataRef; ItemData.GetOwnerDataSource()->ConvertItemForFilter(ItemData, CompiledDataFilter); switch (ItemDataUpdate.GetUpdateType()) { case EContentBrowserItemUpdateType::Added: UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Added item %s:%s"), *WriteToString<256>(OwningContentBrowserName), *WriteToString<256>(ItemData.GetOwnerDataSource()->GetFName()), *WriteToString<256>(ItemData.GetVirtualPath())); if (DoesItemPassFilter(ItemData)) { NewItems.Emplace(AddFolderItemInternal(MoveTemp(ItemData), nullptr)); } break; case EContentBrowserItemUpdateType::Modified: UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Modified item %s:%s"), *WriteToString<256>(OwningContentBrowserName), *WriteToString<256>(ItemData.GetOwnerDataSource()->GetFName()), *WriteToString<256>(ItemData.GetVirtualPath())); if (DoesItemPassFilter(ItemData)) { NewItems.Emplace(AddFolderItemInternal(MoveTemp(ItemData), nullptr)); } else { TSharedPtr Parent = TryRemoveFolderItemInternal(ItemData); if (Parent.IsValid()) { ModifiedParents.Emplace(Parent.ToSharedRef()); } } break; case EContentBrowserItemUpdateType::Moved: { UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Moved item %s:%s->%s"), *WriteToString<256>(OwningContentBrowserName), *WriteToString<256>(ItemData.GetOwnerDataSource()->GetFName()), *WriteToString<256>(ItemDataUpdate.GetPreviousVirtualPath()), *WriteToString<256>(ItemData.GetVirtualPath())); const FContentBrowserMinimalItemData OldItemKey(ItemData.GetItemType(), ItemDataUpdate.GetPreviousVirtualPath(), ItemData.GetOwnerDataSource()); TSharedPtr Parent = TryRemoveFolderItemInternal(OldItemKey); if (DoesItemPassFilter(ItemData)) { NewItems.Emplace(AddFolderItemInternal(MoveTemp(ItemData), nullptr)); } else if (Parent.IsValid()) { ModifiedParents.Emplace(Parent.ToSharedRef()); } } break; case EContentBrowserItemUpdateType::Removed: UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Removed item %s:%s"), *WriteToString<256>(OwningContentBrowserName), *WriteToString<256>(ItemData.GetOwnerDataSource()->GetFName()), *WriteToString<256>(ItemData.GetVirtualPath())); TryRemoveFolderItemInternal(ItemData); break; default: checkf(false, TEXT("Unexpected EContentBrowserItemUpdateType!")); break; } } ++Version; // Determine visibility for new items and their parents if (!FolderPathTextFilter.GetRawFilterText().IsEmpty()) { // Clear visible descendents flag on modified parents because we will reset it for (const TSharedRef& Parent : ModifiedParents) { Parent->SetHasVisibleDescendants(false); } for (const TSharedRef& Item : NewItems) { bool bVisible = PassesTextFilter(Item); Item->SetVisible(bVisible); if (bVisible) { // Propagate to parents for (TSharedPtr Parent = Item->GetParent(); Parent.IsValid() && !Parent->IsVisible(); Parent = Parent->GetParent()) { Parent->SetHasVisibleDescendants(true); } } } // Sort modified parents so if items are related, we visit the items furthest from the root first Algo::Sort(ModifiedParents, [](const TSharedRef& A, const TSharedRef& B) { return A->IsChildOf(*B); }); for (const TSharedRef& Parent : ModifiedParents) { // May have already figured this out when dealing with directly modified items if (!Parent->GetHasVisibleDescendants()) { bool bVisibleChildren = Algo::AnyOf(Parent->GetChildren(), [](const TSharedPtr& Child) { return Child.IsValid() && Child->IsVisible(); }); Parent->SetHasVisibleDescendants(bVisibleChildren); } } } else { // If filtering is not active and we created some new root items, we need them to be visible VisibleRootItems = RootItems; } } bool FPathViewData::PassesTextFilter(const TSharedPtr& InItem) { return FolderPathTextFilter.PassesFilter(WriteToString<256>(InItem->GetItem().GetVirtualPath())) // TODO: this will not match a string like LocName1/LocName2 when both parent and child are localized || FolderPathTextFilter.PassesFilter(FStringView(InItem->GetItem().GetDisplayName().ToString())); } void FPathViewData::ClearItemFilterState() { for (const TPair>& Pair : VirtualPathToItem) { FName VirtualPath = Pair.Key; Pair.Value->SetVisible(true); Pair.Value->SetHasVisibleDescendants(true); } VisibleRootItems = RootItems; ++Version; } void FPathViewData::FilterFullFolderTree() { for (const TPair>& Pair : VirtualPathToItem) { FName VirtualPath = Pair.Key; Pair.Value->SetVisible(PassesTextFilter(Pair.Value)); Pair.Value->SetHasVisibleDescendants(false); } // Propagate visibility down to parents for (const TPair>& Pair : VirtualPathToItem) { if (Pair.Value->IsVisible()) { for (TSharedPtr Parent = Pair.Value->GetParent(); Parent.IsValid() && !Parent->IsVisible(); Parent = Parent->GetParent()) { Parent->SetHasVisibleDescendants(true); } } } VisibleRootItems.Reset(); Algo::CopyIf(RootItems, VisibleRootItems, UE_PROJECTION_MEMBER(FTreeItem, IsVisible)); ++Version; } void FPathViewData::SortRootItems() { UE::PathView::DefaultSort(RootItems); UE::PathView::DefaultSort(VisibleRootItems); } TSharedPtr FPathViewData::FindTreeItem(FName InVirtualPath, bool bVisibleOnly) { if (TSharedPtr Found = VirtualPathToItem.FindRef(InVirtualPath)) { if (bVisibleOnly && !Found->IsVisible()) { return {}; } return Found; } return {}; } TSharedPtr FPathViewData::FindBestItemForPath(FStringView InVirtualPath) { if (bFlat) { return FindTreeItem(FName(InVirtualPath), false); } TSharedPtr Found; FPathViews::IterateAncestors(InVirtualPath, [this, &Found](FStringView Ancestor) { FName ItemName{ Ancestor }; if (TSharedPtr* Item = VirtualPathToItem.Find(ItemName)) { Found = *Item; return false; // Found the leafmost item matching this path } return true; // continue }); return Found; } TSharedRef FPathViewData::AddFolderItem(FContentBrowserItemData&& InItemData) { TSharedRef NewOrUpdatedItem = AddFolderItemInternal(MoveTemp(InItemData), nullptr); ++Version; return NewOrUpdatedItem; } TSharedRef FPathViewData::AddFolderItemInternal(FContentBrowserItemData&& InItemData, TMap>* OldItemsByInvariantPath) { UContentBrowserDataSubsystem* ContentBrowserData = IContentBrowserDataModule::Get().GetSubsystem(); UContentBrowserDataSource* OriginalDataSource = InItemData.GetOwnerDataSource(); FName ItemVirtualPath = InItemData.GetVirtualPath(); TSharedPtr LeafItem = VirtualPathToItem.FindRef(ItemVirtualPath); if (LeafItem.IsValid()) { UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Appending data to existing item %s:%s"), *WriteToString<256>(OwningContentBrowserName), *WriteToString<256>(OriginalDataSource->GetFName()), *WriteToString<256>(ItemVirtualPath)); // Item already existed - duplicate item returned by multiple data sources, merge data and move on. // We will have already created all the parent items. LeafItem->AppendItemData(InItemData); return LeafItem.ToSharedRef(); } UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Creating new tree item for %s:%s"), *WriteToString<256>(OwningContentBrowserName), *WriteToString<256>(OriginalDataSource->GetFName()), *WriteToString<256>(ItemVirtualPath)); FName ItemInvariantPath = InItemData.GetInvariantPath(); TStringBuilder PathBuffer(InPlace, ItemVirtualPath); LeafItem = UE::PathView::CreateOrReuseNode(MoveTemp(InItemData), OldItemsByInvariantPath); // InItemData is now no longer valid!! VirtualPathToItem.Add(ItemVirtualPath, LeafItem); InvariantPathToItem.Add(ItemInvariantPath, LeafItem); if (bFlat) { RootItems.Add(LeafItem); return LeafItem.ToSharedRef(); } TSharedRef PreviousItem = LeafItem.ToSharedRef(); // Work backwards from the leaf path of the requested item until we encounter an item that already existed FPathViews::IterateAncestors(PathBuffer.ToView(), [this, &PathBuffer, &PreviousItem, OriginalDataSource, OldItemsByInvariantPath, &ContentBrowserData](FStringView PathView) { if (PathView.Len() == PathBuffer.Len()) { // This is the item returned by the data source, we already added it return true; } if (PathView == TEXTVIEW("/")) { UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Adding root item %s:%.*s"), *WriteToString<256>(OwningContentBrowserName), *WriteToString<256>(OriginalDataSource->GetFName()), PathView.Len(), PathView.GetData()); // PreviousItem must have been new, add it to the set of root items RootItems.Add(PreviousItem); return false; } FName ParentVirtualPath{ PathView }; TSharedPtr ParentItem = VirtualPathToItem.FindRef(ParentVirtualPath); bool bContinue = false; if (!ParentItem.IsValid()) { UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Creating placeholder or virtual parent %s:%.*s"), *WriteToString<256>(OwningContentBrowserName), *WriteToString<256>(OriginalDataSource->GetFName()), PathView.Len(), PathView.GetData()); // TODO: If another data source provides this path in future, can that data source become the 'primary'? FName ItemName(FPathViews::GetPathLeaf(PathView)); FName InternalPath; if (ContentBrowserData->TryConvertVirtualPath(ParentVirtualPath, InternalPath) != EContentBrowserPathType::Internal) { InternalPath = FName(); // Assuming this is a virtual path with no internal path } ParentItem = UE::PathView::CreateOrReuseNode(FContentBrowserItemData(OriginalDataSource, EContentBrowserItemFlags::Type_Folder, ParentVirtualPath, ItemName, FText(), nullptr, InternalPath ), OldItemsByInvariantPath); VirtualPathToItem.Add(ParentVirtualPath, ParentItem); // TODO: Do fully virtual paths have an invariant path? InvariantPathToItem.Add(ParentItem->GetItem().GetInvariantPath()); bContinue = true; } else { UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Found existing parent %.*s"), *WriteToString<256>(OwningContentBrowserName), PathView.Len(), PathView.GetData()); } ParentItem->AddChild(PreviousItem); PreviousItem = ParentItem.ToSharedRef(); return bContinue; // If we made a node here, keep checking if we need to make more parent nodes }); return LeafItem.ToSharedRef(); } TSharedPtr FPathViewData::TryRemoveFolderItemInternal(const FContentBrowserItemData& InItemData) { return TryRemoveFolderItemInternal(FContentBrowserMinimalItemData(InItemData)); } TSharedPtr FPathViewData::TryRemoveFolderItemInternal(const FContentBrowserMinimalItemData& InItemKey) { // Find the folder in the tree if (TSharedPtr ItemToRemove = VirtualPathToItem.FindRef(InItemKey.GetVirtualPath())) { // Only fully remove this item if every sub-item is removed (items become invalid when empty) FContentBrowserItemData OldItemData = ItemToRemove->RemoveItemData(InItemKey); if (ItemToRemove->GetItem().IsValid()) { return {}; } // Found the folder to remove. Remove it. TSharedPtr ItemParent = ItemToRemove->GetParent(); if (ItemParent.IsValid()) { // Remove the folder from its parent's list ItemParent->RemoveChild(ItemToRemove.ToSharedRef()); } else { // This is a root item. Remove the folder from the root items list. RootItems.Remove(ItemToRemove); VisibleRootItems.Remove(ItemToRemove); } VirtualPathToItem.Remove(InItemKey.GetVirtualPath()); InvariantPathToItem.Remove(OldItemData.GetInvariantPath()); return ItemParent; } // Did not find the folder to remove return {}; } void FPathViewData::RemoveFolderItem(const TSharedRef& TreeItem) { if (TSharedPtr Parent = TreeItem->GetParent()) { // Remove this item from it's parent's list Parent->RemoveChild(TreeItem); } else { // This was a root node, remove from the root list RootItems.Remove(TreeItem); VisibleRootItems.Remove(TreeItem); } VirtualPathToItem.Remove(TreeItem->GetItem().GetVirtualPath()); InvariantPathToItem.Remove(TreeItem->GetItem().GetInvariantPath()); Version++; } SPathView::FScopedSelectionChangedEvent::FScopedSelectionChangedEvent(const TSharedRef& InPathView, const bool InShouldEmitEvent) : PathView(InPathView) , bShouldEmitEvent(InShouldEmitEvent) { PathView->PreventTreeItemChangedDelegateCount++; InitialSelectionSet = GetSelectionSet(); } SPathView::FScopedSelectionChangedEvent::~FScopedSelectionChangedEvent() { check(PathView->PreventTreeItemChangedDelegateCount > 0); PathView->PreventTreeItemChangedDelegateCount--; if (bShouldEmitEvent) { const TSet FinalSelectionSet = GetSelectionSet(); const bool bHasSelectionChanges = InitialSelectionSet.Num() != FinalSelectionSet.Num() || InitialSelectionSet.Difference(FinalSelectionSet).Num() > 0; if (bHasSelectionChanges) { const TArray> NewSelectedItems = PathView->TreeViewPtr->GetSelectedItems(); PathView->TreeSelectionChanged(NewSelectedItems.Num() > 0 ? NewSelectedItems[0] : nullptr, ESelectInfo::Direct); } } } TSet SPathView::FScopedSelectionChangedEvent::GetSelectionSet() const { TSet SelectionSet; Algo::Transform(PathView->TreeViewPtr->GetSelectedItems(), SelectionSet, [](const TSharedPtr& Item) { return Item->GetItem().GetVirtualPath(); }); return SelectionSet; } SPathView::~SPathView() { UE::PathView::AllPathViews.RemoveAllSwap([this](const TWeakPtr Weak) { return Weak.Pin().Get() == this; }); if (IContentBrowserDataModule* ContentBrowserDataModule = IContentBrowserDataModule::GetPtr()) { if (UContentBrowserDataSubsystem* ContentBrowserData = ContentBrowserDataModule->GetSubsystem()) { ContentBrowserData->OnItemDataUpdated().RemoveAll(this); ContentBrowserData->OnItemDataRefreshed().RemoveAll(this); ContentBrowserData->OnItemDataDiscoveryComplete().RemoveAll(this); } } TreeData->GetFolderPathTextFilter().OnChanged().RemoveAll(this); } void SPathView::Construct( const FArguments& InArgs ) { UE::PathView::AllPathViews.Add(SharedThis(this)); OwningContentBrowserName = InArgs._OwningContentBrowserName; OnItemSelectionChanged = InArgs._OnItemSelectionChanged; bAllowContextMenu = InArgs._AllowContextMenu; OnGetItemContextMenu = InArgs._OnGetItemContextMenu; InitialCategoryFilter = InArgs._InitialCategoryFilter; bAllowClassesFolder = InArgs._AllowClassesFolder; bAllowReadOnlyFolders = InArgs._AllowReadOnlyFolders; bShowRedirectors = InArgs._ShowRedirectors; bCanShowDevelopersFolder = InArgs._CanShowDevelopersFolder; bForceShowEngineContent = InArgs._ForceShowEngineContent; bForceShowPluginContent = InArgs._ForceShowPluginContent; bLastShowRedirectors = bShowRedirectors.Get(false); PreventTreeItemChangedDelegateCount = 0; TreeTitle = LOCTEXT("AssetTreeTitle", "Asset Tree"); if ( InArgs._FocusSearchBoxWhenOpened ) { RegisterActiveTimer( 0.f, FWidgetActiveTimerDelegate::CreateSP( this, &SPathView::SetFocusPostConstruct ) ); } TreeData = MakeShared(OwningContentBrowserName, bFlat); TreeData->GetFolderPathTextFilter().OnChanged().AddSP(this, &SPathView::FilterUpdated); UContentBrowserDataSubsystem* ContentBrowserData = IContentBrowserDataModule::Get().GetSubsystem(); ContentBrowserData->OnItemDataUpdated().AddSP(this, &SPathView::HandleItemDataUpdated); ContentBrowserData->OnItemDataRefreshed().AddSP(this, &SPathView::HandleItemDataRefreshed); ContentBrowserData->OnItemDataDiscoveryComplete().AddSP(this, &SPathView::HandleItemDataDiscoveryComplete); FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); FolderPermissionList = AssetToolsModule.Get().GetFolderPermissionList(); WritableFolderPermissionList = AssetToolsModule.Get().GetWritableFolderPermissionList(); // Listen for when view settings are changed FContentBrowserModule& ContentBrowserModule = FModuleManager::GetModuleChecked(TEXT("ContentBrowser")); ContentBrowserModule.GetOnContentBrowserSettingChanged().AddSP(this, &SPathView::HandleSettingChanged); // Binds the commands for the PathView BindCommands(); // Setup plugin filters PluginPathFilters = InArgs._PluginPathFilters; if (PluginPathFilters.IsValid()) { // Add all built-in filters here AllPluginPathFilters.Add( MakeShareable(new FContentBrowserPluginFilter_ContentOnlyPlugins()) ); // Add external filters for (const FContentBrowserModule::FAddPathViewPluginFilters& Delegate : ContentBrowserModule.GetAddPathViewPluginFilters()) { if (Delegate.IsBound()) { Delegate.Execute(AllPluginPathFilters); } } } STreeView>::FArguments TreeViewArgs; ConfigureTreeView(TreeViewArgs); TreeViewPtr = SArgumentNew(TreeViewArgs, STreeView>) .TreeItemsSource(TreeData->GetVisibleRootItems()) .OnGetChildren(this, &SPathView::GetChildrenForTree) .OnGenerateRow(this, &SPathView::GenerateTreeRow) .OnItemScrolledIntoView(this, &SPathView::TreeItemScrolledIntoView) .SelectionMode(InArgs._SelectionMode) .AllowInvisibleItemSelection(true) .OnSelectionChanged(this, &SPathView::TreeSelectionChanged) .OnContextMenuOpening(this, &SPathView::MakePathViewContextMenu) .ClearSelectionOnClick(false); SearchPtr = InArgs._ExternalSearch; if (!SearchPtr) { SearchPtr = MakeShared(); SearchPtr->Initialize(); SearchPtr->SetHintText(LOCTEXT("AssetTreeSearchBoxHint", "Search Folders")); } SearchPtr->OnSearchChanged().AddSP(this, &SPathView::SetSearchFilterText); TSharedRef SearchBox = SNew(SBox); if (!InArgs._ExternalSearch) { SearchBox->SetContent( SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() [ InArgs._SearchContent.Widget ] +SHorizontalBox::Slot() .FillWidth(1.0f) [ SNew(SBox) .Visibility(InArgs._SearchBarVisibility) [ SearchPtr->GetWidget() ] ] +SHorizontalBox::Slot() .Padding(4.f, 0.f, 0.f, 0.f) .AutoWidth() [ SNew(SComboButton) .Visibility(InArgs._ShowViewOptions ? EVisibility::Visible : EVisibility::Collapsed) .ComboButtonStyle(&FAppStyle::Get().GetWidgetStyle("SimpleComboButton")) .OnGetMenuContent(this, &SPathView::GetViewButtonContent) .HasDownArrow(false) .ButtonContent() [ SNew(SImage) .ColorAndOpacity(FSlateColor::UseForeground()) .Image(FAppStyle::Get().GetBrush("Icons.Settings")) ] ] ); } TSharedRef ContentBox = SNew(SVerticalBox); if (!InArgs._ExternalSearch || InArgs._ShowTreeTitle) { ContentBox->AddSlot() .AutoHeight() [ SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("Brushes.Panel")) .Padding(8.f) [ SNew(SVerticalBox) // Search + SVerticalBox::Slot() .AutoHeight() [ SearchBox ] // Tree title +SVerticalBox::Slot() .AutoHeight() [ SNew(STextBlock) .Font(UE::ContentBrowser::Private::FContentBrowserStyle::Get().GetFontStyle("ContentBrowser.SourceTitleFont") ) .Text(this, &SPathView::GetTreeTitle) .Visibility(InArgs._ShowTreeTitle ? EVisibility::Visible : EVisibility::Collapsed) ] ] ]; } // Separator if (InArgs._ShowSeparator) { ContentBox->AddSlot() .AutoHeight() .Padding(0, 0, 0, 1) [ SNew(SSeparator) ]; } if (InArgs._ShowFavorites) { ContentBox->AddSlot() .FillHeight(1.f) [ SNew(SSplitter) .Orientation(Orient_Vertical) + SSplitter::Slot() .SizeRule_Lambda([this]() { return (FavoritesArea.IsValid() && FavoritesArea->IsExpanded()) ? SSplitter::ESizeRule::FractionOfParent : SSplitter::ESizeRule::SizeToContent; }) .MinSize(24) .Value(0.25f) [ CreateFavoritesView() ] + SSplitter::Slot() .Value(0.75f) [ TreeViewPtr.ToSharedRef() ] ]; } else { // Tree ContentBox->AddSlot() .FillHeight(1.f) [ TreeViewPtr.ToSharedRef() ]; } ChildSlot [ ContentBox ]; CustomFolderPermissionList = InArgs._CustomFolderPermissionList; // Add all paths currently gathered from the asset registry Populate(); for (const FName PathToExpand : GetDefaultPathsToExpand()) { if (TSharedPtr FoundItem = TreeData->FindTreeItem(PathToExpand)) { RecursiveExpandParents(FoundItem); TreeViewPtr->SetItemExpansion(FoundItem, true); } } if (!InArgs._DefaultPath.IsEmpty() && InternalPathPassesBlockLists(InArgs._DefaultPath)) { const FName VirtualPath = ContentBrowserData->ConvertInternalPathToVirtual(*InArgs._DefaultPath); FName InternalPath; if (ContentBrowserData->TryConvertVirtualPath(VirtualPath, InternalPath) != EContentBrowserPathType::Internal) { InternalPath = FName(); } if (InArgs._CreateDefaultPath && !TreeData->FindTreeItem(VirtualPath)) { const FString DefaultPathLeafName = FPaths::GetPathLeaf(VirtualPath.ToString()); TreeData->AddFolderItem(FContentBrowserItemData(nullptr, EContentBrowserItemFlags::Type_Folder, VirtualPath, *DefaultPathLeafName, FText(), nullptr, InternalPath)); } SetSelectedPaths({ VirtualPath.ToString() }); } } void SPathView::ConfigureTreeView(STreeView>::FArguments& InArgs) { InArgs.OnExpansionChanged(this, &SPathView::TreeExpansionChanged) .OnSetExpansionRecursive(this, &SPathView::SetTreeItemExpansionRecursive) .HighlightParentNodesForSelection(true); } void SPathView::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { Super::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); if (LastTreeDataVersion != TreeData->GetVersion()) { LastTreeDataVersion = TreeData->GetVersion(); TreeViewPtr->RequestTreeRefresh(); } const bool bNewShowRedirectors = bShowRedirectors.Get(false); if (bNewShowRedirectors != bLastShowRedirectors) { UE_LOG(LogPathView, Verbose, TEXT("PathView bShowRedirectors changed to %d"), bNewShowRedirectors); bLastShowRedirectors = bNewShowRedirectors; HandleSettingChanged("ShowRedirectors"); } if (bLastExpandedPathsDirty) { UpdateLastExpandedPathsIfDirty(); } } FReply SPathView::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) { if (Commands->ProcessCommandBindings(InKeyEvent)) { return FReply::Handled(); } return FReply::Unhandled(); } bool SPathView::IsEmpty() const { return TreeViewPtr->GetRootItems().IsEmpty(); } void SPathView::PopulatePathViewFiltersMenu(UToolMenu* Menu) { { FToolMenuSection& Section = Menu->AddSection("Reset"); Section.AddMenuEntry( "ResetPluginPathFilters", LOCTEXT("ResetPluginPathFilters_Label", "Reset Path View Filters"), LOCTEXT("ResetPluginPathFilters_Tooltip", "Reset current path view filters state"), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &SPathView::ResetPluginPathFilters)) ); } { FToolMenuSection& Section = Menu->AddSection("Filters", LOCTEXT("PathViewFilters_Label", "Filters")); for (const TSharedRef& Filter : AllPluginPathFilters) { Section.AddMenuEntry( NAME_None, Filter->GetDisplayName(), Filter->GetToolTipText(), FSlateIcon(FAppStyle::GetAppStyleSetName(), Filter->GetIconName()), FUIAction( FExecuteAction::CreateSP(this, &SPathView::PluginPathFilterClicked, Filter), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SPathView::IsPluginPathFilterChecked, Filter) ), EUserInterfaceActionType::ToggleButton ); } } } void SPathView::PluginPathFilterClicked(TSharedRef Filter) { SetPluginPathFilterActive(Filter, !IsPluginPathFilterInUse(Filter)); Populate(); } bool SPathView::IsPluginPathFilterChecked(TSharedRef Filter) const { if (IsPluginPathFilterInUse(Filter)) { return !Filter->IsInverseFilter(); } return Filter->IsInverseFilter(); } bool SPathView::IsPluginPathFilterInUse(TSharedRef Filter) const { if (PluginPathFilters.IsValid()) { for (int32 i=0; i < PluginPathFilters->Num(); ++i) { if (PluginPathFilters->GetFilterAtIndex(i) == Filter) { return true; } } } return false; } void SPathView::ResetPluginPathFilters() { for (const TSharedRef& Filter : AllPluginPathFilters) { SetPluginPathFilterActive(Filter, false); } Populate(); } bool SPathView::DisablePluginPathFiltersThatHideItems(TConstArrayView Items) { if (!PluginPathFilters.IsValid()) { return false; } TSet> RelevantPlugins; for (const FContentBrowserItem& Item : Items) { FName InternalPath = Item.GetInternalPath(); if (InternalPath.IsNone()) { continue; } TStringBuilder<256> PathBuffer(InPlace, InternalPath); const FStringView MountPoint = FPathViews::GetMountPointNameFromPath(PathBuffer); if (TSharedPtr Plugin = IPluginManager::Get().FindPlugin(MountPoint)) { RelevantPlugins.Add(Plugin.ToSharedRef()); } } bool bAnyChanges = false; for (const TSharedRef& Filter : AllPluginPathFilters) { if (Algo::AnyOf(RelevantPlugins, [&Filter](const TSharedRef& Plugin) { return !Filter->PassesFilter(Plugin); })) { // Whether the filter is inverse or not, we don't want it in the list if (IsPluginPathFilterInUse(Filter)) { SetPluginPathFilterActive(Filter, Filter->IsInverseFilter()); bAnyChanges = true; } } } return bAnyChanges; } void SPathView::SetPluginPathFilterActive(const TSharedRef& Filter, bool bActive) { if (Filter->IsInverseFilter()) { //Inverse filters are active when they are "disabled" bActive = !bActive; } UE_LOG(LogPathView, Verbose, TEXT("[%s] Setting%s plugin filter %s to %s"), *WriteToString<64>(OwningContentBrowserName), Filter->IsInverseFilter() ? TEXT(" inverse") : TEXT(""), *Filter->GetName(), bActive ? TEXT("Active") : TEXT("Inactive")); Filter->ActiveStateChanged(bActive); if (bActive) { PluginPathFilters->Add(Filter); } else { PluginPathFilters->Remove(Filter); } if (FPathViewConfig* PathViewConfig = GetPathViewConfig()) { if (bActive) { PathViewConfig->PluginFilters.Add(Filter->GetName()); } else { PathViewConfig->PluginFilters.Remove(Filter->GetName()); } UContentBrowserConfig::Get()->SaveEditorConfig(); } } FPathViewConfig* SPathView::GetPathViewConfig() const { return ContentBrowserUtils::GetPathViewConfig(OwningContentBrowserName); } FContentBrowserInstanceConfig* SPathView::GetContentBrowserConfig() const { return ContentBrowserUtils::GetContentBrowserConfig(OwningContentBrowserName); } void SPathView::SetSelectedPaths(const TArray& Paths) { TArray PathStrings; Algo::Transform(Paths, PathStrings, [](const FName& Name) { return Name.ToString(); }); SetSelectedPaths(PathStrings); } void SPathView::SetSelectedPaths(const TArray& Paths) { if (!ensure(TreeViewPtr.IsValid())) { return; } // Prevent the selection changed delegate since the invoking code requested it FScopedPreventTreeItemChangedDelegate DelegatePrevention( SharedThis(this) ); // If the selection was changed before all pending initial paths were found, stop attempting to select them PendingInitialPaths.Empty(); // Clear the selection to start, then add the selected paths as they are found LastSelectedPaths.Empty(); TreeViewPtr->ClearSelection(); for (const FString& Path : Paths) { TSharedPtr BestItem = TreeData->FindBestItemForPath(Path); if (BestItem.IsValid()) { if (!BestItem->IsVisible()) { // Clear the search box if it potentially hides a path we want to select SearchPtr->ClearSearch(); } for (TSharedPtr Parent = BestItem->GetParent(); Parent.IsValid(); Parent = Parent->GetParent()) { TreeViewPtr->SetItemExpansion(Parent, true); } // Set the selection to the closest found folder and scroll it into view LastSelectedPaths.Add(BestItem->GetItem().GetInvariantPath()); TreeViewPtr->SetItemSelection(BestItem, true); TreeViewPtr->RequestScrollIntoView(BestItem); } } } void SPathView::ClearSelection() { // Prevent the selection changed delegate since the invoking code requested it FScopedPreventTreeItemChangedDelegate DelegatePrevention( SharedThis(this) ); // If the selection was changed before all pending initial paths were found, stop attempting to select them PendingInitialPaths.Empty(); // Clear the selection to start, then add the selected paths as they are found TreeViewPtr->ClearSelection(); } FString SPathView::GetSelectedPath() const { // TODO: Abstract away? TArray> Items = TreeViewPtr->GetSelectedItems(); if ( Items.Num() > 0 ) { return Items[0]->GetItem().GetVirtualPath().ToString(); } return FString(); } TArray SPathView::GetSelectedPaths() const { TArray RetArray; // TODO: Abstract away? TArray> Items = TreeViewPtr->GetSelectedItems(); for ( int32 ItemIdx = 0; ItemIdx < Items.Num(); ++ItemIdx ) { RetArray.Add(Items[ItemIdx]->GetItem().GetVirtualPath().ToString()); } return RetArray; } TArray SPathView::GetSelectedFolderItems() const { TArray> SelectedViewItems = TreeViewPtr->GetSelectedItems(); TArray SelectedFolders; for (const TSharedPtr& SelectedViewItem : SelectedViewItems) { if (!SelectedViewItem->GetItem().IsTemporary()) { SelectedFolders.Emplace(SelectedViewItem->GetItem()); } } return SelectedFolders; } void SPathView::RenameFolderItem(const FContentBrowserItem& InItem) { if (!ensure(TreeViewPtr.IsValid())) { // No tree view for some reason return; } if (!InItem.IsFolder()) { // Not a folder return; } // Find the folder in the tree if (TSharedPtr ItemToRename = TreeData->FindTreeItem(InItem.GetVirtualPath())) { if (!ItemToRename->IsVisible()) { SearchPtr->ClearSearch(); } ItemToRename->SetNamingFolder(true); TreeViewPtr->SetSelection(ItemToRename); TreeViewPtr->RequestScrollIntoView(ItemToRename); } } FContentBrowserDataCompiledFilter SPathView::CreateCompiledFolderFilter() const { UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Creating folder filter"), *WriteToString<256>(OwningContentBrowserName)); const UContentBrowserSettings* ContentBrowserSettings = GetDefault(); bool bDisplayPluginFolders = ContentBrowserSettings->GetDisplayPluginFolders(); // check to see if we have an instance config that overrides the default in UContentBrowserSettings if (FContentBrowserInstanceConfig* EditorConfig = GetContentBrowserConfig()) { bDisplayPluginFolders = EditorConfig->bShowPluginContent; } FContentBrowserDataFilter DataFilter; DataFilter.bRecursivePaths = true; DataFilter.ItemTypeFilter = EContentBrowserItemTypeFilter::IncludeFolders; DataFilter.ItemCategoryFilter = GetContentBrowserItemCategoryFilter(); DataFilter.ItemAttributeFilter = GetContentBrowserItemAttributeFilter(); UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] bDisplayPluginFolders:%d ItemCategoryFilter:%d ItemAttributeFilter:%d"), *WriteToString<256>(OwningContentBrowserName), bDisplayPluginFolders, DataFilter.ItemCategoryFilter, DataFilter.ItemAttributeFilter); TSharedPtr CombinedFolderPermissionList = ContentBrowserUtils::GetCombinedFolderPermissionList(FolderPermissionList, bAllowReadOnlyFolders ? nullptr : WritableFolderPermissionList); if (CustomFolderPermissionList.IsValid()) { if (!CombinedFolderPermissionList.IsValid()) { CombinedFolderPermissionList = MakeShared(); } CombinedFolderPermissionList->Append(*CustomFolderPermissionList); } if (PluginPathFilters.IsValid() && PluginPathFilters->Num() > 0 && bDisplayPluginFolders) { UE_SUPPRESS(LogPathView, VeryVerbose, { FString PluginFiltersString; for (int32 i=0; i < PluginPathFilters->Num(); ++i) { if (i != 0) { PluginFiltersString += TEXT(", "); } PluginFiltersString += PluginPathFilters->GetFilterAtIndex(i)->GetName(); } UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Active plugin filters: %s"), *WriteToString<256>(OwningContentBrowserName), *PluginFiltersString); }); TArray> Plugins = IPluginManager::Get().GetEnabledPluginsWithContent(); for (const TSharedRef& Plugin : Plugins) { if (!PluginPathFilters->PassesAllFilters(Plugin)) { FString MountedAssetPath = Plugin->GetMountedAssetPath(); MountedAssetPath.RemoveFromEnd(TEXT("/"), ESearchCase::CaseSensitive); if (!CombinedFolderPermissionList.IsValid()) { CombinedFolderPermissionList = MakeShared(); } CombinedFolderPermissionList->AddDenyListItem("PluginPathFilters", MountedAssetPath); } } } UE_LOG(LogPathView, VeryVerbose, TEXT("Compiled folder permission list: %s"), CombinedFolderPermissionList.IsValid() ? *CombinedFolderPermissionList->ToString() : TEXT("null")); ContentBrowserUtils::AppendAssetFilterToContentBrowserFilter(FARFilter(), nullptr, CombinedFolderPermissionList, DataFilter); FContentBrowserDataCompiledFilter CompiledDataFilter; { static const FName RootPath = "/"; UContentBrowserDataSubsystem* ContentBrowserData = IContentBrowserDataModule::Get().GetSubsystem(); ContentBrowserData->CompileFilter(RootPath, DataFilter, CompiledDataFilter); } return CompiledDataFilter; } EContentBrowserItemCategoryFilter SPathView::GetContentBrowserItemCategoryFilter() const { const UContentBrowserSettings* ContentBrowserSettings = GetDefault(); bool bDisplayCppFolders = ContentBrowserSettings->GetDisplayCppFolders(); // check to see if we have an instance config that overrides the default in UContentBrowserSettings if (FContentBrowserInstanceConfig* EditorConfig = GetContentBrowserConfig()) { bDisplayCppFolders = EditorConfig->bShowCppFolders; } EContentBrowserItemCategoryFilter ItemCategoryFilter = InitialCategoryFilter; if (bAllowClassesFolder && bDisplayCppFolders) { ItemCategoryFilter |= EContentBrowserItemCategoryFilter::IncludeClasses; } else { ItemCategoryFilter &= ~EContentBrowserItemCategoryFilter::IncludeClasses; } ItemCategoryFilter &= ~EContentBrowserItemCategoryFilter::IncludeCollections; if (bShowRedirectors.Get(false)) { ItemCategoryFilter |= EContentBrowserItemCategoryFilter::IncludeRedirectors; } else { ItemCategoryFilter &= ~EContentBrowserItemCategoryFilter::IncludeRedirectors; } return ItemCategoryFilter; } EContentBrowserItemAttributeFilter SPathView::GetContentBrowserItemAttributeFilter() const { const UContentBrowserSettings* ContentBrowserSettings = GetDefault(); bool bDisplayEngineContent = ContentBrowserSettings->GetDisplayEngineFolder(); bool bDisplayPluginContent = ContentBrowserSettings->GetDisplayPluginFolders(); bool bDisplayDevelopersContent = ContentBrowserSettings->GetDisplayDevelopersFolder(); bool bDisplayL10NContent = ContentBrowserSettings->GetDisplayL10NFolder(); // check to see if we have an instance config that overrides the defaults in UContentBrowserSettings if (FContentBrowserInstanceConfig* EditorConfig = GetContentBrowserConfig()) { bDisplayEngineContent = EditorConfig->bShowEngineContent; bDisplayPluginContent = EditorConfig->bShowPluginContent; bDisplayDevelopersContent = EditorConfig->bShowDeveloperContent; bDisplayL10NContent = EditorConfig->bShowLocalizedContent; } return EContentBrowserItemAttributeFilter::IncludeProject | (bDisplayEngineContent || bForceShowEngineContent ? EContentBrowserItemAttributeFilter::IncludeEngine : EContentBrowserItemAttributeFilter::IncludeNone) | (bDisplayPluginContent || bForceShowPluginContent ? EContentBrowserItemAttributeFilter::IncludePlugins : EContentBrowserItemAttributeFilter::IncludeNone) | (bDisplayDevelopersContent && bCanShowDevelopersFolder ? EContentBrowserItemAttributeFilter::IncludeDeveloper : EContentBrowserItemAttributeFilter::IncludeNone) | (bDisplayL10NContent ? EContentBrowserItemAttributeFilter::IncludeLocalized : EContentBrowserItemAttributeFilter::IncludeNone); } FVector2D SPathView::GetScrollDistance() { if (!TreeViewPtr.IsValid()) { return FVector2D::ZeroVector; } return TreeViewPtr->GetScrollDistance(); } FVector2D SPathView::GetScrollDistanceRemaining() { if (!TreeViewPtr.IsValid()) { return FVector2D::ZeroVector; } return TreeViewPtr->GetScrollDistanceRemaining(); } TSharedRef SPathView::GetScrollWidget() { return SharedThis(this); } bool SPathView::InternalPathPassesBlockLists(const FStringView InInternalPath, const int32 InAlreadyCheckedDepth) const { TArray> BlockLists; if (FolderPermissionList.IsValid() && FolderPermissionList->HasFiltering()) { BlockLists.Add(FolderPermissionList.Get()); } if (!bAllowReadOnlyFolders && WritableFolderPermissionList.IsValid() && WritableFolderPermissionList->HasFiltering()) { BlockLists.Add(WritableFolderPermissionList.Get()); } for (const FPathPermissionList* Filter : BlockLists) { if (!Filter->PassesStartsWithFilter(InInternalPath)) { return false; } } if (InAlreadyCheckedDepth < 1 && PluginPathFilters.IsValid() && PluginPathFilters->Num() > 0) { const UContentBrowserSettings* ContentBrowserSettings = GetDefault(); bool bDisplayPluginFolders = ContentBrowserSettings->GetDisplayPluginFolders(); // check to see if we have an instance config that overrides the default in UContentBrowserSettings if (FContentBrowserInstanceConfig* EditorConfig = GetContentBrowserConfig()) { bDisplayPluginFolders = EditorConfig->bShowPluginContent; } if (bDisplayPluginFolders) { const FStringView FirstFolderName = FPathViews::GetMountPointNameFromPath(InInternalPath); if (TSharedPtr Plugin = IPluginManager::Get().FindPlugin(FirstFolderName)) { if (!PluginPathFilters->PassesAllFilters(Plugin.ToSharedRef())) { return false; } } } } return true; } void SPathView::SyncToItems(TArrayView ItemsToSync, const bool bAllowImplicitSync) { TArray VirtualPathsToSync; for (const FContentBrowserItem& Item : ItemsToSync) { if (Item.IsFile()) { // Files need to sync their parent folder in the tree, so chop off the end of their path VirtualPathsToSync.Add(*FPaths::GetPath(Item.GetVirtualPath().ToString())); } else { VirtualPathsToSync.Add(Item.GetVirtualPath()); } } SyncToVirtualPaths(VirtualPathsToSync, bAllowImplicitSync); } void SPathView::SyncToVirtualPaths(TArrayView VirtualPathsToSync, const bool bAllowImplicitSync) { TSet> SyncTreeItems; for (const FName& VirtualPathToSync : VirtualPathsToSync) { TSharedPtr Item = TreeData->FindTreeItem(VirtualPathToSync); if (Item.IsValid()) { SyncTreeItems.Add(Item.ToSharedRef()); } } if (Algo::AnyOf(SyncTreeItems, [](const TSharedRef& Item) { return !Item->IsVisible(); })) { // Clear the search box if it potentially hides a path we want to select SearchPtr->ClearSearch(); } if (SyncTreeItems.Num() > 0) { // Batch the selection changed event FScopedSelectionChangedEvent ScopedSelectionChangedEvent(SharedThis(this)); if (bAllowImplicitSync) { // Prune the current selection so that we don't unnecessarily change the path which might disorientate the user. // If a parent tree item is currently selected we don't need to clear it and select the child TSet> SelectedTreeItems{ TreeViewPtr->GetSelectedItems() }; TSet> FinalItems; for (const TSharedRef& ItemToSelect : SyncTreeItems) { // If the target item or any of its parents are already selected, maintain that object in the final // selection TSharedPtr It = ItemToSelect; while (It.IsValid() && !SelectedTreeItems.Contains(It)) { It = It->GetParent(); } if (It.IsValid()) { FinalItems.Add(It.ToSharedRef()); } else { // Otherwise select the specific folder we were asked for FinalItems.Add(ItemToSelect); } } SyncTreeItems = FinalItems; } // SyncTreeItems now shows exactly what we want to be selected and no more TreeViewPtr->ClearSelection(); // SyncTreeItems should now only contain items which aren't already shown explicitly or implicitly (as a child) for (const TSharedRef& Item : SyncTreeItems) { RecursiveExpandParents(Item); TreeViewPtr->SetItemSelection(Item, true); } } // > 0 as some may have been removed in the code above if (SyncTreeItems.Num() > 0) { // Scroll the first item into view if applicable TreeViewPtr->RequestScrollIntoView(*SyncTreeItems.CreateConstIterator()); } } void SPathView::SyncToLegacy(TArrayView AssetDataList, TArrayView FolderList, const bool bAllowImplicitSync) { TArray VirtualPathsToSync; ContentBrowserUtils::ConvertLegacySelectionToVirtualPaths(AssetDataList, FolderList, /*UseFolderPaths*/true, VirtualPathsToSync); SyncToVirtualPaths(VirtualPathsToSync, bAllowImplicitSync); } bool SPathView::DoesItemExist(FName InVirtualPath) const { return TreeData->FindTreeItem(InVirtualPath).IsValid(); } void SPathView::ApplyHistoryData( const FHistoryData& History ) { // Prevent the selection changed delegate because it would add more history when we are just setting a state FScopedPreventTreeItemChangedDelegate DelegatePrevention( SharedThis(this) ); // Update paths TArray SelectedPaths; for (const FName& HistoryPath : History.ContentSources.GetVirtualPaths()) { SelectedPaths.Add(HistoryPath.ToString()); } SetSelectedPaths(SelectedPaths); } void SPathView::SaveSettings(const FString& IniFilename, const FString& IniSection, const FString& InstanceName) const { FString SelectedPathsString; TArray< TSharedPtr > PathItems = TreeViewPtr->GetSelectedItems(); for (const TSharedPtr& Item : PathItems) { if (SelectedPathsString.Len() > 0) { SelectedPathsString += TEXT(","); } FName InvariantPath; IContentBrowserDataModule::Get().GetSubsystem()->TryConvertVirtualPath(Item->GetItem().GetVirtualPath(), InvariantPath); InvariantPath.AppendString(SelectedPathsString); } GConfig->SetString(*IniSection, *(InstanceName + TEXT(".SelectedPaths")), *SelectedPathsString, IniFilename); FString PluginFiltersString; if (PluginPathFilters.IsValid()) { for (int32 FilterIdx = 0; FilterIdx < PluginPathFilters->Num(); ++FilterIdx) { if (PluginFiltersString.Len() > 0) { PluginFiltersString += TEXT(","); } TSharedPtr Filter = StaticCastSharedPtr(PluginPathFilters->GetFilterAtIndex(FilterIdx)); PluginFiltersString += Filter->GetName(); } GConfig->SetString(*IniSection, *(InstanceName + TEXT(".PluginFilters")), *PluginFiltersString, IniFilename); } } void SPathView::LoadSettings(const FString& IniFilename, const FString& IniSection, const FString& SettingsString) { // Selected Paths TArray NewSelectedPaths; { FString SelectedPathsString; if (GConfig->GetString(*IniSection, *(SettingsString + TEXT(".SelectedPaths")), SelectedPathsString, IniFilename)) { TArray ParsedPaths; SelectedPathsString.ParseIntoArray(ParsedPaths, TEXT(","), /*bCullEmpty*/true); Algo::Transform(ParsedPaths, NewSelectedPaths, [](const FString& Str) { return *Str; }); } } // Replace each path in NewSelectedPaths with virtual version of that path for (FName& Path : NewSelectedPaths) { IContentBrowserDataModule::Get().GetSubsystem()->ConvertInternalPathToVirtual(Path, Path); } UE_LOG(LogPathView, Verbose, TEXT("[%s] LoadSettings: SelectedPaths: %s"), *WriteToString<256>(OwningContentBrowserName), *FString::JoinBy(NewSelectedPaths, TEXT(", "), UE_PROJECTION_MEMBER(FName, ToString))); { // Batch the selection changed event FScopedSelectionChangedEvent ScopedSelectionChangedEvent(SharedThis(this)); UContentBrowserDataSubsystem* ContentBrowserData = IContentBrowserDataModule::Get().GetSubsystem(); if (ContentBrowserData->IsDiscoveringItems()) { PendingInitialPaths = NewSelectedPaths; // If any of the pending paths are available, select only them // otherwise, leave the selection unchanged until we discover some if (Algo::AnyOf(NewSelectedPaths, [this](FName VirtualPath) { return TreeData->FindTreeItem(VirtualPath, /* bVisibleOnly */ true).IsValid(); })) { // Clear any previously selected paths LastSelectedPaths.Empty(); TreeViewPtr->ClearSelection(); } // If the selected paths is empty, the path was "All assets" // This should handle that case properly for (const FName& Path : NewSelectedPaths) { ExplicitlyAddPathToSelection(Path); } // Keep entire list of pending paths around until discovery is complete or all of them are selected PendingInitialPaths = NewSelectedPaths; } else { PendingInitialPaths.Reset(); // If all assets are already discovered, just select paths the best we can SetSelectedPaths(NewSelectedPaths); } } // Plugin Filters if (PluginPathFilters.IsValid()) { TArray NewSelectedFilters; if (FPathViewConfig* PathViewConfig = GetPathViewConfig()) { UE_LOG(LogPathView, Verbose, TEXT("[%s] LoadSettings: Loading plugin filters from editor config: %s"), *WriteToString<256>(OwningContentBrowserName), *FString::Join(NewSelectedFilters, TEXT(", "))); NewSelectedFilters = PathViewConfig->PluginFilters; } else { FString PluginFiltersString; if (GConfig->GetString(*IniSection, *(SettingsString + TEXT(".PluginFilters")), PluginFiltersString, IniFilename)) { UE_LOG(LogPathView, Verbose, TEXT("[%s] LoadSettings: Loading plugin filters from ini: %s"), *WriteToString<256>(OwningContentBrowserName), *PluginFiltersString); PluginFiltersString.ParseIntoArray(NewSelectedFilters, TEXT(","), /*bCullEmpty*/ true); } } for (const TSharedRef& Filter : AllPluginPathFilters) { bool bFilterActive = NewSelectedFilters.Contains(Filter->GetName()); SetPluginPathFilterActive(Filter, bFilterActive); } } } EActiveTimerReturnType SPathView::SetFocusPostConstruct( double InCurrentTime, float InDeltaTime ) { FWidgetPath WidgetToFocusPath; FSlateApplication::Get().GeneratePathToWidgetUnchecked( SearchPtr->GetWidget(), WidgetToFocusPath ); FSlateApplication::Get().SetKeyboardFocus( WidgetToFocusPath, EFocusCause::SetDirectly ); return EActiveTimerReturnType::Stop; } EActiveTimerReturnType SPathView::TriggerRepopulate(double InCurrentTime, float InDeltaTime) { Populate(); return EActiveTimerReturnType::Stop; } TSharedPtr SPathView::MakePathViewContextMenu() { if (!bAllowContextMenu || !OnGetItemContextMenu.IsBound()) { return nullptr; } const TArray CurrentSelectedItems = GetSelectedFolderItems(); if (CurrentSelectedItems.Num() == 0) { return nullptr; } return OnGetItemContextMenu.Execute(CurrentSelectedItems); } void SPathView::NewFolderItemRequested(const FContentBrowserItemTemporaryContext& NewItemContext) { bool bAddedTemporaryFolder = false; TSharedPtr NewItem; // TODO: Consider having FTreeItem explicitly store FContentBrowserItemTemporaryContext for (const FContentBrowserItemData& NewItemData : NewItemContext.GetItem().GetInternalItems()) { NewItem = TreeData->AddFolderItem(CopyTemp(NewItemData)); } if (NewItem.IsValid()) { PendingNewFolderContext = NewItemContext; PendingInitialPaths.Reset(); RecursiveExpandParents(NewItem); TreeViewPtr->SetSelection(NewItem); NewItem->SetNamingFolder(true); TreeViewPtr->RequestScrollIntoView(NewItem); } } bool SPathView::ExplicitlyAddPathToSelection(const FName Path) { if ( !ensure(TreeViewPtr.IsValid()) ) { return false; } if (TSharedPtr FoundItem = TreeData->FindTreeItem(Path)) { if (TreeViewPtr->IsItemSelected(FoundItem)) { return true; } if (!FoundItem->IsVisible()) { SearchPtr->ClearSearch(); } // Set the selection to the closest found folder and scroll it into view RecursiveExpandParents(FoundItem); LastSelectedPaths.Add(FoundItem->GetItem().GetInvariantPath()); TreeViewPtr->SetItemSelection(FoundItem, true); TreeViewPtr->RequestScrollIntoView(FoundItem); return true; } return false; } bool SPathView::ShouldAllowTreeItemChangedDelegate() const { return PreventTreeItemChangedDelegateCount == 0; } void SPathView::RecursiveExpandParents(const TSharedPtr& Item) { if (TSharedPtr Parent = Item->GetParent()) { RecursiveExpandParents(Parent); TreeViewPtr->SetItemExpansion(Parent, true); } } TSharedRef SPathView::GenerateTreeRow( TSharedPtr TreeItem, const TSharedRef& OwnerTable ) { check(TreeItem.IsValid()); return SNew( STableRow< TSharedPtr >, OwnerTable ) .OnDragDetected( this, &SPathView::OnFolderDragDetected ) [ SNew(SAssetTreeItem) .TreeItem(TreeItem) .OnNameChanged(this, &SPathView::FolderNameChanged) .OnVerifyNameChanged(this, &SPathView::VerifyFolderNameChanged) .IsItemExpanded(this, &SPathView::IsTreeItemExpanded, TreeItem) .HighlightText(this, &SPathView::GetHighlightText) .IsSelected(this, &SPathView::IsTreeItemSelected, TreeItem) ]; } void SPathView::TreeItemScrolledIntoView( TSharedPtr TreeItem, const TSharedPtr& Widget ) { if ( TreeItem->IsNamingFolder() && Widget.IsValid() && Widget->GetContent().IsValid() ) { TreeItem->OnRenameRequested().Broadcast(); } } void SPathView::GetChildrenForTree( TSharedPtr< FTreeItem > TreeItem, TArray< TSharedPtr >& OutChildren ) { TreeItem->GetSortedVisibleChildren(OutChildren); } void SPathView::SetTreeItemExpansionRecursive( TSharedPtr< FTreeItem > TreeItem, bool bInExpansionState ) { TreeViewPtr->SetItemExpansion(TreeItem, bInExpansionState); TreeItem->ForAllChildrenRecursive([this, bInExpansionState](const TSharedRef& Child) { TreeViewPtr->SetItemExpansion(Child, bInExpansionState); }); } void SPathView::TreeSelectionChanged( TSharedPtr< FTreeItem > TreeItem, ESelectInfo::Type SelectInfo ) { if (SelectInfo != ESelectInfo::Direct) { PendingInitialPaths.Reset(); } if ( ShouldAllowTreeItemChangedDelegate() ) { const TArray> NewSelectedItems = TreeViewPtr->GetSelectedItems(); LastSelectedPaths.Empty(); for (int32 ItemIdx = 0; ItemIdx < NewSelectedItems.Num(); ++ItemIdx) { const TSharedPtr Item = NewSelectedItems[ItemIdx]; if ( !ensure(Item.IsValid()) ) { // All items must exist continue; } // Keep track of the last paths that we broadcasted for selection reasons when filtering LastSelectedPaths.Add(Item->GetItem().GetInvariantPath()); } if ( OnItemSelectionChanged.IsBound() ) { if ( TreeItem.IsValid() ) { OnItemSelectionChanged.Execute(TreeItem->GetItem(), SelectInfo); } else { OnItemSelectionChanged.Execute(FContentBrowserItem(), SelectInfo); } } } if (TreeItem.IsValid()) { // Prioritize the content scan for the selected path UContentBrowserDataSubsystem* ContentBrowserData = IContentBrowserDataModule::Get().GetSubsystem(); ContentBrowserData->PrioritizeSearchPath(TreeItem->GetItem().GetVirtualPath()); } } void SPathView::TreeExpansionChanged( TSharedPtr< FTreeItem > TreeItem, bool bIsExpanded ) { if ( ShouldAllowTreeItemChangedDelegate() ) { DirtyLastExpandedPaths(); if (!bIsExpanded) { const TArray> CurrentSelectedItems = TreeViewPtr->GetSelectedItems(); bool bSelectTreeItem = false; // If any selected item was a child of the collapsed node, then add the collapsed node to the current selection // This avoids the selection ever becoming empty, as this causes the Content Browser to show everything for (const TSharedPtr& SelectedItem : CurrentSelectedItems) { if (SelectedItem->IsChildOf(*TreeItem.Get())) { bSelectTreeItem = true; break; } } if (bSelectTreeItem) { TreeViewPtr->SetItemSelection(TreeItem, true); } } } } void SPathView::FilterUpdated() { TRACE_CPUPROFILER_EVENT_SCOPE(SPathView::FilterUpdated); // Batch the selection changed event // Only emit events when the user isn't filtering, as the selection may be artificially limited by the filter FScopedSelectionChangedEvent ScopedSelectionChangedEvent(SharedThis(this), false); if (TreeData->GetFolderPathTextFilter().GetRawFilterText().IsEmpty()) { TreeData->ClearItemFilterState(); TreeViewPtr->ClearExpandedItems(); // First expand the default expanded paths for (const FName PathToExpand : GetDefaultPathsToExpand()) { if (TSharedPtr FoundItem = TreeData->FindTreeItem(PathToExpand)) { RecursiveExpandParents(FoundItem); TreeViewPtr->SetItemExpansion(FoundItem, true); } } TArray> SelectedItems = TreeViewPtr->GetSelectedItems(); if (!SelectedItems.IsEmpty()) { for (const TSharedPtr& SelectedItem : SelectedItems) { for (TSharedPtr Parent = SelectedItem->GetParent(); Parent.IsValid(); Parent = Parent->GetParent()) { TreeViewPtr->SetItemExpansion(Parent, true); } } TreeViewPtr->RequestScrollIntoView(SelectedItems[0]); } } else { TreeData->FilterFullFolderTree(); TreeViewPtr->ClearExpandedItems(); for (const TSharedPtr& Root : *TreeData->GetVisibleRootItems()) { TreeViewPtr->SetItemExpansion(Root, true); Root->ForAllChildrenRecursive([this](const TSharedPtr Descendant) { if (Descendant->GetHasVisibleDescendants()) { TreeViewPtr->SetItemExpansion(Descendant, true); } }); } } } void SPathView::SetSearchFilterText(const FText& InSearchText, TArray& OutErrors) { TreeData->GetFolderPathTextFilter().SetRawFilterText(InSearchText); const FText ErrorText = TreeData->GetFolderPathTextFilter().GetFilterErrorText(); if (!ErrorText.IsEmpty()) { OutErrors.Add(ErrorText); } } FText SPathView::GetHighlightText() const { return TreeData->GetFolderPathTextFilter().GetRawFilterText(); } void SPathView::Populate(const bool bIsRefreshingFilter) { TRACE_CPUPROFILER_EVENT_SCOPE(SPathView::Populate); UE_LOG(LogPathView, Verbose, TEXT("Repopulating path view")); const bool bFilteringByText = !TreeData->GetFolderPathTextFilter().GetRawFilterText().IsEmpty(); // Batch the selection changed event // Only emit events when the user isn't filtering, as the selection may be artificially limited by the filter FScopedSelectionChangedEvent ScopedSelectionChangedEvent(SharedThis(this), !bFilteringByText && !bIsRefreshingFilter); TreeData->PopulateFullFolderTree(CreateCompiledFolderFilter()); TreeData->FilterFullFolderTree(); TreeData->SortRootItems(); // Select any of our initial paths which aren't currently selected if (Algo::AllOf(PendingInitialPaths, [this](FName VirtualPath) { return ExplicitlyAddPathToSelection(VirtualPath); })) { UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Resetting pending initial paths as all are selected"), *WriteToString<256>(OwningContentBrowserName)); PendingInitialPaths.Reset(); } } FReply SPathView::OnFolderDragDetected(const FGeometry& Geometry, const FPointerEvent& MouseEvent) { if ( MouseEvent.IsMouseButtonDown( EKeys::LeftMouseButton ) ) { if (TSharedPtr DragDropOp = DragDropHandler::CreateDragOperation(GetSelectedFolderItems())) { return FReply::Handled().BeginDragDrop(DragDropOp.ToSharedRef()); } } return FReply::Unhandled(); } bool SPathView::VerifyFolderNameChanged(const TSharedPtr< FTreeItem >& TreeItem, const FString& ProposedName, FText& OutErrorMessage) const { if (PendingNewFolderContext.IsValid()) { checkf(FContentBrowserItemKey(TreeItem->GetItem()) == FContentBrowserItemKey(PendingNewFolderContext.GetItem()), TEXT("PendingNewFolderContext was still set when attempting to rename a different item!")); return PendingNewFolderContext.ValidateItem(ProposedName, &OutErrorMessage); } else if (!TreeItem->GetItem().GetItemName().ToString().Equals(ProposedName)) { UContentBrowserDataSubsystem* ContentBrowserData = IContentBrowserDataModule::Get().GetSubsystem(); return TreeItem->GetItem().CanRename(&ProposedName, ContentBrowserData->CreateHideFolderIfEmptyFilter().Get(), &OutErrorMessage); } return true; } void SPathView::FolderNameChanged(const TSharedPtr& TreeItem, const FString& ProposedName, const UE::Slate::FDeprecateVector2DParameter& MessageLocation, const ETextCommit::Type CommitType) { if (!TreeItem.IsValid()) { return; } bool bSuccess = false; FText ErrorMessage; // Group the deselect and reselect into a single operation FScopedSelectionChangedEvent ScopedSelectionChangedEvent(SharedThis(this)); FContentBrowserItem NewItem; if (PendingNewFolderContext.IsValid()) { checkf(FContentBrowserItemKey(TreeItem->GetItem()) == FContentBrowserItemKey(PendingNewFolderContext.GetItem()), TEXT("PendingNewFolderContext was still set when attempting to rename a different item!")); // Remove the temporary item before we do any work to ensure the new item creation is not prevented TreeData->RemoveFolderItem(TreeItem.ToSharedRef()); TreeViewPtr->SetItemSelection(TreeItem.ToSharedRef(), false); // Clearing the rename box on a newly created item cancels the entire creation process if (CommitType == ETextCommit::OnCleared) { // We need to select the parent item of this folder, as the folder would have become selected while it was being named if (TSharedPtr ParentTreeItem = TreeItem->GetParent()) { TreeViewPtr->SetItemSelection(ParentTreeItem, true); } else { TreeViewPtr->ClearSelection(); } } else { UContentBrowserDataSubsystem* ContentBrowserData = IContentBrowserDataModule::Get().GetSubsystem(); FScopedSuppressContentBrowserDataTick TickSuppression(ContentBrowserData); if (PendingNewFolderContext.ValidateItem(ProposedName, &ErrorMessage)) { NewItem = PendingNewFolderContext.FinalizeItem(ProposedName, &ErrorMessage); if (NewItem.IsValid()) { bSuccess = true; } } } PendingNewFolderContext = FContentBrowserItemTemporaryContext(); } else if (CommitType != ETextCommit::OnCleared && !TreeItem->GetItem().GetItemName().ToString().Equals(ProposedName)) { UContentBrowserDataSubsystem* ContentBrowserData = IContentBrowserDataModule::Get().GetSubsystem(); FScopedSuppressContentBrowserDataTick TickSuppression(ContentBrowserData); if (TreeItem->GetItem().CanRename(&ProposedName, ContentBrowserData->CreateHideFolderIfEmptyFilter().Get(), &ErrorMessage) && TreeItem->GetItem().Rename(ProposedName, &NewItem)) { bSuccess = true; } } if (bSuccess && NewItem.IsValid()) { // Add result to view TSharedPtr NewTreeItem; for (const FContentBrowserItemData& NewItemData : NewItem.GetInternalItems()) { NewTreeItem = TreeData->AddFolderItem(CopyTemp(NewItemData)); } // Select the new item if (NewTreeItem) { TreeViewPtr->SetItemSelection(NewTreeItem, true); TreeViewPtr->RequestScrollIntoView(NewTreeItem); } } if (!bSuccess && !ErrorMessage.IsEmpty()) { // Display the reason why the folder was invalid FSlateRect MessageAnchor(MessageLocation.X, MessageLocation.Y, MessageLocation.X, MessageLocation.Y); ContentBrowserUtils::DisplayMessage(ErrorMessage, MessageAnchor, SharedThis(this), ContentBrowserUtils::EDisplayMessageType::Error); } } bool SPathView::IsTreeItemExpanded(TSharedPtr TreeItem) const { return TreeViewPtr->IsItemExpanded(TreeItem); } bool SPathView::IsTreeItemSelected(TSharedPtr TreeItem) const { return TreeViewPtr->IsItemSelected(TreeItem); } void SPathView::HandleItemDataUpdated(TArrayView InUpdatedItems) { TRACE_CPUPROFILER_EVENT_SCOPE(SPathView::HandleItemDataUpdated); if (InUpdatedItems.Num() == 0) { return; } // TODO: Consider batching if sometimes we get very few items and filter construction time dominates if (!Algo::AnyOf(InUpdatedItems, [](const FContentBrowserItemDataUpdate& Update) { return Update.GetItemData().IsFolder(); })) { UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Skipping item data update because there were no folders present"), *WriteToString<256>(OwningContentBrowserName)); return; } const bool bFilteringByText = !TreeData->GetFolderPathTextFilter().GetRawFilterText().IsEmpty(); // Batch the selection changed event // Only emit events when the user isn't filtering, as the selection may be artificially limited by the filter FScopedSelectionChangedEvent ScopedSelectionChangedEvent(SharedThis(this), !bFilteringByText); const double HandleItemDataUpdatedStartTime = FPlatformTime::Seconds(); TreeData->ProcessDataUpdates(InUpdatedItems, CreateCompiledFolderFilter()); UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] PathView - HandleItemDataUpdated completed in %0.4f seconds for %d items"), *WriteToString<256>(OwningContentBrowserName), FPlatformTime::Seconds() - HandleItemDataUpdatedStartTime, InUpdatedItems.Num()); // Select any of our initial paths which aren't currently selected if (Algo::AllOf(PendingInitialPaths, [this](FName VirtualPath) { return ExplicitlyAddPathToSelection(VirtualPath); })) { UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Resetting pending initial paths as all are selected"), *WriteToString<256>(OwningContentBrowserName)); PendingInitialPaths.Reset(); } } void SPathView::HandleItemDataRefreshed() { // Populate immediately, as the path view must be up to date for Content Browser selection to work correctly // and since it defaults to being hidden, it potentially won't be ticked to run this update latently Populate(); /* // The class hierarchy has changed in some way, so we need to refresh our set of paths RegisterActiveTimer(0.f, FWidgetActiveTimerDelegate::CreateSP(this, &SPathView::TriggerRepopulate)); */ } void SPathView::HandleItemDataDiscoveryComplete() { // If there were any more initial paths, they no longer exist so clear them now. UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Resetting pending initial paths at end of asset data discovery"), *WriteToString<256>(OwningContentBrowserName)); PendingInitialPaths.Empty(); } void SPathView::HandleSettingChanged(FName PropertyName) { if ((PropertyName == GET_MEMBER_NAME_CHECKED(UContentBrowserSettings, DisplayEmptyFolders)) || (PropertyName == "ShowRedirectors") || (PropertyName == "DisplayDevelopersFolder") || (PropertyName == "DisplayEngineFolder") || (PropertyName == "DisplayPluginFolders") || (PropertyName == "DisplayL10NFolder") || (PropertyName == GET_MEMBER_NAME_CHECKED(UContentBrowserSettings, bDisplayContentFolderSuffix)) || (PropertyName == GET_MEMBER_NAME_CHECKED(UContentBrowserSettings, bDisplayFriendlyNameForPluginFolders)) || (PropertyName == NAME_None)) // @todo: Needed if PostEditChange was called manually, for now { UE_LOG(LogPathView, Log, TEXT("[%s][PathView] HandleSettingChanged %s"), *WriteToString<256>(OwningContentBrowserName), *WriteToString<256>(PropertyName)); const bool bHadSelectedPath = TreeViewPtr->GetNumItemsSelected() > 0; // Update our path view so that it can include/exclude the dev folder Populate(); // If folder is no longer visible but we're inside it... if (TreeViewPtr->GetNumItemsSelected() == 0 && bHadSelectedPath) { for (const FName VirtualPath : GetDefaultPathsToSelect()) { if (TSharedPtr TreeItemToSelect = TreeData->FindTreeItem(VirtualPath)) { TreeViewPtr->SetSelection(TreeItemToSelect); break; } } } // If the dev or engine folder has become visible and we're inside it... const UContentBrowserSettings* ContentBrowserSettings = GetDefault(); bool bDisplayDev = ContentBrowserSettings->GetDisplayDevelopersFolder(); bool bDisplayEngine = ContentBrowserSettings->GetDisplayEngineFolder(); bool bDisplayPlugins = ContentBrowserSettings->GetDisplayPluginFolders(); bool bDisplayL10N = ContentBrowserSettings->GetDisplayL10NFolder(); // check to see if we have an instance config that overrides the default in UContentBrowserSettings if (FContentBrowserInstanceConfig* EditorConfig = GetContentBrowserConfig()) { bDisplayDev = EditorConfig->bShowDeveloperContent; bDisplayEngine = EditorConfig->bShowEngineContent; bDisplayPlugins = EditorConfig->bShowPluginContent; bDisplayL10N = EditorConfig->bShowLocalizedContent; } if (bDisplayDev || bDisplayEngine || bDisplayPlugins || bDisplayL10N) { const TArray NewSelectedItems = GetSelectedFolderItems(); if (NewSelectedItems.Num() > 0) { const FContentBrowserItem& NewSelectedItem = NewSelectedItems[0]; if ((bDisplayDev && ContentBrowserUtils::IsItemDeveloperContent(NewSelectedItem)) || (bDisplayEngine && ContentBrowserUtils::IsItemEngineContent(NewSelectedItem)) || (bDisplayPlugins && ContentBrowserUtils::IsItemPluginContent(NewSelectedItem)) || (bDisplayL10N && ContentBrowserUtils::IsItemLocalizedContent(NewSelectedItem)) ) { // Refresh the contents OnItemSelectionChanged.ExecuteIfBound(NewSelectedItem, ESelectInfo::Direct); } } } } } TArray SPathView::GetDefaultPathsToSelect() const { TArray VirtualPaths; FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().GetModuleChecked(TEXT("ContentBrowser")); if (!ContentBrowserModule.GetDefaultSelectedPathsDelegate().ExecuteIfBound(VirtualPaths)) { VirtualPaths.Add(IContentBrowserDataModule::Get().GetSubsystem()->ConvertInternalPathToVirtual(TEXT("/Game"))); } return VirtualPaths; } TArray SPathView::GetRootPathItemNames() const { TArray RootPathItemNames; RootPathItemNames.Reserve(TreeData->GetVisibleRootItems()->Num()); for (const TSharedPtr& RootItem : *TreeData->GetVisibleRootItems()) { if (RootItem.IsValid()) { RootPathItemNames.Add(RootItem->GetItem().GetItemName()); } } return RootPathItemNames; } TArray SPathView::GetDefaultPathsToExpand() const { TArray VirtualPaths; FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().GetModuleChecked(TEXT("ContentBrowser")); if (!ContentBrowserModule.GetDefaultPathsToExpandDelegate().ExecuteIfBound(VirtualPaths)) { VirtualPaths.Add(IContentBrowserDataModule::Get().GetSubsystem()->ConvertInternalPathToVirtual(TEXT("/Game"))); } return VirtualPaths; } void SPathView::DirtyLastExpandedPaths() { bLastExpandedPathsDirty = true; } void SPathView::UpdateLastExpandedPathsIfDirty() { if (bLastExpandedPathsDirty) { TSet> ExpandedItemSet; TreeViewPtr->GetExpandedItems(ExpandedItemSet); LastExpandedPaths.Empty(ExpandedItemSet.Num()); for (const TSharedPtr& Item : ExpandedItemSet) { if (!ensure(Item.IsValid())) { // All items must exist continue; } // Keep track of the last paths that we broadcasted for expansion reasons when filtering LastExpandedPaths.Add(Item->GetItem().GetInvariantPath()); } bLastExpandedPathsDirty = false; } } TSharedRef SPathView::CreateFavoritesView() { return SAssignNew(FavoritesArea, SExpandableArea) .BorderImage(FAppStyle::Get().GetBrush("Brushes.Header")) .BodyBorderImage(FAppStyle::Get().GetBrush("Brushes.Recessed")) .HeaderPadding(FMargin(4.0f, 4.0f)) .Padding(0.f) .AllowAnimatedTransition(false) .InitiallyCollapsed(true) .HeaderContent() [ SNew(STextBlock) .Text(LOCTEXT("Favorites", "Favorites")) .TextStyle(FAppStyle::Get(), "ButtonText") .Font(FAppStyle::Get().GetFontStyle("NormalFontBold")) ] .BodyContent() [ SNew(SFavoritePathView) .OnItemSelectionChanged(OnItemSelectionChanged) .OnGetItemContextMenu(OnGetItemContextMenu) .FocusSearchBoxWhenOpened(false) .ShowTreeTitle(false) .ShowSeparator(false) .AllowClassesFolder(bAllowClassesFolder) .CanShowDevelopersFolder(bCanShowDevelopersFolder) .AllowReadOnlyFolders(bAllowReadOnlyFolders) .AllowContextMenu(bAllowContextMenu) .AddMetaData(FTagMetaData(TEXT("ContentBrowserFavorites"))) .ExternalSearch(SearchPtr) ]; } void SPathView::RegisterGetViewButtonMenu() { if (!UToolMenus::Get()->IsMenuRegistered("ContentBrowser.PathViewOptions")) { UToolMenu* Menu = UToolMenus::Get()->RegisterMenu("ContentBrowser.PathViewOptions"); Menu->bCloseSelfOnly = true; Menu->AddDynamicSection("DynamicContent", FNewToolMenuDelegate::CreateLambda([](UToolMenu* InMenu) { FName ContextOwningContentBrowserName = NAME_None; FFiltersAdditionalParams Params; if (UContentBrowserPathViewContextMenuContext* Context = InMenu->FindContext()) { if (Context->PathView.IsValid()) { TSharedPtr PathView = Context->PathView.Pin(); PathView->PopulateFilterAdditionalParams(Params); if (!PathView->OwningContentBrowserName.IsNone()) { ContextOwningContentBrowserName = PathView->OwningContentBrowserName; } } if (ContextOwningContentBrowserName.IsNone() && !Context->OwningContentBrowserName.IsNone()) { ContextOwningContentBrowserName = Context->OwningContentBrowserName; } ContentBrowserMenuUtils::AddFiltersToMenu(InMenu, ContextOwningContentBrowserName, Params); } })); } } void SPathView::PopulateFilterAdditionalParams(FFiltersAdditionalParams& OutParams) { OutParams.CanShowCPPClasses = FCanExecuteAction::CreateSP(this, &SPathView::IsToggleShowCppContentAllowed); OutParams.CanShowDevelopersContent = FCanExecuteAction::CreateSP(this, &SPathView::IsToggleShowDevelopersContentAllowed); OutParams.CanShowEngineFolder = FCanExecuteAction::CreateSP(this, &SPathView::IsToggleShowEngineContentAllowed); OutParams.CanShowPluginFolder = FCanExecuteAction::CreateSP(this, &SPathView::IsToggleShowPluginContentAllowed); OutParams.CanShowLocalizedContent = FCanExecuteAction::CreateSP(this, &SPathView::IsToggleShowLocalizedContentAllowed); } bool SPathView::IsToggleShowCppContentAllowed() const { return bAllowClassesFolder; } bool SPathView::IsToggleShowDevelopersContentAllowed() const { return bCanShowDevelopersFolder; } bool SPathView::IsToggleShowEngineContentAllowed() const { return !bForceShowEngineContent; } bool SPathView::IsToggleShowPluginContentAllowed() const { return !bForceShowPluginContent; } bool SPathView::IsToggleShowLocalizedContentAllowed() const { return true; } TSharedRef SPathView::GetViewButtonContent() { SPathView::RegisterGetViewButtonMenu(); UContentBrowserPathViewContextMenuContext* Context = NewObject(); Context->PathView = SharedThis(this); Context->OwningContentBrowserName = OwningContentBrowserName; const FToolMenuContext MenuContext(Context); return UToolMenus::Get()->GenerateWidget("ContentBrowser.PathViewOptions", MenuContext); } void SPathView::CopySelectedFolder() const { ContentBrowserUtils::CopyFolderReferencesToClipboard(GetSelectedFolderItems()); } void SPathView::BindCommands() { Commands = TSharedPtr(new FUICommandList); Commands->MapAction(FGenericCommands::Get().Copy, FUIAction( FExecuteAction::CreateSP(this, &SPathView::CopySelectedFolder) )); } void SFavoritePathView::Construct(const FArguments& InArgs) { // Bind the favorites menu to update after folder changes AssetViewUtils::OnFolderPathChanged().AddSP(this, &SFavoritePathView::FixupFavoritesFromExternalChange); OnFavoritesChangedHandle = FContentBrowserSingleton::Get().RegisterOnFavoritesChangedHandler( FOnFavoritesChanged::FDelegate::CreateSP(this, &SFavoritePathView::OnFavoriteAdded)); SPathView::Construct(InArgs); } void SFavoritePathView::ConfigureTreeView(STreeView>::FArguments& InArgs) { // Don't bind some stuff that the parent class binds such as item expansion } SFavoritePathView::SFavoritePathView() { bFlat = true; } SFavoritePathView::~SFavoritePathView() { FContentBrowserSingleton::Get().UnregisterOnFavoritesChangedDelegate(OnFavoritesChangedHandle); } void SFavoritePathView::Populate(const bool bIsRefreshingFilter) { TRACE_CPUPROFILER_EVENT_SCOPE(SFavoritePathView::Populate); // Don't allow the selection changed delegate to be fired here FScopedPreventTreeItemChangedDelegate DelegatePrevention(SharedThis(this)); TreeData->PopulateWithFavorites(CreateCompiledFolderFilter()); TreeData->SortRootItems(); TreeData->FilterFullFolderTree(); } void SFavoritePathView::SaveSettings(const FString& IniFilename, const FString& IniSection, const FString& SettingsString) const { SPathView::SaveSettings(IniFilename, IniSection, SettingsString); FString FavoritePathsString; const TArray& FavoritePaths = ContentBrowserUtils::GetFavoriteFolders(); for (const FString& PathIt : FavoritePaths) { if (FavoritePathsString.Len() > 0) { FavoritePathsString += TEXT(","); } FavoritePathsString += PathIt; } GConfig->SetString(*IniSection, TEXT("FavoritePaths"), *FavoritePathsString, IniFilename); GConfig->Flush(false, IniFilename); } void SFavoritePathView::LoadSettings(const FString& IniFilename, const FString& IniSection, const FString& SettingsString) { TGuardValue Guard(bIsLoadingSettings, true); SPathView::LoadSettings(IniFilename, IniSection, SettingsString); // We clear the initial selection for the favorite view, as it conflicts with the main paths view and results in a phantomly selected favorite item ClearSelection(); // Favorite Paths FString FavoritePathsString; TArray NewFavoritePaths; if (GConfig->GetString(*IniSection, TEXT("FavoritePaths"), FavoritePathsString, IniFilename)) { FavoritePathsString.ParseIntoArray(NewFavoritePaths, TEXT(","), /*bCullEmpty*/true); } if (NewFavoritePaths.Num() > 0) { // Keep track if we changed at least one source so we know to fire the bulk selection changed delegate later bool bAddedAtLeastOnePath = false; { // If the selected paths is empty, the path was "All assets" // This should handle that case properly for (const FString& InvariantPath : NewFavoritePaths) { FStringView InvariantPathView(InvariantPath); InvariantPathView.TrimStartAndEndInline(); if (!InvariantPathView.IsEmpty() && InvariantPathView != TEXT("None")) { ContentBrowserUtils::AddFavoriteFolder(FContentBrowserItemPath(InvariantPathView, EContentBrowserPathType::Internal)); bAddedAtLeastOnePath = true; } } } if (bAddedAtLeastOnePath) { Populate(); } } } TSharedPtr SFavoritePathView::GetContentBrowserDragDropOpFromEvent(const FDragDropEvent& DragDropEvent) const { TSharedPtr Operation = DragDropEvent.GetOperation(); if (Operation.IsValid() && OnFolderFavoriteAdd.IsBound()) { if (Operation->IsOfType()) { TSharedPtr DragDropOp = StaticCastSharedPtr(Operation); // Only agree to the operation if the drag op only contains folders, since favorites cannot contain files. if (DragDropOp && !DragDropOp->GetDraggedFolders().IsEmpty() && DragDropOp->GetDraggedFiles().IsEmpty()) { return DragDropOp; } } } return {}; } void SFavoritePathView::OnDragEnter(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) { // If we don't have the appropriate drop content, indicate to the user that nothing will happen. if (!GetContentBrowserDragDropOpFromEvent(DragDropEvent)) { DragDropEvent.GetOperation()->SetCursorOverride(EMouseCursor::SlashedCircle); } } void SFavoritePathView::OnDragLeave(const FDragDropEvent& DragDropEvent) { TSharedPtr Operation = DragDropEvent.GetOperation(); if (Operation.IsValid()) { Operation->SetCursorOverride(TOptional()); } } FReply SFavoritePathView::OnDrop(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) { TSharedPtr DragDropOp = GetContentBrowserDragDropOpFromEvent(DragDropEvent); if (!DragDropOp.IsValid()) { return FReply::Unhandled(); } if (OnFolderFavoriteAdd.IsBound()) { TArray FolderPaths; for (const FContentBrowserItem& BrowserItem: DragDropOp->GetDraggedFolders()) { FolderPaths.Add(BrowserItem.GetVirtualPath().ToString()); } OnFolderFavoriteAdd.Execute(FolderPaths); } return FReply::Handled(); } void SFavoritePathView::SetOnFolderFavoriteAdd(const FOnFolderFavoriteAdd& InOnFolderFavoriteAdd) { OnFolderFavoriteAdd = InOnFolderFavoriteAdd; } TSharedRef SFavoritePathView::GenerateTreeRow(TSharedPtr TreeItem, const TSharedRef& OwnerTable) { check(TreeItem.IsValid()); return SNew( STableRow< TSharedPtr >, OwnerTable ) .OnDragDetected( this, &SFavoritePathView::OnFolderDragDetected ) [ SNew(SAssetTreeItem) .TreeItem(TreeItem) .OnNameChanged(this, &SFavoritePathView::FolderNameChanged) .OnVerifyNameChanged(this, &SFavoritePathView::VerifyFolderNameChanged) .IsItemExpanded(false) .HighlightText(this, &SFavoritePathView::GetHighlightText) .IsSelected(this, &SFavoritePathView::IsTreeItemSelected, TreeItem) .FontOverride(UE::ContentBrowser::Private::FContentBrowserStyle::Get().GetFontStyle("ContentBrowser.SourceTreeItemFont")) ]; } void SFavoritePathView::OnFavoriteAdded(const FContentBrowserItemPath&, bool) { if (!bIsLoadingSettings) { Populate(); } } void SFavoritePathView::HandleItemDataUpdated(TArrayView InUpdatedItems) { if (InUpdatedItems.Num() == 0) { return; } if (!Algo::AnyOf(InUpdatedItems, [](const FContentBrowserItemDataUpdate& Update) { return Update.GetItemData().IsFolder(); })) { return; } TSet FavoritePaths; { const TArray& FavoritePathStrs = ContentBrowserUtils::GetFavoriteFolders(); for (const FString& InvariantPath : FavoritePathStrs) { FName VirtualPath; IContentBrowserDataModule::Get().GetSubsystem()->ConvertInternalPathToVirtual(InvariantPath, VirtualPath); FavoritePaths.Add(VirtualPath); } } if (FavoritePaths.Num() == 0) { UE_LOG(LogPathView, VeryVerbose, TEXT("[%s] Skipping item data update because there were no favorites present"), *WriteToString<256>(OwningContentBrowserName)); return; } // Don't allow the selection changed delegate to be fired here FScopedPreventTreeItemChangedDelegate DelegatePrevention(SharedThis(this)); const double HandleItemDataUpdatedStartTime = FPlatformTime::Seconds(); // Limit the updates to only folders which are favorites TArray FilteredUpdates; Algo::CopyIf(InUpdatedItems, FilteredUpdates, [FavoritePaths](const FContentBrowserItemDataUpdate& Update) { return FavoritePaths.Contains(Update.GetItemData().GetVirtualPath()); }); if (FilteredUpdates.Num()) { TreeData->ProcessDataUpdates(MakeArrayView(FilteredUpdates), CreateCompiledFolderFilter()); } // Update saved favorites for (const FContentBrowserItemDataUpdate& ItemDataUpdate : InUpdatedItems) { const FContentBrowserItemData& ItemData = ItemDataUpdate.GetItemData(); if (!ItemData.IsFolder()) { continue; } switch (ItemDataUpdate.GetUpdateType()) { case EContentBrowserItemUpdateType::Added: break; case EContentBrowserItemUpdateType::Modified: break; case EContentBrowserItemUpdateType::Moved: ContentBrowserUtils::RemoveFavoriteFolder( FContentBrowserItemPath(ItemDataUpdate.GetPreviousVirtualPath(), EContentBrowserPathType::Virtual)); break; case EContentBrowserItemUpdateType::Removed: ContentBrowserUtils::RemoveFavoriteFolder( FContentBrowserItemPath(ItemData.GetVirtualPath(), EContentBrowserPathType::Virtual)); break; default: checkf(false, TEXT("Unexpected EContentBrowserItemUpdateType!")); break; } } UE_LOG(LogPathView, VeryVerbose, TEXT("FavoritePathView - HandleItemDataUpdated completed in %0.4f seconds for %d items"), FPlatformTime::Seconds() - HandleItemDataUpdatedStartTime, InUpdatedItems.Num()); } void SFavoritePathView::FixupFavoritesFromExternalChange(TArrayView MovedFolders) { for (const AssetViewUtils::FMovedContentFolder& MovedFolder : MovedFolders) { FContentBrowserItemPath ItemPath(MovedFolder.Key, EContentBrowserPathType::Virtual); const bool bWasFavorite = ContentBrowserUtils::IsFavoriteFolder(ItemPath); if (bWasFavorite) { // Remove the original path ContentBrowserUtils::RemoveFavoriteFolder(ItemPath); // Add the new path to favorites instead const FString& NewPath = MovedFolder.Value; ContentBrowserUtils::AddFavoriteFolder(FContentBrowserItemPath(NewPath, EContentBrowserPathType::Virtual)); TSharedPtr Item = TreeData->FindTreeItem(*NewPath); if (Item.IsValid()) { TreeViewPtr->SetItemSelection(Item, true); TreeViewPtr->RequestScrollIntoView(Item); } } } Populate(); } #undef LOCTEXT_NAMESPACE