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

1731 lines
58 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AssetContextMenu.h"
#include "Algo/Transform.h"
#include "AssetDefinition.h"
#include "AssetDefinitionRegistry.h"
#include "AssetRegistry/AssetData.h"
#include "AssetRegistry/AssetRegistryHelpers.h"
#include "AssetToolsModule.h"
#include "AssetViewUtils.h"
#include "CollectionAssetManagement.h"
#include "CollectionManagerModule.h"
#include "CollectionManagerTypes.h"
#include "Containers/Map.h"
#include "Containers/UnrealString.h"
#include "ContentBrowserCommands.h"
#include "ContentBrowserDataSource.h"
#include "ContentBrowserDelegates.h"
#include "ContentBrowserItemData.h"
#include "ContentBrowserMenuContexts.h"
#include "ContentBrowserModule.h"
#include "ContentBrowserSingleton.h"
#include "ContentBrowserStyle.h"
#include "ContentBrowserUtils.h"
#include "Engine/AssetManager.h"
#include "Framework/Commands/GenericCommands.h"
#include "Framework/Commands/UIAction.h"
#include "Framework/Commands/UICommandInfo.h"
#include "Framework/Commands/UICommandList.h"
#include "Framework/MultiBox/MultiBoxExtender.h"
#include "Framework/SlateDelegates.h"
#include "HAL/IConsoleManager.h"
#include "HAL/PlatformCrt.h"
#include "HAL/PlatformStackWalk.h"
#include "IAssetTools.h"
#include "IAssetTypeActions.h"
#include "ICollectionContainer.h"
#include "ICollectionManager.h"
#include "ICollectionSource.h"
#include "Internationalization/Internationalization.h"
#include "Logging/MessageLog.h"
#include "Misc/AssertionMacros.h"
#include "Misc/Attribute.h"
#include "Misc/NamePermissionList.h"
#include "Misc/PathViews.h"
#include "Misc/Paths.h"
#include "Misc/ScopedSlowTask.h"
#include "Misc/StringBuilder.h"
#include "Misc/WarnIfAssetsLoadedInScope.h"
#include "Modules/ModuleManager.h"
#include "SAssetView.h"
#include "Styling/AppStyle.h"
#include "Styling/SlateTypes.h"
#include "TelemetryRouter.h"
#include "Templates/Casts.h"
#include "Templates/Tuple.h"
#include "Templates/UnrealTemplate.h"
#include "Textures/SlateIcon.h"
#include "ToolMenu.h"
#include "ToolMenuContext.h"
#include "ToolMenuDelegates.h"
#include "ToolMenuSection.h"
#include "ToolMenus.h"
#include "Toolkits/GlobalEditorCommonCommands.h"
#include "UObject/Class.h"
#include "UObject/Object.h"
#include "UObject/ObjectMacros.h"
#include "UObject/ObjectPtr.h"
#include "UObject/Package.h"
#include "UObject/UObjectGlobals.h"
#include "UObject/UnrealNames.h"
#include "UObject/WeakObjectPtr.h"
#include "UObject/WeakObjectPtrTemplates.h"
class FMenuBuilder;
class SWidget;
#define LOCTEXT_NAMESPACE "ContentBrowser"
namespace AssetContextMenu
{
bool IsEpicInternalAssetFeatureEnabled()
{
static IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(TEXT("AssetTools.EnableEpicInternalAssetFeature"));
return ensure(CVar) ? CVar->GetBool() : false;
}
static bool bAlwaysShowSetAssetScopeMenus = false;
static FAutoConsoleVariableRef CVarAlwaysShowSetAssetScopeMenus(
TEXT("ContentBrowser.AlwaysShowSetAssetScopeMenus"),
bAlwaysShowSetAssetScopeMenus,
TEXT("Enables always showing menu entries to change asset referencable scope (False: disabled, True: enabled")
);
bool IsAlwaysShowSetAssetScopeMenus()
{
return bAlwaysShowSetAssetScopeMenus;
}
bool GetAssetAccessSpecifierFromItem(const FContentBrowserItem& InContentBrowserItem, EAssetAccessSpecifier& OutAssetAccessSpecifier)
{
FAssetData ItemAssetData;
if (InContentBrowserItem.Legacy_TryGetAssetData(ItemAssetData))
{
OutAssetAccessSpecifier = ItemAssetData.GetAssetAccessSpecifier();
return true;
}
return false;
}
}
FAssetContextMenu::FAssetContextMenu(const TWeakPtr<SAssetView>& InAssetView)
: AssetView(InAssetView)
{
}
void FAssetContextMenu::BindCommands(TSharedPtr< FUICommandList >& Commands)
{
Commands->MapAction(FGenericCommands::Get().Duplicate, FUIAction(
FExecuteAction::CreateSP(this, &FAssetContextMenu::ExecuteDuplicate),
FCanExecuteAction::CreateSP(this, &FAssetContextMenu::CanExecuteDuplicate)
));
Commands->MapAction(FGlobalEditorCommonCommands::Get().FindInContentBrowser, FUIAction(
FExecuteAction::CreateSP(this, &FAssetContextMenu::ExecuteSyncToAssetTree),
FCanExecuteAction::CreateSP(this, &FAssetContextMenu::CanExecuteSyncToAssetTree)
));
if (const TSharedPtr<SAssetView>& AssetViewPinned = AssetView.Pin())
{
const SAssetView* AssetViewPtr = AssetViewPinned.Get();
Commands->MapAction(FGenericCommands::Get().Copy, FUIAction(
FExecuteAction::CreateSP(AssetViewPtr, &SAssetView::ExecuteCopy, EAssetViewCopyType::ExportTextPath)
));
Commands->MapAction(FContentBrowserCommands::Get().AssetViewCopyObjectPath, FUIAction(
FExecuteAction::CreateSP(AssetViewPtr, &SAssetView::ExecuteCopy, EAssetViewCopyType::ObjectPath)
));
Commands->MapAction(FContentBrowserCommands::Get().AssetViewCopyPackageName, FUIAction(
FExecuteAction::CreateSP(AssetViewPtr, &SAssetView::ExecuteCopy, EAssetViewCopyType::PackageName)
));
}
}
TSharedRef<SWidget> FAssetContextMenu::MakeContextMenu(TArrayView<const FContentBrowserItem> InSelectedItems, const FAssetViewContentSources& InContentSources, TSharedPtr< FUICommandList > InCommandList)
{
FWarnIfAssetsLoadedInScope WarnIfAssetsLoaded;
SetSelectedItems(InSelectedItems);
ContentSources = InContentSources;
// Cache any vars that are used in determining if you can execute any actions.
// Useful for actions whose "CanExecute" will not change or is expensive to calculate.
CacheCanExecuteVars();
// Get all menu extenders for this context menu from the content browser module
FContentBrowserModule& ContentBrowserModule = FModuleManager::GetModuleChecked<FContentBrowserModule>( TEXT("ContentBrowser") );
TArray<FContentBrowserMenuExtender_SelectedAssets> MenuExtenderDelegates = ContentBrowserModule.GetAllAssetViewContextMenuExtenders();
TSharedPtr<FExtender> MenuExtender;
{
TArray<FAssetData> SelectedAssets;
for (const FContentBrowserItem& SelectedFile : SelectedFiles)
{
FAssetData ItemAssetData;
if (SelectedFile.Legacy_TryGetAssetData(ItemAssetData))
{
SelectedAssets.Add(ItemAssetData);
}
}
if (SelectedAssets.Num() > 0)
{
TArray<TSharedPtr<FExtender>> Extenders;
for (int32 i = 0; i < MenuExtenderDelegates.Num(); ++i)
{
if (MenuExtenderDelegates[i].IsBound())
{
Extenders.Add(MenuExtenderDelegates[i].Execute(SelectedAssets));
}
}
MenuExtender = FExtender::Combine(Extenders);
}
}
UContentBrowserAssetContextMenuContext* ContextObject = NewObject<UContentBrowserAssetContextMenuContext>();
ContextObject->AssetContextMenu = SharedThis(this);
UToolMenus* ToolMenus = UToolMenus::Get();
static const FName BaseMenuName("ContentBrowser.AssetContextMenu");
static const FName ItemContextMenuName("ContentBrowser.ItemContextMenu");
RegisterContextMenu(BaseMenuName);
// Create menu hierarchy based on class hierarchy
FName MenuName = BaseMenuName;
{
// TODO: Ideally all of this asset specific stuff would happen in the asset data source, however we
// need to keep it here for now to build the correct menu name and register the correct extenders
// Objects must be loaded for this operation... for now
UContentBrowserDataSource* CommonDataSource = nullptr;
{
TArray<FAssetData> SelectedAssets;
bool bKeepCheckingCommonDataSource = true;
for (const FContentBrowserItem& SelectedItem : SelectedItems)
{
if (bKeepCheckingCommonDataSource)
{
if (const FContentBrowserItemData* PrimaryInternalItem = SelectedItem.GetPrimaryInternalItem())
{
if (UContentBrowserDataSource* OwnerDataSource = PrimaryInternalItem->GetOwnerDataSource())
{
if (CommonDataSource == nullptr)
{
CommonDataSource = OwnerDataSource;
}
else if (CommonDataSource != OwnerDataSource)
{
CommonDataSource = nullptr;
bKeepCheckingCommonDataSource = false;
}
}
}
}
FAssetData ItemAssetData;
if (SelectedItem.Legacy_TryGetAssetData(ItemAssetData))
{
SelectedAssets.Add(MoveTemp(ItemAssetData));
}
}
ContextObject->bCanBeModified = SelectedAssets.Num() == 0;
ContextObject->SelectedAssets = MoveTemp(SelectedAssets);
}
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
const TSharedRef<FPathPermissionList>& WritableFolderPermission = AssetToolsModule.Get().GetWritableFolderPermissionList();
ContextObject->bContainsUnsupportedAssets = false;
for (const FContentBrowserItem& SelectedItem : SelectedItems)
{
if(!SelectedItem.IsSupported())
{
ContextObject->bContainsUnsupportedAssets = true;
break;
}
}
const TArray<FAssetData>& SelectedAssets = ContextObject->SelectedAssets;
if (SelectedAssets.Num() > 0 && SelectedAssets.Num() == SelectedItems.Num())
{
// Find common class for selected objects
UClass* CommonClass = nullptr;
for (int32 ObjIdx = 0; ObjIdx < SelectedAssets.Num(); ++ObjIdx)
{
if (CommonClass == nullptr)
{
CommonClass = UAssetRegistryHelpers::FindAssetNativeClass(SelectedAssets[ObjIdx]);
continue;
}
// Update the CommonClass until we find a common shared class, ignore anything that's not native.
UClass* Class = UAssetRegistryHelpers::FindAssetNativeClass(SelectedAssets[ObjIdx]);
while (Class && !Class->IsChildOf(CommonClass))
{
CommonClass = CommonClass->GetSuperClass();
}
}
ContextObject->CommonClass = CommonClass;
ContextObject->bCanBeModified = true;
ContextObject->bHasCookedPackages = false;
for (const FAssetData& SelectedAsset : SelectedAssets)
{
if (SelectedAsset.HasAnyPackageFlags(PKG_Cooked | PKG_FilterEditorOnly))
{
ContextObject->bCanBeModified = false;
ContextObject->bHasCookedPackages = true;
break;
}
if (WritableFolderPermission->HasFiltering() && !WritableFolderPermission->PassesStartsWithFilter(SelectedAsset.PackageName))
{
ContextObject->bCanBeModified = false;
break;
}
if (const UClass* AssetClass = SelectedAsset.GetClass())
{
if (AssetClass->IsChildOf<UClass>())
{
ContextObject->bCanBeModified = false;
break;
}
}
}
// We can have a null common class if an asset is from unloaded plugin or an missing class.
if (CommonClass)
{
MenuName = UToolMenus::JoinMenuPaths(BaseMenuName, CommonClass->GetFName());
RegisterMenuHierarchy(CommonClass);
// Find asset actions for common class
ContextObject->CommonAssetDefinition = UAssetDefinitionRegistry::Get()->GetAssetDefinitionForClass(ContextObject->CommonClass);
PRAGMA_DISABLE_DEPRECATION_WARNINGS
ContextObject->CommonAssetTypeActions = AssetToolsModule.Get().GetAssetTypeActionsForClass(ContextObject->CommonClass).Pin();
PRAGMA_ENABLE_DEPRECATION_WARNINGS
}
}
else
{
if (CommonDataSource)
{
ContextObject->bCanBeModified = true;
ContextObject->bHasCookedPackages = false;
if (WritableFolderPermission->HasFiltering())
{
for (const FContentBrowserItem& SelectedItem : SelectedItems)
{
if (!WritableFolderPermission->PassesStartsWithFilter(SelectedItem.GetInternalPath()))
{
ContextObject->bCanBeModified = false;
break;
}
}
}
for (const FAssetData& SelectedAsset : SelectedAssets)
{
if (SelectedAsset.HasAnyPackageFlags(PKG_Cooked | PKG_FilterEditorOnly))
{
ContextObject->bCanBeModified = false;
ContextObject->bHasCookedPackages = true;
break;
}
if (const UClass* AssetClass = SelectedAsset.GetClass())
{
if (AssetClass->IsChildOf<UClass>())
{
ContextObject->bCanBeModified = false;
break;
}
}
}
MenuName = UToolMenus::JoinMenuPaths(ItemContextMenuName, CommonDataSource->GetFName());
if (!ToolMenus->IsMenuRegistered(MenuName))
{
ToolMenus->RegisterMenu(MenuName, BaseMenuName);
}
}
}
}
ContextObject->bCanView = false;
if (!SelectedItems.IsEmpty())
{
for (const FContentBrowserItem& SelectedItem : SelectedItems)
{
if (SelectedItem.CanView())
{
ContextObject->bCanView = true;
break;
}
}
}
FToolMenuContext MenuContext(InCommandList, MenuExtender, ContextObject);
{
UContentBrowserDataMenuContext_FileMenu* DataContextObject = NewObject<UContentBrowserDataMenuContext_FileMenu>();
DataContextObject->SelectedItems = SelectedItems;
DataContextObject->CollectionSources = ContentSources.GetCollections();
PRAGMA_DISABLE_DEPRECATION_WARNINGS
// Fill out deprecated SelectedCollections with game project collections for backwards compatibility.
Algo::TransformIf(
DataContextObject->CollectionSources,
DataContextObject->SelectedCollections,
[](const FCollectionRef& Collection) { return Collection.Container == FCollectionManagerModule::GetModule().Get().GetProjectCollectionContainer(); },
[](const FCollectionRef& Collection) { return FCollectionNameType(Collection.Name, Collection.Type); });
PRAGMA_ENABLE_DEPRECATION_WARNINGS
DataContextObject->bCanBeModified = ContextObject->bCanBeModified;
DataContextObject->bCanView = ContextObject->bCanView;
DataContextObject->bHasCookedPackages = ContextObject->bHasCookedPackages;
DataContextObject->bContainsUnsupportedAssets = ContextObject->bContainsUnsupportedAssets;
DataContextObject->ParentWidget = AssetView;
DataContextObject->OnShowInPathsView = OnShowInPathsViewRequested;
DataContextObject->OnRefreshView = OnAssetViewRefreshRequested;
MenuContext.AddObject(DataContextObject);
}
return ToolMenus->GenerateWidget(MenuName, MenuContext);
}
void FAssetContextMenu::RegisterMenuHierarchy(UClass* InClass)
{
static const FName BaseMenuName("ContentBrowser.AssetContextMenu");
UToolMenus* ToolMenus = UToolMenus::Get();
for (UClass* CurrentClass = InClass; CurrentClass; CurrentClass = CurrentClass->GetSuperClass())
{
FName CurrentMenuName = UToolMenus::JoinMenuPaths(BaseMenuName, CurrentClass->GetFName());
if (!ToolMenus->IsMenuRegistered(CurrentMenuName))
{
FName ParentMenuName;
UClass* ParentClass = CurrentClass->GetSuperClass();
if (ParentClass == UObject::StaticClass() || ParentClass == nullptr)
{
ParentMenuName = BaseMenuName;
}
else
{
ParentMenuName = UToolMenus::JoinMenuPaths(BaseMenuName, ParentClass->GetFName());
}
ToolMenus->RegisterMenu(CurrentMenuName, ParentMenuName);
if (ParentMenuName == BaseMenuName)
{
break;
}
}
}
}
void FAssetContextMenu::RegisterContextMenu(const FName MenuName)
{
UToolMenus* ToolMenus = UToolMenus::Get();
if (!ToolMenus->IsMenuRegistered(MenuName))
{
UToolMenu* Menu = ToolMenus->RegisterMenu(MenuName);
FToolMenuSection& Section = Menu->FindOrAddSection("GetAssetActions");
// TODO Remove when IAssetTypeActions is dead or fully deprecated.
{
// Note: Do not use "GetActions" again when copying this code, otherwise "GetActions" menu entry will be overwritten
Section.AddDynamicEntry("GetActions", FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection)
{
UContentBrowserAssetContextMenuContext* Context = InSection.FindContext<UContentBrowserAssetContextMenuContext>();
PRAGMA_DISABLE_DEPRECATION_WARNINGS
if (Context && Context->CommonAssetTypeActions.IsValid() && Context->CommonAssetTypeActions.Pin()->ShouldCallGetActions())
{
TArray<UObject*> SelectedObjects = Context->LoadSelectedObjectsIfNeeded();
// It's possible for an unloaded object to be selected if the content browser is out of date, in that case it is unnecessary to call `GetActions`
if (SelectedObjects.Num() > 0)
{
Context->CommonAssetTypeActions.Pin()->GetActions(SelectedObjects, InSection);
}
}
PRAGMA_ENABLE_DEPRECATION_WARNINGS
}));
Section.AddDynamicEntry("GetActionsLegacy", FNewToolMenuDelegateLegacy::CreateLambda([](FMenuBuilder& MenuBuilder, UToolMenu* InMenu)
{
UContentBrowserAssetContextMenuContext* Context = InMenu->FindContext<UContentBrowserAssetContextMenuContext>();
PRAGMA_DISABLE_DEPRECATION_WARNINGS
if (Context && Context->CommonAssetTypeActions.IsValid() && Context->CommonAssetTypeActions.Pin()->ShouldCallGetActions())
{
TArray<UObject*> SelectedObjects = Context->LoadSelectedObjectsIfNeeded();
if (SelectedObjects.Num() > 0)
{
Context->CommonAssetTypeActions.Pin()->GetActions(SelectedObjects, MenuBuilder);
}
}
PRAGMA_ENABLE_DEPRECATION_WARNINGS
}));
}
Menu->AddDynamicSection("AddMenuOptions", FNewToolMenuDelegate::CreateLambda([](UToolMenu* InMenu)
{
const UContentBrowserAssetContextMenuContext* Context = InMenu->FindContext<UContentBrowserAssetContextMenuContext>();
if (Context && Context->AssetContextMenu.IsValid())
{
Context->AssetContextMenu.Pin()->AddMenuOptions(InMenu);
}
}));
}
}
void FAssetContextMenu::AddMenuOptions(UToolMenu* InMenu)
{
// Add any type-specific context menu options
AddAssetTypeMenuOptions(InMenu);
// Add quick access to common commands.
AddCommonMenuOptions(InMenu);
// Add quick access to view commands
AddExploreMenuOptions(InMenu);
static const IConsoleVariable* EnablePublicAssetFeatureCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("AssetTools.EnablePublicAssetFeature"));
if (EnablePublicAssetFeatureCVar && EnablePublicAssetFeatureCVar->GetBool())
{
AddPublicStateMenuOptions(InMenu);
}
// Add reference options
AddReferenceMenuOptions(InMenu);
// Add collection options
AddCollectionMenuOptions(InMenu);
}
void FAssetContextMenu::SetSelectedItems(TArrayView<const FContentBrowserItem> InSelectedItems)
{
SelectedItems.Reset();
SelectedItems.Append(InSelectedItems.GetData(), InSelectedItems.Num());
SelectedFiles.Reset();
SelectedFolders.Reset();
for (const FContentBrowserItem& SelectedItem : SelectedItems)
{
if (SelectedItem.IsFile())
{
SelectedFiles.Add(SelectedItem);
}
if (SelectedItem.IsFolder())
{
SelectedFolders.Add(SelectedItem);
}
}
}
void FAssetContextMenu::SetOnShowInPathsViewRequested(const FOnShowInPathsViewRequested& InOnShowInPathsViewRequested)
{
OnShowInPathsViewRequested = InOnShowInPathsViewRequested;
}
void FAssetContextMenu::SetOnRenameRequested(const FOnRenameRequested& InOnRenameRequested)
{
OnRenameRequested = InOnRenameRequested;
}
void FAssetContextMenu::SetOnDuplicateRequested(const FOnDuplicateRequested& InOnDuplicateRequested)
{
OnDuplicateRequested = InOnDuplicateRequested;
}
void FAssetContextMenu::SetOnAssetViewRefreshRequested(const FOnAssetViewRefreshRequested& InOnAssetViewRefreshRequested)
{
OnAssetViewRefreshRequested = InOnAssetViewRefreshRequested;
}
bool FAssetContextMenu::AddCommonMenuOptions(UToolMenu* Menu)
{
UContentBrowserDataMenuContext_FileMenu* Context = Menu->FindContext<UContentBrowserDataMenuContext_FileMenu>();
const bool bCanBeModified = !Context || Context->bCanBeModified;
const bool bCanBeViewed = !Context || Context->bCanView;
{
FToolMenuSection& Section = Menu->AddSection("CommonAssetActions", LOCTEXT("CommonAssetActionsMenuHeading", "Common"));
if (bCanBeModified || bCanBeViewed)
{
// Open/Edit Asset
Section.AddMenuEntry(FContentBrowserCommands::Get().OpenAssetsOrFolders,
GetEditAssetEditorLabel(bCanBeModified, bCanBeViewed),
GetEditAssetEditorTooltip(bCanBeModified, bCanBeViewed),
GetEditAssetEditorIcon(bCanBeModified, bCanBeViewed)
);
}
if (bCanBeModified)
{
// Rename
Section.AddMenuEntry(FGenericCommands::Get().Rename,
LOCTEXT("Rename", "Rename"),
LOCTEXT("RenameTooltip", "Rename the selected item.")
);
// Duplicate
Section.AddMenuEntry(FGenericCommands::Get().Duplicate,
LOCTEXT("Duplicate", "Duplicate"),
LOCTEXT("DuplicateTooltip", "Create a copy of the selected item(s)."),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Duplicate")
);
// Save
Section.AddMenuEntry(FContentBrowserCommands::Get().SaveSelectedAsset,
LOCTEXT("SaveAsset", "Save"),
LOCTEXT("SaveAssetTooltip", "Saves the item to file."),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Save")
);
// Delete
Section.AddMenuEntry(FGenericCommands::Get().Delete,
LOCTEXT("Delete", "Delete"),
TAttribute<FText>::Create(TAttribute<FText>::FGetter::CreateSP(this, &FAssetContextMenu::GetDeleteToolTip))
);
}
}
return true;
}
void FAssetContextMenu::AddExploreMenuOptions(UToolMenu* Menu)
{
UContentBrowserDataMenuContext_FileMenu* Context = Menu->FindContext<UContentBrowserDataMenuContext_FileMenu>();
FToolMenuSection& Section = Menu->AddSection("AssetContextExploreMenuOptions", LOCTEXT("AssetContextExploreMenuOptionsHeading", "Explore"));
{
// Find in Content Browser
Section.AddMenuEntry(
FGlobalEditorCommonCommands::Get().FindInContentBrowser,
LOCTEXT("ShowInFolderView", "Show in Folder View"),
LOCTEXT("ShowInFolderViewTooltip", "Selects the folder that contains this asset in the Content Browser Sources Panel.")
);
if (!Context->bHasCookedPackages)
{
// Find in Explorer
Section.AddMenuEntry(
"FindInExplorer",
ContentBrowserUtils::GetExploreFolderText(),
LOCTEXT("FindInExplorerTooltip", "Finds this asset on disk"),
FSlateIcon(UE::ContentBrowser::Private::FContentBrowserStyle::Get().GetStyleSetName(), "ContentBrowser.ShowInExplorer"),
FUIAction(
FExecuteAction::CreateSP(this, &FAssetContextMenu::ExecuteFindInExplorer),
FCanExecuteAction::CreateSP(this, &FAssetContextMenu::CanExecuteFindInExplorer)
)
);
}
}
}
bool FAssetContextMenu::AddPublicStateMenuOptions(UToolMenu* Menu)
{
if (!(bCanExecuteSetPublicAsset || bCanExecuteSetEpicInternalAsset || bCanExecuteSetPrivateAsset || bCanExecuteBulkSetPublicAsset || bCanExecuteBulkSetEpicInternalAsset || bCanExecuteBulkSetPrivateAsset))
{
return false;
}
UContentBrowserDataMenuContext_FileMenu* Context = Menu->FindContext<UContentBrowserDataMenuContext_FileMenu>();
{
FToolMenuSection& Section = Menu->AddSection("AssetPublicState", LOCTEXT("PublicStateHandling", "Asset State"));
if (SelectedFiles.Num() == 1)
{
if (bCanExecuteSetPublicAsset || bCanExecuteSetEpicInternalAsset || bCanExecuteSetPrivateAsset)
{
Section.AddMenuEntry(
"PublicAsset",
LOCTEXT("PublicAssetToggle", "Public Asset"),
LOCTEXT("PublicAssetToggleTooltip", "Sets the asset to be referencable by other Plugins"),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateSP(this, &FAssetContextMenu::ExecuteSetAssetAccessSpecifier, EAssetAccessSpecifier::Public),
FCanExecuteAction::CreateSP(this, &FAssetContextMenu::CanSetAssetAccessSpecifier, EAssetAccessSpecifier::Public),
FIsActionChecked::CreateSP(this, &FAssetContextMenu::IsSelectedAssetAccessSpecifier, EAssetAccessSpecifier::Public)
),
EUserInterfaceActionType::RadioButton
);
if (bCanExecuteSetEpicInternalAsset)
{
Section.AddMenuEntry(
"EpicInternalAsset",
LOCTEXT("SetInternalAsset", "Epic Internal Asset"),
LOCTEXT("SetInternalAssetTooltip", "Sets the asset to be referencable by Epic internal plugins and mount points"),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateSP(this, &FAssetContextMenu::ExecuteSetAssetAccessSpecifier, EAssetAccessSpecifier::EpicInternal),
FCanExecuteAction::CreateSP(this, &FAssetContextMenu::CanSetAssetAccessSpecifier, EAssetAccessSpecifier::EpicInternal),
FIsActionChecked::CreateSP(this, &FAssetContextMenu::IsSelectedAssetAccessSpecifier, EAssetAccessSpecifier::EpicInternal)
),
EUserInterfaceActionType::RadioButton
);
}
Section.AddMenuEntry(
"PrivateAsset",
LOCTEXT("SetPrivateAsset", "Private Asset"),
LOCTEXT("SetAssetPrivateTooltip", "Sets the asset so it can't be referenced by other Plugins"),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateSP(this, &FAssetContextMenu::ExecuteSetAssetAccessSpecifier, EAssetAccessSpecifier::Private),
FCanExecuteAction::CreateSP(this, &FAssetContextMenu::CanSetAssetAccessSpecifier, EAssetAccessSpecifier::Private),
FIsActionChecked::CreateSP(this, &FAssetContextMenu::IsSelectedAssetAccessSpecifier, EAssetAccessSpecifier::Private)
),
EUserInterfaceActionType::RadioButton
);
}
}
else if (SelectedFiles.Num() > 1)
{
if (bCanExecuteBulkSetPublicAsset || bCanExecuteBulkSetEpicInternalAsset || bCanExecuteBulkSetPrivateAsset)
{
Section.AddMenuEntry(
"MarkSelectedAsPublic",
LOCTEXT("MarkSelectedAsPublic", "Mark Selected As Public"),
LOCTEXT("MarkSelectedAsPublicTooltip", "Sets all selected assets to be publicly available for reference by other plugins"),
FSlateIcon(UE::ContentBrowser::Private::FContentBrowserStyle::Get().GetStyleSetName(), "ContentBrowser.AssetActions.PublicAssetToggle"),
FUIAction(
FExecuteAction::CreateSP(this, &FAssetContextMenu::ExecuteBulkSetAssetAccessSpecifier, EAssetAccessSpecifier::Public),
FCanExecuteAction::CreateSP(this, &FAssetContextMenu::CanExecuteBulkSetAssetAccessSpecifier, EAssetAccessSpecifier::Public)
)
);
if (bCanExecuteBulkSetEpicInternalAsset)
{
Section.AddMenuEntry(
"MarkSelectedAsInternal",
LOCTEXT("MarkSelectedAsInternal", "Mark Selected As Epic Internal"),
LOCTEXT("MarkSelectedAsInternalTooltip", "Sets all selected assets to be available as Epic Internal for reference by other plugins"),
FSlateIcon(UE::ContentBrowser::Private::FContentBrowserStyle::Get().GetStyleSetName(), "ContentBrowser.AssetActions.PublicAssetToggle"),
FUIAction(
FExecuteAction::CreateSP(this, &FAssetContextMenu::ExecuteBulkSetAssetAccessSpecifier, EAssetAccessSpecifier::EpicInternal),
FCanExecuteAction::CreateSP(this, &FAssetContextMenu::CanExecuteBulkSetAssetAccessSpecifier, EAssetAccessSpecifier::EpicInternal)
)
);
}
Section.AddMenuEntry(
"MarkSelectedAsPrivate",
LOCTEXT("MarkSelectedAsPrivate", "Mark Selected As Private"),
LOCTEXT("MarkSelectedAsPrivateTooltip", "Sets all selected assets to be private and unavailable for reference by other plugins"),
FSlateIcon(UE::ContentBrowser::Private::FContentBrowserStyle::Get().GetStyleSetName(), "ContentBrowser.AssetActions.PublicAssetToggle"),
FUIAction(
FExecuteAction::CreateSP(this, &FAssetContextMenu::ExecuteBulkSetAssetAccessSpecifier, EAssetAccessSpecifier::Private),
FCanExecuteAction::CreateSP(this, &FAssetContextMenu::CanExecuteBulkSetAssetAccessSpecifier, EAssetAccessSpecifier::Private)
)
);
}
}
}
return true;
}
bool FAssetContextMenu::AddReferenceMenuOptions(UToolMenu* Menu)
{
UContentBrowserDataMenuContext_FileMenu* Context = Menu->FindContext<UContentBrowserDataMenuContext_FileMenu>();
{
FToolMenuSection& Section = Menu->AddSection("AssetContextReferences", LOCTEXT("ReferencesMenuHeading", "References"));
Section.AddMenuEntry(
FGenericCommands::Get().Copy,
LOCTEXT("CopyReference", "Copy Reference"),
GetCopyTooltip(EAssetViewCopyType::ExportTextPath)
);
Section.AddMenuEntry(
FContentBrowserCommands::Get().AssetViewCopyObjectPath,
LOCTEXT("CopyObjectPath", "Copy Object Path"),
GetCopyTooltip(EAssetViewCopyType::ObjectPath)
);
Section.AddMenuEntry(
FContentBrowserCommands::Get().AssetViewCopyPackageName,
LOCTEXT("CopyPackageName", "Copy Package Path"),
GetCopyTooltip(EAssetViewCopyType::PackageName)
);
if (!Context->bHasCookedPackages)
{
Section.AddMenuEntry(
"CopyFilePath",
LOCTEXT("CopyFilePath", "Copy File Path"),
LOCTEXT("CopyFilePathTooltip", "Copies the file paths on disk for the selected assets to the clipboard."),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "GenericCommands.Copy"),
FUIAction(FExecuteAction::CreateSP(this, &FAssetContextMenu::ExecuteCopyFilePath))
);
}
}
return true;
}
FText FAssetContextMenu::GetEditAssetEditorLabel(bool bInCanEdit, bool bInCanView) const
{
static const FText EditLabel = LOCTEXT("EditAsset", "Edit...");
static const FText OpenLabel = LOCTEXT("OpenReadOnlyAsset", "Open as Read-Only...");
if (bInCanEdit || bInCanView)
{
return bInCanEdit ? EditLabel : OpenLabel;
}
return FText::GetEmpty();
}
FText FAssetContextMenu::GetEditAssetEditorTooltip(bool bInCanEdit, bool bInCanView) const
{
static const FText EditTooltip = LOCTEXT("EditAssetTooltip", "Opens the selected item(s) for edit");
static const FText OpenTooltip = LOCTEXT("OpenAssetTooltip", "Opens the selected item(s)");
if (bInCanEdit || bInCanView)
{
return bInCanEdit ? EditTooltip : OpenTooltip;
}
return FText::GetEmpty();
}
FSlateIcon FAssetContextMenu::GetEditAssetEditorIcon(bool bInCanEdit, bool bInCanView) const
{
if (bInCanEdit || bInCanView)
{
return bInCanEdit ? FSlateIcon(FAppStyle::GetAppStyleSetName(), TEXT("Icons.Edit")) : FSlateIcon(FAppStyle::GetAppStyleSetName(), TEXT("ContentBrowser.AssetActions.OpenReadOnly"));
}
return FSlateIcon();
}
FText FAssetContextMenu::GetCopyTooltip(EAssetViewCopyType InCopyType) const
{
static const FText CopyExportPathBaseTooltip = LOCTEXT("CopyExportPathBaseTooltip", "Copies reference paths for the selected asset(s) to the clipboard:");
static const FText CopyObjectPathBaseTooltip = LOCTEXT("CopyObjectPathBaseTooltip", "Copies object paths for the selected asset(s) to the clipboard:");
static const FText CopyPackageNameBaseTooltip = LOCTEXT("CopyPackageNameBaseTooltip", "Copies package paths for the selected asset(s) to the clipboard:");
TArray<FText> TextsToJoin;
TextsToJoin.Reserve(2);
switch (InCopyType)
{
case EAssetViewCopyType::ExportTextPath:
{
TextsToJoin.Add(CopyExportPathBaseTooltip);
}
break;
case EAssetViewCopyType::ObjectPath:
{
TextsToJoin.Add(CopyObjectPathBaseTooltip);
}
break;
case EAssetViewCopyType::PackageName:
{
TextsToJoin.Add(CopyPackageNameBaseTooltip);
}
break;
}
if (!TextsToJoin.IsEmpty())
{
TextsToJoin.Add(GetSelectionInformationForCopy(InCopyType));
}
return FText::Join(LOCTEXT("CopyTooltipDelimiter", "\n"), TextsToJoin);
}
FText FAssetContextMenu::GetSelectionInformationForCopy(EAssetViewCopyType InCopyType) const
{
FText SelectionInformationForCopy;
if (!SelectedFiles.IsEmpty())
{
const FContentBrowserItem& FirstSelectedFile = SelectedFiles[0];
{
FString AdditionalInformation = TEXT("");
switch (InCopyType)
{
case EAssetViewCopyType::ExportTextPath:
FirstSelectedFile.AppendItemReference(AdditionalInformation);
break;
case EAssetViewCopyType::ObjectPath:
FirstSelectedFile.AppendItemObjectPath(AdditionalInformation);
break;
case EAssetViewCopyType::PackageName:
FirstSelectedFile.AppendItemPackageName(AdditionalInformation);
break;
}
SelectionInformationForCopy = FText::FromString(MoveTemp(AdditionalInformation));
}
if (SelectedFiles.Num() > 1)
{
FText MoreFileCount = FText::Format(LOCTEXT("AdditionalFileCountTooltip", "+{0} more"), FText::AsNumber(SelectedFiles.Num() - 1));
TArray<FText> SelectionInformationTexts;
SelectionInformationTexts.Reserve(2);
SelectionInformationTexts.Add(MoveTemp(SelectionInformationForCopy));
SelectionInformationTexts.Add(MoveTemp(MoreFileCount));
SelectionInformationForCopy = FText::Join(LOCTEXT("JoinAdditionalFileCountTooltip", "\n"), SelectionInformationTexts);
}
}
return SelectionInformationForCopy;
}
bool FAssetContextMenu::AddAssetTypeMenuOptions(UToolMenu* Menu)
{
bool bAnyTypeOptions = false;
UContentBrowserAssetContextMenuContext* Context = Menu->FindContext<UContentBrowserAssetContextMenuContext>();
if (Context && Context->SelectedAssets.Num() > 0)
{
// Label "GetAssetActions" section
FToolMenuSection& Section = Menu->FindOrAddSection("GetAssetActions");
if (Context->CommonAssetDefinition)
{
Section.Label = FText::Format(NSLOCTEXT("AssetTools", "AssetSpecificOptionsMenuHeading", "{0} Actions"), Context->CommonAssetDefinition->GetAssetDisplayName());
}
else if (Context->CommonClass)
{
Section.Label = FText::Format(NSLOCTEXT("AssetTools", "AssetSpecificOptionsMenuHeading", "{0} Actions"), FText::FromName(Context->CommonClass->GetFName()));
}
else
{
Section.Label = FText::Format(NSLOCTEXT("AssetTools", "AssetSpecificOptionsMenuHeading", "{0} Actions"), FText::FromString(TEXT("Asset")));
}
bAnyTypeOptions = true;
}
return bAnyTypeOptions;
}
bool FAssetContextMenu::AddCollectionMenuOptions(UToolMenu* Menu)
{
class FManageCollectionsContextMenu
{
public:
static bool CanManageCollectionContainer(const TSharedPtr<ICollectionContainer>& CollectionContainer)
{
return !CollectionContainer->IsHidden() && !CollectionContainer->IsReadOnly(ECollectionShareType::CST_All) && CollectionContainer->HasCollections();
}
static void CreateManageCollectionsSubMenu(UToolMenu* SubMenu, TArray<FSoftObjectPath> CurrentAssetPaths, TArray<TSharedPtr<ICollectionContainer>> CollectionContainers)
{
CollectionContainers.RemoveAll([](const TSharedPtr<ICollectionContainer>& CollectionContainer) { return !CanManageCollectionContainer(CollectionContainer); });
for (TSharedPtr<ICollectionContainer>& CollectionContainer : CollectionContainers)
{
TAttribute<FText> Label;
if (CollectionContainers.Num() != 1)
{
Label.Set(CollectionContainer->GetCollectionSource()->GetTitle());
}
TSharedRef<FCollectionAssetManagement> QuickAssetManagement = MakeShared<FCollectionAssetManagement>(CollectionContainer.ToSharedRef());
QuickAssetManagement->SetCurrentAssetPaths(CurrentAssetPaths);
TArray<FCollectionNameType> AvailableCollections;
CollectionContainer->GetRootCollections(AvailableCollections);
CreateManageCollectionsSubMenu(SubMenu, QuickAssetManagement, CollectionContainer, Label, MoveTemp(AvailableCollections));
}
}
static void CreateManageCollectionsSubMenu(UToolMenu* SubMenu, TSharedRef<FCollectionAssetManagement> QuickAssetManagement, TSharedPtr<ICollectionContainer> CollectionContainer, TAttribute<FText> Label, TArray<FCollectionNameType> AvailableCollections)
{
AvailableCollections.Sort([](const FCollectionNameType& One, const FCollectionNameType& Two) -> bool
{
return One.Name.LexicalLess(Two.Name);
});
FToolMenuSection& Section = SubMenu->AddSection(CollectionContainer->GetCollectionSource()->GetName(), Label);
for (const FCollectionNameType& AvailableCollection : AvailableCollections)
{
// Never display system collections
if (AvailableCollection.Type == ECollectionShareType::CST_System)
{
continue;
}
// Can only manage assets for static collections
ECollectionStorageMode::Type StorageMode = ECollectionStorageMode::Static;
CollectionContainer->GetCollectionStorageMode(AvailableCollection.Name, AvailableCollection.Type, StorageMode);
if (StorageMode != ECollectionStorageMode::Static)
{
continue;
}
TArray<FCollectionNameType> AvailableChildCollections;
CollectionContainer->GetChildCollections(AvailableCollection.Name, AvailableCollection.Type, AvailableChildCollections);
if (AvailableChildCollections.Num() > 0)
{
Section.AddSubMenu(
NAME_None,
FText::FromName(AvailableCollection.Name),
FText::GetEmpty(),
FNewToolMenuDelegate::CreateStatic(&FManageCollectionsContextMenu::CreateManageCollectionsSubMenu, QuickAssetManagement, CollectionContainer, TAttribute<FText>(), AvailableChildCollections),
FUIAction(
FExecuteAction::CreateStatic(&FManageCollectionsContextMenu::OnCollectionClicked, QuickAssetManagement, AvailableCollection),
FCanExecuteAction::CreateStatic(&FManageCollectionsContextMenu::IsCollectionEnabled, QuickAssetManagement, AvailableCollection),
FGetActionCheckState::CreateStatic(&FManageCollectionsContextMenu::GetCollectionCheckState, QuickAssetManagement, AvailableCollection)
),
EUserInterfaceActionType::ToggleButton,
false,
FSlateIcon(FAppStyle::GetAppStyleSetName(), ECollectionShareType::GetIconStyleName(AvailableCollection.Type))
);
}
else
{
Section.AddMenuEntry(
NAME_None,
FText::FromName(AvailableCollection.Name),
FText::GetEmpty(),
FSlateIcon(FAppStyle::GetAppStyleSetName(), ECollectionShareType::GetIconStyleName(AvailableCollection.Type)),
FUIAction(
FExecuteAction::CreateStatic(&FManageCollectionsContextMenu::OnCollectionClicked, QuickAssetManagement, AvailableCollection),
FCanExecuteAction::CreateStatic(&FManageCollectionsContextMenu::IsCollectionEnabled, QuickAssetManagement, AvailableCollection),
FGetActionCheckState::CreateStatic(&FManageCollectionsContextMenu::GetCollectionCheckState, QuickAssetManagement, AvailableCollection)
),
EUserInterfaceActionType::ToggleButton
);
}
}
}
private:
static bool IsCollectionEnabled(TSharedRef<FCollectionAssetManagement> QuickAssetManagement, FCollectionNameType InCollectionKey)
{
return QuickAssetManagement->IsCollectionEnabled(InCollectionKey);
}
static ECheckBoxState GetCollectionCheckState(TSharedRef<FCollectionAssetManagement> QuickAssetManagement, FCollectionNameType InCollectionKey)
{
return QuickAssetManagement->GetCollectionCheckState(InCollectionKey);
}
static void OnCollectionClicked(TSharedRef<FCollectionAssetManagement> QuickAssetManagement, FCollectionNameType InCollectionKey)
{
const double BeginTimeSec = FPlatformTime::Seconds();
const int32 ObjectCount = QuickAssetManagement->GetCurrentAssetCount();
// The UI actions don't give you the new check state, so we need to emulate the behavior of SCheckBox
// Basically, checked will transition to unchecked (removing items), and anything else will transition to checked (adding items)
const bool RemoveFromCollection = GetCollectionCheckState(QuickAssetManagement, InCollectionKey) == ECheckBoxState::Checked;
if (RemoveFromCollection)
{
QuickAssetManagement->RemoveCurrentAssetsFromCollection(InCollectionKey);
}
else
{
QuickAssetManagement->AddCurrentAssetsToCollection(InCollectionKey);
}
const double DurationSec = FPlatformTime::Seconds() - BeginTimeSec;
{
if (RemoveFromCollection)
{
FAssetRemovedFromCollectionTelemetryEvent AssetRemoved;
AssetRemoved.DurationSec = DurationSec;
AssetRemoved.NumRemoved = ObjectCount;
AssetRemoved.CollectionShareType = InCollectionKey.Type;
AssetRemoved.Workflow = ECollectionTelemetryAssetRemovedWorkflow::ContextMenu;
FTelemetryRouter::Get().ProvideTelemetry(AssetRemoved);
}
else
{
FAssetAddedToCollectionTelemetryEvent AssetAdded;
AssetAdded.DurationSec = DurationSec;
AssetAdded.NumAdded = ObjectCount;
AssetAdded.CollectionShareType = InCollectionKey.Type;
AssetAdded.Workflow = ECollectionTelemetryAssetAddedWorkflow::ContextMenu;
FTelemetryRouter::Get().ProvideTelemetry(AssetAdded);
}
}
}
};
bool bHasAddedItems = false;
FCollectionManagerModule& CollectionManagerModule = FCollectionManagerModule::GetModule();
TArray<TSharedPtr<ICollectionContainer>> CollectionContainers;
CollectionManagerModule.Get().GetVisibleCollectionContainers(CollectionContainers);
FToolMenuSection& Section = Menu->AddSection("AssetContextCollections", LOCTEXT("AssetCollectionOptionsMenuHeading", "Collections"));
// Show a sub-menu that allows you to quickly add or remove the current asset selection from the available collections
if (CollectionContainers.FindByPredicate(FManageCollectionsContextMenu::CanManageCollectionContainer) != nullptr)
{
TArray<FSoftObjectPath> SelectedItemCollectionIds;
for (const FContentBrowserItem& SelectedItem : SelectedFiles)
{
FSoftObjectPath ItemCollectionId;
if (SelectedItem.TryGetCollectionId(ItemCollectionId))
{
SelectedItemCollectionIds.Add(ItemCollectionId);
}
}
Section.AddSubMenu(
"ManageCollections",
LOCTEXT("ManageCollections", "Manage Collections"),
FText::Format(LOCTEXT("ManageCollections_ToolTip", "Manage the collections that the selected {0}|plural(one=item belongs, other=items belong) to."), SelectedFiles.Num()),
FNewToolMenuDelegate::CreateStatic(&FManageCollectionsContextMenu::CreateManageCollectionsSubMenu, SelectedItemCollectionIds, CollectionContainers),
false, // default value
FSlateIcon(UE::ContentBrowser::Private::FContentBrowserStyle::Get().GetStyleSetName(), "ContentBrowser.Collections")
);
bHasAddedItems = true;
}
// "Remove from collection"
if (CanExecuteRemoveFromCollection())
{
Section.AddMenuEntry(
"RemoveFromCollection",
FText::Format(LOCTEXT("RemoveFromCollectionFmt", "Remove From {0}"), FText::FromName(ContentSources.GetCollections()[0].Name)),
LOCTEXT("RemoveFromCollection_ToolTip", "Removes the selected item from the current collection."),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateSP( this, &FAssetContextMenu::ExecuteRemoveFromCollection ),
FCanExecuteAction::CreateSP( this, &FAssetContextMenu::CanExecuteRemoveFromCollection )
)
);
bHasAddedItems = true;
}
return bHasAddedItems;
}
void FAssetContextMenu::ExecuteSyncToAssetTree()
{
// Copy this as the sync may adjust our selected assets array
const TArray<FContentBrowserItem> SelectedFilesCopy = SelectedFiles;
OnShowInPathsViewRequested.ExecuteIfBound(SelectedFilesCopy);
}
void FAssetContextMenu::ExecuteFindInExplorer()
{
ContentBrowserUtils::ExploreFolders(SelectedFiles, AssetView.Pin().ToSharedRef());
}
void FAssetContextMenu::ExecuteSaveAsset()
{
const EContentBrowserItemSaveFlags SaveFlags = EContentBrowserItemSaveFlags::None;
// Batch these by their data sources
TMap<UContentBrowserDataSource*, TArray<FContentBrowserItemData>> SourcesAndItems;
for (const FContentBrowserItem& SelectedItem : SelectedFiles)
{
FContentBrowserItem::FItemDataArrayView ItemDataArray = SelectedItem.GetInternalItems();
for (const FContentBrowserItemData& ItemData : ItemDataArray)
{
if (UContentBrowserDataSource* ItemDataSource = ItemData.GetOwnerDataSource())
{
FText SaveErrorMsg;
if (ItemDataSource->CanSaveItem(ItemData, SaveFlags, &SaveErrorMsg))
{
TArray<FContentBrowserItemData>& ItemsForSource = SourcesAndItems.FindOrAdd(ItemDataSource);
ItemsForSource.Add(ItemData);
}
else
{
AssetViewUtils::ShowErrorNotifcation(SaveErrorMsg);
}
}
}
}
// Execute the operation now
for (const auto& SourceAndItemsPair : SourcesAndItems)
{
SourceAndItemsPair.Key->BulkSaveItems(SourceAndItemsPair.Value, SaveFlags);
}
}
void FAssetContextMenu::ExecuteDuplicate()
{
if (SelectedFiles.Num() > 0)
{
OnDuplicateRequested.ExecuteIfBound(SelectedFiles);
}
}
void FAssetContextMenu::ExecuteRename(EContentBrowserViewContext ViewContext)
{
if (SelectedItems.Num() == 1)
{
OnRenameRequested.ExecuteIfBound(SelectedItems[0], ViewContext);
}
}
void FAssetContextMenu::ExecuteDelete()
{
// Batch these by their data sources
TMap<UContentBrowserDataSource*, TArray<FContentBrowserItemData>> SourcesAndItems;
for (const FContentBrowserItem& SelectedItem : SelectedFiles)
{
FContentBrowserItem::FItemDataArrayView ItemDataArray = SelectedItem.GetInternalItems();
for (const FContentBrowserItemData& ItemData : ItemDataArray)
{
if (UContentBrowserDataSource* ItemDataSource = ItemData.GetOwnerDataSource())
{
FText DeleteErrorMsg;
if (ItemDataSource->CanDeleteItem(ItemData, &DeleteErrorMsg))
{
TArray<FContentBrowserItemData>& ItemsForSource = SourcesAndItems.FindOrAdd(ItemDataSource);
ItemsForSource.Add(ItemData);
}
else
{
AssetViewUtils::ShowErrorNotifcation(DeleteErrorMsg);
}
}
}
}
// Execute the operation now
for (const auto& SourceAndItemsPair : SourcesAndItems)
{
SourceAndItemsPair.Key->BulkDeleteItems(SourceAndItemsPair.Value);
}
// If we had any folders selected, ask the user whether they want to delete them
// as it can be slow to build the deletion dialog on an accidental click
if (SelectedFolders.Num() > 0)
{
FText Prompt;
if (SelectedFolders.Num() == 1)
{
Prompt = FText::Format(LOCTEXT("FolderDeleteConfirm_Single", "Delete folder '{0}'?"), SelectedFolders[0].GetDisplayName());
}
else
{
Prompt = FText::Format(LOCTEXT("FolderDeleteConfirm_Multiple", "Delete {0} folders?"), SelectedFolders.Num());
}
// Spawn a confirmation dialog since this is potentially a highly destructive operation
ContentBrowserUtils::DisplayConfirmationPopup(
Prompt,
LOCTEXT("FolderDeleteConfirm_Yes", "Delete"),
LOCTEXT("FolderDeleteConfirm_No", "Cancel"),
AssetView.Pin().ToSharedRef(),
FOnClicked::CreateSP(this, &FAssetContextMenu::ExecuteDeleteFolderConfirmed)
);
}
}
FReply FAssetContextMenu::ExecuteDeleteFolderConfirmed()
{
// Batch these by their data sources
TMap<UContentBrowserDataSource*, TArray<FContentBrowserItemData>> SourcesAndItems;
for (const FContentBrowserItem& SelectedItem : SelectedFolders)
{
FContentBrowserItem::FItemDataArrayView ItemDataArray = SelectedItem.GetInternalItems();
for (const FContentBrowserItemData& ItemData : ItemDataArray)
{
if (UContentBrowserDataSource* ItemDataSource = ItemData.GetOwnerDataSource())
{
FText DeleteErrorMsg;
if (ItemDataSource->CanDeleteItem(ItemData, &DeleteErrorMsg))
{
TArray<FContentBrowserItemData>& ItemsForSource = SourcesAndItems.FindOrAdd(ItemDataSource);
ItemsForSource.Add(ItemData);
}
else
{
AssetViewUtils::ShowErrorNotifcation(DeleteErrorMsg);
}
}
}
}
// Execute the operation now
for (const auto& SourceAndItemsPair : SourcesAndItems)
{
SourceAndItemsPair.Key->BulkDeleteItems(SourceAndItemsPair.Value);
}
return FReply::Handled();
}
bool FAssetContextMenu::SetAssetAccessSpecifier(FAssetData& ItemAssetData, const EAssetAccessSpecifier InAssetAccessSpecifier, const bool bEmitEvent)
{
UPackage* ItemAssetPackage = ItemAssetData.GetPackage();
if (!ItemAssetPackage)
{
return false;
}
const EAssetAccessSpecifier OldAssetAccessSpecifier = ItemAssetPackage->GetAssetAccessSpecifier();
if (OldAssetAccessSpecifier == InAssetAccessSpecifier)
{
return false;
}
bool bModified = false;
if (OldAssetAccessSpecifier == EAssetAccessSpecifier::Private)
{
bModified = ItemAssetPackage->SetAssetAccessSpecifier(InAssetAccessSpecifier);
}
else
{
ExecuteBulkSetAssetAccessSpecifier(InAssetAccessSpecifier);
// Unknown if there were modifications
return bModified;
}
if (bModified)
{
if (bEmitEvent)
{
OnAssetViewRefreshRequested.ExecuteIfBound();
}
}
return bModified;
}
void FAssetContextMenu::ExecuteSetAssetAccessSpecifier(EAssetAccessSpecifier InAssetAccessSpecifier)
{
if (ensure(SelectedFiles.Num() == 1))
{
FAssetData ItemAssetData;
if (SelectedFiles[0].Legacy_TryGetAssetData(ItemAssetData))
{
SetAssetAccessSpecifier(ItemAssetData, InAssetAccessSpecifier, true);
}
}
}
bool FAssetContextMenu::CanSetAssetAccessSpecifier(const EAssetAccessSpecifier InAssetAccessSpecifier) const
{
if (InAssetAccessSpecifier == EAssetAccessSpecifier::Public)
{
return bCanExecuteSetPublicAsset;
}
else if (InAssetAccessSpecifier == EAssetAccessSpecifier::EpicInternal)
{
return bCanExecuteSetEpicInternalAsset;
}
return bCanExecuteSetPrivateAsset;
}
void FAssetContextMenu::ExecuteBulkSetAssetAccessSpecifier(EAssetAccessSpecifier DestScope)
{
TMap<UContentBrowserDataSource*, TArray<FContentBrowserItemData>> PrivatizeSourcesAndItems;
auto AddForBulkPrivatizeItems = [&PrivatizeSourcesAndItems](const FContentBrowserItem& InItem)
{
FContentBrowserItem::FItemDataArrayView ItemDataArray = InItem.GetInternalItems();
for (const FContentBrowserItemData& ItemData : ItemDataArray)
{
if (UContentBrowserDataSource* ItemDataSource = ItemData.GetOwnerDataSource())
{
FText PrivateErrorMsg;
// CanPrivatizeItem checks !GetAssetFolderItemPayload(), CanModifyAssetFileItem(), !IsRunningPIE(), !IsRedirector()
if (ItemDataSource->CanPrivatizeItem(ItemData, &PrivateErrorMsg))
{
TArray<FContentBrowserItemData>& ItemsForSource = PrivatizeSourcesAndItems.FindOrAdd(ItemDataSource);
ItemsForSource.Add(ItemData);
}
else
{
AssetViewUtils::ShowErrorNotifcation(PrivateErrorMsg);
}
}
}
};
if (DestScope == EAssetAccessSpecifier::Public)
{
FScopedSlowTask SlowTask((float)SelectedFiles.Num(), LOCTEXT("SetAssetScope", "Loading assets and modifying scope..."));
SlowTask.MakeDialog(/*bShowCancelButton*/true);
for (const FContentBrowserItem& SelectedItem : SelectedFiles)
{
SlowTask.EnterProgressFrame(1);
if (SlowTask.ShouldCancel())
{
break;
}
FAssetData ItemAssetData;
if (SelectedItem.Legacy_TryGetAssetData(ItemAssetData))
{
if (UPackage* ItemAssetPackage = ItemAssetData.GetPackage())
{
ItemAssetPackage->SetAssetAccessSpecifier(DestScope);
}
}
}
}
else if (DestScope == EAssetAccessSpecifier::EpicInternal)
{
FScopedSlowTask SlowTask((float)SelectedFiles.Num(), LOCTEXT("SetAssetScope", "Loading assets and modifying scope..."));
SlowTask.MakeDialog(/*bShowCancelButton*/true);
for (const FContentBrowserItem& SelectedItem : SelectedFiles)
{
SlowTask.EnterProgressFrame(1);
if (SlowTask.ShouldCancel())
{
break;
}
FAssetData ItemAssetData;
if (!SelectedItem.Legacy_TryGetAssetData(ItemAssetData))
{
break;
}
UPackage* ItemAssetPackage = ItemAssetData.GetPackage();
if (!ItemAssetPackage)
{
break;
}
const EAssetAccessSpecifier OldAssetAccessSpecifier = ItemAssetPackage->GetAssetAccessSpecifier();
if (OldAssetAccessSpecifier == DestScope)
{
break;
}
if (OldAssetAccessSpecifier == EAssetAccessSpecifier::Private)
{
// Ok to raise EAssetAccessSpecifier::Private to DestScope without calling BulkPrivatizeItems
ItemAssetPackage->SetAssetAccessSpecifier(DestScope);
}
else
{
// Lowering EAssetAccessSpecifier::Public to EAssetAccessSpecifier::EpicInternal uses BulkPrivatizeItems workflow instead of directly calling SetAssetAccessSpecifier
AddForBulkPrivatizeItems(SelectedItem);
}
}
}
else if (DestScope == EAssetAccessSpecifier::Private)
{
// Batch these by their data sources
for (const FContentBrowserItem& SelectedItem : SelectedFiles)
{
AddForBulkPrivatizeItems(SelectedItem);
}
}
else
{
checkf(false, TEXT("Unknown EAssetAccessSpecifier dest scope: %d"), DestScope);
}
if (!PrivatizeSourcesAndItems.IsEmpty())
{
for (const auto& SourceAndItemsPair : PrivatizeSourcesAndItems)
{
SourceAndItemsPair.Key->BulkPrivatizeItems(SourceAndItemsPair.Value, DestScope);
}
}
OnAssetViewRefreshRequested.ExecuteIfBound();
}
bool FAssetContextMenu::CanExecuteBulkSetAssetAccessSpecifier(EAssetAccessSpecifier InAssetAccessSpecifier)
{
if (InAssetAccessSpecifier == EAssetAccessSpecifier::Public)
{
return bCanExecuteBulkSetPublicAsset;
}
else if (InAssetAccessSpecifier == EAssetAccessSpecifier::EpicInternal)
{
return bCanExecuteBulkSetEpicInternalAsset;
}
else if (InAssetAccessSpecifier == EAssetAccessSpecifier::Private)
{
return bCanExecuteBulkSetPrivateAsset;
}
return false;
}
bool FAssetContextMenu::GetAssetAccessSpecifierFromSelection(EAssetAccessSpecifier& OutAssetAccessSpecifier)
{
if (ensure(SelectedFiles.Num() == 1))
{
return AssetContextMenu::GetAssetAccessSpecifierFromItem(SelectedFiles[0], OutAssetAccessSpecifier);
}
return false;
}
bool FAssetContextMenu::IsSelectedAssetAccessSpecifier(EAssetAccessSpecifier InAssetAccessSpecifier)
{
EAssetAccessSpecifier AssetAccessSpecifier;
return GetAssetAccessSpecifierFromSelection(AssetAccessSpecifier) && InAssetAccessSpecifier == AssetAccessSpecifier;
}
void FAssetContextMenu::ExecuteCopyFilePath()
{
if (SelectedFiles.Num() > 0)
{
ContentBrowserUtils::CopyFilePathsToClipboard(SelectedFiles);
}
}
void FAssetContextMenu::ExecuteRemoveFromCollection()
{
if ( ensure(CanExecuteRemoveFromCollection()) )
{
TArray<FSoftObjectPath> SelectedItemCollectionIds;
for (const FContentBrowserItem& SelectedItem : SelectedFiles)
{
FSoftObjectPath ItemCollectionId;
if (SelectedItem.TryGetCollectionId(ItemCollectionId))
{
SelectedItemCollectionIds.Add(ItemCollectionId);
}
}
if ( SelectedItemCollectionIds.Num() > 0 )
{
const FCollectionRef& Collection = ContentSources.GetCollections()[0];
const double BeginTimeSec = FPlatformTime::Seconds();
Collection.Container->RemoveFromCollection(Collection.Name, Collection.Type, SelectedItemCollectionIds);
const double DurationSec = FPlatformTime::Seconds() - BeginTimeSec;
OnAssetViewRefreshRequested.ExecuteIfBound();
FAssetRemovedFromCollectionTelemetryEvent AssetRemoved;
AssetRemoved.DurationSec = DurationSec;
AssetRemoved.NumRemoved = SelectedItemCollectionIds.Num();
AssetRemoved.CollectionShareType = Collection.Type;
AssetRemoved.Workflow = ECollectionTelemetryAssetRemovedWorkflow::ContextMenu;
FTelemetryRouter::Get().ProvideTelemetry(AssetRemoved);
}
}
}
bool FAssetContextMenu::CanExecuteSyncToAssetTree() const
{
return SelectedFiles.Num() > 0;
}
bool FAssetContextMenu::CanExecuteFindInExplorer() const
{
return bCanExecuteFindInExplorer;
}
bool FAssetContextMenu::CanExecuteRemoveFromCollection() const
{
return ContentSources.GetCollections().Num() == 1 &&
!ContentSources.GetCollections()[0].Container->IsReadOnly(ContentSources.GetCollections()[0].Type) &&
!ContentSources.IsDynamicCollection();
}
bool FAssetContextMenu::CanExecuteDuplicate() const
{
bool bCanDuplicate = false;
for (const FContentBrowserItem& SelectedItem : SelectedFiles)
{
bCanDuplicate |= SelectedItem.CanDuplicate();
}
return bCanDuplicate;
}
bool FAssetContextMenu::CanExecuteRename() const
{
return ContentBrowserUtils::CanRenameFromAssetView(AssetView);
}
bool FAssetContextMenu::CanExecuteDelete() const
{
return ContentBrowserUtils::CanDeleteFromAssetView(AssetView);
}
FText FAssetContextMenu::GetDeleteToolTip() const
{
FText ErrorMessage;
if (!ContentBrowserUtils::CanDeleteFromAssetView(AssetView, &ErrorMessage) && !ErrorMessage.IsEmpty())
{
return ErrorMessage;
}
return LOCTEXT("DeleteTooltip", "Delete the selected items.");
}
bool FAssetContextMenu::CanExecuteSaveAsset() const
{
bool bCanSave = false;
for (const FContentBrowserItem& SelectedItem : SelectedFiles)
{
bCanSave |= SelectedItem.CanSave(EContentBrowserItemSaveFlags::None);
}
return bCanSave;
}
void FAssetContextMenu::CacheCanExecuteVars()
{
bCanExecuteFindInExplorer = false;
FContentBrowserModule& ContentBrowserModule = FModuleManager::GetModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));
const bool bIsEpicInternalAssetFeatureEnabled = AssetContextMenu::IsEpicInternalAssetFeatureEnabled();
const TSharedPtr<FPathPermissionList>& ShowEpicInternalContentPermissionList = ContentBrowserModule.Get().GetShowEpicInternalContentPermissionList();
bool bAnyPathPassesShowEpicInternal = !ShowEpicInternalContentPermissionList->HasFiltering();
const TSharedPtr<FPathPermissionList>& ShowPrivateContentPermissionList = ContentBrowserModule.Get().GetShowPrivateContentPermissionList();
if (SelectedFiles.Num() == 1)
{
const FContentBrowserItem& SelectedItem = SelectedFiles[0];
FString ItemFilename;
if (!bCanExecuteFindInExplorer && SelectedItem.GetItemPhysicalPath(ItemFilename) && FPaths::FileExists(ItemFilename))
{
bCanExecuteFindInExplorer = true;
}
bCanExecuteSetPublicAsset = false;
bCanExecuteSetEpicInternalAsset = false;
bCanExecuteSetPrivateAsset = false;
if (SelectedItem.CanEdit())
{
const FNameBuilder ItemInternalPath(SelectedItem.GetInternalPath());
const FStringView AssetPath(ItemInternalPath);
// Only show menu entries when asset can be made externally referenceable
if (IAssetTools::Get().CanAssetBePublic(AssetPath))
{
bCanExecuteSetPublicAsset = true;
if (bIsEpicInternalAssetFeatureEnabled && (ShowEpicInternalContentPermissionList->PassesStartsWithFilter(AssetPath) || ShowPrivateContentPermissionList->PassesStartsWithFilter(AssetPath)))
{
bCanExecuteSetEpicInternalAsset = true;
}
bCanExecuteSetPrivateAsset = true;
}
}
bCanExecuteBulkSetPublicAsset = false;
bCanExecuteBulkSetEpicInternalAsset = false;
bCanExecuteBulkSetPrivateAsset = false;
}
else
{
bCanExecuteBulkSetPublicAsset = false;
bCanExecuteBulkSetEpicInternalAsset = false;
bCanExecuteBulkSetPrivateAsset = false;
for (const FContentBrowserItem& SelectedItem : SelectedFiles)
{
FString ItemFilename;
if (!bCanExecuteFindInExplorer && SelectedItem.GetItemPhysicalPath(ItemFilename) && FPaths::FileExists(ItemFilename))
{
bCanExecuteFindInExplorer = true;
}
if (SelectedItem.CanEdit())
{
const FNameBuilder ItemInternalPath(SelectedItem.GetInternalPath());
const FStringView AssetPath(ItemInternalPath);
const FAssetData AssetData = IAssetRegistry::GetChecked().GetAssetByObjectPath(FSoftObjectPath(AssetPath));
if (AssetData.IsValid())
{
// If any selected asset can be made public, show menu entries to bulk set as Public/Internal/Private
if (!bCanExecuteBulkSetPublicAsset && IAssetTools::Get().CanAssetBePublic(AssetPath))
{
bCanExecuteBulkSetPublicAsset = true;
bCanExecuteBulkSetEpicInternalAsset = bIsEpicInternalAssetFeatureEnabled;
bCanExecuteBulkSetPrivateAsset = true;
}
// If any selected can be set to Epic internal
if (!bAnyPathPassesShowEpicInternal && bIsEpicInternalAssetFeatureEnabled)
{
if (IAssetTools::Get().CanAssetBePublic(AssetPath))
{
if (ShowEpicInternalContentPermissionList->PassesStartsWithFilter(AssetPath) || (ShowPrivateContentPermissionList->HasFiltering() && ShowPrivateContentPermissionList->PassesStartsWithFilter(AssetPath)))
{
bAnyPathPassesShowEpicInternal = true;
}
}
}
// If any selected asset not private, show menu entry to bulk set to private
if (!bCanExecuteBulkSetPrivateAsset && (AssetData.GetAssetAccessSpecifier() != EAssetAccessSpecifier::Private))
{
bCanExecuteBulkSetPrivateAsset = true;
}
}
}
}
// Hide menu entries when nothing passes IAssetTools::Get().CanAssetBePublic()
if (!bCanExecuteBulkSetPublicAsset)
{
bAnyPathPassesShowEpicInternal = false;
bCanExecuteBulkSetEpicInternalAsset = false;
bCanExecuteBulkSetPrivateAsset = false;
}
if (!bAnyPathPassesShowEpicInternal)
{
bCanExecuteBulkSetEpicInternalAsset = false;
}
}
if (!bIsEpicInternalAssetFeatureEnabled)
{
bAnyPathPassesShowEpicInternal = false;
bCanExecuteBulkSetEpicInternalAsset = false;
}
// CVar bypass to always show. For repairing broken assets stuck with the wrong flags.
if (AssetContextMenu::IsAlwaysShowSetAssetScopeMenus())
{
if (SelectedFiles.Num() == 1)
{
bCanExecuteSetPublicAsset = true;
bCanExecuteSetEpicInternalAsset = bIsEpicInternalAssetFeatureEnabled;
bCanExecuteSetPrivateAsset = true;
}
else
{
bCanExecuteBulkSetPublicAsset = true;
bCanExecuteBulkSetEpicInternalAsset = bIsEpicInternalAssetFeatureEnabled;
bCanExecuteBulkSetPrivateAsset = true;
}
}
}
#undef LOCTEXT_NAMESPACE