// Copyright Epic Games, Inc. All Rights Reserved. #include "Palette/SPaletteViewModel.h" #include "Palette/SPaletteView.h" #include "UObject/UObjectIterator.h" #include "Widgets/Views/STableViewBase.h" #include "Widgets/Views/STableRow.h" #include "WidgetBlueprint.h" #include "Editor.h" #if WITH_EDITOR #include "Styling/AppStyle.h" #endif // WITH_EDITOR #include "ClassViewerModule.h" #include "ClassViewerFilter.h" #include "EditorClassUtils.h" #include "Engine/BlueprintGeneratedClass.h" #include "DragDrop/WidgetTemplateDragDropOp.h" #include "Templates/WidgetTemplateClass.h" #include "Templates/WidgetTemplateBlueprintClass.h" #include "AssetRegistry/AssetRegistryModule.h" #include "AssetRegistry/AssetRegistryHelpers.h" #include "WidgetBlueprintEditorUtils.h" #include "Misc/NamePermissionList.h" #include "Settings/ContentBrowserSettings.h" #include "Settings/WidgetDesignerSettings.h" #include "WidgetEditingProjectSettings.h" #include "WidgetPaletteFavorites.h" #define LOCTEXT_NAMESPACE "UMG" FWidgetTemplateViewModel::FWidgetTemplateViewModel() : FavortiesViewModel(nullptr), bIsFavorite(false) { } FText FWidgetTemplateViewModel::GetName() const { return Template->Name; } bool FWidgetTemplateViewModel::IsTemplate() const { return true; } void FWidgetTemplateViewModel::GetFilterStrings(TArray& OutStrings) const { Template->GetFilterStrings(OutStrings); } TSharedRef FWidgetTemplateViewModel::BuildRow(const TSharedRef& OwnerTable) { return SNew(STableRow>, OwnerTable) .Padding(2.0f) .OnDragDetected(this, &FWidgetTemplateViewModel::OnDraggingWidgetTemplateItem) [ SNew(SPaletteViewItem, SharedThis(this)) .HighlightText(FavortiesViewModel, &FWidgetCatalogViewModel::GetSearchText) ]; } FReply FWidgetTemplateViewModel::OnDraggingWidgetTemplateItem(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { return FReply::Handled().BeginDragDrop(FWidgetTemplateDragDropOp::New(Template)); } void FWidgetTemplateViewModel::AddToFavorites() { bIsFavorite = true; FavortiesViewModel->AddToFavorites(this); } void FWidgetTemplateViewModel::RemoveFromFavorites() { bIsFavorite = false; FavortiesViewModel->RemoveFromFavorites(this); } TSharedRef FWidgetHeaderViewModel::BuildRow(const TSharedRef& OwnerTable) { return SNew(STableRow>, OwnerTable) .Style(FAppStyle::Get(), "UMGEditor.PaletteHeader") .Padding(5.0f) .ShowSelection(false) [ SNew(STextBlock) .TransformPolicy(ETextTransformPolicy::ToUpper) .Text(GroupName) .Font(FAppStyle::Get().GetFontStyle("SmallFontBold")) ]; } void FWidgetHeaderViewModel::GetChildren(TArray< TSharedPtr >& OutChildren) { for (TSharedPtr& Child : Children) { OutChildren.Add(Child); } } FWidgetCatalogViewModel::FWidgetCatalogViewModel(TSharedPtr InBlueprintEditor) : bRebuildRequested(true) { BlueprintEditor = InBlueprintEditor; FavoriteHeader = MakeShareable(new FWidgetHeaderViewModel()); FavoriteHeader->GroupName = LOCTEXT("Favorites", "Favorites"); } void FWidgetCatalogViewModel::RegisterToEvents() { // Register for events that can trigger a palette rebuild GEditor->OnBlueprintReinstanced().AddRaw(this, &FWidgetCatalogViewModel::OnBlueprintReinstanced); FEditorDelegates::OnAssetsDeleted.AddSP(this, &FWidgetCatalogViewModel::HandleOnAssetsDeleted); FCoreUObjectDelegates::ReloadCompleteDelegate.AddSP(this, &FWidgetCatalogViewModel::OnReloadComplete); // register for any objects replaced FCoreUObjectDelegates::OnObjectsReplaced.AddRaw(this, &FWidgetCatalogViewModel::OnObjectsReplaced); // Register for favorite list update to handle the case where a favorite is added in another window of the UMG Designer UWidgetPaletteFavorites* Favorites = GetDefault()->Favorites; Favorites->OnFavoritesUpdated.AddSP(this, &FWidgetCatalogViewModel::OnFavoritesUpdated); } FWidgetCatalogViewModel::~FWidgetCatalogViewModel() { GEditor->OnBlueprintReinstanced().RemoveAll(this); FEditorDelegates::OnAssetsDeleted.RemoveAll(this); FCoreUObjectDelegates::ReloadCompleteDelegate.RemoveAll(this); FCoreUObjectDelegates::OnObjectsReplaced.RemoveAll(this); UWidgetPaletteFavorites* Favorites = GetDefault()->Favorites; Favorites->OnFavoritesUpdated.RemoveAll(this); } void FWidgetCatalogViewModel::Update() { if (bRebuildRequested) { OnUpdating.Broadcast(); BuildWidgetList(); bRebuildRequested = false; OnUpdated.Broadcast(); } } UWidgetBlueprint* FWidgetCatalogViewModel::GetBlueprint() const { if (BlueprintEditor.IsValid()) { UBlueprint* BP = BlueprintEditor.Pin()->GetBlueprintObj(); return Cast(BP); } return NULL; } void FWidgetCatalogViewModel::BuildWidgetList() { // Clear the current list of view models and categories WidgetViewModels.Reset(); WidgetTemplateCategories.Reset(); // Generate a list of templates BuildClassWidgetList(); // Clear the Favorite section bool bHasFavorites = FavoriteHeader->Children.Num() != 0; FavoriteHeader->Children.Reset(); // Build ViewModel and clean the Favorite list if needed { // Copy of the list of favorites to be able to do some cleanup in the real list UWidgetPaletteFavorites* FavoritesPalette = GetDefault()->Favorites; TArray FavoritesList = FavoritesPalette->GetFavorites(); // For each entry in the category create a view model for the widget template for (auto& Entry : WidgetTemplateCategories) { BuildWidgetTemplateCategory(Entry.Key, Entry.Value, FavoritesList); } // Remove all Favorites that may be left in the list.Typically happening when the list of favorite contains widget that were deleted since the last opening. for (const FString& FavoriteName : FavoritesList) { FavoritesPalette->Remove(FavoriteName); } } // Sort the view models by name WidgetViewModels.Sort([] (TSharedPtr L, TSharedPtr R) { return R->GetName().CompareTo(L->GetName()) > 0; }); // Add the Favorite section at the top if (FavoriteHeader->Children.Num() != 0) { // We force expansion of the favorite header when we add favorites for the first time. FavoriteHeader->SetForceExpansion(!bHasFavorites); FavoriteHeader->Children.Sort([](TSharedPtr L, TSharedPtr R) { return R->GetName().CompareTo(L->GetName()) > 0; }); WidgetViewModels.Insert(FavoriteHeader, 0); } // Take the Advanced Section, and put it at the end. TSharedPtr* advancedSectionPtr = WidgetViewModels.FindByPredicate([](TSharedPtr widget) {return widget->GetName().CompareTo(LOCTEXT("Advanced", "Advanced")) == 0; }); if (advancedSectionPtr) { TSharedPtr advancedSection = *advancedSectionPtr; WidgetViewModels.Remove(advancedSection); WidgetViewModels.Push(advancedSection); } } void FWidgetCatalogViewModel::BuildClassWidgetList() { const UClass* ActiveWidgetBlueprintClass = GetBlueprint()->GeneratedClass; FName ActiveWidgetBlueprintClassName = ActiveWidgetBlueprintClass->GetFName(); TSharedPtr PinnedBPEditor = BlueprintEditor.Pin(); if (!PinnedBPEditor) { return; } // Locate all UWidget classes from code and loaded widget BPs TArray Widgets; GetDerivedClasses(UWidget::StaticClass(), Widgets); TSet ProcessedPackages; for (UClass* WidgetClass : Widgets) { const bool bIsSameClass = WidgetClass->GetFName() == ActiveWidgetBlueprintClassName; if (bIsSameClass) { continue; } // even if we reject the widget, consider its package processed: ProcessedPackages.Add(WidgetClass->GetPackage()->GetFName()); if (!FWidgetBlueprintEditorUtils::IsUsableWidgetClass(WidgetClass, PinnedBPEditor.ToSharedRef())) { continue; } if (WidgetClass->HasAnyClassFlags(CLASS_HideDropDown | CLASS_Hidden)) { continue; } if (WidgetClass->IsChildOf(UUserWidget::StaticClass())) { AddWidgetTemplate(MakeShared(FAssetData(WidgetClass), WidgetClass)); } else { // For UWidget AddWidgetTemplate(MakeShared(WidgetClass)); } } // Locate all UWidget BP assets, include loaded and unloaded. Only parsed the unloaded. const FAssetRegistryModule* AssetRegistryModule = FModuleManager::GetModulePtr(TEXT("AssetRegistry")); if (!AssetRegistryModule) { return; } TArray AllWidgetClassAssetData; UAssetRegistryHelpers::GetDerivedClassAssetData( { UWidget::StaticClass()->GetClassPathName() }, AllWidgetClassAssetData ); // for a large working set with thousands of entries in AllWidgetClassAssetData // this loop costs us 100ms - acceptable for editor performance. Much of the // time is spent finding the native parent class, which we could probably do // more efficiently. for (const FAssetData& BPAssetData : AllWidgetClassAssetData) { const bool bIsSameClass = BPAssetData.AssetName == ActiveWidgetBlueprintClassName; if (bIsSameClass) { continue; } // Was already parsed by the GetDerivedClasses(UWidget::StaticClass()) if (ProcessedPackages.Contains(BPAssetData.PackageName)) { continue; } ProcessedPackages.Add(BPAssetData.PackageName); TValueOrError Usable = FWidgetBlueprintEditorUtils::IsUsableWidgetClass(BPAssetData, PinnedBPEditor.ToSharedRef()); if (Usable.HasError()) { continue; } if ((Usable.GetValue().AssetClassFlags & (CLASS_Hidden | CLASS_HideDropDown)) != 0) { continue; } if (Usable.GetValue().NativeParentClass->IsChildOf(UUserWidget::StaticClass())) { AddWidgetTemplate(MakeShared(BPAssetData, nullptr)); } else { AddWidgetTemplate(MakeShared(BPAssetData, nullptr)); } } TArray AllGeneratedBPsAssetData; // if it's a widget already compiled // this logic will take >500ms in a large project, but only if bUseEditorConfigPaletteFiltering is enabled // It likely can be replaced with a call to UAssetRegistryHelpers::GetDerivedClassAssetData // unfortunately at least for loose cooked classes that function does not work, so i'm leaving // this in place for users relying on bUseEditorConfigPaletteFiltering if (FWidgetBlueprintEditorUtils::GetRelevantSettings(BlueprintEditor)->bUseEditorConfigPaletteFiltering) { const IAssetRegistry& AssetRegistry = AssetRegistryModule->Get(); AssetRegistry.GetAssetsByClass(UBlueprintGeneratedClass::StaticClass()->GetClassPathName(), AllGeneratedBPsAssetData, true); } for (const FAssetData& BPAssetData : AllGeneratedBPsAssetData) { const bool bIsSameClass = BPAssetData.AssetName == ActiveWidgetBlueprintClassName; if (bIsSameClass) { continue; } // Was already parsed by the TObjectIterator if (BPAssetData.IsAssetLoaded()) { continue; } TValueOrError Usable = FWidgetBlueprintEditorUtils::IsUsableWidgetClass(BPAssetData, PinnedBPEditor.ToSharedRef()); if (Usable.HasError()) { continue; } if ((Usable.GetValue().AssetClassFlags & (CLASS_Hidden | CLASS_HideDropDown)) != 0) { continue; } AddWidgetTemplate(MakeShared(BPAssetData, nullptr)); } } void FWidgetCatalogViewModel::AddHeader(TSharedPtr& Header) { WidgetViewModels.Add(Header); } void FWidgetCatalogViewModel::AddToFavoriteHeader(TSharedPtr& Favorite) { if (FavoriteHeader) { FavoriteHeader->Children.Add(Favorite); } } void FWidgetCatalogViewModel::AddWidgetTemplate(TSharedPtr Template) { FString Category = *Template->GetCategory().ToString(); // Hide user specific categories const TArray& CategoriesToHide = FWidgetBlueprintEditorUtils::GetRelevantSettings(BlueprintEditor)->CategoriesToHide; for (const FString& CategoryName : CategoriesToHide) { if (Category == CategoryName) { return; } } WidgetTemplateArray& Group = WidgetTemplateCategories.FindOrAdd(Category); Group.Add(Template); } void FWidgetCatalogViewModel::OnObjectsReplaced(const TMap& ReplacementMap) { } void FWidgetCatalogViewModel::OnBlueprintReinstanced() { bRebuildRequested = true; } void FWidgetCatalogViewModel::OnFavoritesUpdated() { bRebuildRequested = true; } void FWidgetCatalogViewModel::OnReloadComplete(EReloadCompleteReason Reason) { bRebuildRequested = true; } void FWidgetCatalogViewModel::HandleOnAssetsDeleted(const TArray& DeletedAssetClasses) { for (const UClass* DeletedAssetClass : DeletedAssetClasses) { if ((DeletedAssetClass == nullptr) || DeletedAssetClass->IsChildOf(UWidgetBlueprint::StaticClass())) { bRebuildRequested = true; } } } void FPaletteViewModel::BuildWidgetTemplateCategory(FString& Category, TArray>& Templates, TArray& FavoritesList) { TSharedPtr Header = MakeShareable(new FWidgetHeaderViewModel()); Header->GroupName = FText::FromString(Category); for (auto& Template : Templates) { TSharedPtr TemplateViewModel = MakeShareable(new FWidgetTemplateViewModel()); TemplateViewModel->Template = Template; TemplateViewModel->FavortiesViewModel = this; Header->Children.Add(TemplateViewModel); // If it's a favorite, we also add it to the Favorite section int32 index = FavoritesList.Find(Template->Name.ToString()); if (index != INDEX_NONE) { TemplateViewModel->SetFavorite(); // We have to create a second copy of the ViewModel for the treeview has it doesn't support to have the same element twice. TSharedPtr FavoriteTemplateViewModel = MakeShareable(new FWidgetTemplateViewModel()); FavoriteTemplateViewModel->Template = Template; FavoriteTemplateViewModel->FavortiesViewModel = this; FavoriteTemplateViewModel->SetFavorite(); AddToFavoriteHeader(FavoriteTemplateViewModel); // Remove the favorite from the temporary list FavoritesList.RemoveAt(index); } } Header->Children.Sort([](const TSharedPtr& L, const TSharedPtr& R) { return R->GetName().CompareTo(L->GetName()) > 0; }); AddHeader(Header); } void FWidgetCatalogViewModel::AddToFavorites(const FWidgetTemplateViewModel* WidgetTemplateViewModel) { UWidgetPaletteFavorites* Favorites = GetDefault()->Favorites; Favorites->Add(WidgetTemplateViewModel->GetName().ToString()); } void FWidgetCatalogViewModel::RemoveFromFavorites(const FWidgetTemplateViewModel* WidgetTemplateViewModel) { UWidgetPaletteFavorites* Favorites = GetDefault()->Favorites; Favorites->Remove(WidgetTemplateViewModel->GetName().ToString()); } #undef LOCTEXT_NAMESPACE