// Copyright Epic Games, Inc. All Rights Reserved. #include "SCollectionView.h" #include "Algo/AllOf.h" #include "Algo/Transform.h" #include "AssetRegistry/AssetData.h" #include "CollectionAssetManagement.h" #include "CollectionContextMenu.h" #include "CollectionViewTypes.h" #include "CollectionViewUtils.h" #include "ContentBrowserConfig.h" #include "ContentBrowserDelegates.h" #include "ContentBrowserModule.h" #include "ContentBrowserPluginFilters.h" #include "ContentBrowserStyle.h" #include "ContentBrowserUtils.h" #include "CoreGlobals.h" #include "DragAndDrop/AssetDragDropOp.h" #include "DragAndDrop/CollectionDragDropOp.h" #include "Framework/Application/MenuStack.h" #include "Framework/Application/SlateApplication.h" #include "Framework/Commands/UIAction.h" #include "Framework/Commands/UICommandList.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Framework/MultiBox/MultiBoxExtender.h" #include "Framework/SlateDelegates.h" #include "Framework/Views/ITypedTableView.h" #include "GenericPlatform/ICursor.h" #include "HistoryManager.h" #include "ICollectionContainer.h" #include "ICollectionManager.h" #include "ISourceControlModule.h" #include "ISourceControlProvider.h" #include "ISourceControlState.h" #include "Input/DragAndDrop.h" #include "Input/Events.h" #include "InputCoreTypes.h" #include "Layout/Children.h" #include "Layout/Geometry.h" #include "Layout/Margin.h" #include "Layout/SlateRect.h" #include "Layout/WidgetPath.h" #include "Math/Color.h" #include "Math/Vector2D.h" #include "Misc/CString.h" #include "Misc/ConfigCacheIni.h" #include "Misc/TextFilterUtils.h" #include "Modules/ModuleManager.h" #include "ProfilingDebugging/CpuProfilerTrace.h" #include "SAssetTagItem.h" #include "SlotBase.h" #include "SourcesSearch.h" #include "SourcesViewWidgets.h" #include "TelemetryRouter.h" #include "Settings/ContentBrowserSettings.h" #include "Styling/AppStyle.h" #include "Styling/ISlateStyle.h" #include "Styling/SlateColor.h" #include "Templates/Tuple.h" #include "Textures/SlateIcon.h" #include "UObject/NameTypes.h" #include "UObject/UnrealNames.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SNullWidget.h" #include "Widgets/SOverlay.h" #include "Widgets/Views/ITableRow.h" #include "Widgets/Views/STableRow.h" class SWidget; struct FSlateBrush; #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; bAllowExternalSearch = false; bDraggedOver = false; bQueueCollectionItemsUpdate = false; bQueueSCCRefresh = true; IsDocked = InArgs._IsDocked; CollectionContainer = InArgs._CollectionContainer; if (CollectionContainer) { CollectionContainer->OnCollectionCreated().AddSP(this, &SCollectionView::HandleCollectionCreated); CollectionContainer->OnCollectionRenamed().AddSP(this, &SCollectionView::HandleCollectionRenamed); CollectionContainer->OnCollectionReparented().AddSP(this, &SCollectionView::HandleCollectionReparented); CollectionContainer->OnCollectionDestroyed().AddSP(this, &SCollectionView::HandleCollectionDestroyed); CollectionContainer->OnCollectionUpdated().AddSP(this, &SCollectionView::HandleCollectionUpdated); CollectionContainer->OnAssetsAddedToCollection().AddSP(this, &SCollectionView::HandleAssetsAddedToCollection); CollectionContainer->OnAssetsRemovedFromCollection().AddSP(this, &SCollectionView::HandleAssetsRemovedFromCollection); } ISourceControlModule::Get().RegisterProviderChanged(FSourceControlProviderChanged::FDelegate::CreateSP(this, &SCollectionView::HandleSourceControlProviderChanged)); SourceControlStateChangedDelegateHandle = ISourceControlModule::Get().GetProvider().RegisterSourceControlStateChanged_Handle(FSourceControlStateChanged::FDelegate::CreateSP(this, &SCollectionView::HandleSourceControlStateChanged)); 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 ( CollectionContainer && InArgs._AllowQuickAssetManagement ) { QuickAssetManagement = MakeShareable(new FCollectionAssetManagement(CollectionContainer.ToSharedRef())); } FOnContextMenuOpening CollectionListContextMenuOpening; if ( InArgs._AllowContextMenu ) { CollectionListContextMenuOpening = FOnContextMenuOpening::CreateSP( this, &SCollectionView::MakeCollectionTreeContextMenu ); } SearchPtr = InArgs._ExternalSearch; if (SearchPtr) { SearchPtr->OnSearchChanged().AddSP(this, &SCollectionView::SetCollectionsSearchFilterText); } ExternalSearchPtr = InArgs._ExternalSearch; TitleContent = SNew(SHorizontalBox); UContentBrowserCollectionProjectSettings* CollectionProjectSettings = GetMutableDefault(); if (CollectionProjectSettings) { CollectionProjectSettings->OnSettingChanged().AddSPLambda(this, [this](UObject* CollectionProjectSettings_, struct FPropertyChangedEvent&) { UpdateFilteredCollectionItems(); }); } PreventSelectionChangedDelegateCount = 0; TSharedRef< SWidget > HeaderContent = SNew(SHorizontalBox) .Visibility(this, &SCollectionView::GetHeaderVisibility) + SHorizontalBox::Slot() .FillWidth(1.0f) .Padding(0.0f) [ TitleContent.ToSharedRef() ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(2.0f, 0.0f, 0.0f, 0.0f) [ SNew(SButton) .ButtonStyle(FAppStyle::Get(), "SimpleButton") .ToolTipText(LOCTEXT("AddCollectionButtonTooltip", "Add a collection.")) .OnClicked(this, &SCollectionView::OnAddCollectionClicked) .ContentPadding( FMargin(2, 2) ) .Visibility(this, &SCollectionView::GetAddCollectionButtonVisibility) [ SNew(SImage) .Image(FAppStyle::Get().GetBrush("Icons.PlusCircle")) .ColorAndOpacity(FSlateColor::UseForeground()) ] ]; TSharedRef< SWidget > BodyContent = SNew(SVerticalBox) // Collections tree +SVerticalBox::Slot() .FillHeight(1.f) [ SAssignNew(CollectionTreePtr, STreeView< TSharedPtr >) .TreeItemsSource(&VisibleRootCollectionItems) .OnGenerateRow(this, &SCollectionView::GenerateCollectionRow) .OnGetChildren(this, &SCollectionView::GetCollectionItemChildren) .SelectionMode(ESelectionMode::Multi) .OnSelectionChanged(this, &SCollectionView::CollectionSelectionChanged) .OnContextMenuOpening(CollectionListContextMenuOpening) .OnItemScrolledIntoView(this, &SCollectionView::CollectionItemScrolledIntoView) .ClearSelectionOnClick(false) .Visibility(this, &SCollectionView::GetCollectionTreeVisibility) ]; ChildSlot [ SNew(SOverlay) // Main content +SOverlay::Slot() [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .Padding(FMargin(12.0f, 0.0f, 0.0f, 0.0f)) [ HeaderContent ] + SVerticalBox::Slot() [ BodyContent ] ] // Drop target overlay +SOverlay::Slot() [ SNew(SBorder) .Padding(0) .Visibility(EVisibility::HitTestInvisible) .BorderImage(this, &SCollectionView::GetCollectionViewDropTargetBorder) .BorderBackgroundColor(FLinearColor::Yellow) [ SNullWidget::NullWidget ] ] ]; UpdateCollectionItems(); } bool SCollectionView::IsEmpty() const { return AvailableCollections.IsEmpty(); } void SCollectionView::HandleCollectionCreated( ICollectionContainer& InCollectionContainer, const FCollectionNameType& Collection ) { bQueueCollectionItemsUpdate = true; } void SCollectionView::HandleCollectionRenamed( ICollectionContainer& InCollectionContainer, const FCollectionNameType& OriginalCollection, const FCollectionNameType& NewCollection ) { bQueueCollectionItemsUpdate = true; // Rename the item in-place so we can maintain its expansion and selection states correctly once the view is refreshed on the next Tick TSharedPtr CollectionItem = AvailableCollections.FindRef(OriginalCollection); if (CollectionItem.IsValid()) { CollectionItem->CollectionName = NewCollection.Name; CollectionItem->CollectionType = NewCollection.Type; AvailableCollections.Remove(OriginalCollection); AvailableCollections.Add(NewCollection, CollectionItem); } } void SCollectionView::HandleCollectionReparented( ICollectionContainer& InCollectionContainer, const FCollectionNameType& Collection, const TOptional& OldParent, const TOptional& NewParent ) { bQueueCollectionItemsUpdate = true; } void SCollectionView::HandleCollectionDestroyed( ICollectionContainer& InCollectionContainer, const FCollectionNameType& Collection ) { bQueueCollectionItemsUpdate = true; } void SCollectionView::HandleCollectionUpdated( ICollectionContainer& InCollectionContainer, const FCollectionNameType& Collection ) { TSharedPtr CollectionItemToUpdate = AvailableCollections.FindRef(Collection); if (CollectionItemToUpdate.IsValid()) { bQueueSCCRefresh = true; CollectionItemToUpdate->CollectionColor = CollectionViewUtils::ResolveColor(InCollectionContainer, Collection.Name, Collection.Type); UpdateCollectionItemStatus(CollectionItemToUpdate.ToSharedRef()); } } void SCollectionView::HandleAssetsAddedToCollection( ICollectionContainer& InCollectionContainer, const FCollectionNameType& Collection, TConstArrayView AssetsAdded ) { HandleCollectionUpdated(InCollectionContainer, Collection); } void SCollectionView::HandleAssetsRemovedFromCollection( ICollectionContainer& InCollectionContainer, const FCollectionNameType& Collection, TConstArrayView AssetsRemoved ) { HandleCollectionUpdated(InCollectionContainer, Collection); } void SCollectionView::HandleSourceControlProviderChanged(ISourceControlProvider& OldProvider, ISourceControlProvider& NewProvider) { OldProvider.UnregisterSourceControlStateChanged_Handle(SourceControlStateChangedDelegateHandle); SourceControlStateChangedDelegateHandle = NewProvider.RegisterSourceControlStateChanged_Handle(FSourceControlStateChanged::FDelegate::CreateSP(this, &SCollectionView::HandleSourceControlStateChanged)); bQueueSCCRefresh = true; HandleSourceControlStateChanged(); } void SCollectionView::HandleSourceControlStateChanged() { bQueueItemStatusUpdate = true; } void SCollectionView::UpdateCollectionItemStatus( const TSharedRef& CollectionItem ) { TRACE_CPUPROFILER_EVENT_SCOPE(SCollectionView::UpdateCollectionItemStatus); int32 NewObjectCount = 0; TOptional NewStatus; // Check IsModuleAvailable as we might be in the process of shutting down, and were notified due to the SCC provider being nulled out... if (ensure(CollectionItem->CollectionContainer)) { FCollectionStatusInfo StatusInfo; if (CollectionItem->CollectionContainer->GetCollectionStatusInfo(CollectionItem->CollectionName, CollectionItem->CollectionType, StatusInfo)) { NewObjectCount = StatusInfo.NumObjects; // Test the SCC state first as this should take priority when reporting the status back to the user if (StatusInfo.bUseSCC) { if (StatusInfo.SCCState.IsValid() && StatusInfo.SCCState->IsSourceControlled()) { if (StatusInfo.SCCState->IsCheckedOutOther()) { NewStatus = ECollectionItemStatus::IsCheckedOutByAnotherUser; } else if (StatusInfo.SCCState->IsConflicted()) { NewStatus = ECollectionItemStatus::IsConflicted; } else if (!StatusInfo.SCCState->IsCurrent()) { NewStatus = ECollectionItemStatus::IsOutOfDate; } else if (StatusInfo.SCCState->IsModified()) { NewStatus = ECollectionItemStatus::HasLocalChanges; } } else { NewStatus = ECollectionItemStatus::IsMissingSCCProvider; } } // Not set by the SCC status, so check just use the local state if (!NewStatus.IsSet()) { if (StatusInfo.bIsDirty) { NewStatus = ECollectionItemStatus::HasLocalChanges; } else if (StatusInfo.bIsEmpty) { NewStatus = ECollectionItemStatus::IsUpToDateAndEmpty; } else { NewStatus = ECollectionItemStatus::IsUpToDateAndPopulated; } } } } CollectionItem->NumObjects = NewObjectCount; CollectionItem->CurrentStatus = NewStatus.Get(ECollectionItemStatus::IsUpToDateAndEmpty); } void SCollectionView::UpdateCollectionItems() { struct FGatherCollectionItems { static void GatherCollectionItems(const TSharedRef& InCollectionContainer, FAvailableCollectionsMap& OutAvailableCollections) { TArray RootCollections; InCollectionContainer->GetRootCollections(RootCollections); ProcessGatheredCollectionsAndRecurse(InCollectionContainer, RootCollections, nullptr, OutAvailableCollections); } private: static void GatherChildCollectionItems(const TSharedRef& InCollectionContainer, const TSharedPtr& InParentCollectionItem, FAvailableCollectionsMap& OutAvailableCollections) { TArray ChildCollections; InCollectionContainer->GetChildCollections(InParentCollectionItem->CollectionName, InParentCollectionItem->CollectionType, ChildCollections); ProcessGatheredCollectionsAndRecurse(InCollectionContainer, ChildCollections, InParentCollectionItem, OutAvailableCollections); } static void ProcessGatheredCollectionsAndRecurse(const TSharedRef& InCollectionContainer, const TArray& InCollections, const TSharedPtr& InParentCollectionItem, FAvailableCollectionsMap& OutAvailableCollections) { for (const FCollectionNameType& Collection : InCollections) { // Never display system collections if (Collection.Type == ECollectionShareType::CST_System) { continue; } TSharedRef CollectionItem = MakeShareable(new FCollectionItem(InCollectionContainer, Collection.Name, Collection.Type)); OutAvailableCollections.Add(Collection, CollectionItem); InCollectionContainer->GetCollectionStorageMode(Collection.Name, Collection.Type, CollectionItem->StorageMode); CollectionItem->CollectionColor = CollectionViewUtils::ResolveColor(*InCollectionContainer, Collection.Name, Collection.Type); SCollectionView::UpdateCollectionItemStatus(CollectionItem); if (InParentCollectionItem.IsValid()) { // Fixup the parent and child pointers InParentCollectionItem->ChildCollections.Add(CollectionItem); CollectionItem->ParentCollection = InParentCollectionItem; } // Recurse GatherChildCollectionItems(InCollectionContainer, CollectionItem, OutAvailableCollections); } } }; // 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)); } } AvailableCollections.Reset(); if (CollectionContainer) { FGatherCollectionItems::GatherCollectionItems(CollectionContainer.ToSharedRef(), AvailableCollections); } UpdateFilteredCollectionItems(); // Restore selection and expansion SetSelectedCollections(SelectedCollections, false); SetExpandedCollections(ExpandedCollections); bQueueSCCRefresh = true; } void SCollectionView::UpdateFilteredCollectionItems() { VisibleCollections.Reset(); VisibleRootCollectionItems.Reset(); const UContentBrowserCollectionProjectSettings* CollectionProjectSettings = GetDefault(); const UContentBrowserSettings* ContentBrowserSettings = GetDefault(); auto AddVisibleCollection = [&](const FCollectionNameType& NameTypePair, const TSharedPtr& InCollectionItem) { if (!ContentBrowserSettings->bDisplayExcludedCollections) { const int32 FoundIndex = CollectionProjectSettings->ExcludedCollectionsFromView.Find(NameTypePair.Name); if (FoundIndex != INDEX_NONE) { return; } } VisibleCollections.Add(FCollectionNameType(InCollectionItem->CollectionName, InCollectionItem->CollectionType)); if (!InCollectionItem->ParentCollection.IsValid()) { VisibleRootCollectionItems.AddUnique(InCollectionItem); } }; auto AddVisibleCollectionRecursive = [&](const FCollectionNameType& NameTypePair, const TSharedPtr& InCollectionItem) { TSharedPtr CollectionItemToAdd = InCollectionItem; do { AddVisibleCollection(NameTypePair, 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.Key, 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(AvailableCollectionInfo.Key, 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); } } UContentBrowserSettings::OnSettingChanged().AddSP(this, &SCollectionView::HandleSettingChanged); VisibleRootCollectionItems.Sort(FCollectionItem::FCompareFCollectionItemByName()); CollectionTreePtr->RequestTreeRefresh(); } void SCollectionView::SetCollectionsSearchFilterText( const FText& InSearchText, TArray& OutErrors ) { CollectionItemTextFilter->SetRawFilterText( InSearchText ); const FText ErrorText = CollectionItemTextFilter->GetFilterErrorText(); if (!ErrorText.IsEmpty()) { OutErrors.Add(ErrorText); } } 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) ); // 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(*CollectionItemToSelect)) { SearchPtr->ClearSearch(); } } } } 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(); } const TSharedPtr& SCollectionView::GetCollectionContainer() const { return CollectionContainer; } 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::SetSelectedAssetPaths(const TArray& SelectedAssets) { if ( QuickAssetManagement.IsValid() ) { QuickAssetManagement->SetCurrentAssetPaths(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 (const FCollectionRef& HistoryCollection : History.ContentSources.GetCollections()) { if (CollectionContainer == HistoryCollection.Container) { TSharedPtr CollectionHistoryItem = AvailableCollections.FindRef(FCollectionNameType(HistoryCollection.Name, HistoryCollection.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); }; 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; }; // 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); } } void SCollectionView::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) { SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); if (bQueueCollectionItemsUpdate) { bQueueCollectionItemsUpdate = false; UpdateCollectionItems(); } if (bQueueSCCRefresh && CollectionContainer && ISourceControlModule::Get().IsEnabled()) { bQueueSCCRefresh = false; TArray CollectionFilesToRefresh; for (const auto& AvailableCollectionInfo : AvailableCollections) { FCollectionStatusInfo StatusInfo; if (CollectionContainer->GetCollectionStatusInfo(AvailableCollectionInfo.Value->CollectionName, AvailableCollectionInfo.Value->CollectionType, StatusInfo)) { if (StatusInfo.bUseSCC && StatusInfo.SCCState.IsValid() && StatusInfo.SCCState->IsSourceControlled()) { CollectionFilesToRefresh.Add(StatusInfo.SCCState->GetFilename()); } } } if (CollectionFilesToRefresh.Num() > 0) { ISourceControlModule::Get().QueueStatusUpdate(CollectionFilesToRefresh); } } if (bQueueItemStatusUpdate) { TRACE_CPUPROFILER_EVENT_SCOPE_STR("CollectionView Update Collection Items Status"); bQueueItemStatusUpdate = false; // Update the status of each collection for (const TPair>& AvailableCollectionInfo : AvailableCollections) { UpdateCollectionItemStatus(AvailableCollectionInfo.Value.ToSharedRef()); } } } 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(); } void SCollectionView::MakeSaveDynamicCollectionMenu(FMenuBuilder& InMenuBuilder, FText InQueryString) { CollectionContextMenu->UpdateProjectSourceControl(); CollectionContextMenu->MakeSaveDynamicCollectionSubMenu(InMenuBuilder, InQueryString); } FReply SCollectionView::OnAddCollectionClicked() { MakeAddCollectionMenu(AsShared()); return FReply::Handled(); } bool SCollectionView::ShouldAllowSelectionChangedDelegate() const { return PreventSelectionChangedDelegateCount == 0; } void SCollectionView::MakeAddCollectionMenu(TSharedRef MenuParent) { // 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, ECollectionStorageMode::Static, SCollectionView::FCreateCollectionPayload()); FSlateApplication::Get().PushMenu( MenuParent, FWidgetPath(), MenuBuilder.MakeWidget(), FSlateApplication::Get().GetCursorPos(), FPopupTransitionEffect( FPopupTransitionEffect::TopMenu ) ); } FVector2D SCollectionView::GetScrollDistance() { if (!CollectionTreePtr.IsValid()) { return FVector2D::ZeroVector; } return CollectionTreePtr->GetScrollDistance(); } FVector2D SCollectionView::GetScrollDistanceRemaining() { if (!CollectionTreePtr.IsValid()) { return FVector2D::ZeroVector; } return CollectionTreePtr->GetScrollDistanceRemaining(); } TSharedRef SCollectionView::GetScrollWidget() { return SharedThis(this); } void SCollectionView::CreateCollectionItem( ECollectionShareType::Type CollectionType, ECollectionStorageMode::Type StorageMode, const FCreateCollectionPayload& InCreationPayload ) { if ( ensure(CollectionContainer && CollectionType != ECollectionShareType::CST_All) ) { const FName BaseCollectionName = *LOCTEXT("NewCollectionName", "NewCollection").ToString(); FName CollectionName; CollectionContainer->CreateUniqueCollectionName(BaseCollectionName, ECollectionShareType::CST_All, CollectionName); TSharedPtr NewItem = MakeShareable(new FCollectionItem(CollectionContainer.ToSharedRef(), CollectionName, CollectionType)); NewItem->StorageMode = StorageMode; // Adding a new collection now, so clear any filter we may have applied SearchPtr->ClearSearch(); if ( InCreationPayload.ParentCollection.IsSet() ) { TSharedPtr ParentCollectionItem = AvailableCollections.FindRef(InCreationPayload.ParentCollection.GetValue()); if ( ParentCollectionItem.IsValid() ) { ParentCollectionItem->ChildCollections.Add(NewItem); NewItem->ParentCollection = ParentCollectionItem; // Make sure the parent is expanded so we can see its newly added child item CollectionTreePtr->SetItemExpansion(ParentCollectionItem, true); } } // 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; NewItem->OnCollectionCreatedEvent = InCreationPayload.OnCollectionCreatedEvent; 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; for (const TSharedPtr& ItemToDelete : ItemsToDelete) { if (!ensure(ItemToDelete->CollectionContainer)) { continue; } FText Error; if (ItemToDelete->CollectionContainer->DestroyCollection(ItemToDelete->CollectionName, ItemToDelete->CollectionType, &Error)) { if (PreviouslySelectedItems.Contains(ItemToDelete)) { ++NumSelectedItemsDeleted; } } else { // Display a warning const FVector2f& 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}"), Error), MessageAnchor, CollectionTreePtr.ToSharedRef(), ContentBrowserUtils::EDisplayMessageType::Error ); } } // 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; } EVisibility SCollectionView::GetHeaderVisibility() const { return IsDocked.Get() ? EVisibility::Collapsed : EVisibility::SelfHitTestInvisible; } EVisibility SCollectionView::GetAddCollectionButtonVisibility() const { return bAllowCollectionButtons && CollectionContainer.IsValid() && !CollectionContainer->IsReadOnly(ECollectionShareType::CST_All) ? EVisibility::Visible : EVisibility::Collapsed; } const FSlateBrush* SCollectionView::GetCollectionViewDropTargetBorder() const { return bDraggedOver ? UE::ContentBrowser::Private::FContentBrowserStyle::Get().GetBrush("ContentBrowser.CollectionTreeDragDropBorder") : FAppStyle::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() ) { // Can only manage assets for static collections if (CollectionItem->StorageMode == ECollectionStorageMode::Static) { IsCollectionCheckBoxEnabledAttribute.Bind(TAttribute::FGetter::CreateSP(this, &SCollectionView::IsCollectionCheckBoxEnabled, CollectionItem)); IsCollectionCheckedAttribute.Bind(TAttribute::FGetter::CreateSP(this, &SCollectionView::IsCollectionChecked, CollectionItem)); OnCollectionCheckStateChangedDelegate.BindSP(this, &SCollectionView::OnCollectionCheckStateChanged, CollectionItem); } else { IsCollectionCheckBoxEnabledAttribute.Bind(TAttribute::FGetter::CreateLambda([]{ return false; })); IsCollectionCheckedAttribute.Bind(TAttribute::FGetter::CreateLambda([]{ return ECheckBoxState::Unchecked; })); } } TSharedPtr< SAssetTagItemTableRow< TSharedPtr > > TableRow = SNew( SAssetTagItemTableRow< TSharedPtr >, OwnerTable ) .OnDragDetected(this, &SCollectionView::OnCollectionDragDetected); TSharedPtr CollectionTreeItem; TableRow->SetContent ( SAssignNew(CollectionTreeItem, 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) ); TableRow->SetIsDropTarget(MakeAttributeSP(CollectionTreeItem.Get(), &SCollectionTreeItem::IsDraggedOver)); 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) { TArray CollectionRefs; CollectionRefs.Reserve(SelectedCollections.Num()); Algo::Transform( SelectedCollections, CollectionRefs, [this](const FCollectionNameType& Collection) { return FCollectionRef(CollectionContainer, Collection); }); TSharedRef DragDropOp = FCollectionDragDropOp::New(CollectionRefs); CurrentCollectionDragDropOp = DragDropOp; return FReply::Handled().BeginDragDrop(DragDropOp); } } return FReply::Unhandled(); } bool SCollectionView::ValidateDragDropOnCollectionTree(const FGeometry& Geometry, const FDragDropEvent& DragDropEvent, bool& OutIsKnownDragOperation) { OutIsKnownDragOperation = false; if (!CollectionContainer.IsValid()) { return false; } TSharedPtr Operation = DragDropEvent.GetOperation(); if (!Operation.IsValid()) { return false; } if (Operation->IsOfType()) { TSharedPtr DragDropOp = StaticCastSharedPtr(Operation); OutIsKnownDragOperation = true; if (Algo::AllOf(DragDropOp->CollectionRefs, [this](const FCollectionRef& Collection) { return CollectionContainer == Collection.Container; })) { return true; } else { DragDropOp->SetToolTip( LOCTEXT("InvalidParentCollectionContainer", "A collection cannot be parented to a different collection container"), FAppStyle::GetBrush(TEXT("Graph.ConnectorFeedback.Error"))); Operation->SetCursorOverride(EMouseCursor::SlashedCircle); } } else if (Operation->IsOfType()) { TSharedPtr DragDropOp = StaticCastSharedPtr(Operation); if (!DragDropOp->HasAssets()) { DragDropOp->SetCursorOverride(EMouseCursor::SlashedCircle); DragDropOp->SetToolTip(LOCTEXT("CollectionView_DragDrop_NoAsset", "There is no asset being dragged"), FAppStyle::GetBrush(TEXT("Graph.ConnectorFeedback.Error"))); return false; } const int32 NumDraggedItems = DragDropOp->GetAssets().Num(); const FText FirstItemText = FText::FromName(DragDropOp->GetAssets()[0].AssetName); const FText AddToCollectionText = (NumDraggedItems > 1) ? FText::Format(LOCTEXT("CollectionView_DragDrop_MultipleItems", "Add '{0}' and {1} {1}|plural(one=other,other=others)"), FirstItemText, NumDraggedItems - 1) : FText::Format(LOCTEXT("CollectionView_DragDrop_SingularItems", "Add '{0}'"), FirstItemText); DragDropOp->SetToolTip(AddToCollectionText, FAppStyle::GetBrush(TEXT("Graph.ConnectorFeedback.OK"))); 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()); 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 FCollectionRef& NewChildCollection : DragDropOp->CollectionRefs) { check(CollectionContainer == NewChildCollection.Container); FText Error; if (!CollectionContainer->ReparentCollection( NewChildCollection.Name, NewChildCollection.Type, NAME_None, ECollectionShareType::CST_All, &Error )) { ContentBrowserUtils::DisplayMessage(Error, Geometry.GetLayoutBoundingRect(), SharedThis(this), ContentBrowserUtils::EDisplayMessageType::Error); } } return FReply::Handled(); } return FReply::Unhandled(); } bool SCollectionView::ValidateDragDropOnCollectionItem(TSharedRef CollectionItem, const FGeometry& Geometry, const FDragDropEvent& DragDropEvent, bool& OutIsKnownDragOperation) { OutIsKnownDragOperation = false; if (!ensure(CollectionItem->CollectionContainer)) { return false; } TSharedPtr Operation = DragDropEvent.GetOperation(); if (!Operation.IsValid()) { return false; } bool bIsValidDrag = false; TOptional NewDragCursor; if (Operation->IsOfType()) { TSharedPtr DragDropOp = StaticCastSharedPtr(Operation); OutIsKnownDragOperation = true; bIsValidDrag = true; for (const FCollectionRef& PotentialChildCollection : DragDropOp->CollectionRefs) { FText Error; if (CollectionItem->CollectionContainer == PotentialChildCollection.Container) { bIsValidDrag = CollectionItem->CollectionContainer->IsValidParentCollection( PotentialChildCollection.Name, PotentialChildCollection.Type, CollectionItem->CollectionName, CollectionItem->CollectionType, &Error); } else { bIsValidDrag = false; Error = LOCTEXT("InvalidParentCollectionContainer", "A collection cannot be parented to a different collection container"); } if (!bIsValidDrag) { DragDropOp->SetToolTip(Error, FAppStyle::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->HasAssets(); if (!bIsValidDrag) { DragDropOp->SetToolTip(LOCTEXT("CollectionViewItem_DragDrop_NoAsset", "There is no asset being dragged"), FAppStyle::GetBrush(TEXT("Graph.ConnectorFeedback.Error"))); } else { const int32 NumDraggedItems = DragDropOp->GetAssets().Num(); const FText FirstItemText = FText::FromName(DragDropOp->GetAssets()[0].AssetName); const FText AddToCollectionText = (NumDraggedItems > 1) ? FText::Format(LOCTEXT("CollectionViewItem_DragDrop_MultipleItems", "Add '{0}' and {1} {1}|plural(one=other,other=others)"), FirstItemText, NumDraggedItems - 1) : FText::Format(LOCTEXT("CollectionViewItem_DragDrop_SingularItems", "Add '{0}'"), FirstItemText); DragDropOp->SetToolTip(AddToCollectionText, FAppStyle::GetBrush(TEXT("Graph.ConnectorFeedback.OK"))); } } // 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()); 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 FCollectionRef& NewChildCollection : DragDropOp->CollectionRefs) { check(CollectionItem->CollectionContainer == NewChildCollection.Container); FText Error; if (!CollectionItem->CollectionContainer->ReparentCollection( NewChildCollection.Name, NewChildCollection.Type, CollectionItem->CollectionName, CollectionItem->CollectionType, &Error )) { ContentBrowserUtils::DisplayMessage(Error, Geometry.GetLayoutBoundingRect(), SharedThis(this), ContentBrowserUtils::EDisplayMessageType::Error); } } return FReply::Handled(); } else if (Operation->IsOfType()) { TSharedPtr DragDropOp = StaticCastSharedPtr(Operation); const TArray& DroppedAssets = DragDropOp->GetAssets(); TArray ObjectPaths; ObjectPaths.Reserve(DroppedAssets.Num()); for (const FAssetData& AssetData : DroppedAssets) { ObjectPaths.Add(AssetData.GetSoftObjectPath()); } const double BeginTimeSec = FPlatformTime::Seconds(); int32 NumAdded = 0; FText Message; ContentBrowserUtils::EDisplayMessageType MessageType = ContentBrowserUtils::EDisplayMessageType::Info; if (CollectionItem->CollectionContainer->AddToCollection( CollectionItem->CollectionName, CollectionItem->CollectionType, ObjectPaths, &NumAdded, &Message)) { if (UE::Editor::ContentBrowser::IsNewStyleEnabled()) { Message = FText::Format(LOCTEXT("AddingToCollection", "{0} {0}|plural(one=item,other=items) added to '{1}'"), NumAdded, FText::FromName(CollectionItem->CollectionName)); MessageType = ContentBrowserUtils::EDisplayMessageType::Successful; } else { if (DroppedAssets.Num() == 1) { FFormatNamedArguments Args; Args.Add(TEXT("AssetName"), FText::FromName(DroppedAssets[0].AssetName)); Args.Add(TEXT("CollectionName"), FText::FromName(CollectionItem->CollectionName)); Message = FText::Format(LOCTEXT("CollectionAssetAdded", "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); } MessageType = ContentBrowserUtils::EDisplayMessageType::Info; } const double DurationSec = FPlatformTime::Seconds() - BeginTimeSec; { FAssetAddedToCollectionTelemetryEvent AssetAdded; AssetAdded.DurationSec = DurationSec; AssetAdded.NumAdded = NumAdded; AssetAdded.CollectionShareType = CollectionItem->CollectionType; AssetAdded.Workflow = ECollectionTelemetryAssetAddedWorkflow::DragAndDrop; FTelemetryRouter::Get().ProvideTelemetry(AssetAdded); } } // Added items to the collection or failed. Either way, display the message. ContentBrowserUtils::DisplayMessage(Message, Geometry.GetLayoutBoundingRect(), SharedThis(this), MessageType); return FReply::Handled(); } return FReply::Unhandled(); } void SCollectionView::HandleSettingChanged(FName PropertyName) { if (PropertyName == GET_MEMBER_NAME_CHECKED(UContentBrowserSettings, bDisplayExcludedCollections)) { UpdateFilteredCollectionItems(); } } 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() ) { FCollectionNameType CollectionKey(CollectionItem->CollectionName, CollectionItem->CollectionType); if (QuickAssetManagement->GetCollectionCheckState(CollectionKey) == ECheckBoxState::Checked) { QuickAssetManagement->RemoveCurrentAssetsFromCollection(CollectionKey); } else { QuickAssetManagement->AddCurrentAssetsToCollection(CollectionKey); } } } 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->CollectionRefs.ContainsByPredicate([&SelectedCollectionItem](const FCollectionRef& Collection) { return SelectedCollectionItem->CollectionContainer == Collection.Container && SelectedCollectionItem->CollectionName == Collection.Name && SelectedCollectionItem->CollectionType == Collection.Type; })) { return true; } } } CollectionContextMenu->UpdateProjectSourceControl(); return !CollectionContextMenu->CanRenameSelectedCollections(); } bool SCollectionView::CollectionNameChangeCommit( const TSharedPtr< FCollectionItem >& CollectionItem, const FString& NewName, bool bChangeConfirmed, FText& OutWarningMessage ) { if (!ensure(CollectionItem->CollectionContainer)) { return false; } // 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; // Cache this here as CreateCollection will invalidate the current parent pointer TOptional NewCollectionParentKey; TSharedPtr ParentCollectionItem = CollectionItem->ParentCollection.Pin(); if ( ParentCollectionItem.IsValid() ) { NewCollectionParentKey = FCollectionNameType(ParentCollectionItem->CollectionName, ParentCollectionItem->CollectionType); } double BeginTimeSec = FPlatformTime::Seconds(); // If we 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; } FText Error; if ( !CollectionItem->CollectionContainer->CreateCollection(NewNameFinal, CollectionItem->CollectionType, CollectionItem->StorageMode, &Error) ) { // 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}"), Error); return false; } // Since we're really adding a new collection (as our placeholder item is currently transient), we don't get a rename event from the collections manager // We'll spoof one here that so that our placeholder tree item is updated with the final name - this will preserve its expansion and selection state HandleCollectionRenamed( *CollectionItem->CollectionContainer, FCollectionNameType(CollectionItem->CollectionName, CollectionItem->CollectionType), FCollectionNameType(NewNameFinal, CollectionItem->CollectionType) ); if ( NewCollectionParentKey.IsSet() ) { // Try and set the parent correctly (if this fails for any reason, the collection will still be added, but will just appear at the root) CollectionItem->CollectionContainer->ReparentCollection(NewNameFinal, CollectionItem->CollectionType, NewCollectionParentKey->Name, NewCollectionParentKey->Type); } // Notify anything that cares that this collection has been created now if ( CollectionItem->OnCollectionCreatedEvent.IsBound()) { CollectionItem->OnCollectionCreatedEvent.Execute(FCollectionNameType(NewNameFinal, CollectionItem->CollectionType)); CollectionItem->OnCollectionCreatedEvent.Unbind(); } { FCollectionCreatedTelemetryEvent CollectionCreatedEvent; CollectionCreatedEvent.DurationSec = FPlatformTime::Seconds() - BeginTimeSec; CollectionCreatedEvent.CollectionShareType = CollectionItem->CollectionType; FTelemetryRouter::Get().ProvideTelemetry(CollectionCreatedEvent); } } 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(CollectionItem->CollectionContainer.ToSharedRef(), NewNameFinal, CollectionItem->CollectionType) ) ) { SearchPtr->ClearSearch(); } // Otherwise perform the rename FText Error; if ( !CollectionItem->CollectionContainer->RenameCollection(CollectionItem->CollectionName, CollectionItem->CollectionType, NewNameFinal, CollectionItem->CollectionType, &Error) ) { // Failed to rename the collection OutWarningMessage = FText::Format( LOCTEXT("RenameCollectionFailed", "Failed to rename the collection. {0}"), Error); 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, CollectionItem->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) { // If the new name is the same as the old name, consider this to be unchanged, and accept it. if (CollectionItem->CollectionName.ToString() == NewName) { return true; } if (!ensure(CollectionItem->CollectionContainer) || !CollectionItem->CollectionContainer->IsValidCollectionName(NewName, ECollectionShareType::CST_All, &OutErrorMessage)) { return false; } return true; } #undef LOCTEXT_NAMESPACE