// Copyright Epic Games, Inc. All Rights Reserved. #include "SAssetDialog.h" #include "AssetRegistry/ARFilter.h" #include "AssetRegistry/AssetData.h" #include "AssetRegistry/AssetRegistryModule.h" #include "AssetRegistry/IAssetRegistry.h" #include "AssetViewUtils.h" #include "Containers/ArrayView.h" #include "Containers/Map.h" #include "ContentBrowserCommands.h" #include "ContentBrowserDataSource.h" #include "ContentBrowserDataSubsystem.h" #include "ContentBrowserItem.h" #include "ContentBrowserItemData.h" #include "ContentBrowserPluginFilters.h" #include "ContentBrowserSingleton.h" #include "ContentBrowserStyle.h" #include "ContentBrowserUtils.h" #include "CoreGlobals.h" #include "Delegates/Delegate.h" #include "Editor.h" #include "ICollectionSource.h" #include "Editor/EditorEngine.h" #include "Framework/Application/SlateApplication.h" #include "Framework/Commands/GenericCommands.h" #include "Framework/Commands/UIAction.h" #include "Framework/Commands/UICommandList.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Framework/Notifications/NotificationManager.h" #include "Framework/SlateDelegates.h" #include "Framework/Views/ITypedTableView.h" #include "HAL/FileManager.h" #include "HAL/PlatformCrt.h" #include "HAL/PlatformMisc.h" #include "HAL/PlatformProcess.h" #include "IContentBrowserDataModule.h" #include "Input/Events.h" #include "InputCoreTypes.h" #include "Layout/Children.h" #include "Misc/AssertionMacros.h" #include "Misc/Attribute.h" #include "Misc/MessageDialog.h" #include "Misc/PackageName.h" #include "Misc/Paths.h" #include "Modules/ModuleManager.h" #include "SAssetPicker.h" #include "SAssetView.h" #include "SPathPicker.h" #include "SPathView.h" #include "SPrimaryButton.h" #include "SlateOptMacros.h" #include "SlotBase.h" #include "SNavigationBar.h" #include "SWarningOrErrorBox.h" #include "Experimental/ContentBrowserExtensionUtils.h" #include "Styling/AppStyle.h" #include "Templates/Tuple.h" #include "Templates/UnrealTemplate.h" #include "Textures/SlateIcon.h" #include "Types/SlateStructs.h" #include "Types/WidgetActiveTimerDelegate.h" #include "UObject/Class.h" #include "UObject/TopLevelAssetPath.h" #include "UObject/UObjectGlobals.h" #include "UObject/UnrealNames.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SEditableTextBox.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SSplitter.h" #include "Widgets/Notifications/SNotificationList.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SWindow.h" #include "Widgets/Text/STextBlock.h" class FExtender; class SWidget; struct FGeometry; struct FWorldContext; #define LOCTEXT_NAMESPACE "ContentBrowser" SAssetDialog::SAssetDialog() : DialogType(EAssetDialogType::Open) , ExistingAssetPolicy(ESaveAssetDialogExistingAssetPolicy::Disallow) , bLastInputValidityCheckSuccessful(false) , bPendingFocusNextFrame(true) , bValidAssetsChosen(false) , OpenedContextMenuWidget(EOpenedContextMenuWidget::None) { } SAssetDialog::~SAssetDialog() { if (!bValidAssetsChosen) { OnAssetDialogCancelled.ExecuteIfBound(); } } BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void SAssetDialog::Construct(const FArguments& InArgs, const FSharedAssetDialogConfig& InConfig) { DialogType = InConfig.GetDialogType(); AssetClassNames = InConfig.AssetClassNames; HistoryManager.SetOnApplyHistoryData(FOnApplyHistoryData::CreateSP(this, &SAssetDialog::OnApplyHistoryData)); HistoryManager.SetOnUpdateHistoryData(FOnUpdateHistoryData::CreateSP(this, &SAssetDialog::OnUpdateHistoryData)); RecentDirectories.MaxItems = 30; const FString DefaultPath = InConfig.DefaultPath; RegisterActiveTimer( 0.f, FWidgetActiveTimerDelegate::CreateSP( this, &SAssetDialog::SetFocusPostConstruct ) ); FPathPickerConfig PathPickerConfig; PathPickerConfig.DefaultPath = DefaultPath; PathPickerConfig.bFocusSearchBoxWhenOpened = false; PathPickerConfig.OnPathSelected = FOnPathSelected::CreateSP(this, &SAssetDialog::SetCurrentlySelectedPath, EContentBrowserPathType::Virtual); PathPickerConfig.SetPathsDelegates.Add(&SetPathsDelegate); PathPickerConfig.OnGetFolderContextMenu = FOnGetFolderContextMenu::CreateSP(this, &SAssetDialog::OnGetFolderContextMenu); PathPickerConfig.bOnPathSelectedPassesVirtualPaths = true; FAssetPickerConfig AssetPickerConfig; AssetPickerConfig.Filter.ClassPaths.Append(AssetClassNames); AssetPickerConfig.bAllowDragging = false; AssetPickerConfig.InitialAssetViewType = EAssetViewType::Tile; AssetPickerConfig.OnAssetSelected = FOnAssetSelected::CreateSP(this, &SAssetDialog::OnAssetSelected); AssetPickerConfig.OnAssetsActivated = FOnAssetsActivated::CreateSP(this, &SAssetDialog::OnAssetsActivated); AssetPickerConfig.SetFilterDelegates.Add(&SetFilterDelegate); AssetPickerConfig.GetCurrentSelectionDelegates.Add(&GetCurrentSelectionDelegate); AssetPickerConfig.SaveSettingsName = TEXT("AssetDialog"); AssetPickerConfig.bCanShowFolders = true; AssetPickerConfig.bCanShowDevelopersFolder = true; AssetPickerConfig.OnFolderEntered = FOnPathSelected::CreateSP(this, &SAssetDialog::SetCurrentlySelectedPath, EContentBrowserPathType::Virtual); AssetPickerConfig.OnGetAssetContextMenu = FOnGetAssetContextMenu::CreateSP(this, &SAssetDialog::OnGetAssetContextMenu); AssetPickerConfig.OnGetFolderContextMenu = FOnGetFolderContextMenu::CreateSP(this, &SAssetDialog::OnGetFolderContextMenu); OnPathSelected = InConfig.OnPathSelected; // Open and save specific configuration FText ConfirmButtonText; bool bIncludeNameBox = false; switch (DialogType) { case EAssetDialogType::Open: { const FOpenAssetDialogConfig& OpenAssetConfig = static_cast(InConfig); PathPickerConfig.bAllowContextMenu = true; ConfirmButtonText = LOCTEXT("AssetDialogOpenButton", "Open"); AssetPickerConfig.SelectionMode = OpenAssetConfig.bAllowMultipleSelection ? ESelectionMode::Multi : ESelectionMode::Single; AssetPickerConfig.bFocusSearchBoxWhenOpened = true; bIncludeNameBox = false; break; } case EAssetDialogType::Save: { const FSaveAssetDialogConfig& SaveAssetConfig = static_cast(InConfig); PathPickerConfig.bAllowContextMenu = true; PathPickerConfig.bAllowReadOnlyFolders = false; ConfirmButtonText = LOCTEXT("AssetDialogSaveButton", "Save"); AssetPickerConfig.SelectionMode = ESelectionMode::Single; AssetPickerConfig.bFocusSearchBoxWhenOpened = false; AssetPickerConfig.bCanShowReadOnlyFolders = false; bIncludeNameBox = true; ExistingAssetPolicy = SaveAssetConfig.ExistingAssetPolicy; SetCurrentlyEnteredAssetName(SaveAssetConfig.DefaultAssetName); break; } default: ensureMsgf(0, TEXT("AssetDialog type %d is not supported."), DialogType); break; } PathPicker = StaticCastSharedRef(FContentBrowserSingleton::Get().CreatePathPicker(PathPickerConfig)); TArray SelectedVirtualPaths = PathPicker->GetPaths(); if (SelectedVirtualPaths.Num() == 0) { // No paths selected, choose PathView's default selection const TSharedPtr& PathView = PathPicker->GetPathView(); const TArray DefaultPathsToSelect = PathView->GetDefaultPathsToSelect(); if (DefaultPathsToSelect.Num() > 0) { // Try select path PathPicker->SetPaths({ DefaultPathsToSelect[0].ToString() }); // Get paths that were successfully selected SelectedVirtualPaths = PathPicker->GetPaths(); } if (SelectedVirtualPaths.Num() == 0) { // No paths selected, choose selection based on first root folder displayed in PathView const TArray RootPathItemNames = PathView->GetRootPathItemNames(); if (RootPathItemNames.Num() > 0) { // Try select path PathPicker->SetPaths({ FString(TEXT("/")) + RootPathItemNames[0].ToString() }); // Get paths that were successfully selected SelectedVirtualPaths = PathPicker->GetPaths(); } } } // Update AssetPickerConfig's selection to match PathPicker if (SelectedVirtualPaths.Num() > 0) { AssetPickerConfig.Filter.PackagePaths = { FName(*SelectedVirtualPaths[0]) }; } AssetPicker = StaticCastSharedRef(FContentBrowserSingleton::Get().CreateAssetPicker(AssetPickerConfig)); FContentBrowserCommands::Register(); BindCommands(); // The root widget in this dialog. TSharedRef MainVerticalBox = SNew(SVerticalBox); // Navigation & History MainVerticalBox->AddSlot() .AutoHeight() [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ SNew(SHorizontalBox) // History Back Button +SHorizontalBox::Slot() .Padding(10, 0, 0, 0) .AutoWidth() [ SNew(SButton) .VAlign(VAlign_Center) .ButtonStyle(FAppStyle::Get(), "SimpleButton") .ToolTipText(this, &SAssetDialog::GetBackTooltip ) .ContentPadding( FMargin(1, 0) ) .OnClicked(this, &SAssetDialog::OnBackClicked) .IsEnabled(this, &SAssetDialog::IsBackEnabled) [ SNew(SImage) .Image(FAppStyle::Get().GetBrush("Icons.CircleArrowLeft")) .ColorAndOpacity(FSlateColor::UseForeground()) ] ] // History Forward Button + SHorizontalBox::Slot() .Padding(2, 0, 0, 0) .AutoWidth() [ SNew(SButton) .VAlign(VAlign_Center) .ButtonStyle(FAppStyle::Get(), "SimpleButton") .ToolTipText( this, &SAssetDialog::GetForwardTooltip ) .ContentPadding( FMargin(1, 0) ) .OnClicked(this, &SAssetDialog::OnForwardClicked) .IsEnabled(this, &SAssetDialog::IsForwardEnabled) [ SNew(SImage) .Image(FAppStyle::Get().GetBrush("Icons.CircleArrowRight")) .ColorAndOpacity(FSlateColor::UseForeground()) ] ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .HAlign(HAlign_Fill) .FillWidth(1.0f) .Padding(2, 0, 0, 0) [ SAssignNew(NavigationBar, SNavigationBar) .OnPathClicked(this, &SAssetDialog::SetCurrentlySelectedPath, EContentBrowserPathType::Virtual) .OnNavigateToPath(this, &SAssetDialog::OnPathTextEdited) .OnCanEditPathAsText(this, &SAssetDialog::OnCanEditPathAsText) .OnCompletePrefix(this, &SAssetDialog::OnCompletePathPrefix) .GetPathMenuContent(this, &SAssetDialog::OnGetCrumbDelimiterContent) .GetComboOptions(this, &SAssetDialog::GetRecentPaths) ] ] ]; // Path/Asset view MainVerticalBox->AddSlot() .FillHeight(1) .Padding(0, 0, 0, 4) [ SNew(SSplitter) +SSplitter::Slot() .Value(0.25f) [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ PathPicker.ToSharedRef() ] ] +SSplitter::Slot() .Value(0.75f) [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ AssetPicker.ToSharedRef() ] ] ]; // Input error strip, if we are using a name box if (bIncludeNameBox) { // Name Error label MainVerticalBox->AddSlot() .AutoHeight() [ SNew(SWarningOrErrorBox) .Padding(FMargin(8.0f, 4.0f, 4.0f, 4.0f)) .IconSize(FVector2D(16,16)) .MessageStyle(EMessageStyle::Error) .Message(this, &SAssetDialog::GetNameErrorLabelText) .Visibility(this, &SAssetDialog::GetNameErrorLabelVisibility) ]; } TSharedRef LabelsBox = SNew(SVerticalBox) + SVerticalBox::Slot() .FillHeight(1) .VAlign(VAlign_Center) .Padding(0, 4, 0, 4) [ SNew(STextBlock).Text(LOCTEXT("PathBoxLabel", "Path:")) ]; TSharedRef ContentBox = SNew(SVerticalBox) + SVerticalBox::Slot() .FillHeight(1) .VAlign(VAlign_Center) .Padding(0, 4, 0, 4) [ SAssignNew(PathText, STextBlock) .Text(this, &SAssetDialog::GetPathNameText) ]; if (bIncludeNameBox) { LabelsBox->AddSlot() .FillHeight(1) .VAlign(VAlign_Center) .Padding(0, 4, 0, 4) [ SNew(STextBlock).Text(LOCTEXT("NameBoxLabel", "Name:")) ]; ContentBox->AddSlot() .AutoHeight() .VAlign(VAlign_Center) .Padding(0, 0, 0, 0) [ SAssignNew(NameEditableText, SEditableTextBox) .Text(this, &SAssetDialog::GetAssetNameText) .OnTextCommitted(this, &SAssetDialog::OnAssetNameTextCommited) .OnTextChanged(this, &SAssetDialog::OnAssetNameTextCommited, ETextCommit::Default) .SelectAllTextWhenFocused(true) ]; } // Buttons and asset name TSharedRef ButtonsAndNameBox = SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .HAlign(HAlign_Right) .VAlign(VAlign_Bottom) .Padding(0.f, 0.f, 4.f, 0.f) [ LabelsBox ] + SHorizontalBox::Slot() .FillWidth(1) .VAlign(VAlign_Bottom) .Padding(4.f, 0.f) [ ContentBox ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Bottom) .Padding(4.f, 0.f) [ SNew(SPrimaryButton) .Text(ConfirmButtonText) .IsEnabled(this, &SAssetDialog::IsConfirmButtonEnabled) .OnClicked(this, &SAssetDialog::OnConfirmClicked) ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Bottom) .Padding(4.f, 0.f, 0.0f, 0.0f) [ SNew(SButton) .TextStyle(FAppStyle::Get(), "DialogButtonText") .Text(LOCTEXT("AssetDialogCancelButton", "Cancel")) .OnClicked(this, &SAssetDialog::OnCancelClicked) ]; MainVerticalBox->AddSlot() .AutoHeight() .HAlign(HAlign_Fill) .Padding(16.f, 4.f, 16.f, 16.f) [ ButtonsAndNameBox ]; ChildSlot [ MainVerticalBox ]; // Sync path selection with the path picker if (SelectedVirtualPaths.Num() > 0) { SetCurrentlySelectedPath(SelectedVirtualPaths[0], EContentBrowserPathType::Virtual); } else { SetCurrentlySelectedPath(DefaultPath, EContentBrowserPathType::Internal); } } END_SLATE_FUNCTION_BUILD_OPTIMIZATION FReply SAssetDialog::OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent ) { if ( InKeyEvent.GetKey() == EKeys::Escape ) { CloseDialog(); return FReply::Handled(); } else if (Commands->ProcessCommandBindings(InKeyEvent)) { return FReply::Handled(); } return SCompoundWidget::OnKeyDown(MyGeometry, InKeyEvent); } FReply SAssetDialog::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { // Mouse back and forward buttons traverse history if (MouseEvent.GetEffectingButton() == EKeys::ThumbMouseButton) { HistoryManager.GoBack(); return FReply::Handled(); } if (MouseEvent.GetEffectingButton() == EKeys::ThumbMouseButton2) { HistoryManager.GoForward(); return FReply::Handled(); } return FReply::Unhandled(); } FReply SAssetDialog::OnMouseButtonDoubleClick(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent) { // Mouse back and forward buttons traverse history if (InMouseEvent.GetEffectingButton() == EKeys::ThumbMouseButton) { HistoryManager.GoBack(); return FReply::Handled(); } if (InMouseEvent.GetEffectingButton() == EKeys::ThumbMouseButton2) { HistoryManager.GoForward(); return FReply::Handled(); } return FReply::Unhandled(); } void SAssetDialog::BindCommands() { Commands = TSharedPtr< FUICommandList >(new FUICommandList); Commands->MapAction(FGenericCommands::Get().Rename, FUIAction( FExecuteAction::CreateSP(this, &SAssetDialog::ExecuteRename), FCanExecuteAction::CreateSP(this, &SAssetDialog::CanExecuteRename) )); Commands->MapAction(FGenericCommands::Get().Delete, FUIAction( FExecuteAction::CreateSP(this, &SAssetDialog::ExecuteDelete), FCanExecuteAction::CreateSP(this, &SAssetDialog::CanExecuteDelete) )); Commands->MapAction(FContentBrowserCommands::Get().CreateNewFolder, FUIAction( FExecuteAction::CreateSP(this, &SAssetDialog::ExecuteCreateNewFolder), FCanExecuteAction::CreateSP(this, &SAssetDialog::CanExecuteCreateNewFolder) )); } bool SAssetDialog::CanExecuteRename() const { switch (OpenedContextMenuWidget) { case EOpenedContextMenuWidget::AssetView: return ContentBrowserUtils::CanRenameFromAssetView(AssetPicker->GetAssetView()); case EOpenedContextMenuWidget::PathView: return ContentBrowserUtils::CanRenameFromPathView(PathPicker->GetPathView()); } return false; } void SAssetDialog::ExecuteRename() { const TArray SelectedItems = AssetPicker->GetAssetView()->GetSelectedItems(); if (SelectedItems.Num() > 0) { if (SelectedItems.Num() == 1) { AssetPicker->GetAssetView()->RenameItem(SelectedItems[0]); } } else { const TArray SelectedFolders = PathPicker->GetPathView()->GetSelectedFolderItems(); if (SelectedFolders.Num() == 1) { PathPicker->GetPathView()->RenameFolderItem(SelectedFolders[0]); } } } bool SAssetDialog::CanExecuteDelete() const { switch (OpenedContextMenuWidget) { case EOpenedContextMenuWidget::AssetView: return ContentBrowserUtils::CanDeleteFromAssetView(AssetPicker->GetAssetView()); case EOpenedContextMenuWidget::PathView: return ContentBrowserUtils::CanDeleteFromPathView(PathPicker->GetPathView()); } return false; } void SAssetDialog::ExecuteDelete() { // Don't allow asset deletion during PIE if (GIsEditor) { UEditorEngine* Editor = GEditor; FWorldContext* PIEWorldContext = GEditor->GetPIEWorldContext(); if (PIEWorldContext) { FNotificationInfo Notification(LOCTEXT("CannotDeleteAssetInPIE", "Assets cannot be deleted while in PIE.")); Notification.ExpireDuration = 3.0f; FSlateNotificationManager::Get().AddNotification(Notification); return; } } if (OpenedContextMenuWidget != EOpenedContextMenuWidget::PathView) { const TArray SelectedFiles = AssetPicker->GetAssetView()->GetSelectedFileItems(); // 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); } } // List selected folders that can be deleted FText FirstFolderDisplayName; TArray SelectedFolderInternalPaths; { const TArray SelectedFolderItems = (OpenedContextMenuWidget == EOpenedContextMenuWidget::PathView) ? PathPicker->GetPathView()->GetSelectedFolderItems() : AssetPicker->GetAssetView()->GetSelectedFolderItems(); for (const FContentBrowserItem& SelectedItem : SelectedFolderItems) { if (SelectedItem.CanDelete()) { // Only internal folders supported currently const FName ConvertedPath = SelectedItem.GetInternalPath(); if (!ConvertedPath.IsNone()) { if (SelectedFolderInternalPaths.Num() == 0) { FirstFolderDisplayName = SelectedItem.GetDisplayName(); } SelectedFolderInternalPaths.Add(ConvertedPath.ToString()); } } } } // 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 (SelectedFolderInternalPaths.Num() > 0) { FText Prompt; if (SelectedFolderInternalPaths.Num() == 1) { Prompt = FText::Format(LOCTEXT("FolderDeleteConfirm_Single", "Delete folder '{0}'?"), FirstFolderDisplayName); } else { Prompt = FText::Format(LOCTEXT("FolderDeleteConfirm_Multiple", "Delete {0} folders?"), SelectedFolderInternalPaths.Num()); } const bool bResetSelection = OpenedContextMenuWidget == EOpenedContextMenuWidget::PathView; // Spawn a confirmation dialog since this is potentially a highly destructive operation ContentBrowserUtils::DisplayConfirmationPopup( Prompt, LOCTEXT("FolderDeleteConfirm_Yes", "Delete"), LOCTEXT("FolderDeleteConfirm_No", "Cancel"), AssetPicker->GetAssetView().ToSharedRef(), FOnClicked::CreateSP(this, &SAssetDialog::ExecuteDeleteFolderConfirmed, SelectedFolderInternalPaths, bResetSelection) ); } } FReply SAssetDialog::ExecuteDeleteFolderConfirmed(const TArray SelectedFolderInternalPaths, const bool bResetSelection) { if (SelectedFolderInternalPaths.Num() > 0) { if (ContentBrowserUtils::DeleteFolders(SelectedFolderInternalPaths)) { if (bResetSelection) { // Since the contents of the asset view have just been deleted, set the default selected paths SelectDefaultPaths(); } } } return FReply::Handled(); } void SAssetDialog::SelectDefaultPaths() { const TArray DefaultVirtualPathsToSelect = PathPicker->GetPathView()->GetDefaultPathsToSelect(); TArray DefaultSelectedPaths; DefaultSelectedPaths.Reserve(DefaultVirtualPathsToSelect.Num()); for (const FName DefaultVirtualPathToSelect : DefaultVirtualPathsToSelect) { DefaultSelectedPaths.Add(DefaultVirtualPathToSelect.ToString()); } PathPicker->GetPathView()->SetSelectedPaths(DefaultSelectedPaths); FAssetViewContentSources DefaultContentSources(DefaultVirtualPathsToSelect); AssetPicker->GetAssetView()->SetContentSources(DefaultContentSources); } bool SAssetDialog::ExecuteExploreInternal(bool bTest) { bool bCanExplore = false; TArray SelectedItems = AssetPicker->GetAssetView()->GetSelectedItems(); if (SelectedItems.Num() == 0) { SelectedItems = PathPicker->GetPathView()->GetSelectedFolderItems(); } for (const FContentBrowserItem& SelectedItem : SelectedItems) { FString ItemFilename; if (SelectedItem.GetItemPhysicalPath(ItemFilename)) { const bool bExists = SelectedItem.IsFile() ? FPaths::FileExists(ItemFilename) : FPaths::DirectoryExists(ItemFilename); if (bExists) { bCanExplore = true; if (!bTest) { FPlatformProcess::ExploreFolder(*IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*ItemFilename)); } else { break; } } } } return bCanExplore; } void SAssetDialog::ExecuteExplore() { //ExecuteExploreInternal(/*bTest*/ false); ContentBrowserUtils::ExploreFolders(AssetPicker->GetAssetView()->GetSelectedItems(), AssetPicker->GetAssetView().ToSharedRef()); } bool SAssetDialog::CanExecuteExplore() { return ExecuteExploreInternal(/*bTest*/ true); } bool SAssetDialog::CanExecuteCreateNewFolder() const { // We can only create folders when we have a single path selected UContentBrowserDataSubsystem* ContentBrowserData = IContentBrowserDataModule::Get().GetSubsystem(); return ContentBrowserData->CanCreateFolder(GetCurrentSelectedVirtualPath(), nullptr); } void SAssetDialog::ExecuteCreateNewFolder() { PathPicker->CreateNewFolder(GetCurrentSelectedVirtualPath().ToString(), CurrentContextMenuCreateNewFolderDelegate); } TSharedPtr SAssetDialog::OnGetFolderContextMenu(const TArray& SelectedPaths, FContentBrowserMenuExtender_SelectedPaths InMenuExtender, FOnCreateNewFolder InOnCreateNewFolder) { if (FSlateApplication::Get().HasFocusedDescendants(PathPicker.ToSharedRef())) { OpenedContextMenuWidget = EOpenedContextMenuWidget::PathView; } else if (FSlateApplication::Get().HasFocusedDescendants(AssetPicker.ToSharedRef())) { OpenedContextMenuWidget = EOpenedContextMenuWidget::AssetView; } TSharedPtr Extender; if (InMenuExtender.IsBound()) { Extender = InMenuExtender.Execute(SelectedPaths); } if (FSlateApplication::Get().HasFocusedDescendants(PathPicker.ToSharedRef())) { PathPicker->SetPaths(SelectedPaths); } CurrentContextMenuCreateNewFolderDelegate = InOnCreateNewFolder; FMenuBuilder MenuBuilder(true /*bInShouldCloseWindowAfterMenuSelection*/, Commands, Extender); SetupContextMenuContent(MenuBuilder, SelectedPaths); return MenuBuilder.MakeWidget(); } TSharedPtr SAssetDialog::OnGetAssetContextMenu(const TArray& SelectedAssets) { OpenedContextMenuWidget = EOpenedContextMenuWidget::AssetView; FMenuBuilder MenuBuilder(true /*bInShouldCloseWindowAfterMenuSelection*/, Commands); CurrentContextMenuCreateNewFolderDelegate = FOnCreateNewFolder::CreateSP(AssetPicker->GetAssetView().Get(), &SAssetView::NewFolderItemRequested); TArray Paths; SetupContextMenuContent(MenuBuilder, Paths); return MenuBuilder.MakeWidget(); } void SAssetDialog::SetupContextMenuContent(FMenuBuilder& MenuBuilder, const TArray& SelectedPaths) { const FName ContentBrowserStyleSetName = UE::ContentBrowser::Private::FContentBrowserStyle::Get().GetStyleSetName(); FText NewFolderToolTip; if (SelectedPaths.Num() > 0) { if (CanExecuteCreateNewFolder()) { NewFolderToolTip = FText::Format(LOCTEXT("NewFolderTooltip_CreateIn", "Create a new folder in {0}."), FText::FromString(SelectedPaths[0])); } else { NewFolderToolTip = FText::Format(LOCTEXT("NewFolderTooltip_InvalidPath", "Cannot create new folders in {0}."), FText::FromString(SelectedPaths[0])); } } else { NewFolderToolTip = FText(LOCTEXT("NewFolderTooltip_InvalidAction", "Cannot create new folders when an asset is selected.")); } MenuBuilder.BeginSection("AssetDialogOptions", LOCTEXT("AssetDialogMenuHeading", "Options")); MenuBuilder.AddMenuEntry(FContentBrowserCommands::Get().CreateNewFolder, NAME_None, LOCTEXT("NewFolder", "New Folder"), NewFolderToolTip, FSlateIcon(ContentBrowserStyleSetName, "ContentBrowser.NewFolderIcon")); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Rename, NAME_None, LOCTEXT("RenameFolder", "Rename"), LOCTEXT("RenameFolderTooltip", "Rename the selected folder."), FSlateIcon(ContentBrowserStyleSetName, "ContentBrowser.AssetActions.Rename")); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Delete, NAME_None, LOCTEXT("DeleteFolder", "Delete"), LOCTEXT("DeleteFolderTooltip", "Removes this folder and all assets it contains.")); MenuBuilder.EndSection(); if (CanExecuteExplore()) { MenuBuilder.BeginSection("AssetDialogExplore", LOCTEXT("AssetDialogExploreHeading", "Explore")); MenuBuilder.AddMenuEntry(ContentBrowserUtils::GetExploreFolderText(), LOCTEXT("ExploreTooltip", "Finds this folder on disk."), FSlateIcon(ContentBrowserStyleSetName, "SystemWideCommands.FindInContentBrowser"), FUIAction(FExecuteAction::CreateSP(this, &SAssetDialog::ExecuteExplore))); MenuBuilder.EndSection(); } } EActiveTimerReturnType SAssetDialog::SetFocusPostConstruct( double InCurrentTime, float InDeltaTime ) { FocusNameBox(); return EActiveTimerReturnType::Stop; } void SAssetDialog::SetOnAssetsChosenForOpen(const FOnAssetsChosenForOpen& InOnAssetsChosenForOpen) { OnAssetsChosenForOpen = InOnAssetsChosenForOpen; } void SAssetDialog::SetOnObjectPathChosenForSave(const FOnObjectPathChosenForSave& InOnObjectPathChosenForSave) { OnObjectPathChosenForSave = InOnObjectPathChosenForSave; } void SAssetDialog::SetOnAssetDialogCancelled(const FOnAssetDialogCancelled& InOnAssetDialogCancelled) { OnAssetDialogCancelled = InOnAssetDialogCancelled; } void SAssetDialog::FocusNameBox() { if ( NameEditableText.IsValid() ) { FSlateApplication::Get().SetKeyboardFocus(NameEditableText.ToSharedRef(), EFocusCause::SetDirectly); } } bool SAssetDialog::IsBackEnabled() const { return HistoryManager.CanGoBack(); } bool SAssetDialog::IsForwardEnabled() const { return HistoryManager.CanGoForward(); } FText SAssetDialog::GetBackTooltip() const { if (HistoryManager.CanGoBack()) { return FText::Format(LOCTEXT("HistoryBackTooltipFmt", "Back to {0}"), HistoryManager.GetBackDesc()); } return FText::GetEmpty(); } FText SAssetDialog::GetForwardTooltip() const { if (HistoryManager.CanGoForward()) { return FText::Format(LOCTEXT("HistoryForwardTooltipFmt", "Forward to {0}"), HistoryManager.GetForwardDesc()); } return FText::GetEmpty(); } FReply SAssetDialog::OnBackClicked() { HistoryManager.GoBack(); return FReply::Handled(); } FReply SAssetDialog::OnForwardClicked() { HistoryManager.GoForward(); return FReply::Handled(); } void SAssetDialog::OnApplyHistoryData(const FHistoryData& History) { PathPicker->GetPathView()->ApplyHistoryData(History); AssetPicker->GetAssetView()->ApplyHistoryData(History); ContentBrowserUtils::UpdateNavigationBar(NavigationBar.ToSharedRef(), AssetPicker->GetAssetView().ToSharedRef(), PathPicker->GetPathView().ToSharedRef()); } void SAssetDialog::OnUpdateHistoryData(FHistoryData& HistoryData) const { const FAssetViewContentSources& ContentSources = AssetPicker->GetAssetView()->GetContentSources(); const TArray SelectedItems = AssetPicker->GetAssetView()->GetSelectedItems(); const FText NewSource = ContentSources.HasVirtualPaths() ? FText::FromName(ContentSources.GetVirtualPaths()[0]) : (ContentSources.HasCollections() ? FText::FromName(ContentSources.GetCollections()[0].Name) : LOCTEXT("AllAssets", "All Assets")); HistoryData.HistoryDesc = NewSource; HistoryData.ContentSources = ContentSources; HistoryData.SelectionData.Reset(); for (const FContentBrowserItem& SelectedItem : SelectedItems) { HistoryData.SelectionData.SelectedVirtualPaths.Add(SelectedItem.GetVirtualPath()); } } FText SAssetDialog::GetAssetNameText() const { return FText::FromString(CurrentlyEnteredAssetName); } FText SAssetDialog::GetPathNameText() const { return FText::FromName(GetCurrentSelectedVirtualPath()); } void SAssetDialog::OnAssetNameTextCommited(const FText& InText, ETextCommit::Type InCommitType) { SetCurrentlyEnteredAssetName(InText.ToString()); if ( InCommitType == ETextCommit::OnEnter ) { CommitObjectPathForSave(); } } EVisibility SAssetDialog::GetNameErrorLabelVisibility() const { return GetNameErrorLabelText().IsEmpty() ? EVisibility::Hidden : EVisibility::Visible; } FText SAssetDialog::GetNameErrorLabelText() const { if (!bLastInputValidityCheckSuccessful) { return LastInputValidityErrorText; } return FText::GetEmpty(); } bool SAssetDialog::OnCanEditPathAsText(const FString& Text) const { const FAssetViewContentSources& ContentSources = AssetPicker->GetAssetView()->GetContentSources(); if (ContentSources.HasCollections()) { return false; } return true; } void SAssetDialog::OnPathTextEdited(const FString& NewPath) { FContentBrowserItem Item = ContentBrowserUtils::TryGetItemFromUserProvidedPath(NewPath); if (Item.IsValid()) { SyncToItems({ Item }); } } TArray SAssetDialog::OnCompletePathPrefix(const FString& Prefix) const { FStringView PrefixView = Prefix; // Strip to last path separator FName Parent; if (int32 Index = UE::String::FindLastChar(PrefixView, '/'); Index != INDEX_NONE) { PrefixView.LeftInline(Index); Parent = FName(PrefixView); } // Find PrefixView in the available tree of data sources, get its direct children, and filter them by SuffixView TArray SubItems = ContentBrowserUtils::GetChildItemsFromVirtualPath( Parent, PathPicker->GetPathView()->GetContentBrowserItemCategoryFilter(), PathPicker->GetPathView()->GetContentBrowserItemAttributeFilter(), NAME_None, *PathPicker->GetPathView() ); TArray Results; for (const FContentBrowserItem& Item : SubItems) { FName Path = Item.GetVirtualPath(); FNameBuilder PathBuilder(Path); if (PathBuilder.ToView().StartsWith(Prefix)) { Results.Add(Item.GetVirtualPath().ToString()); } } return Results; } TSharedRef SAssetDialog::OnGetCrumbDelimiterContent(const FString& CrumbData) const { TArray SubItems = ContentBrowserUtils::GetChildItemsFromVirtualPath( *CrumbData, PathPicker->GetPathView()->GetContentBrowserItemCategoryFilter(), PathPicker->GetPathView()->GetContentBrowserItemAttributeFilter(), NAME_None, *PathPicker->GetPathView() ); SubItems.Sort([](const FContentBrowserItem& ItemOne, const FContentBrowserItem& ItemTwo) { return ItemOne.GetDisplayName().CompareTo(ItemTwo.GetDisplayName()) < 0; }); if (SubItems.Num() > 0) { FMenuBuilder MenuBuilder( true, nullptr ); for (const FContentBrowserItem& SubItem : SubItems) { FName FolderBrushName = NAME_None; FName FolderShadowBrushName = NAME_None; ContentBrowserUtils::TryGetFolderBrushAndShadowNameSmall(SubItem, FolderBrushName, FolderShadowBrushName); FText EntryName = SubItem.GetDisplayName(); FUIAction EntryAction = FUIAction(FExecuteAction::CreateSP(const_cast(this), &SAssetDialog::OnCrumbDelimiterItemClicked, SubItem.GetVirtualPath().ToString())); if (FolderBrushName != NAME_None) { FLinearColor FolderColor = UE::Editor::ContentBrowser::ExtensionUtils::GetFolderColor(SubItem).Get(ContentBrowserUtils::GetDefaultColor()); FMenuEntryParams Params; Params.EntryWidget = ContentBrowserUtils::GetFolderWidgetForNavigationBar(EntryName, FolderBrushName, FolderColor); Params.DirectActions = EntryAction; MenuBuilder.AddMenuEntry(Params); } else { MenuBuilder.AddMenuEntry( EntryName, FText::GetEmpty(), FSlateIcon(FAppStyle::GetAppStyleSetName(), FolderBrushName), EntryAction ); } } return SNew(SVerticalBox) +SVerticalBox::Slot() .MaxHeight(400.0f) [ MenuBuilder.MakeWidget() ]; } return SNullWidget::NullWidget; } void SAssetDialog::OnCrumbDelimiterItemClicked(FString ClickedPath) { SetCurrentlySelectedPath(ClickedPath, EContentBrowserPathType::Virtual); } TArray SAssetDialog::GetRecentPaths() const { return RecentDirectories; } void SAssetDialog::SetCurrentlySelectedPath(const FString& NewPath, const EContentBrowserPathType InPathType) { CurrentlySelectedPath = NewPath; CurrentlySelectedPathType = InPathType; const FName NewVirtualPath = InPathType == EContentBrowserPathType::Virtual ? FName(NewPath) : IContentBrowserDataModule::Get().GetSubsystem()->ConvertInternalPathToVirtual(*NewPath); // Update Path View if (PathPicker) { TArray SelectedVirtualPaths = PathPicker->GetPaths(); if (SelectedVirtualPaths.Num() == 0 || NewVirtualPath != *SelectedVirtualPaths[0]) { SetPathsDelegate.Execute({ NewVirtualPath.ToString() }); } } if (AssetPicker) { // The asset picker uses the FARFilter::PackagePaths field to fill content sources. // Thus, content sources must be checked rather than the filter itself. const FAssetViewContentSources& Sources = AssetPicker->GetAssetView()->GetContentSources(); if (!Sources.HasVirtualPaths() || Sources.GetVirtualPaths()[0] != NewVirtualPath) { FARFilter NewFilter; NewFilter.ClassPaths.Append(AssetClassNames); NewFilter.PackagePaths.Add(NewVirtualPath); SetFilterDelegate.Execute(NewFilter); } } // Update Navigation Bar if (NavigationBar && AssetPicker && PathPicker) { ContentBrowserUtils::UpdateNavigationBar(NavigationBar, AssetPicker->GetAssetView(), PathPicker->GetPathView()); } UpdateInputValidity(); HistoryManager.AddHistoryData(); RecentDirectories.AddUnique(InPathType == EContentBrowserPathType::Virtual ? NewPath : NewVirtualPath.ToString()); OnPathSelected.ExecuteIfBound(NewPath); } void SAssetDialog::SyncToItems(TArrayView ItemsToSync, const bool bAllowImplicitSync) { TArray NewItemsToSync = ContentBrowserUtils::FilterOrAliasItems(ItemsToSync); ItemsToSync = NewItemsToSync; // Tell the sources view first so the asset view will be up to date by the time we request the sync PathPicker->SyncToItems(ItemsToSync, bAllowImplicitSync); AssetPicker->SyncToItems(ItemsToSync, bAllowImplicitSync, true); } bool SAssetDialog::IsConfirmButtonEnabled() const { switch (DialogType) { case EAssetDialogType::Open: return CurrentlySelectedAssets.Num() > 0; case EAssetDialogType::Save: return bLastInputValidityCheckSuccessful; default: ensureMsgf(0, TEXT("AssetDialog type %d is not supported."), DialogType); return false; } } FReply SAssetDialog::OnConfirmClicked() { switch (DialogType) { case EAssetDialogType::Open: { TArray SelectedAssets = GetCurrentSelectionDelegate.Execute(); if (SelectedAssets.Num() > 0) { ChooseAssetsForOpen(SelectedAssets); } break; } case EAssetDialogType::Save: // @todo save asset validation (e.g. "asset already exists" check) CommitObjectPathForSave(); break; default: ensureMsgf(0, TEXT("AssetDialog type %d is not supported."), DialogType); break; } return FReply::Handled(); } FReply SAssetDialog::OnCancelClicked() { CloseDialog(); return FReply::Handled(); } void SAssetDialog::OnAssetSelected(const FAssetData& AssetData) { CurrentlySelectedAssets = GetCurrentSelectionDelegate.Execute(); if ( AssetData.IsValid() ) { SetCurrentlySelectedPath(AssetData.PackagePath.ToString(), EContentBrowserPathType::Internal); SetCurrentlyEnteredAssetName(AssetData.AssetName.ToString()); } } void SAssetDialog::OnAssetsActivated(const TArray& SelectedAssets, EAssetTypeActivationMethod::Type ActivationType) { const bool bCorrectActivationMethod = (ActivationType == EAssetTypeActivationMethod::DoubleClicked || ActivationType == EAssetTypeActivationMethod::Opened); if (SelectedAssets.Num() > 0 && bCorrectActivationMethod) { switch (DialogType) { case EAssetDialogType::Open: ChooseAssetsForOpen(SelectedAssets); break; case EAssetDialogType::Save: { const FAssetData& AssetData = SelectedAssets[0]; SetCurrentlySelectedPath(AssetData.PackagePath.ToString(), EContentBrowserPathType::Internal); SetCurrentlyEnteredAssetName(AssetData.AssetName.ToString()); CommitObjectPathForSave(); break; } default: ensureMsgf(0, TEXT("AssetDialog type %d is not supported."), DialogType); break; } } } void SAssetDialog::CloseDialog() { TSharedPtr ContainingWindow = FSlateApplication::Get().FindWidgetWindow(AsShared()); if (ContainingWindow.IsValid()) { ContainingWindow->RequestDestroyWindow(); } } void SAssetDialog::SetCurrentlyEnteredAssetName(const FString& NewName) { CurrentlyEnteredAssetName = NewName; UpdateInputValidity(); } void SAssetDialog::UpdateInputValidity() { bLastInputValidityCheckSuccessful = true; if ( CurrentlyEnteredAssetName.IsEmpty() ) { // No error text for an empty name. Just fail validity. LastInputValidityErrorText = FText::GetEmpty(); bLastInputValidityCheckSuccessful = false; } if (bLastInputValidityCheckSuccessful) { if ( CurrentlySelectedPath.IsEmpty() ) { LastInputValidityErrorText = LOCTEXT("AssetDialog_NoPathSelected", "You must select a path."); bLastInputValidityCheckSuccessful = false; } else if (CurrentlySelectedPathType == EContentBrowserPathType::Virtual) { FName ConvertedPath; const EContentBrowserPathType ConvertedType = IContentBrowserDataModule::Get().GetSubsystem()->TryConvertVirtualPath(CurrentlySelectedPath, ConvertedPath); bool bIsMountedInternalPath = false; if (ConvertedType == EContentBrowserPathType::Internal) { FString CheckPath = ConvertedPath.ToString(); if (!CheckPath.EndsWith(TEXT("/"))) { CheckPath += TEXT("/"); } if (FPackageName::IsValidPath(CheckPath)) { bIsMountedInternalPath = true; } } if (!bIsMountedInternalPath) { LastInputValidityErrorText = LOCTEXT("AssetDialog_VirtualPathSelected", "The selected folder cannot be modified."); bLastInputValidityCheckSuccessful = false; } } } if ( DialogType == EAssetDialogType::Save ) { if ( bLastInputValidityCheckSuccessful ) { const FString ObjectPath = GetObjectPathForSave(); FText ErrorMessage; const bool bAllowExistingAsset = (ExistingAssetPolicy == ESaveAssetDialogExistingAssetPolicy::AllowButWarn); FTopLevelAssetPath AssetClassName = AssetClassNames.Num() == 1 ? AssetClassNames[0] : FTopLevelAssetPath(); UClass* AssetClass = !AssetClassName.IsNull() ? FindObject(AssetClassName, true) : nullptr; if ( !ContentBrowserUtils::IsValidObjectPathForCreate(ObjectPath, AssetClass, ErrorMessage, bAllowExistingAsset) ) { LastInputValidityErrorText = ErrorMessage; bLastInputValidityCheckSuccessful = false; } else if(bAllowExistingAsset && AssetClassNames.Num() > 1) // If for some reason we have multiple names, perform additional logic here... { FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); FAssetData ExistingAsset = AssetRegistryModule.Get().GetAssetByObjectPath(FSoftObjectPath(ObjectPath)); if (ExistingAsset.IsValid() && !AssetClassNames.Contains(ExistingAsset.AssetClassPath)) { const FString ObjectName = FPackageName::ObjectPathToObjectName(ObjectPath); LastInputValidityErrorText = FText::Format(LOCTEXT("AssetDialog_AssetAlreadyExists", "An asset of type '{0}' already exists at this location with the name '{1}'."), FText::FromString(ExistingAsset.AssetClassPath.ToString()), FText::FromString(ObjectName)); bLastInputValidityCheckSuccessful = false; } } } } } FName SAssetDialog::GetCurrentSelectedVirtualPath() const { if (CurrentlySelectedPathType == EContentBrowserPathType::Virtual) { return *CurrentlySelectedPath; } else { return IContentBrowserDataModule::Get().GetSubsystem()->ConvertInternalPathToVirtual(*CurrentlySelectedPath); } } void SAssetDialog::ChooseAssetsForOpen(const TArray& SelectedAssets) { if ( ensure(DialogType == EAssetDialogType::Open) ) { if (SelectedAssets.Num() > 0) { bValidAssetsChosen = true; OnAssetsChosenForOpen.ExecuteIfBound(SelectedAssets); CloseDialog(); } } } FString SAssetDialog::GetObjectPathForSave() const { FString Base = CurrentlySelectedPath; if (CurrentlySelectedPathType == EContentBrowserPathType::Virtual) { FName ConvertedPath; const EContentBrowserPathType ConvertedType = IContentBrowserDataModule::Get().GetSubsystem()->TryConvertVirtualPath(CurrentlySelectedPath, ConvertedPath); if (ConvertedType == EContentBrowserPathType::Internal) { Base = ConvertedPath.ToString(); } else { return FString(); } } return Base / CurrentlyEnteredAssetName + TEXT(".") + CurrentlyEnteredAssetName; } void SAssetDialog::CommitObjectPathForSave() { if ( ensure(DialogType == EAssetDialogType::Save) ) { if ( bLastInputValidityCheckSuccessful ) { const FString ObjectPath = GetObjectPathForSave(); bool bProceedWithSave = true; // If we were asked to warn on existing assets, do it now if ( ExistingAssetPolicy == ESaveAssetDialogExistingAssetPolicy::AllowButWarn ) { FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); FAssetData ExistingAsset = AssetRegistryModule.Get().GetAssetByObjectPath(FSoftObjectPath(ObjectPath)); if ( ExistingAsset.IsValid() && AssetClassNames.Contains(ExistingAsset.AssetClassPath) ) { EAppReturnType::Type ShouldReplace = FMessageDialog::Open( EAppMsgType::YesNo, FText::Format(LOCTEXT("ReplaceAssetMessage", "{0} already exists. Do you want to replace it?"), FText::FromString(CurrentlyEnteredAssetName)) ); bProceedWithSave = (ShouldReplace == EAppReturnType::Yes); } } if ( bProceedWithSave ) { bValidAssetsChosen = true; OnObjectPathChosenForSave.ExecuteIfBound(ObjectPath); CloseDialog(); } } } } #undef LOCTEXT_NAMESPACE