// Copyright Epic Games, Inc. All Rights Reserved. #include "PathContextMenu.h" #include "AssetViewUtils.h" #include "Containers/ArrayView.h" #include "Containers/Map.h" #include "Containers/StringFwd.h" #include "Containers/StringView.h" #include "Containers/UnrealString.h" #include "ContentBrowserCommands.h" #include "ContentBrowserDataFilter.h" #include "ContentBrowserDataSource.h" #include "ContentBrowserDataSubsystem.h" #include "ContentBrowserDelegates.h" #include "ContentBrowserMenuContexts.h" #include "ContentBrowserModule.h" #include "ContentBrowserSingleton.h" #include "ContentBrowserStyle.h" #include "ContentBrowserUtils.h" #include "Framework/Application/SlateApplication.h" #include "Framework/Commands/GenericCommands.h" #include "Framework/Commands/UIAction.h" #include "Framework/Commands/UICommandInfo.h" #include "Framework/MultiBox/MultiBoxExtender.h" #include "Framework/SlateDelegates.h" #include "HAL/IConsoleManager.h" #include "HAL/PlatformApplicationMisc.h" #include "HAL/PlatformCrt.h" #include "IContentBrowserDataModule.h" #include "Math/Vector2D.h" #include "Misc/AssertionMacros.h" #include "Misc/Attribute.h" #include "Modules/ModuleManager.h" #include "SlotBase.h" #include "SourceControlOperations.h" #include "Styling/AppStyle.h" #include "Templates/Casts.h" #include "Templates/Tuple.h" #include "Templates/UnrealTemplate.h" #include "Textures/SlateIcon.h" #include "ToolMenu.h" #include "ToolMenuDelegates.h" #include "ToolMenuEntry.h" #include "ToolMenuSection.h" #include "ToolMenus.h" #include "UObject/NameTypes.h" #include "UObject/UnrealNames.h" #include "Widgets/Colors/SColorBlock.h" #include "Widgets/Colors/SColorPicker.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/Input/SButton.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SWidget.h" #include "Widgets/SWindow.h" #define LOCTEXT_NAMESPACE "ContentBrowser" FPathContextMenu::FPathContextMenu(const TWeakPtr& InParentContent) : ParentContent(InParentContent) { } void FPathContextMenu::SetOnRenameFolderRequested(const FOnRenameFolderRequested& InOnRenameFolderRequested) { OnRenameFolderRequested = InOnRenameFolderRequested; } void FPathContextMenu::SetOnFolderDeleted(const FOnFolderDeleted& InOnFolderDeleted) { OnFolderDeleted = InOnFolderDeleted; } void FPathContextMenu::SetOnFolderFavoriteToggled(const FOnFolderFavoriteToggled& InOnFolderFavoriteToggled) { OnFolderFavoriteToggled = InOnFolderFavoriteToggled; } void FPathContextMenu::SetOnPrivateContentEditToggled(const FOnPrivateContentEditToggled& InOnPrivateContentEditToggled) { OnPrivateContentEditToggled = InOnPrivateContentEditToggled; } const TArray& FPathContextMenu::GetSelectedFolders() const { return SelectedFolders; } void FPathContextMenu::SetSelectedFolders(const TArray& InSelectedFolders) { SelectedFolders = InSelectedFolders; } TSharedRef FPathContextMenu::MakePathViewContextMenuExtender(const TArray& InSelectedPaths) { // Get all menu extenders for this context menu from the content browser module FContentBrowserModule& ContentBrowserModule = FModuleManager::GetModuleChecked( TEXT("ContentBrowser") ); TArray MenuExtenderDelegates = ContentBrowserModule.GetAllPathViewContextMenuExtenders(); TArray> Extenders; for (int32 i = 0; i < MenuExtenderDelegates.Num(); ++i) { if (MenuExtenderDelegates[i].IsBound()) { Extenders.Add(MenuExtenderDelegates[i].Execute( InSelectedPaths )); } } TSharedPtr MenuExtender = FExtender::Combine(Extenders); return MenuExtender.ToSharedRef(); } void FPathContextMenu::MakePathViewContextMenu(UToolMenu* Menu) { UContentBrowserFolderContext* Context = Menu->FindContext(); // Only add something if at least one folder is selected if ( SelectedFolders.Num() > 0 ) { // Common operations section // { FToolMenuSection& Section = Menu->AddSection("PathViewFolderOptions", LOCTEXT("PathViewOptionsMenuHeading", "Folder Options") ); { FText NewAssetToolTip; if(SelectedFolders.Num() == 1) { if(Context->bCanBeModified) { NewAssetToolTip = FText::Format(LOCTEXT("NewAssetTooltip_CreateIn", "Create a new item in {0}."), FText::FromName(SelectedFolders[0].GetVirtualPath())); } } else { NewAssetToolTip = LOCTEXT("NewAssetTooltip_InvalidNumberOfPaths", "Can only create items when there is a single path selected."); } // New Asset (submenu) if (Context->bCanBeModified) { FToolMenuEntry& NewAssetEntry = Section.AddSubMenu( "NewAsset", LOCTEXT("AddImportLabel", "Add/Import Content"), NewAssetToolTip, FNewToolMenuDelegate::CreateRaw(this, &FPathContextMenu::MakeNewAssetSubMenu), FUIAction(), EUserInterfaceActionType::Button, false, FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Import") ); NewAssetEntry.SubMenuData.Style.StyleSet = &UE::ContentBrowser::Private::FContentBrowserStyle::Get(); } } // Explore if (!Context->bNoFolderOnDisk) { Section.AddMenuEntry( "Explore", ContentBrowserUtils::GetExploreFolderText(), LOCTEXT("ExploreTooltip", "Finds this folder on disk."), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.BrowseContent"), FUIAction( FExecuteAction::CreateSP( this, &FPathContextMenu::ExecuteExplore ) ) ); } // Assume paths with an on-disk representation also have an internal path to copy if (!Context->bNoFolderOnDisk) { Section.AddMenuEntry( "CopyPath", LOCTEXT("CopyFolderPath", "Copy Path"), LOCTEXT("CopyFolderTooltip", "Copy the paths of the selected folder(s)"), FSlateIcon(FAppStyle::GetAppStyleSetName(), "GenericCommands.Copy"), FExecuteAction::CreateSP(this, &FPathContextMenu::CopySelectedFolder) ); } if (Context->bCanBeModified) { Section.AddMenuEntry(FGenericCommands::Get().Rename, LOCTEXT("RenameFolder", "Rename"), LOCTEXT("RenameFolderTooltip", "Rename the selected folder.") ); } // If any colors have already been set, display color options as a sub menu if ( ContentBrowserUtils::HasCustomColors() ) { // Set Color (submenu) Section.AddSubMenu( "SetColor", LOCTEXT("SetColor", "Set Color"), LOCTEXT("SetColorTooltip", "Sets the color this folder should appear as."), FNewToolMenuDelegate::CreateRaw( this, &FPathContextMenu::MakeSetColorSubMenu ), false, FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Color") ); } else { // Set Color Section.AddMenuEntry( "SetColor", LOCTEXT("SetColor", "Set Color"), LOCTEXT("SetColorTooltip", "Sets the color this folder should appear as."), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Color"), FUIAction( FExecuteAction::CreateSP( this, &FPathContextMenu::ExecutePickColor ) ) ); } FString SelectedFolderPath = SelectedFolders[0].GetVirtualPath().ToString(); FContentBrowserItemPath SelectedFolderItemPath(SelectedFolders[0].GetVirtualPath(), EContentBrowserPathType::Virtual); // If this folder is already favorited, show the option to remove from favorites if (ContentBrowserUtils::IsFavoriteFolder(SelectedFolderItemPath)) { // Remove from favorites Section.AddMenuEntry( "RemoveFromFavorites", LOCTEXT("RemoveFromFavorites", "Remove From Favorites"), LOCTEXT("RemoveFromFavoritesTooltip", "Removes this folder from the favorites section."), FSlateIcon(FAppStyle::GetAppStyleSetName(), "PropertyWindow.Favorites_Disabled"), FUIAction(FExecuteAction::CreateSP(this, &FPathContextMenu::ExecuteFavorite)) ); } else { // Add to favorites Section.AddMenuEntry( "AddToFavorites", LOCTEXT("AddToFavorites", "Add To Favorites"), LOCTEXT("AddToFavoritesTooltip", "Adds this folder to the favorites section for easy access."), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Star"), FUIAction(FExecuteAction::CreateSP(this, &FPathContextMenu::ExecuteFavorite)) ); } static const IConsoleVariable* EnablePublicAssetFeatureCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("AssetTools.EnablePublicAssetFeature")); const bool bIsPublicAssetUIEnabled = EnablePublicAssetFeatureCVar && EnablePublicAssetFeatureCVar->GetBool(); FStringView SelectedFolderPathView(SelectedFolderPath); if (bIsPublicAssetUIEnabled && FContentBrowserSingleton::Get().IsFolderShowPrivateContentToggleable(SelectedFolderPathView)) { if (FContentBrowserSingleton::Get().IsShowingPrivateContent(SelectedFolderPathView)) { Section.AddMenuEntry( "DisallowPrivateContentEditing", LOCTEXT("DisallowPrivateContentEditing", "Disallow Private Content Editing"), LOCTEXT("DisallowPrivateContentEditingTooltip", "Hides Private Content and prevents editing the Public/Private state of content in this folder"), FSlateIcon(UE::ContentBrowser::Private::FContentBrowserStyle::Get().GetStyleSetName(), "ContentBrowser.PrivateContentEdit"), FUIAction(FExecuteAction::CreateSP(this, &FPathContextMenu::ExecutePrivateContentEdit)) ); } else { Section.AddMenuEntry( "AllowPrivateContentEditing", LOCTEXT("AllowPrivateContentEditing", "Allow Private Content Editing"), LOCTEXT("AllowPrivateContentEditingTooltip", "Reveals Private Content and allows editing the Public/Private state of content in this folder"), FSlateIcon(UE::ContentBrowser::Private::FContentBrowserStyle::Get().GetStyleSetName(), "ContentBrowser.PrivateContentEdit"), FUIAction(FExecuteAction::CreateSP(this, &FPathContextMenu::ExecutePrivateContentEdit)) ); } } } if(Context->bCanBeModified) { // Bulk operations section // { FToolMenuSection& Section = Menu->AddSection("PathContextBulkOperations", LOCTEXT("AssetTreeBulkMenuHeading", "Bulk Operations") ); // Save Section.AddMenuEntry(FContentBrowserCommands::Get().SaveAllCurrentFolder, LOCTEXT("SaveFolder", "Save All"), LOCTEXT("SaveFolderTooltip", "Saves all modified assets in this folder.") ); // Resave Section.AddMenuEntry(FContentBrowserCommands::Get().ResaveAllCurrentFolder); // Delete Section.AddMenuEntry(FGenericCommands::Get().Delete, LOCTEXT("DeleteFolder", "Delete"), TAttribute::Create(TAttribute::FGetter::CreateSP(this, &FPathContextMenu::GetDeleteToolTip)) ); } } } } void FPathContextMenu::MakeNewAssetSubMenu(UToolMenu* Menu) { UToolMenus::Get()->AssembleMenuHierarchy(Menu, UToolMenus::Get()->CollectHierarchy("ContentBrowser.AddNewContextMenu")); } void FPathContextMenu::MakeSetColorSubMenu(UToolMenu* Menu) { { FToolMenuSection& Section = Menu->AddSection("Section"); // New Color Section.AddMenuEntry( "NewColor", LOCTEXT("NewColor", "New Color"), LOCTEXT("NewColorTooltip", "Changes the color this folder should appear as."), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &FPathContextMenu::ExecutePickColor)) ); // Clear Color (only required if any of the selection has one) if (SelectedHasCustomColors()) { Section.AddMenuEntry( "ClearColor", LOCTEXT("ClearColor", "Clear Color"), LOCTEXT("ClearColorTooltip", "Resets the color this folder appears as."), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &FPathContextMenu::ExecuteResetColor)) ); } } // Add all the custom colors the user has chosen so far TArray< FLinearColor > CustomColors; if ( ContentBrowserUtils::HasCustomColors( &CustomColors ) ) { { FToolMenuSection& Section = Menu->AddSection("PathContextCustomColors", LOCTEXT("CustomColorsExistingColors", "Existing Colors") ); for ( int32 ColorIndex = 0; ColorIndex < CustomColors.Num(); ColorIndex++ ) { const FLinearColor& Color = CustomColors[ ColorIndex ]; Section.AddEntry(FToolMenuEntry::InitWidget( NAME_None, SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .Padding(2, 0, 0, 0) [ SNew(SButton) .ButtonStyle( FAppStyle::Get(), "Menu.Button" ) .OnClicked( this, &FPathContextMenu::OnColorClicked, Color ) [ SNew(SColorBlock) .Color( Color ) .Size( FVector2D(77,16) ) ] ], FText::GetEmpty(), /*bNoIndent=*/true )); } } } } void FPathContextMenu::ExecuteExplore() { ContentBrowserUtils::ExploreFolders(SelectedFolders, ParentContent.Pin().ToSharedRef()); } bool FPathContextMenu::CanExecuteRename() const { return SelectedFolders.Num() == 1 && SelectedFolders[0].CanRename(nullptr); } void FPathContextMenu::ExecuteRename(EContentBrowserViewContext ViewContext) { check(SelectedFolders.Num() == 1); if (OnRenameFolderRequested.IsBound()) { OnRenameFolderRequested.Execute(SelectedFolders[0], ViewContext); } } void FPathContextMenu::ExecuteResetColor() { ResetColors(); } void FPathContextMenu::ExecutePickColor() { if (SelectedFolders.Num() == 0) { return; } // Spawn a color picker, so the user can select which color they want FLinearColor InitialColor = ContentBrowserUtils::GetDefaultColor(); if (SelectedFolders.Num() > 0) { // Make sure an color entry exists for all the paths, otherwise they won't update in realtime with the widget color for (const FContentBrowserItem& SelectedItem : SelectedFolders) { const FString Path = SelectedItem.GetInvariantPath().ToString(); TOptional Color = ContentBrowserUtils::GetPathColor(Path); if (Color.IsSet()) { // Default the color to the first valid entry InitialColor = Color.GetValue(); break; } } } FColorPickerArgs PickerArgs = FColorPickerArgs(InitialColor, FOnLinearColorValueChanged::CreateSP(this, &FPathContextMenu::OnLinearColorValueChanged)); PickerArgs.bIsModal = false; PickerArgs.ParentWidget = ParentContent.Pin(); OpenColorPicker(PickerArgs); } void FPathContextMenu::ExecuteFavorite() { TArray PathsToUpdate; for (const FContentBrowserItem& SelectedItem : SelectedFolders) { PathsToUpdate.Add(SelectedItem.GetVirtualPath().ToString()); } OnFolderFavoriteToggled.ExecuteIfBound(PathsToUpdate); } void FPathContextMenu::ExecutePrivateContentEdit() { TArray PathsToUpdate; for (const FContentBrowserItem& SelectedItem : SelectedFolders) { PathsToUpdate.Add(SelectedItem.GetVirtualPath().ToString()); } OnPrivateContentEditToggled.ExecuteIfBound(PathsToUpdate); } void FPathContextMenu::OnLinearColorValueChanged(const FLinearColor InColor) { OnColorClicked(InColor); } FReply FPathContextMenu::OnColorClicked( const FLinearColor InColor ) { // Make sure a color entry exists for all the paths, otherwise it can't save correctly for (const FContentBrowserItem& SelectedItem : SelectedFolders) { const FString Path = SelectedItem.GetInvariantPath().ToString(); ContentBrowserUtils::SetPathColor(Path, InColor); } // Dismiss the menu here, as we can't make the 'clear' option appear if a folder has just had a color set for the first time FSlateApplication::Get().DismissAllMenus(); return FReply::Handled(); } void FPathContextMenu::ResetColors() { // Clear the custom colors for all the selected paths for (const FContentBrowserItem& SelectedItem : SelectedFolders) { ContentBrowserUtils::SetPathColor(SelectedItem.GetInvariantPath().ToString(), TOptional()); } } void FPathContextMenu::ExecuteSaveFolder() { SaveFilesWithinSelectedFolders(EContentBrowserItemSaveFlags::SaveOnlyIfDirty | EContentBrowserItemSaveFlags::SaveOnlyIfLoaded); } void FPathContextMenu::ExecuteResaveFolder() { SaveFilesWithinSelectedFolders(EContentBrowserItemSaveFlags::None); } void FPathContextMenu::CopySelectedFolder() { CopySelectedFoldersToClipoard(); } void FPathContextMenu::SaveFilesWithinSelectedFolders(EContentBrowserItemSaveFlags InSaveFlags) { UContentBrowserDataSubsystem* ContentBrowserData = IContentBrowserDataModule::Get().GetSubsystem(); // Batch these by their data sources TMap> SourcesAndItems; for (const FContentBrowserItem& SelectedItem : SelectedFolders) { FContentBrowserDataFilter SubFileFilter; SubFileFilter.bRecursivePaths = true; SubFileFilter.ItemTypeFilter = EContentBrowserItemTypeFilter::IncludeFiles; // Get the file items within this folder ContentBrowserData->EnumerateItemsUnderPath(SelectedItem.GetVirtualPath(), SubFileFilter, [InSaveFlags , &SourcesAndItems](FContentBrowserItemData&& InFileItem) { if (UContentBrowserDataSource* ItemDataSource = InFileItem.GetOwnerDataSource()) { if (ItemDataSource->CanSaveItem(InFileItem, InSaveFlags, nullptr)) { TArray& ItemsForSource = SourcesAndItems.FindOrAdd(ItemDataSource); ItemsForSource.Add(MoveTemp(InFileItem)); } } return true; }); } // Execute the operation now for (const auto& SourceAndItemsPair : SourcesAndItems) { SourceAndItemsPair.Key->BulkSaveItems(SourceAndItemsPair.Value, InSaveFlags); } } void FPathContextMenu::CopySelectedFoldersToClipoard() { ContentBrowserUtils::CopyFolderReferencesToClipboard(SelectedFolders); } bool FPathContextMenu::CanExecuteDelete() const { bool bCanDelete = false; for (const FContentBrowserItem& SelectedItem : SelectedFolders) { bCanDelete |= SelectedItem.CanDelete(); } return bCanDelete; } FText FPathContextMenu::GetDeleteToolTip() const { FText ErrorMessage; bool bCanDelete = false; for (const FContentBrowserItem& SelectedItem : SelectedFolders) { bCanDelete |= SelectedItem.CanDelete(&ErrorMessage); } if (!bCanDelete && !ErrorMessage.IsEmpty()) { return ErrorMessage; } return LOCTEXT("DeleteFolderTooltip", "Removes this folder and all assets it contains."); } void FPathContextMenu::ExecuteDelete() { // 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 TSharedPtr ParentContentPtr = ParentContent.Pin(); if (ParentContentPtr && 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"), ParentContentPtr.ToSharedRef(), FOnClicked::CreateSP(this, &FPathContextMenu::ExecuteDeleteFolderConfirmed) ); } } FReply FPathContextMenu::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 bool bDidDelete = false; for (const auto& SourceAndItemsPair : SourcesAndItems) { bDidDelete |= SourceAndItemsPair.Key->BulkDeleteItems(SourceAndItemsPair.Value); } if (bDidDelete) { ResetColors(); OnFolderDeleted.ExecuteIfBound(); } return FReply::Handled(); } bool FPathContextMenu::SelectedHasCustomColors() const { for (const FContentBrowserItem& SelectedItem : SelectedFolders) { if (const TOptional Color = ContentBrowserUtils::GetPathColor(SelectedItem.GetInvariantPath().ToString())) { // Ignore any that are the default color, in case the user used the deprecated SaveColor with bForce if (!Color->Equals(ContentBrowserUtils::GetDefaultColor())) { return true; } } } return false; } #undef LOCTEXT_NAMESPACE