// 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& 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& 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 FAssetContextMenu::MakeContextMenu(TArrayView 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( TEXT("ContentBrowser") ); TArray MenuExtenderDelegates = ContentBrowserModule.GetAllAssetViewContextMenuExtenders(); TSharedPtr MenuExtender; { TArray SelectedAssets; for (const FContentBrowserItem& SelectedFile : SelectedFiles) { FAssetData ItemAssetData; if (SelectedFile.Legacy_TryGetAssetData(ItemAssetData)) { SelectedAssets.Add(ItemAssetData); } } if (SelectedAssets.Num() > 0) { TArray> 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(); 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 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("AssetTools"); const TSharedRef& WritableFolderPermission = AssetToolsModule.Get().GetWritableFolderPermissionList(); ContextObject->bContainsUnsupportedAssets = false; for (const FContentBrowserItem& SelectedItem : SelectedItems) { if(!SelectedItem.IsSupported()) { ContextObject->bContainsUnsupportedAssets = true; break; } } const TArray& 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()) { 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()) { 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(); 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(); PRAGMA_DISABLE_DEPRECATION_WARNINGS if (Context && Context->CommonAssetTypeActions.IsValid() && Context->CommonAssetTypeActions.Pin()->ShouldCallGetActions()) { TArray 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(); PRAGMA_DISABLE_DEPRECATION_WARNINGS if (Context && Context->CommonAssetTypeActions.IsValid() && Context->CommonAssetTypeActions.Pin()->ShouldCallGetActions()) { TArray 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(); 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 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(); 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::Create(TAttribute::FGetter::CreateSP(this, &FAssetContextMenu::GetDeleteToolTip)) ); } } return true; } void FAssetContextMenu::AddExploreMenuOptions(UToolMenu* Menu) { UContentBrowserDataMenuContext_FileMenu* Context = Menu->FindContext(); 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(); { 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(); { 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 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 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(); 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& CollectionContainer) { return !CollectionContainer->IsHidden() && !CollectionContainer->IsReadOnly(ECollectionShareType::CST_All) && CollectionContainer->HasCollections(); } static void CreateManageCollectionsSubMenu(UToolMenu* SubMenu, TArray CurrentAssetPaths, TArray> CollectionContainers) { CollectionContainers.RemoveAll([](const TSharedPtr& CollectionContainer) { return !CanManageCollectionContainer(CollectionContainer); }); for (TSharedPtr& CollectionContainer : CollectionContainers) { TAttribute Label; if (CollectionContainers.Num() != 1) { Label.Set(CollectionContainer->GetCollectionSource()->GetTitle()); } TSharedRef QuickAssetManagement = MakeShared(CollectionContainer.ToSharedRef()); QuickAssetManagement->SetCurrentAssetPaths(CurrentAssetPaths); TArray AvailableCollections; CollectionContainer->GetRootCollections(AvailableCollections); CreateManageCollectionsSubMenu(SubMenu, QuickAssetManagement, CollectionContainer, Label, MoveTemp(AvailableCollections)); } } static void CreateManageCollectionsSubMenu(UToolMenu* SubMenu, TSharedRef QuickAssetManagement, TSharedPtr CollectionContainer, TAttribute Label, TArray 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 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(), 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 QuickAssetManagement, FCollectionNameType InCollectionKey) { return QuickAssetManagement->IsCollectionEnabled(InCollectionKey); } static ECheckBoxState GetCollectionCheckState(TSharedRef QuickAssetManagement, FCollectionNameType InCollectionKey) { return QuickAssetManagement->GetCollectionCheckState(InCollectionKey); } static void OnCollectionClicked(TSharedRef 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> 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 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 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> 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& 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> 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& 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> 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& 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> 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& 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 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(TEXT("ContentBrowser")); const bool bIsEpicInternalAssetFeatureEnabled = AssetContextMenu::IsEpicInternalAssetFeatureEnabled(); const TSharedPtr& ShowEpicInternalContentPermissionList = ContentBrowserModule.Get().GetShowEpicInternalContentPermissionList(); bool bAnyPathPassesShowEpicInternal = !ShowEpicInternalContentPermissionList->HasFiltering(); const TSharedPtr& 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