diff --git a/Source/SpotlightSearch/Private/Extensions/OpenAssetExtension.cpp b/Source/SpotlightSearch/Private/Extensions/OpenAssetExtension.cpp new file mode 100644 index 0000000..41b1b6b --- /dev/null +++ b/Source/SpotlightSearch/Private/Extensions/OpenAssetExtension.cpp @@ -0,0 +1,65 @@ +// Copyright Out-of-the-Box Plugins 2018-2023. All Rights Reserved. + +#include "OpenAssetExtension.h" + +#include "AssetToolsModule.h" +#include "IAssetTypeActions.h" +#include "QuickMenuSettings.h" +#include "AssetRegistry/IAssetRegistry.h" +#include "ThumbnailRendering/ThumbnailManager.h" + +#define LOCTEXT_NAMESPACE "QuickActions" + +TArray> UOpenAssetExtension::GetCommands(const FQuickCommandContext& Context) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(UOpenAssetExtension::GetCommands); + + TArray> OutCommands; + + TArray AllAssets; + IAssetRegistry* AssetRegistry = IAssetRegistry::Get(); + AssetRegistry->GetAllAssets(AllAssets, true); + + AllAssets.RemoveAll([](const FAssetData& AssetData) + { + return AssetData.PackageName.ToString().StartsWith(TEXT("/Engine")); + }); + + for (const auto& AssetData : AllAssets) + { + TSharedPtr OpenAsset = MakeShared(); + OpenAsset->Title = FText::Format(LOCTEXT("OpenAsset", "Open {0}"), FText::FromName(AssetData.AssetName)); + OpenAsset->Tooltip = FText::Format(LOCTEXT("OpenAssetTip", "Open the editor for {0}"), FText::FromName(AssetData.PackagePath)); + + TSharedRef IconBox = MakeShared(); + OpenAsset->CustomIconWidget = IconBox; + OpenAsset->OnEntryInitialized = FSimpleDelegate::CreateSPLambda(IconBox, [WeakBox = IconBox.ToWeakPtr(), AssetData]() + { + auto Box = WeakBox.Pin(); + TSharedRef AssetThumbnail = MakeShared(AssetData, 64, 64, UThumbnailManager::Get().GetSharedThumbnailPool()); + TSharedRef ThumbnailWidget = AssetThumbnail->MakeThumbnailWidget(); + WeakBox.Pin()->SetContent(ThumbnailWidget); + }); + + OpenAsset->ExecuteCallback = FSimpleDelegate::CreateLambda([AssetData]() + { + GEditor->GetEditorSubsystem()->OpenEditorForAsset(AssetData.ToSoftObjectPath()); + }); + + OutCommands.Add(OpenAsset); + } + + return OutCommands; +} + +int32 UOpenAssetExtension::GetPriority() const +{ + return 200; +} + +bool UOpenAssetExtension::ShouldShow() const +{ + return GetDefault()->bIncludeAssets; +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/SpotlightSearch/Private/Extensions/OpenAssetExtension.h b/Source/SpotlightSearch/Private/Extensions/OpenAssetExtension.h new file mode 100644 index 0000000..b225a54 --- /dev/null +++ b/Source/SpotlightSearch/Private/Extensions/OpenAssetExtension.h @@ -0,0 +1,20 @@ +// Copyright Out-of-the-Box Plugins 2018-2023. All Rights Reserved. + +#pragma once + +#include "QuickMenuExtension.h" + +#include "OpenAssetExtension.generated.h" + +/** + * Creates a quick menu entry for each Asset to open it directly. + */ +UCLASS() +class UOpenAssetExtension : public UQuickMenuExtension +{ + GENERATED_BODY() + + virtual TArray> GetCommands(const FQuickCommandContext& Context) override; + virtual int32 GetPriority() const override; + virtual bool ShouldShow() const override; +}; \ No newline at end of file diff --git a/Source/SpotlightSearch/Private/QuickMenuHelpers.cpp b/Source/SpotlightSearch/Private/QuickMenuHelpers.cpp index 35b30eb..18d3508 100644 --- a/Source/SpotlightSearch/Private/QuickMenuHelpers.cpp +++ b/Source/SpotlightSearch/Private/QuickMenuHelpers.cpp @@ -20,7 +20,23 @@ bool QuickMenuHelpers::IsAbbreviation(const FString& Candidate, const FString& S bool QuickMenuHelpers::IsPotentialMatchTo(const FString& Candidate, const FString& Search) { - return Candidate.Contains(Search, ESearchCase::IgnoreCase); + TArray Words; + Search.ParseIntoArray(Words, TEXT(" ")); + + if (Words.IsEmpty()) + { + return false; + } + + for (const FString& Word : Words) + { + if (!Candidate.Contains(Word, ESearchCase::IgnoreCase)) + { + return false; + } + } + + return true; } float QuickMenuHelpers::GetMatchPercentage(const FString& Candidate, const FString& Search) diff --git a/Source/SpotlightSearch/Private/QuickMenuSettings.h b/Source/SpotlightSearch/Private/QuickMenuSettings.h index 29cee38..d240887 100644 --- a/Source/SpotlightSearch/Private/QuickMenuSettings.h +++ b/Source/SpotlightSearch/Private/QuickMenuSettings.h @@ -30,6 +30,11 @@ class QUICKMENU_API UQuickMenuSettings : public UDeveloperSettings */ UPROPERTY(EditAnywhere, Category = Customization, config) bool bIncludeSettingSections = true; + /** + * @brief Includes shortcuts to open specific assets inside the entries + */ + UPROPERTY(EditAnywhere, Category = Customization, config) + bool bIncludeAssets = true; /** * @brief Matching percentage required for an entry to show up as a fuzzy search. */ diff --git a/Source/SpotlightSearch/Private/SQuickMenuWindow.cpp b/Source/SpotlightSearch/Private/SQuickMenuWindow.cpp index 7138e12..43411eb 100644 --- a/Source/SpotlightSearch/Private/SQuickMenuWindow.cpp +++ b/Source/SpotlightSearch/Private/SQuickMenuWindow.cpp @@ -24,8 +24,13 @@ namespace void SQuickMenuWindow::Construct(const FArguments& InArgs) { + TRACE_CPUPROFILER_EVENT_SCOPE(SQuickMenuWindow::Construct); + UQuickMenuSettings* Settings = GetMutableDefault(); + const UQuickMenuDiscoverySubsystem* DiscoverySubsystem = GEditor->GetEditorSubsystem(); + AvailableCommands = DiscoverySubsystem->GetAllCommands(); + // clang-format off SWindow::Construct(SWindow::FArguments() .Style(&FAppStyle::Get().GetWidgetStyle("NotificationWindow")) @@ -92,6 +97,7 @@ void SQuickMenuWindow::Construct(const FArguments& InArgs) SAssignNew(ListView, SNonFocusingListView) .ListItemsSource(&FilteredCommands) .OnGenerateRow(this, &SQuickMenuWindow::MakeCommandListItem) + .OnEntryInitialized(this, &SQuickMenuWindow::OnEntryInitialized) .ScrollbarVisibility(EVisibility::Collapsed) .IsFocusable(false) .OnMouseButtonClick(this, &SQuickMenuWindow::OnItemClicked) @@ -168,9 +174,9 @@ FReply SQuickMenuWindow::OnSearchKeyDown(const FGeometry& MyGeometry, const FKey void SQuickMenuWindow::OnFilterTextChanged(const FText& Text) { - const UQuickMenuDiscoverySubsystem* DiscoverySubsystem = GEditor->GetEditorSubsystem(); - TArray AvailableCommands = DiscoverySubsystem->GetAllCommands(); + TRACE_CPUPROFILER_EVENT_SCOPE(SQuickMenuWindow::OnFilterTextChanged); + TArray LocalCommands = AvailableCommands; FilteredCommands.Empty(); if(Text.IsEmpty()) @@ -178,19 +184,19 @@ void SQuickMenuWindow::OnFilterTextChanged(const FText& Text) const UQuickMenuSettings* Settings = GetDefault(); if(Settings->bShowAllOptionsWhenEmpty) { - FilteredCommands = AvailableCommands; + FilteredCommands = LocalCommands; } else { - GetRecentCommands(AvailableCommands, FilteredCommands); + GetRecentCommands(LocalCommands, FilteredCommands); } } else { const FString& FilterText = Text.ToString(); - GetAbbreviationsCommands(AvailableCommands, FilteredCommands, FilterText); - GetPerfectMatchesCommands(AvailableCommands, FilteredCommands, FilterText); - GetFuzzyMatchesCommands(AvailableCommands, FilteredCommands, FilterText); + GetAbbreviationsCommands(LocalCommands, FilteredCommands, FilterText); + GetPerfectMatchesCommands(LocalCommands, FilteredCommands, FilterText); + GetFuzzyMatchesCommands(LocalCommands, FilteredCommands, FilterText); } ListView->RequestListRefresh(); @@ -199,6 +205,8 @@ void SQuickMenuWindow::OnFilterTextChanged(const FText& Text) void SQuickMenuWindow::GetRecentCommands(TArray& AvailableActions, TArray& OutResult) { + TRACE_CPUPROFILER_EVENT_SCOPE(SQuickMenuWindow::GetRecentCommands); + const UQuickMenuSettings* Settings = GetDefault(); const TArray& RecentCommands = Settings->GetRecentCommands(); @@ -239,7 +247,7 @@ void SQuickMenuWindow::OnWindowSizeSettingsChanged(FVector2D NewSize) MoveWindowTo(ScreenCenter); } -void SQuickMenuWindow::GetAbbreviationsCommands(TArray& AvailableActions, TArray& OutResult, const FString& FilterText) +void SQuickMenuWindow::GetAbbreviationsCommands(TArray& AvailableActions, TArray& OutResult, const FString& FilterText) const { for (auto It = AvailableActions.CreateIterator(); It; ++It) { @@ -252,7 +260,7 @@ void SQuickMenuWindow::GetAbbreviationsCommands(TArray& Availabl } } -void SQuickMenuWindow::GetPerfectMatchesCommands(TArray& AvailableActions, TArray& OutResult, const FString& FilterText) +void SQuickMenuWindow::GetPerfectMatchesCommands(TArray& AvailableActions, TArray& OutResult, const FString& FilterText) const { for (auto It = AvailableActions.CreateIterator(); It; ++It) { @@ -265,7 +273,7 @@ void SQuickMenuWindow::GetPerfectMatchesCommands(TArray& Availab } } -void SQuickMenuWindow::GetFuzzyMatchesCommands(TArray& AvailableActions, TArray& OutResult, const FString& FilterText) +void SQuickMenuWindow::GetFuzzyMatchesCommands(TArray& AvailableActions, TArray& OutResult, const FString& FilterText) const { const UQuickMenuSettings* Settings = GetDefault(); const float MinimumMatchPercentage = Settings->FuzzySearchMatchPercentage; @@ -289,10 +297,25 @@ void SQuickMenuWindow::GetFuzzyMatchesCommands(TArray& Available } } +void SQuickMenuWindow::OnEntryInitialized(TSharedRef QuickCommandEntry, const TSharedRef& TableRow) +{ + QuickCommandEntry->OnEntryInitialized.ExecuteIfBound(); +} + TSharedRef SQuickMenuWindow::MakeCommandListItem(FQuickMenuItem Selection, const TSharedRef& OwnerTable) { const bool bCanExecute = Selection->IsAllowedToExecute(); + TSharedPtr Icon; + if (Selection->CustomIconWidget.IsSet()) + { + Icon = Selection->CustomIconWidget.Get(); + } + else + { + Icon = SNew(SImage).Image(Selection->Icon.Get().GetIcon()); + } + // clang-format off return SNew(STableRow, OwnerTable) .Style(&FQuickMenuStyle::Get().GetWidgetStyle("ActionMenuRow")) @@ -310,8 +333,7 @@ TSharedRef SQuickMenuWindow::MakeCommandListItem(FQuickMenuItem Selec .WidthOverride(30) .HeightOverride(30) [ - SNew(SImage) - .Image(Selection->Icon.Get().GetIcon()) + Icon.ToSharedRef() ] ] diff --git a/Source/SpotlightSearch/Private/SQuickMenuWindow.h b/Source/SpotlightSearch/Private/SQuickMenuWindow.h index 99eacec..aabb169 100644 --- a/Source/SpotlightSearch/Private/SQuickMenuWindow.h +++ b/Source/SpotlightSearch/Private/SQuickMenuWindow.h @@ -57,21 +57,27 @@ class SQuickMenuWindow : public SWindow * @param OutResult Output list we will add the matching commands to * @param FilterText String we will use to perform to perform the filtering */ - void GetAbbreviationsCommands(TArray& AvailableActions, TArray& OutResult, const FString& FilterText); + void GetAbbreviationsCommands(TArray& AvailableActions, TArray& OutResult, const FString& FilterText) const; /** * @brief Filters out the commands with perfect matches from AvailableActions and constructs a list of them in OutResult * @param AvailableActions Input list we will remove the matching commands from * @param OutResult Output list we will add the matching commands to * @param FilterText String we will use to perform to perform the filtering */ - void GetPerfectMatchesCommands(TArray& AvailableActions, TArray& OutResult, const FString& FilterText); + void GetPerfectMatchesCommands(TArray& AvailableActions, TArray& OutResult, const FString& FilterText) const; /** * @brief Filters out the commands with fuzzy matches from AvailableActions and constructs a list of them in OutResult * @param AvailableActions Input list we will remove the matching commands from * @param OutResult Output list we will add the matching commands to * @param FilterText String we will use to perform to perform the filtering */ - void GetFuzzyMatchesCommands(TArray& AvailableActions, TArray& OutResult, const FString& FilterText); + void GetFuzzyMatchesCommands(TArray& AvailableActions, TArray& OutResult, const FString& FilterText) const; + /** + * @brief Callback executed when a command entry is initialized by the ListView + * @param QuickCommandEntry The command entry that was initialized + * @param TableRow The row that was initialized in the ListView + */ + void OnEntryInitialized(TSharedRef QuickCommandEntry, const TSharedRef& TableRow); /** * @brief Generates the entry for a ListView based on a QuickCommandEntry */ @@ -119,6 +125,10 @@ class SQuickMenuWindow : public SWindow * @brief ListView containing all the commands available after filtering */ TSharedPtr> ListView; + /** + * @brief List of all commands available to the users before filtering (fetched only once during construction) + */ + TArray AvailableCommands; /** * @brief List of commands available to the users after filtering */ diff --git a/Source/SpotlightSearch/Private/Tests/QuickMenuHelperTests.cpp b/Source/SpotlightSearch/Private/Tests/QuickMenuHelperTests.cpp index a27ad5c..99d8aa8 100644 --- a/Source/SpotlightSearch/Private/Tests/QuickMenuHelperTests.cpp +++ b/Source/SpotlightSearch/Private/Tests/QuickMenuHelperTests.cpp @@ -44,6 +44,10 @@ bool QuickMenuHelperMatchTests::RunTest(const FString& Parameters) TestPattern("Widget Reflector", "Widget Reflector", true); TestPattern("Widget Reflector", "Wwidget rrflector", false); + TestPattern("BP_PlayerCharacter", "BP_ Character", true); + TestPattern("BP_PlayerCharacter", "BP_Character", false); + TestPattern("BP_PlayerCharacter", "BP Player", true); + return true; } @@ -55,7 +59,7 @@ bool QuickMenuHelperFuzzyTests::RunTest(const FString& Parameters) { const FString TextName = FString::Printf(TEXT("{ %s, %s } -> %s"), *Candidate, *Search, *LexToString(ExpectedResult)); const float MatchPercentage = QuickMenuHelpers::GetMatchPercentage(Candidate, Search); - TestTrue(*TextName, FMath::IsNearlyEqual(MatchPercentage, ExpectedResult, 0.01)); + TestEqual(*TextName, MatchPercentage, ExpectedResult, 0.01f); }; // Calculated with: https://awsm-tools.com/levenshtein-distance diff --git a/Source/SpotlightSearch/Public/QuickMenuExtension.h b/Source/SpotlightSearch/Public/QuickMenuExtension.h index 8fe41bd..51bf1f7 100644 --- a/Source/SpotlightSearch/Public/QuickMenuExtension.h +++ b/Source/SpotlightSearch/Public/QuickMenuExtension.h @@ -36,7 +36,10 @@ struct QUICKMENU_API FQuickCommandEntry * @brief Icon displayed as the entry icon in the command list */ TAttribute Icon; - + /** + * @brief Override the standard Icon above with a complety custom widget + */ + TAttribute> CustomIconWidget; /** * @brief Callback executed when we want to execute this action */ @@ -46,6 +49,10 @@ struct QUICKMENU_API FQuickCommandEntry * @note If the callback is not bound, we are always allowed to execute the action */ FCanExecuteCommandDelegate CanExecuteCallback; + /** + * @brief Callback executed when the entry is initialized by the list view + */ + FSimpleDelegate OnEntryInitialized; /** * @brief Evaluates the current command state to determine if we are allowed to execute the command now.