// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. #include "ContentBrowserPCH.h" #include "CollectionAssetManagement.h" #include "CollectionViewTypes.h" #include "CollectionContextMenu.h" #include "DragAndDrop/AssetDragDropOp.h" #include "DragAndDrop/CollectionDragDropOp.h" #include "ObjectTools.h" #include "SourcesViewWidgets.h" #include "ContentBrowserModule.h" #include "SExpandableArea.h" #include "SSearchBox.h" #define LOCTEXT_NAMESPACE "ContentBrowser" namespace CollectionViewFilter { void GetBasicStrings(const FCollectionItem& InCollection, TArray& OutBasicStrings) { OutBasicStrings.Add(InCollection.CollectionName.ToString()); } bool TestComplexExpression(const FCollectionItem& InCollection, const FName& InKey, const FTextFilterString& InValue, ETextFilterComparisonOperation InComparisonOperation, ETextFilterTextComparisonMode InTextComparisonMode) { static const FName NameKeyName = "Name"; static const FName TypeKeyName = "Type"; // Handle the collection name if (InKey == NameKeyName) { // Names can only work with Equal or NotEqual type tests if (InComparisonOperation != ETextFilterComparisonOperation::Equal && InComparisonOperation != ETextFilterComparisonOperation::NotEqual) { return false; } const bool bIsMatch = TextFilterUtils::TestBasicStringExpression(InCollection.CollectionName.ToString(), InValue, InTextComparisonMode); return (InComparisonOperation == ETextFilterComparisonOperation::Equal) ? bIsMatch : !bIsMatch; } // Handle the collection type if (InKey == TypeKeyName) { // Types can only work with Equal or NotEqual type tests if (InComparisonOperation != ETextFilterComparisonOperation::Equal && InComparisonOperation != ETextFilterComparisonOperation::NotEqual) { return false; } const bool bIsMatch = TextFilterUtils::TestBasicStringExpression(ECollectionShareType::ToString(InCollection.CollectionType), InValue, InTextComparisonMode); return (InComparisonOperation == ETextFilterComparisonOperation::Equal) ? bIsMatch : !bIsMatch; } return false; } } // namespace CollectionViewFilter void SCollectionView::Construct( const FArguments& InArgs ) { OnCollectionSelected = InArgs._OnCollectionSelected; bAllowCollectionButtons = InArgs._AllowCollectionButtons; bAllowRightClickMenu = InArgs._AllowRightClickMenu; bAllowCollectionDrag = InArgs._AllowCollectionDrag; bDraggedOver = false; FCollectionManagerModule& CollectionManagerModule = FCollectionManagerModule::GetModule(); CollectionManagerModule.Get().OnCollectionCreated().AddSP( this, &SCollectionView::HandleCollectionCreated ); CollectionManagerModule.Get().OnCollectionRenamed().AddSP( this, &SCollectionView::HandleCollectionRenamed ); CollectionManagerModule.Get().OnCollectionReparented().AddSP( this, &SCollectionView::HandleCollectionReparented ); CollectionManagerModule.Get().OnCollectionDestroyed().AddSP( this, &SCollectionView::HandleCollectionDestroyed ); Commands = TSharedPtr< FUICommandList >(new FUICommandList); CollectionContextMenu = MakeShareable(new FCollectionContextMenu( SharedThis(this) )); CollectionContextMenu->BindCommands(Commands); CollectionItemTextFilter = MakeShareable(new FCollectionItemTextFilter( FCollectionItemTextFilter::FItemToStringArray::CreateStatic(&CollectionViewFilter::GetBasicStrings), FCollectionItemTextFilter::FItemTestComplexExpression::CreateStatic(&CollectionViewFilter::TestComplexExpression) )); CollectionItemTextFilter->OnChanged().AddSP(this, &SCollectionView::UpdateFilteredCollectionItems); if ( InArgs._AllowQuickAssetManagement ) { QuickAssetManagement = MakeShareable(new FCollectionAssetManagement()); } FOnContextMenuOpening CollectionListContextMenuOpening; if ( InArgs._AllowContextMenu ) { CollectionListContextMenuOpening = FOnContextMenuOpening::CreateSP( this, &SCollectionView::MakeCollectionTreeContextMenu ); } PreventSelectionChangedDelegateCount = 0; TSharedRef< SWidget > HeaderContent = SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0f) .Padding(0.0f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() [ SNew(STextBlock) .Font( FEditorStyle::GetFontStyle("ContentBrowser.SourceTitleFont") ) .Text( LOCTEXT("CollectionsListTitle", "Collections") ) .Visibility( this, &SCollectionView::GetCollectionsTitleTextVisibility ) ] + SHorizontalBox::Slot() [ SAssignNew(SearchBoxPtr, SSearchBox) .HintText( LOCTEXT( "CollectionsViewSearchBoxHint", "Search Collections" ) ) .OnTextChanged( this, &SCollectionView::SetCollectionsSearchFilterText ) .Visibility( this, &SCollectionView::GetCollectionsSearchBoxVisibility ) ] ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(2.0f, 0.0f, 0.0f, 0.0f) [ SNew(SButton) .ButtonStyle(FEditorStyle::Get(), "FlatButton") .ToolTipText(LOCTEXT("AddCollectionButtonTooltip", "Add a collection.")) .OnClicked(this, &SCollectionView::MakeAddCollectionMenu) .ContentPadding( FMargin(2, 2) ) .Visibility(this, &SCollectionView::GetAddCollectionButtonVisibility) [ SNew(SImage) .Image( FEditorStyle::GetBrush("ContentBrowser.AddCollectionButtonIcon") ) ] ]; TSharedRef< SWidget > BodyContent = SNew(SVerticalBox) // Separator +SVerticalBox::Slot() .AutoHeight() [ SNew(SSeparator) ] // Collections tree +SVerticalBox::Slot() .FillHeight(1.f) [ SAssignNew(CollectionTreePtr, STreeView< TSharedPtr >) .TreeItemsSource(&VisibleRootCollectionItems) .OnGenerateRow(this, &SCollectionView::GenerateCollectionRow) .OnGetChildren(this, &SCollectionView::GetCollectionItemChildren) .ItemHeight(18) .SelectionMode(ESelectionMode::Multi) .OnSelectionChanged(this, &SCollectionView::CollectionSelectionChanged) .OnContextMenuOpening(CollectionListContextMenuOpening) .OnItemScrolledIntoView(this, &SCollectionView::CollectionItemScrolledIntoView) .ClearSelectionOnClick(false) .Visibility(this, &SCollectionView::GetCollectionTreeVisibility) ]; TSharedPtr< SWidget > Content; if ( InArgs._AllowCollapsing ) { Content = SAssignNew(CollectionsExpandableAreaPtr, SExpandableArea) .MaxHeight(200) .BorderImage( FEditorStyle::GetBrush("NoBorder") ) .HeaderPadding( FMargin(4.0f, 0.0f, 0.0f, 0.0f) ) .HeaderContent() [ SNew(SBox) .Padding(FMargin(6.0f, 0.0f, 0.0f, 0.0f)) [ HeaderContent ] ] .BodyContent() [ BodyContent ]; } else { Content = SNew( SVerticalBox ) +SVerticalBox::Slot() .AutoHeight() [ HeaderContent ] +SVerticalBox::Slot() [ BodyContent ]; } ChildSlot [ SNew(SOverlay) // Main content +SOverlay::Slot() [ Content.ToSharedRef() ] // Drop target overlay +SOverlay::Slot() [ SNew(SBorder) .Padding(0) .Visibility(EVisibility::HitTestInvisible) .BorderImage(this, &SCollectionView::GetCollectionViewDropTargetBorder) .BorderBackgroundColor(FLinearColor::Yellow) [ SNullWidget::NullWidget ] ] ]; UpdateCollectionItems(); } void SCollectionView::HandleCollectionCreated( const FCollectionNameType& Collection ) { UpdateCollectionItems(); } void SCollectionView::HandleCollectionRenamed( const FCollectionNameType& OriginalCollection, const FCollectionNameType& NewCollection ) { // If the original collection was expanded, we want to pass that expansion state onto its new entry bool bWasExpanded = false; { TSharedPtr OriginalCollectionItem = AvailableCollections.FindRef(OriginalCollection); if (OriginalCollectionItem.IsValid()) { bWasExpanded = CollectionTreePtr->IsItemExpanded(OriginalCollectionItem); } } UpdateCollectionItems(); if (bWasExpanded) { TSharedPtr NewCollectionItem = AvailableCollections.FindRef(NewCollection); if (NewCollectionItem.IsValid()) { CollectionTreePtr->SetItemExpansion(NewCollectionItem, true); } } } void SCollectionView::HandleCollectionReparented( const FCollectionNameType& Collection, const TOptional& OldParent, const TOptional& NewParent ) { UpdateCollectionItems(); } void SCollectionView::HandleCollectionDestroyed( const FCollectionNameType& Collection ) { UpdateCollectionItems(); } void SCollectionView::UpdateCollectionItems() { struct FGatherCollectionItems { FGatherCollectionItems() : CollectionManagerModule(FCollectionManagerModule::GetModule()) { } void GatherCollectionItems(FAvailableCollectionsMap& OutAvailableCollections) { OutAvailableCollections.Reset(); TArray RootCollections; CollectionManagerModule.Get().GetRootCollections(RootCollections); ProcessGatheredCollectionsAndRecurse(RootCollections, nullptr, OutAvailableCollections); } void GatherChildCollectionItems(const TSharedPtr& InParentCollectionItem, FAvailableCollectionsMap& OutAvailableCollections) { TArray ChildCollections; CollectionManagerModule.Get().GetChildCollections(InParentCollectionItem->CollectionName, InParentCollectionItem->CollectionType, ChildCollections); ProcessGatheredCollectionsAndRecurse(ChildCollections, InParentCollectionItem, OutAvailableCollections); } void ProcessGatheredCollectionsAndRecurse(const TArray& InCollections, const TSharedPtr& InParentCollectionItem, FAvailableCollectionsMap& OutAvailableCollections) { for (const FCollectionNameType& Collection : InCollections) { // Never display system collections if (Collection.Type == ECollectionShareType::CST_System) { continue; } TSharedPtr CollectionItem = MakeShareable(new FCollectionItem(Collection.Name, Collection.Type)); OutAvailableCollections.Add(Collection, CollectionItem); if (InParentCollectionItem.IsValid()) { // Fixup the parent and child pointers InParentCollectionItem->ChildCollections.Add(CollectionItem); CollectionItem->ParentCollection = InParentCollectionItem; } // Recurse GatherChildCollectionItems(CollectionItem, OutAvailableCollections); } } FCollectionManagerModule& CollectionManagerModule; }; // Backup the current selection and expansion state of our collections // We're about to re-create the tree, so we'll need to re-apply this again afterwards TArray SelectedCollections; TArray ExpandedCollections; { const auto SelectedCollectionItems = CollectionTreePtr->GetSelectedItems(); SelectedCollections.Reserve(SelectedCollectionItems.Num()); for (const TSharedPtr& SelectedCollectionItem : SelectedCollectionItems) { SelectedCollections.Add(FCollectionNameType(SelectedCollectionItem->CollectionName, SelectedCollectionItem->CollectionType)); } } { TSet> ExpandedCollectionItems; CollectionTreePtr->GetExpandedItems(ExpandedCollectionItems); ExpandedCollections.Reserve(ExpandedCollectionItems.Num()); for (const TSharedPtr& ExpandedCollectionItem : ExpandedCollectionItems) { ExpandedCollections.Add(FCollectionNameType(ExpandedCollectionItem->CollectionName, ExpandedCollectionItem->CollectionType)); } } FGatherCollectionItems GatherCollectionItems; GatherCollectionItems.GatherCollectionItems(AvailableCollections); UpdateFilteredCollectionItems(); // Restore selection and expansion SetSelectedCollections(SelectedCollections, false); SetExpandedCollections(ExpandedCollections); } void SCollectionView::UpdateFilteredCollectionItems() { VisibleCollections.Reset(); VisibleRootCollectionItems.Reset(); auto AddVisibleCollection = [&](const TSharedPtr& InCollectionItem) { VisibleCollections.Add(FCollectionNameType(InCollectionItem->CollectionName, InCollectionItem->CollectionType)); if (!InCollectionItem->ParentCollection.IsValid()) { VisibleRootCollectionItems.AddUnique(InCollectionItem); } }; auto AddVisibleCollectionRecursive = [&](const TSharedPtr& InCollectionItem) { TSharedPtr CollectionItemToAdd = InCollectionItem; do { AddVisibleCollection(CollectionItemToAdd); CollectionItemToAdd = CollectionItemToAdd->ParentCollection.Pin(); } while(CollectionItemToAdd.IsValid()); }; // Do we have an active filter to test against? if (CollectionItemTextFilter->GetRawFilterText().IsEmpty()) { // No filter, just mark everything as visible for (const auto& AvailableCollectionInfo : AvailableCollections) { AddVisibleCollection(AvailableCollectionInfo.Value); } } else { TArray> CollectionsToExpandTo; // Test everything against the filter - a visible child needs to make sure its parents are also marked as visible for (const auto& AvailableCollectionInfo : AvailableCollections) { const TSharedPtr& CollectionItem = AvailableCollectionInfo.Value; if (CollectionItemTextFilter->PassesFilter(*CollectionItem)) { AddVisibleCollectionRecursive(CollectionItem); CollectionsToExpandTo.Add(CollectionItem.ToSharedRef()); } } // Make sure all matching items have their parents expanded so they can be seen for (const TSharedRef& CollectionItem : CollectionsToExpandTo) { ExpandParentItems(CollectionItem); } } VisibleRootCollectionItems.Sort(FCollectionItem::FCompareFCollectionItemByName()); CollectionTreePtr->RequestTreeRefresh(); } void SCollectionView::SetCollectionsSearchFilterText( const FText& InSearchText ) { CollectionItemTextFilter->SetRawFilterText( InSearchText ); SearchBoxPtr->SetError( CollectionItemTextFilter->GetFilterErrorText() ); } FText SCollectionView::GetCollectionsSearchFilterText() const { return CollectionItemTextFilter->GetRawFilterText(); } void SCollectionView::SetSelectedCollections(const TArray& CollectionsToSelect, const bool bEnsureVisible) { // Prevent the selection changed delegate since the invoking code requested it FScopedPreventSelectionChangedDelegate DelegatePrevention( SharedThis(this) ); // Expand the collections area if we are indeed selecting at least one collection if ( bEnsureVisible && CollectionsToSelect.Num() > 0 && CollectionsExpandableAreaPtr.IsValid() ) { CollectionsExpandableAreaPtr->SetExpanded(true); } // Clear the selection to start, then add the selected items as they are found CollectionTreePtr->ClearSelection(); for (const FCollectionNameType& CollectionToSelect : CollectionsToSelect) { TSharedPtr CollectionItemToSelect = AvailableCollections.FindRef(CollectionToSelect); if (CollectionItemToSelect.IsValid()) { if (bEnsureVisible) { ExpandParentItems(CollectionItemToSelect.ToSharedRef()); CollectionTreePtr->RequestScrollIntoView(CollectionItemToSelect); } CollectionTreePtr->SetItemSelection(CollectionItemToSelect, true); // If the selected collection doesn't pass our current filter, we need to clear it if (bEnsureVisible && !CollectionItemTextFilter->PassesFilter(FCollectionItem(CollectionItemToSelect->CollectionName, CollectionItemToSelect->CollectionType))) { SearchBoxPtr->SetText(FText::GetEmpty()); } } } } void SCollectionView::SetExpandedCollections(const TArray& CollectionsToExpand) { // Clear the expansion to start, then add the expanded items as they are found CollectionTreePtr->ClearExpandedItems(); for (const FCollectionNameType& CollectionToExpand : CollectionsToExpand) { TSharedPtr CollectionItemToExpand = AvailableCollections.FindRef(CollectionToExpand); if (CollectionItemToExpand.IsValid()) { CollectionTreePtr->SetItemExpansion(CollectionItemToExpand, true); } } } void SCollectionView::ClearSelection() { // Prevent the selection changed delegate since the invoking code requested it FScopedPreventSelectionChangedDelegate DelegatePrevention( SharedThis(this) ); // Clear the selection to start, then add the selected paths as they are found CollectionTreePtr->ClearSelection(); } TArray SCollectionView::GetSelectedCollections() const { TArray RetArray; TArray> Items = CollectionTreePtr->GetSelectedItems(); for ( int32 ItemIdx = 0; ItemIdx < Items.Num(); ++ItemIdx ) { const TSharedPtr& Item = Items[ItemIdx]; RetArray.Add(FCollectionNameType(Item->CollectionName, Item->CollectionType)); } return RetArray; } void SCollectionView::SetSelectedAssets(const TArray& SelectedAssets) { if ( QuickAssetManagement.IsValid() ) { QuickAssetManagement->SetCurrentAssets(SelectedAssets); } } void SCollectionView::ApplyHistoryData( const FHistoryData& History ) { // Prevent the selection changed delegate because it would add more history when we are just setting a state FScopedPreventSelectionChangedDelegate DelegatePrevention( SharedThis(this) ); CollectionTreePtr->ClearSelection(); for ( auto HistoryIt = History.SourcesData.Collections.CreateConstIterator(); HistoryIt; ++HistoryIt) { TSharedPtr CollectionHistoryItem = AvailableCollections.FindRef(FCollectionNameType((*HistoryIt).Name, (*HistoryIt).Type)); if (CollectionHistoryItem.IsValid()) { ExpandParentItems(CollectionHistoryItem.ToSharedRef()); CollectionTreePtr->RequestScrollIntoView(CollectionHistoryItem); CollectionTreePtr->SetItemSelection(CollectionHistoryItem, true); } } } void SCollectionView::SaveSettings(const FString& IniFilename, const FString& IniSection, const FString& SettingsString) const { auto SaveCollectionsArrayToIni = [&](const FString& InSubKey, const TArray>& InCollectionItems) { FString CollectionsString; for (const TSharedPtr& CollectionItem : InCollectionItems) { if (CollectionsString.Len() > 0) { CollectionsString += TEXT(","); } CollectionsString += CollectionItem->CollectionName.ToString(); CollectionsString += TEXT("?"); CollectionsString += FString::FromInt(CollectionItem->CollectionType); } GConfig->SetString(*IniSection, *(SettingsString + InSubKey), *CollectionsString, IniFilename); }; const bool IsCollectionsExpanded = CollectionsExpandableAreaPtr.IsValid() ? CollectionsExpandableAreaPtr->IsExpanded() : true; GConfig->SetBool(*IniSection, *(SettingsString + TEXT(".CollectionsExpanded")), IsCollectionsExpanded, IniFilename); SaveCollectionsArrayToIni(TEXT(".SelectedCollections"), CollectionTreePtr->GetSelectedItems()); { TSet> ExpandedCollectionItems; CollectionTreePtr->GetExpandedItems(ExpandedCollectionItems); SaveCollectionsArrayToIni(TEXT(".ExpandedCollections"), ExpandedCollectionItems.Array()); } } void SCollectionView::LoadSettings(const FString& IniFilename, const FString& IniSection, const FString& SettingsString) { auto LoadCollectionsArrayFromIni = [&](const FString& InSubKey) -> TArray { TArray RetCollectionsArray; FString CollectionsArrayString; if (GConfig->GetString(*IniSection, *(SettingsString + InSubKey), CollectionsArrayString, IniFilename)) { TArray CollectionStrings; CollectionsArrayString.ParseIntoArray(CollectionStrings, TEXT(","), /*bCullEmpty*/true); for (const FString& CollectionString : CollectionStrings) { FString CollectionName; FString CollectionTypeString; if (CollectionString.Split(TEXT("?"), &CollectionName, &CollectionTypeString)) { const int32 CollectionType = FCString::Atoi(*CollectionTypeString); if (CollectionType >= 0 && CollectionType < ECollectionShareType::CST_All) { RetCollectionsArray.Add(FCollectionNameType(FName(*CollectionName), ECollectionShareType::Type(CollectionType))); } } } } return RetCollectionsArray; }; // Collection expansion state bool bCollectionsExpanded = false; if (CollectionsExpandableAreaPtr.IsValid() && GConfig->GetBool(*IniSection, *(SettingsString + TEXT(".CollectionsExpanded")), bCollectionsExpanded, IniFilename)) { CollectionsExpandableAreaPtr->SetExpanded(bCollectionsExpanded); } // Selected Collections TArray NewSelectedCollections = LoadCollectionsArrayFromIni(TEXT(".SelectedCollections")); if (NewSelectedCollections.Num() > 0) { SetSelectedCollections(NewSelectedCollections); const TArray> SelectedCollectionItems = CollectionTreePtr->GetSelectedItems(); if (SelectedCollectionItems.Num() > 0) { CollectionSelectionChanged(SelectedCollectionItems[0], ESelectInfo::Direct); } } // Expanded Collections TArray NewExpandedCollections = LoadCollectionsArrayFromIni(TEXT(".ExpandedCollections")); if (NewExpandedCollections.Num() > 0) { SetExpandedCollections(NewExpandedCollections); } } FReply SCollectionView::OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent ) { if( Commands->ProcessCommandBindings( InKeyEvent ) ) { return FReply::Handled(); } return FReply::Unhandled(); } void SCollectionView::OnDragEnter( const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent ) { ValidateDragDropOnCollectionTree(MyGeometry, DragDropEvent, bDraggedOver); // updates bDraggedOver } void SCollectionView::OnDragLeave( const FDragDropEvent& DragDropEvent ) { bDraggedOver = false; } FReply SCollectionView::OnDragOver( const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent ) { ValidateDragDropOnCollectionTree(MyGeometry, DragDropEvent, bDraggedOver); // updates bDraggedOver return (bDraggedOver) ? FReply::Handled() : FReply::Unhandled(); } FReply SCollectionView::OnDrop( const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent ) { if (ValidateDragDropOnCollectionTree(MyGeometry, DragDropEvent, bDraggedOver)) // updates bDraggedOver { bDraggedOver = false; return HandleDragDropOnCollectionTree(MyGeometry, DragDropEvent); } if (bDraggedOver) { // We were able to handle this operation, but could not due to another error - still report this drop as handled so it doesn't fall through to other widgets bDraggedOver = false; return FReply::Handled(); } return FReply::Unhandled(); } bool SCollectionView::ShouldAllowSelectionChangedDelegate() const { return PreventSelectionChangedDelegateCount == 0; } FReply SCollectionView::MakeAddCollectionMenu() { // Get all menu extenders for this context menu from the content browser module FContentBrowserModule& ContentBrowserModule = FModuleManager::GetModuleChecked( TEXT("ContentBrowser") ); TArray MenuExtenderDelegates = ContentBrowserModule.GetAllCollectionViewContextMenuExtenders(); TArray> Extenders; for (int32 i = 0; i < MenuExtenderDelegates.Num(); ++i) { if (MenuExtenderDelegates[i].IsBound()) { Extenders.Add(MenuExtenderDelegates[i].Execute()); } } TSharedPtr MenuExtender = FExtender::Combine(Extenders); FMenuBuilder MenuBuilder(/*bInShouldCloseWindowAfterMenuSelection=*/true, NULL, MenuExtender, true); CollectionContextMenu->UpdateProjectSourceControl(); CollectionContextMenu->MakeNewCollectionSubMenu(MenuBuilder); FSlateApplication::Get().PushMenu( AsShared(), FWidgetPath(), MenuBuilder.MakeWidget(), FSlateApplication::Get().GetCursorPos(), FPopupTransitionEffect( FPopupTransitionEffect::TopMenu ) ); return FReply::Handled(); } EVisibility SCollectionView::GetCollectionsTitleTextVisibility() const { // Only show the title text if we have an expansion area, but are collapsed return (CollectionsExpandableAreaPtr.IsValid() && !CollectionsExpandableAreaPtr->IsExpanded()) ? EVisibility::Visible : EVisibility::Collapsed; } EVisibility SCollectionView::GetCollectionsSearchBoxVisibility() const { // Only show the search box if we have an expanded expansion area, or aren't currently using an expansion area return (!CollectionsExpandableAreaPtr.IsValid() || CollectionsExpandableAreaPtr->IsExpanded()) ? EVisibility::Visible : EVisibility::Collapsed; } EVisibility SCollectionView::GetAddCollectionButtonVisibility() const { return (bAllowCollectionButtons && ( !CollectionsExpandableAreaPtr.IsValid() || CollectionsExpandableAreaPtr->IsExpanded() ) ) ? EVisibility::Visible : EVisibility::Collapsed; } void SCollectionView::CreateCollectionItem( ECollectionShareType::Type CollectionType ) { if ( ensure(CollectionType != ECollectionShareType::CST_All) ) { FCollectionManagerModule& CollectionManagerModule = FCollectionManagerModule::GetModule(); const FName BaseCollectionName = *LOCTEXT("NewCollectionName", "NewCollection").ToString(); FName CollectionName; CollectionManagerModule.Get().CreateUniqueCollectionName(BaseCollectionName, CollectionType, CollectionName); TSharedPtr NewItem = MakeShareable(new FCollectionItem(CollectionName, CollectionType)); // Adding a new collection now, so clear any filter we may have applied SearchBoxPtr->SetText(FText::GetEmpty()); // Mark the new collection for rename and that it is new so it will be created upon successful rename NewItem->bRenaming = true; NewItem->bNewCollection = true; AvailableCollections.Add( FCollectionNameType(NewItem->CollectionName, NewItem->CollectionType), NewItem ); UpdateFilteredCollectionItems(); CollectionTreePtr->RequestScrollIntoView(NewItem); CollectionTreePtr->SetSelection( NewItem ); } } void SCollectionView::RenameCollectionItem( const TSharedPtr& ItemToRename ) { if ( ensure(ItemToRename.IsValid()) ) { ItemToRename->bRenaming = true; CollectionTreePtr->RequestScrollIntoView(ItemToRename); } } void SCollectionView::DeleteCollectionItems( const TArray>& ItemsToDelete ) { if (ItemsToDelete.Num() == 0) { return; } // Before we delete anything (as this will trigger a tree update) we need to work out what our new selection should be in the case that // all of the selected items are removed const TArray> PreviouslySelectedItems = CollectionTreePtr->GetSelectedItems(); // Get the first selected item that will be deleted so we can find a suitable new selection TSharedPtr FirstSelectedItemDeleted; for (const auto& ItemToDelete : ItemsToDelete) { if (PreviouslySelectedItems.Contains(ItemToDelete)) { FirstSelectedItemDeleted = ItemToDelete; break; } } // Build up an array of potential new selections (in the case that we're deleting everything that's selected) // Earlier items should be considered first, we base this list on the first selected item that will be deleted, and include previous siblings, and then all parents and roots TArray PotentialNewSelections; if (FirstSelectedItemDeleted.IsValid()) { TSharedPtr RootSelectedItemDeleted = FirstSelectedItemDeleted; TSharedPtr ParentCollectionItem = FirstSelectedItemDeleted->ParentCollection.Pin(); if (ParentCollectionItem.IsValid()) { // Add all the siblings until we find the item that will be deleted for (const auto& ChildItemWeakPtr : ParentCollectionItem->ChildCollections) { TSharedPtr ChildItem = ChildItemWeakPtr.Pin(); if (ChildItem.IsValid()) { if (ChildItem == FirstSelectedItemDeleted) { break; } // We add siblings as the start, as the closest sibling should be the first match PotentialNewSelections.Insert(FCollectionNameType(ChildItem->CollectionName, ChildItem->CollectionType), 0); } } // Now add this parent, and all other parents too do { PotentialNewSelections.Add(FCollectionNameType(ParentCollectionItem->CollectionName, ParentCollectionItem->CollectionType)); RootSelectedItemDeleted = ParentCollectionItem; ParentCollectionItem = ParentCollectionItem->ParentCollection.Pin(); } while (ParentCollectionItem.IsValid()); } if (RootSelectedItemDeleted.IsValid()) { // Add all the root level items before this one const int32 InsertionPoint = PotentialNewSelections.Num(); for (const auto& RootItem : VisibleRootCollectionItems) { if (RootItem == RootSelectedItemDeleted) { break; } // Add each root item at the insertion point, as the closest item should be a better match PotentialNewSelections.Insert(FCollectionNameType(RootItem->CollectionName, RootItem->CollectionType), InsertionPoint); } } } // Delete all given collections int32 NumSelectedItemsDeleted = 0; FCollectionManagerModule& CollectionManagerModule = FCollectionManagerModule::GetModule(); for (const TSharedPtr& ItemToDelete : ItemsToDelete) { if (CollectionManagerModule.Get().DestroyCollection(ItemToDelete->CollectionName, ItemToDelete->CollectionType)) { if (PreviouslySelectedItems.Contains(ItemToDelete)) { ++NumSelectedItemsDeleted; } } else { // Display a warning const FVector2D& CursorPos = FSlateApplication::Get().GetCursorPos(); FSlateRect MessageAnchor(CursorPos.X, CursorPos.Y, CursorPos.X, CursorPos.Y); ContentBrowserUtils::DisplayMessage( FText::Format( LOCTEXT("CollectionDestroyFailed", "Failed to destroy collection. {0}"), CollectionManagerModule.Get().GetLastError() ), MessageAnchor, CollectionTreePtr.ToSharedRef() ); } } // DestroyCollection will have triggered a notification that will have updated the tree, we now need to apply a suitable selection... // Did this delete change the list of selected items? if (NumSelectedItemsDeleted > 0 || PreviouslySelectedItems.Num() == 0) { // If we removed everything that was selected, we need to try and find a suitable replacement... if (NumSelectedItemsDeleted >= PreviouslySelectedItems.Num() && VisibleCollections.Num() > 1) { // Include the first visible item as an absolute last resort should everything else suitable have been removed from the tree PotentialNewSelections.Add(*VisibleCollections.CreateConstIterator()); // Check the potential new selections array and try and select the first one that's still visible in the tree TArray NewItemSelection; for (const FCollectionNameType& PotentialNewSelection : PotentialNewSelections) { if (VisibleCollections.Contains(PotentialNewSelection)) { NewItemSelection.Add(PotentialNewSelection); break; } } SetSelectedCollections(NewItemSelection, true); } // Broadcast the new selection const TArray> UpdatedSelectedItems = CollectionTreePtr->GetSelectedItems(); CollectionSelectionChanged((UpdatedSelectedItems.Num() > 0) ? UpdatedSelectedItems[0] : nullptr, ESelectInfo::Direct); } } EVisibility SCollectionView::GetCollectionTreeVisibility() const { return AvailableCollections.Num() > 0 ? EVisibility::Visible : EVisibility::Collapsed; } const FSlateBrush* SCollectionView::GetCollectionViewDropTargetBorder() const { return bDraggedOver ? FEditorStyle::GetBrush("ContentBrowser.CollectionTreeDragDropBorder") : FEditorStyle::GetBrush("NoBorder"); } TSharedRef SCollectionView::GenerateCollectionRow( TSharedPtr CollectionItem, const TSharedRef& OwnerTable ) { check(CollectionItem.IsValid()); // Only bind the check box callbacks if we're allowed to show check boxes TAttribute IsCollectionCheckBoxEnabledAttribute; TAttribute IsCollectionCheckedAttribute; FOnCheckStateChanged OnCollectionCheckStateChangedDelegate; if ( QuickAssetManagement.IsValid() ) { IsCollectionCheckBoxEnabledAttribute.Bind(TAttribute::FGetter::CreateSP(this, &SCollectionView::IsCollectionCheckBoxEnabled, CollectionItem)); IsCollectionCheckedAttribute.Bind(TAttribute::FGetter::CreateSP(this, &SCollectionView::IsCollectionChecked, CollectionItem)); OnCollectionCheckStateChangedDelegate.BindSP(this, &SCollectionView::OnCollectionCheckStateChanged, CollectionItem); } TSharedPtr< STableRow< TSharedPtr > > TableRow = SNew( STableRow< TSharedPtr >, OwnerTable ) .OnDragDetected(this, &SCollectionView::OnCollectionDragDetected); TableRow->SetContent ( SNew(SCollectionTreeItem) .ParentWidget(SharedThis(this)) .CollectionItem(CollectionItem) .OnNameChangeCommit(this, &SCollectionView::CollectionNameChangeCommit) .OnVerifyRenameCommit(this, &SCollectionView::CollectionVerifyRenameCommit) .OnValidateDragDrop(this, &SCollectionView::ValidateDragDropOnCollectionItem) .OnHandleDragDrop(this, &SCollectionView::HandleDragDropOnCollectionItem) .IsSelected(TableRow.Get(), &STableRow< TSharedPtr >::IsSelectedExclusively) .IsReadOnly(this, &SCollectionView::IsCollectionNameReadOnly) .HighlightText(this, &SCollectionView::GetCollectionsSearchFilterText) .IsCheckBoxEnabled(IsCollectionCheckBoxEnabledAttribute) .IsCollectionChecked(IsCollectionCheckedAttribute) .OnCollectionCheckStateChanged(OnCollectionCheckStateChangedDelegate) ); return TableRow.ToSharedRef(); } void SCollectionView::GetCollectionItemChildren( TSharedPtr InParentItem, TArray< TSharedPtr >& OutChildItems ) const { for (const auto& ChildItemWeakPtr : InParentItem->ChildCollections) { TSharedPtr ChildItem = ChildItemWeakPtr.Pin(); if (ChildItem.IsValid() && VisibleCollections.Contains(FCollectionNameType(ChildItem->CollectionName, ChildItem->CollectionType))) { OutChildItems.Add(ChildItem); } } OutChildItems.Sort(FCollectionItem::FCompareFCollectionItemByName()); } FReply SCollectionView::OnCollectionDragDetected(const FGeometry& Geometry, const FPointerEvent& MouseEvent) { if (bAllowCollectionDrag && MouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton)) { const TArray SelectedCollections = GetSelectedCollections(); if (SelectedCollections.Num() > 0) { TSharedRef DragDropOp = FCollectionDragDropOp::New(SelectedCollections); CurrentCollectionDragDropOp = DragDropOp; return FReply::Handled().BeginDragDrop(DragDropOp); } } return FReply::Unhandled(); } bool SCollectionView::ValidateDragDropOnCollectionTree(const FGeometry& Geometry, const FDragDropEvent& DragDropEvent, bool& OutIsKnownDragOperation) { OutIsKnownDragOperation = false; TSharedPtr Operation = DragDropEvent.GetOperation(); if (!Operation.IsValid()) { return false; } if (Operation->IsOfType()) { OutIsKnownDragOperation = true; return true; } return false; } FReply SCollectionView::HandleDragDropOnCollectionTree(const FGeometry& Geometry, const FDragDropEvent& DragDropEvent) { // Should have already called ValidateDragDropOnCollectionTree prior to calling this... TSharedPtr Operation = DragDropEvent.GetOperation(); check(Operation.IsValid()); FCollectionManagerModule& CollectionManagerModule = FCollectionManagerModule::GetModule(); if (Operation->IsOfType()) { TSharedPtr DragDropOp = StaticCastSharedPtr(Operation); // Reparent all of the collections in the drag drop so that they are root level items for (const FCollectionNameType& NewChildCollection : DragDropOp->Collections) { if (!CollectionManagerModule.Get().ReparentCollection( NewChildCollection.Name, NewChildCollection.Type, NAME_None, ECollectionShareType::CST_All )) { ContentBrowserUtils::DisplayMessage(CollectionManagerModule.Get().GetLastError(), Geometry.GetClippingRect(), SharedThis(this)); } } return FReply::Handled(); } return FReply::Unhandled(); } bool SCollectionView::ValidateDragDropOnCollectionItem(TSharedRef CollectionItem, const FGeometry& Geometry, const FDragDropEvent& DragDropEvent, bool& OutIsKnownDragOperation) { OutIsKnownDragOperation = false; TSharedPtr Operation = DragDropEvent.GetOperation(); if (!Operation.IsValid()) { return false; } bool bIsValidDrag = false; TOptional NewDragCursor; if (Operation->IsOfType()) { TSharedPtr DragDropOp = StaticCastSharedPtr(Operation); OutIsKnownDragOperation = true; FCollectionManagerModule& CollectionManagerModule = FCollectionManagerModule::GetModule(); bIsValidDrag = true; for (const FCollectionNameType& PotentialChildCollection : DragDropOp->Collections) { bIsValidDrag = CollectionManagerModule.Get().IsValidParentCollection( PotentialChildCollection.Name, PotentialChildCollection.Type, CollectionItem->CollectionName, CollectionItem->CollectionType ); if (!bIsValidDrag) { DragDropOp->SetToolTip(CollectionManagerModule.Get().GetLastError(), FEditorStyle::GetBrush(TEXT("Graph.ConnectorFeedback.Error"))); break; } } // If we are dragging over a child collection item, then this view as a whole should not be marked as dragged over bDraggedOver = false; } else if (Operation->IsOfType()) { TSharedPtr DragDropOp = StaticCastSharedPtr(Operation); OutIsKnownDragOperation = true; bIsValidDrag = DragDropOp->AssetData.Num() > 0; } // Set the default slashed circle if this drag is invalid and a drag operation hasn't set NewDragCursor to something custom if (!bIsValidDrag && !NewDragCursor.IsSet()) { NewDragCursor = EMouseCursor::SlashedCircle; } Operation->SetCursorOverride(NewDragCursor); return bIsValidDrag; } FReply SCollectionView::HandleDragDropOnCollectionItem(TSharedRef CollectionItem, const FGeometry& Geometry, const FDragDropEvent& DragDropEvent) { // Should have already called ValidateDragDropOnCollectionItem prior to calling this... TSharedPtr Operation = DragDropEvent.GetOperation(); check(Operation.IsValid()); FCollectionManagerModule& CollectionManagerModule = FCollectionManagerModule::GetModule(); if (Operation->IsOfType()) { TSharedPtr DragDropOp = StaticCastSharedPtr(Operation); // Make sure our drop item is marked as expanded so that we'll be able to see the newly added children CollectionTreePtr->SetItemExpansion(CollectionItem, true); // Reparent all of the collections in the drag drop so that they are our immediate children for (const FCollectionNameType& NewChildCollection : DragDropOp->Collections) { if (!CollectionManagerModule.Get().ReparentCollection( NewChildCollection.Name, NewChildCollection.Type, CollectionItem->CollectionName, CollectionItem->CollectionType )) { ContentBrowserUtils::DisplayMessage(CollectionManagerModule.Get().GetLastError(), Geometry.GetClippingRect(), SharedThis(this)); } } return FReply::Handled(); } else if (Operation->IsOfType()) { TSharedPtr DragDropOp = StaticCastSharedPtr(Operation); TArray ObjectPaths; ObjectPaths.Reserve(DragDropOp->AssetData.Num()); for (const FAssetData& AssetData : DragDropOp->AssetData) { ObjectPaths.Add(AssetData.ObjectPath); } int32 NumAdded = 0; FText Message; if (CollectionManagerModule.Get().AddToCollection(CollectionItem->CollectionName, CollectionItem->CollectionType, ObjectPaths, &NumAdded)) { if (DragDropOp->AssetData.Num() == 1) { FFormatNamedArguments Args; Args.Add(TEXT("AssetName"), FText::FromName(DragDropOp->AssetData[0].AssetName)); Args.Add(TEXT("CollectionName"), FText::FromName(CollectionItem->CollectionName)); Message = FText::Format(LOCTEXT("CollectionAssetsAdded", "Added {AssetName} to {CollectionName}"), Args); } else { FFormatNamedArguments Args; Args.Add(TEXT("Number"), NumAdded); Args.Add(TEXT("CollectionName"), FText::FromName(CollectionItem->CollectionName)); Message = FText::Format(LOCTEXT("CollectionAssetsAdded", "Added {Number} asset(s) to {CollectionName}"), Args); } } else { Message = CollectionManagerModule.Get().GetLastError(); } // Added items to the collection or failed. Either way, display the message. ContentBrowserUtils::DisplayMessage(Message, Geometry.GetClippingRect(), SharedThis(this)); return FReply::Handled(); } return FReply::Unhandled(); } void SCollectionView::ExpandParentItems(const TSharedRef& InCollectionItem) { for (TSharedPtr CollectionItemToExpand = InCollectionItem->ParentCollection.Pin(); CollectionItemToExpand.IsValid(); CollectionItemToExpand = CollectionItemToExpand->ParentCollection.Pin() ) { CollectionTreePtr->SetItemExpansion(CollectionItemToExpand, true); } } TSharedPtr SCollectionView::MakeCollectionTreeContextMenu() { if ( !bAllowRightClickMenu ) { return NULL; } return CollectionContextMenu->MakeCollectionTreeContextMenu(Commands); } bool SCollectionView::IsCollectionCheckBoxEnabled( TSharedPtr CollectionItem ) const { return QuickAssetManagement.IsValid() && QuickAssetManagement->IsCollectionEnabled(FCollectionNameType(CollectionItem->CollectionName, CollectionItem->CollectionType)); } ECheckBoxState SCollectionView::IsCollectionChecked( TSharedPtr CollectionItem ) const { if ( QuickAssetManagement.IsValid() ) { return QuickAssetManagement->GetCollectionCheckState(FCollectionNameType(CollectionItem->CollectionName, CollectionItem->CollectionType)); } return ECheckBoxState::Unchecked; } void SCollectionView::OnCollectionCheckStateChanged( ECheckBoxState NewState, TSharedPtr CollectionItem ) { if ( QuickAssetManagement.IsValid() ) { switch(NewState) { case ECheckBoxState::Checked: QuickAssetManagement->AddCurrentAssetsToCollection(FCollectionNameType(CollectionItem->CollectionName, CollectionItem->CollectionType)); break; case ECheckBoxState::Unchecked: QuickAssetManagement->RemoveCurrentAssetsFromCollection(FCollectionNameType(CollectionItem->CollectionName, CollectionItem->CollectionType)); break; default: break; } } } void SCollectionView::CollectionSelectionChanged( TSharedPtr< FCollectionItem > CollectionItem, ESelectInfo::Type /*SelectInfo*/ ) { if ( ShouldAllowSelectionChangedDelegate() && OnCollectionSelected.IsBound() ) { if ( CollectionItem.IsValid() ) { OnCollectionSelected.Execute(FCollectionNameType(CollectionItem->CollectionName, CollectionItem->CollectionType)); } else { OnCollectionSelected.Execute(FCollectionNameType(NAME_None, ECollectionShareType::CST_All)); } } } void SCollectionView::CollectionItemScrolledIntoView( TSharedPtr CollectionItem, const TSharedPtr& Widget ) { if ( CollectionItem->bRenaming && Widget.IsValid() && Widget->GetContent().IsValid() ) { CollectionItem->OnRenamedRequestEvent.Broadcast(); } } bool SCollectionView::IsCollectionNameReadOnly() const { // We can't rename collections while they're being dragged TSharedPtr DragDropOp = CurrentCollectionDragDropOp.Pin(); if (DragDropOp.IsValid()) { TArray> SelectedCollectionItems = CollectionTreePtr->GetSelectedItems(); for (const auto& SelectedCollectionItem : SelectedCollectionItems) { if (DragDropOp->Collections.Contains(FCollectionNameType(SelectedCollectionItem->CollectionName, SelectedCollectionItem->CollectionType))) { return true; } } } CollectionContextMenu->UpdateProjectSourceControl(); return !CollectionContextMenu->CanRenameSelectedCollections(); } bool SCollectionView::CollectionNameChangeCommit( const TSharedPtr< FCollectionItem >& CollectionItem, const FString& NewName, bool bChangeConfirmed, FText& OutWarningMessage ) { // There should only ever be one item selected when renaming check(CollectionTreePtr->GetNumItemsSelected() == 1); FCollectionManagerModule& CollectionManagerModule = FCollectionManagerModule::GetModule(); ECollectionShareType::Type CollectionType = CollectionItem->CollectionType; // If new name is empty, set it back to the original name const FName NewNameFinal( NewName.IsEmpty() ? CollectionItem->CollectionName : FName(*NewName) ); if ( CollectionItem->bNewCollection ) { CollectionItem->bNewCollection = false; // If we can canceled the name change when creating a new asset, we want to silently remove it if ( !bChangeConfirmed ) { AvailableCollections.Remove(FCollectionNameType(CollectionItem->CollectionName, CollectionItem->CollectionType)); UpdateFilteredCollectionItems(); return false; } if ( !CollectionManagerModule.Get().CreateCollection(NewNameFinal, CollectionType) ) { // Failed to add the collection, remove it from the list AvailableCollections.Remove(FCollectionNameType(CollectionItem->CollectionName, CollectionItem->CollectionType)); UpdateFilteredCollectionItems(); OutWarningMessage = FText::Format( LOCTEXT("CreateCollectionFailed", "Failed to create the collection. {0}"), CollectionManagerModule.Get().GetLastError()); return false; } } else { // If the old name is the same as the new name, just early exit here. if ( CollectionItem->CollectionName == NewNameFinal ) { return true; } // If the new name doesn't pass our current filter, we need to clear it if ( !CollectionItemTextFilter->PassesFilter( FCollectionItem(NewNameFinal, CollectionType) ) ) { SearchBoxPtr->SetText(FText::GetEmpty()); } // Otherwise perform the rename if ( !CollectionManagerModule.Get().RenameCollection(CollectionItem->CollectionName, CollectionType, NewNameFinal, CollectionType) ) { // Failed to rename the collection OutWarningMessage = FText::Format( LOCTEXT("RenameCollectionFailed", "Failed to rename the collection. {0}"), CollectionManagerModule.Get().GetLastError()); return false; } } // At this point CollectionItem is no longer a member of the CollectionItems list (as the list is repopulated by // UpdateCollectionItems, which is called by a broadcast from CollectionManagerModule::RenameCollection, above). // So search again for the item by name and type. auto NewCollectionItemPtr = AvailableCollections.Find( FCollectionNameType(NewNameFinal, CollectionType) ); // Reselect the path to notify that the selection has changed { FScopedPreventSelectionChangedDelegate DelegatePrevention( SharedThis(this) ); CollectionTreePtr->ClearSelection(); } // Set the selection if (NewCollectionItemPtr) { const auto& NewCollectionItem = *NewCollectionItemPtr; CollectionTreePtr->RequestScrollIntoView(NewCollectionItem); CollectionTreePtr->SetItemSelection(NewCollectionItem, true); } return true; } bool SCollectionView::CollectionVerifyRenameCommit(const TSharedPtr< FCollectionItem >& CollectionItem, const FString& NewName, const FSlateRect& MessageAnchor, FText& OutErrorMessage) { const FName NewNameFinal = *NewName; // If the new name is the same as the old name, consider this to be unchanged, and accept it. if (CollectionItem->CollectionName == NewNameFinal) { return true; } FCollectionManagerModule& CollectionManagerModule = FCollectionManagerModule::GetModule(); if (CollectionManagerModule.Get().CollectionExists(NewNameFinal, ECollectionShareType::CST_All)) { // This collection already exists, inform the user and continue OutErrorMessage = FText::Format(LOCTEXT("RenameCollectionAlreadyExists", "A collection already exists with the name '{0}'."), FText::FromName(NewNameFinal)); // Return false to indicate that the user should enter a new name return false; } return true; } #undef LOCTEXT_NAMESPACE