Files
UnrealEngine/Engine/Source/Editor/Blutility/Private/BlutilityMenuExtensions.cpp
2025-05-18 13:04:45 +08:00

1189 lines
40 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "BlutilityMenuExtensions.h"
#include "ActorActionUtility.h"
#include "AssetActionUtility.h"
#include "AssetRegistry/ARFilter.h"
#include "AssetRegistry/AssetData.h"
#include "AssetRegistry/AssetDataTagMap.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "Blueprint/BlueprintSupport.h"
#include "BlueprintEditorModule.h"
#include "Containers/UnrealString.h"
#include "Delegates/Delegate.h"
#include "DetailsViewArgs.h"
#include "EdGraphSchema_K2.h"
#include "Editor.h"
#include "EditorUtilityAssetPrototype.h"
#include "Editor/EditorEngine.h"
#include "EditorUtilityBlueprint.h"
#include "EditorUtilityWidgetProjectSettings.h"
#include "FrontendFilters.h"
#include "Engine/Blueprint.h"
#include "Engine/BlueprintGeneratedClass.h"
#include "Framework/Application/SlateApplication.h"
#include "Framework/Commands/UIAction.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "GameFramework/Actor.h"
#include "GenericPlatform/GenericApplication.h"
#include "IDetailsView.h"
#include "IEditorUtilityExtension.h"
#include "IStructureDetailsView.h"
#include "Input/Reply.h"
#include "Internationalization/Internationalization.h"
#include "Internationalization/Text.h"
#include "Layout/BasicLayoutWidgetSlot.h"
#include "Layout/Children.h"
#include "Layout/Margin.h"
#include "Math/Color.h"
#include "Math/Vector2D.h"
#include "Misc/AssertionMacros.h"
#include "Misc/Attribute.h"
#include "Misc/PackageName.h"
#include "Modules/ModuleManager.h"
#include "PropertyEditorDelegates.h"
#include "PropertyEditorModule.h"
#include "ScopedTransaction.h"
#include "SlotBase.h"
#include "SPrimaryButton.h"
#include "Styling/AppStyle.h"
#include "Styling/SlateColor.h"
#include "Subsystems/AssetEditorSubsystem.h"
#include "Templates/Casts.h"
#include "Templates/SharedPointer.h"
#include "Textures/SlateIcon.h"
#include "Toolkits/IToolkit.h"
#include "ToolMenu.h"
#include "Types/SlateEnums.h"
#include "UObject/Class.h"
#include "UObject/Field.h"
#include "UObject/Object.h"
#include "UObject/Package.h"
#include "UObject/PropertyPortFlags.h"
#include "UObject/Script.h"
#include "UObject/StructOnScope.h"
#include "UObject/UnrealNames.h"
#include "UObject/UnrealType.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Layout/SBorder.h"
#include "Widgets/Layout/SScrollBox.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/SCompoundWidget.h"
#include "Widgets/SWindow.h"
#include "Widgets/Text/STextBlock.h"
class IToolkitHost;
#define LOCTEXT_NAMESPACE "BlutilityMenuExtensions"
namespace BlutilityUtil
{
/** Mapping of asset property tag aliases that can be used by text searches */
class FAssetPropertyTagAliases
{
public:
static FAssetPropertyTagAliases& Get()
{
static FAssetPropertyTagAliases Singleton;
return Singleton;
}
/** Get the source tag for the given asset data and alias, or none if there is no match */
FName GetSourceTagFromAlias(const FAssetData& InAssetData, const FName InAlias)
{
TSharedPtr<TMap<FName, FName>>& AliasToSourceTagMapping = ClassToAliasTagsMapping.FindOrAdd(InAssetData.AssetClassPath);
if (!AliasToSourceTagMapping.IsValid())
{
static const FName NAME_DisplayName(TEXT("DisplayName"));
AliasToSourceTagMapping = MakeShared<TMap<FName, FName>>();
UClass* AssetClass = InAssetData.GetClass();
if (AssetClass)
{
TMap<FName, UObject::FAssetRegistryTagMetadata> AssetTagMetaData;
AssetClass->GetDefaultObject()->GetAssetRegistryTagMetadata(AssetTagMetaData);
for (const auto& AssetTagMetaDataPair : AssetTagMetaData)
{
if (!AssetTagMetaDataPair.Value.DisplayName.IsEmpty())
{
const FName DisplayName = MakeObjectNameFromDisplayLabel(AssetTagMetaDataPair.Value.DisplayName.ToString(), NAME_None);
AliasToSourceTagMapping->Add(DisplayName, AssetTagMetaDataPair.Key);
}
}
for (const auto& KeyValuePair : InAssetData.TagsAndValues)
{
if (FProperty* Field = FindFProperty<FProperty>(AssetClass, KeyValuePair.Key))
{
if (Field->HasMetaData(NAME_DisplayName))
{
const FName DisplayName = MakeObjectNameFromDisplayLabel(Field->GetMetaData(NAME_DisplayName), NAME_None);
AliasToSourceTagMapping->Add(DisplayName, KeyValuePair.Key);
}
}
}
}
}
return AliasToSourceTagMapping.IsValid() ? AliasToSourceTagMapping->FindRef(InAlias) : NAME_None;
}
private:
/** Mapping from class name -> (alias -> source) */
TMap<FTopLevelAssetPath, TSharedPtr<TMap<FName, FName>>> ClassToAliasTagsMapping;
};
/** Expression context to test the given asset data against the current text filter */
class FTextFilterExpressionContext : public ITextFilterExpressionContext
{
public:
typedef TRemoveReference<const FAssetData&>::Type* FAssetFilterTypePtr;
FTextFilterExpressionContext()
: AssetPtr(nullptr)
, bIncludeClassName(true)
, bIncludeAssetPath(false)
, bIncludeCollectionNames(true)
, NameKeyName("Name")
, PathKeyName("Path")
, ClassKeyName("Class")
, TypeKeyName("Type")
, CollectionKeyName("Collection")
, TagKeyName("Tag")
{
}
void SetAsset(FAssetFilterTypePtr InAsset)
{
AssetPtr = InAsset;
if (bIncludeAssetPath)
{
// Get the full asset path, and also split it so we can compare each part in the filter
AssetPtr->PackageName.AppendString(AssetFullPath);
AssetFullPath.ParseIntoArray(AssetSplitPath, TEXT("/"));
AssetFullPath.ToUpperInline();
if (bIncludeClassName)
{
// Get the full export text path as people sometimes search by copying this (requires class and asset path search to be enabled in order to match)
AssetPtr->GetExportTextName(AssetExportTextName);
AssetExportTextName.ToUpperInline();
}
}
}
const FAssetData* GetAsset() const { return AssetPtr; }
void ClearAsset()
{
AssetPtr = nullptr;
AssetFullPath.Reset();
AssetExportTextName.Reset();
AssetSplitPath.Reset();
AssetCollectionNames.Reset();
}
void SetIncludeClassName(const bool InIncludeClassName)
{
bIncludeClassName = InIncludeClassName;
}
bool GetIncludeClassName() const
{
return bIncludeClassName;
}
void SetIncludeAssetPath(const bool InIncludeAssetPath)
{
bIncludeAssetPath = InIncludeAssetPath;
}
bool GetIncludeAssetPath() const
{
return bIncludeAssetPath;
}
void SetIncludeCollectionNames(const bool InIncludeCollectionNames)
{
bIncludeCollectionNames = InIncludeCollectionNames;
}
bool GetIncludeCollectionNames() const
{
return bIncludeCollectionNames;
}
virtual bool TestBasicStringExpression(const FTextFilterString& InValue, const ETextFilterTextComparisonMode InTextComparisonMode) const override
{
if (InValue.CompareName(AssetPtr->AssetName, InTextComparisonMode))
{
return true;
}
if (bIncludeAssetPath)
{
if (InValue.CompareFString(AssetFullPath, InTextComparisonMode))
{
return true;
}
for (const FString& AssetPathPart : AssetSplitPath)
{
if (InValue.CompareFString(AssetPathPart, InTextComparisonMode))
{
return true;
}
}
}
if (bIncludeClassName)
{
if (InValue.CompareFString(AssetPtr->AssetClassPath.ToString(), InTextComparisonMode))
{
return true;
}
}
if (bIncludeClassName && bIncludeAssetPath)
{
// Only test this if we're searching the class name and asset path too, as the exported text contains the type and path in the string
if (InValue.CompareFString(AssetExportTextName, InTextComparisonMode))
{
return true;
}
}
if (bIncludeCollectionNames)
{
for (const FName& AssetCollectionName : AssetCollectionNames)
{
if (InValue.CompareName(AssetCollectionName, InTextComparisonMode))
{
return true;
}
}
}
return false;
}
virtual bool TestComplexExpression(const FName& InKey, const FTextFilterString& InValue, const ETextFilterComparisonOperation InComparisonOperation, const ETextFilterTextComparisonMode InTextComparisonMode) const override
{
// Special case for the asset name, as this isn't contained within the asset registry meta-data
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(AssetPtr->AssetName, InValue, InTextComparisonMode);
return (InComparisonOperation == ETextFilterComparisonOperation::Equal) ? bIsMatch : !bIsMatch;
}
// Special case for the asset path, as this isn't contained within the asset registry meta-data
if (InKey == PathKeyName)
{
// Paths can only work with Equal or NotEqual type tests
if (InComparisonOperation != ETextFilterComparisonOperation::Equal && InComparisonOperation != ETextFilterComparisonOperation::NotEqual)
{
return false;
}
// If the comparison mode is partial, then we only need to test the ObjectPath as that contains the other two as sub-strings
bool bIsMatch = false;
if (InTextComparisonMode == ETextFilterTextComparisonMode::Partial)
{
bIsMatch = TextFilterUtils::TestBasicStringExpression(AssetPtr->GetObjectPathString(), InValue, InTextComparisonMode);
}
else
{
bIsMatch = TextFilterUtils::TestBasicStringExpression(AssetPtr->GetObjectPathString(), InValue, InTextComparisonMode)
|| TextFilterUtils::TestBasicStringExpression(AssetPtr->PackageName, InValue, InTextComparisonMode)
|| TextFilterUtils::TestBasicStringExpression(AssetPtr->PackagePath, InValue, InTextComparisonMode);
}
return (InComparisonOperation == ETextFilterComparisonOperation::Equal) ? bIsMatch : !bIsMatch;
}
// Special case for the asset type, as this isn't contained within the asset registry meta-data
if (InKey == ClassKeyName || InKey == TypeKeyName)
{
// Class names can only work with Equal or NotEqual type tests
if (InComparisonOperation != ETextFilterComparisonOperation::Equal && InComparisonOperation != ETextFilterComparisonOperation::NotEqual)
{
return false;
}
const bool bIsMatch = TextFilterUtils::TestBasicStringExpression(AssetPtr->AssetClassPath.ToString(), InValue, InTextComparisonMode);
return (InComparisonOperation == ETextFilterComparisonOperation::Equal) ? bIsMatch : !bIsMatch;
}
// Special case for collections, as these aren't contained within the asset registry meta-data
if (InKey == CollectionKeyName || InKey == TagKeyName)
{
// Collections can only work with Equal or NotEqual type tests
if (InComparisonOperation != ETextFilterComparisonOperation::Equal && InComparisonOperation != ETextFilterComparisonOperation::NotEqual)
{
return false;
}
bool bFoundMatch = false;
for (const FName& AssetCollectionName : AssetCollectionNames)
{
if (TextFilterUtils::TestBasicStringExpression(AssetCollectionName, InValue, InTextComparisonMode))
{
bFoundMatch = true;
break;
}
}
return (InComparisonOperation == ETextFilterComparisonOperation::Equal) ? bFoundMatch : !bFoundMatch;
}
// Generic handling for anything in the asset meta-data
{
auto GetMetaDataValue = [this, &InKey](FString& OutMetaDataValue) -> bool
{
// Check for a literal key
if (AssetPtr->GetTagValue(InKey, OutMetaDataValue))
{
return true;
}
// Check for an alias key
const FName LiteralKey = FAssetPropertyTagAliases::Get().GetSourceTagFromAlias(*AssetPtr, InKey);
if (!LiteralKey.IsNone() && AssetPtr->GetTagValue(LiteralKey, OutMetaDataValue))
{
return true;
}
return false;
};
FString MetaDataValue;
if (GetMetaDataValue(MetaDataValue))
{
return TextFilterUtils::TestComplexExpression(MetaDataValue, InValue, InComparisonOperation, InTextComparisonMode);
}
}
return false;
}
private:
/** Pointer to the asset we're currently filtering */
FAssetFilterTypePtr AssetPtr;
/** Full path of the current asset */
FString AssetFullPath;
/** The export text name of the current asset */
FString AssetExportTextName;
/** Split path of the current asset */
TArray<FString> AssetSplitPath;
/** Names of the collections that the current asset is in */
TArray<FName> AssetCollectionNames;
/** Are we supposed to include the class name in our basic string tests? */
bool bIncludeClassName;
/** Search inside the entire asset path? */
bool bIncludeAssetPath;
/** Search collection names? */
bool bIncludeCollectionNames;
/** Keys used by TestComplexExpression */
const FName NameKeyName;
const FName PathKeyName;
const FName ClassKeyName;
const FName TypeKeyName;
const FName CollectionKeyName;
const FName TagKeyName;
};
}
/** Dialog widget used to display function properties */
class SFunctionParamDialog : public SCompoundWidget
{
SLATE_BEGIN_ARGS(SFunctionParamDialog) {}
/** Text to display on the "OK" button */
SLATE_ARGUMENT(FText, OkButtonText)
/** Tooltip text for the "OK" button */
SLATE_ARGUMENT(FText, OkButtonTooltipText)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs, TWeakPtr<SWindow> InParentWindow, TSharedRef<FStructOnScope> InStructOnScope, FName HiddenPropertyName)
{
bOKPressed = false;
// Initialize details view
FDetailsViewArgs DetailsViewArgs;
{
DetailsViewArgs.bAllowSearch = false;
DetailsViewArgs.bHideSelectionTip = true;
DetailsViewArgs.bLockable = false;
DetailsViewArgs.bSearchInitialKeyFocus = true;
DetailsViewArgs.bUpdatesFromSelection = false;
DetailsViewArgs.bShowOptions = false;
DetailsViewArgs.bShowModifiedPropertiesOption = false;
DetailsViewArgs.bShowObjectLabel = false;
DetailsViewArgs.bForceHiddenPropertyVisibility = true;
DetailsViewArgs.bShowScrollBar = false;
}
FStructureDetailsViewArgs StructureViewArgs;
{
StructureViewArgs.bShowObjects = true;
StructureViewArgs.bShowAssets = true;
StructureViewArgs.bShowClasses = true;
StructureViewArgs.bShowInterfaces = true;
}
FPropertyEditorModule& PropertyEditorModule = FModuleManager::Get().LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
TSharedRef<IStructureDetailsView> StructureDetailsView = PropertyEditorModule.CreateStructureDetailView(DetailsViewArgs, StructureViewArgs, InStructOnScope);
// Hide any property that has been marked as such
StructureDetailsView->GetDetailsView()->SetIsPropertyVisibleDelegate(FIsPropertyVisible::CreateLambda([HiddenPropertyName](const FPropertyAndParent& InPropertyAndParent)
{
if (InPropertyAndParent.Property.GetFName() == HiddenPropertyName)
{
return false;
}
if (InPropertyAndParent.Property.HasAnyPropertyFlags(CPF_Parm))
{
return true;
}
for (const FProperty* Parent : InPropertyAndParent.ParentProperties)
{
if (Parent->HasAnyPropertyFlags(CPF_Parm))
{
return true;
}
}
return false;
}));
StructureDetailsView->GetDetailsView()->ForceRefresh();
ChildSlot
[
SNew(SVerticalBox)
+SVerticalBox::Slot()
.FillHeight(1.0f)
[
SNew(SScrollBox)
+SScrollBox::Slot()
[
StructureDetailsView->GetWidget().ToSharedRef()
]
]
+SVerticalBox::Slot()
.AutoHeight()
[
SNew(SBorder)
.BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder"))
.Padding(16.0f)
.VAlign(VAlign_Center)
.HAlign(HAlign_Right)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SPrimaryButton)
.Text(InArgs._OkButtonText)
.ToolTipText(InArgs._OkButtonTooltipText)
.OnClicked_Lambda([this, InParentWindow, InArgs]()
{
if(InParentWindow.IsValid())
{
InParentWindow.Pin()->RequestDestroyWindow();
}
bOKPressed = true;
return FReply::Handled();
})
]
+SHorizontalBox::Slot()
.Padding(FMargin(8.0f, 0.0f, 0.0f, 0.0f))
.AutoWidth()
[
SNew(SButton)
.TextStyle(FAppStyle::Get(), "DialogButtonText")
.Text(LOCTEXT("Cancel", "Cancel"))
.OnClicked_Lambda([InParentWindow]()
{
if(InParentWindow.IsValid())
{
InParentWindow.Pin()->RequestDestroyWindow();
}
return FReply::Handled();
})
]
]
]
];
}
bool bOKPressed;
};
void FBlutilityMenuExtensions::GetBlutilityClasses(TArray<FAssetData>& OutAssets, FTopLevelAssetPath InClassName)
{
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
// Get class names
TArray<FTopLevelAssetPath> BaseNames;
BaseNames.Add(InClassName);
TSet<FTopLevelAssetPath> DerivedClasses;
AssetRegistry.GetDerivedClassNames(BaseNames, TSet<FTopLevelAssetPath>(), DerivedClasses);
// Now get all UEditorUtilityBlueprint assets
FARFilter Filter;
Filter.ClassPaths.Add(UEditorUtilityBlueprint::StaticClass()->GetClassPathName());
Filter.bRecursiveClasses = true;
Filter.bRecursivePaths = true;
TArray<FAssetData> AssetList;
AssetRegistry.GetAssets(Filter, AssetList);
// Check each asset to see if it matches our type
for (const FAssetData& Asset : AssetList)
{
// Abstract or deprecated blutilities should not be included.
const EClassFlags BPFlags = static_cast<EClassFlags>(Asset.GetTagValueRef<uint32>(FBlueprintTags::ClassFlags));
if (EnumHasAnyFlags(BPFlags, CLASS_Abstract | CLASS_Deprecated))
{
continue;
}
FAssetDataTagMapSharedView::FFindTagResult Result = Asset.TagsAndValues.FindTag(FBlueprintTags::GeneratedClassPath);
if (Result.IsSet())
{
// If it's a menu extension blutility, don't include ones from other peoples developer folders, just your own.
if (FPaths::IsUnderDirectory(Asset.PackagePath.ToString(), FPaths::GameDevelopersDir()))
{
if (!FPaths::IsUnderDirectory(Asset.PackagePath.ToString(), FPaths::GameUserDeveloperFolderName()))
{
continue;
}
}
const FTopLevelAssetPath ClassObjectPath(FPackageName::ExportTextPathToObjectPath(Result.GetValue()));
if (DerivedClasses.Contains(ClassObjectPath))
{
OutAssets.Add(Asset);
}
}
}
const UEditorUtilityWidgetProjectSettings* EditorUtilitySettings = GetDefault<UEditorUtilityWidgetProjectSettings>();
if (EditorUtilitySettings->bSearchGeneratedClassesForScriptedActions)
{
auto FilterAssets = [&AssetRegistry, &OutAssets](const FNamePermissionList& PermissionList)
{
FARFilter GeneratedFilter;
GeneratedFilter.ClassPaths.Add(UBlueprintGeneratedClass::StaticClass()->GetClassPathName());
GeneratedFilter.bRecursiveClasses = true;
GeneratedFilter.bRecursivePaths = true;
TArray<FAssetData> GeneratedAssetList;
AssetRegistry.GetAssets(GeneratedFilter, GeneratedAssetList);
for (const FAssetData& Asset : GeneratedAssetList)
{
// Abstract or deprecated blutilities should not be included.
const EClassFlags BPFlags = static_cast<EClassFlags>(Asset.GetTagValueRef<uint32>(FBlueprintTags::ClassFlags));
if (EnumHasAnyFlags(BPFlags, CLASS_Abstract | CLASS_Deprecated))
{
continue;
}
if (PermissionList.PassesFilter(FName(Asset.GetObjectPathString())))
{
OutAssets.Add(Asset);
}
}
};
if (InClassName == UAssetActionUtility::StaticClass()->GetClassPathName())
{
FilterAssets(EditorUtilitySettings->GetAllowedEditorUtilityAssetActions());
}
else if (InClassName == UActorActionUtility::StaticClass()->GetClassPathName())
{
FilterAssets(EditorUtilitySettings->GetAllowedEditorUtilityActorActions());
}
}
}
void FBlutilityMenuExtensions::CreateActorBlutilityActionsMenu(FToolMenuSection& InSection, TMap<TSharedRef<FAssetActionUtilityPrototype>, TSet<int32>> Utils, const TArray<AActor*> SelectedSupportedActors)
{
CreateBlutilityActionsMenu<AActor*>(InSection, Utils,
"ScriptedActorActions",
LOCTEXT("ScriptedActorActions", "Scripted Actor Actions"),
LOCTEXT("ScriptedActorActionsTooltip", "Scripted actions available for the selected actors"),
[](const FProperty* Property) -> bool
{
if (const FObjectProperty* ObjectProperty = CastField<const FObjectProperty>(Property))
{
return ObjectProperty->PropertyClass == AActor::StaticClass();
}
return false;
},
SelectedSupportedActors,
"Actors.ScripterActorActions"
);
}
void FBlutilityMenuExtensions::CreateAssetBlutilityActionsMenu(FToolMenuSection& InSection, TMap<TSharedRef<FAssetActionUtilityPrototype>, TSet<int32>> Utils, const TArray<FAssetData> SelectedSupportedAssets)
{
CreateBlutilityActionsMenu<FAssetData>(InSection, Utils,
"ScriptedAssetActions",
LOCTEXT("ScriptedAssetActions", "Scripted Asset Actions"),
LOCTEXT("ScriptedAssetActionsTooltip", "Scripted actions available for the selected assets"),
[](const FProperty* Property) -> bool
{
const FFieldClass* ClassOfProperty = Property->GetClass();
if (ClassOfProperty == FStructProperty::StaticClass())
{
const FStructProperty* StructProperty = CastField<const FStructProperty>(Property);
return StructProperty->Struct->GetName() == TEXT("AssetData");
}
return false;
},
SelectedSupportedAssets,
"Actors.ScripterActorActions"
);
}
void FBlutilityMenuExtensions::OpenEditorForUtility(const FFunctionAndUtil& FunctionAndUtil)
{
// Edit the script if we have shift held down
if (UBlueprint* Blueprint = Cast<UBlueprint>(Cast<UObject>(FunctionAndUtil.Util->LoadUtilityAsset())->GetClass()->ClassGeneratedBy))
{
if (IAssetEditorInstance* AssetEditor = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->FindEditorForAsset(Blueprint, true))
{
check(AssetEditor->GetEditorName() == TEXT("BlueprintEditor"));
IBlueprintEditor* BlueprintEditor = static_cast<IBlueprintEditor*>(AssetEditor);
BlueprintEditor->JumpToHyperlink(FunctionAndUtil.GetFunction(), false);
}
else
{
FBlueprintEditorModule& BlueprintEditorModule = FModuleManager::LoadModuleChecked<FBlueprintEditorModule>("Kismet");
TSharedRef<IBlueprintEditor> BlueprintEditor = BlueprintEditorModule.CreateBlueprintEditor(EToolkitMode::Standalone, TSharedPtr<IToolkitHost>(), Blueprint, false);
BlueprintEditor->JumpToHyperlink(FunctionAndUtil.GetFunction(), false);
}
}
}
void FBlutilityMenuExtensions::ExtractFunctions(TMap<TSharedRef<FAssetActionUtilityPrototype>, TSet<int32>>& Utils, TMap<FString, TArray<FFunctionAndUtil>>& OutCategoryFunctions)
{
// Find the exposed functions available in each class, making sure to not list shared functions from a parent class more than once
for (TPair<TSharedRef<FAssetActionUtilityPrototype>, TSet<int32>> UtilitySelectionPair : Utils)
{
const TSharedRef<FAssetActionUtilityPrototype>& Util = UtilitySelectionPair.Key;
TArray<FBlutilityFunctionData> FunctionDatas = Util->GetCallableFunctions();
for (const FBlutilityFunctionData& FunctionData : FunctionDatas)
{
TArray<FFunctionAndUtil>& Functions = OutCategoryFunctions.FindOrAdd(FunctionData.Category);
Functions.AddUnique(FFunctionAndUtil(FunctionData, Util, UtilitySelectionPair.Value));
}
}
for (TPair<FString, TArray<FFunctionAndUtil>>& CategoryFunctionPair : OutCategoryFunctions)
{
// Sort the functions by name
CategoryFunctionPair.Value.Sort([](const FFunctionAndUtil& A, const FFunctionAndUtil& B) { return A.FunctionData.Name.LexicalLess(B.FunctionData.Name); });
}
}
template<typename SelectionType>
void FBlutilityMenuExtensions::CreateBlutilityActionsMenu(FMenuBuilder& MenuBuilder, TMap<TSharedRef<FAssetActionUtilityPrototype>, TSet<int32>> Utils, const FText& MenuLabel, const FText& MenuToolTip, TFunction<bool(const FProperty * Property)> IsValidPropertyType, const TArray<SelectionType> Selection, const FName& IconName)
{
TMap<FString, TArray<FFunctionAndUtil>> CategoryFunctions;
ExtractFunctions(Utils, CategoryFunctions);
auto AddFunctionEntries = [Selection, IsValidPropertyType](const TArray<FFunctionAndUtil>& FunctionUtils)
{
BlutilityUtil::FTextFilterExpressionContext TextFilterContext;
FTextFilterExpressionEvaluator TextFilterExpressionEvaluator(ETextFilterExpressionEvaluatorMode::Complex);
TArray<FMenuEntryParams> GeneratedMenuEntryParams;
for (const FFunctionAndUtil& FunctionAndUtil : FunctionUtils)
{
bool PassesFilterCondition = true;
bool bShowInMenu = true;
FString FilterFailureMessage;
if constexpr ( std::is_same_v<FAssetData, SelectionType> )
{
TArray<FAssetActionSupportCondition> Conditions = FunctionAndUtil.Util->GetAssetActionSupportConditions();
for (const FAssetActionSupportCondition& Condition : Conditions)
{
TextFilterExpressionEvaluator.SetFilterText(FText::FromString(Condition.Filter));
for (const int32& SelectionIndex : FunctionAndUtil.SelectionIndices)
{
const auto SelectedAsset = Selection[SelectionIndex];
TextFilterContext.SetAsset(&SelectedAsset);
if (!TextFilterExpressionEvaluator.TestTextFilter(TextFilterContext))
{
PassesFilterCondition = false;
FilterFailureMessage = Condition.FailureReason;
break;
}
}
if (!PassesFilterCondition)
{
bShowInMenu = Condition.bShowInMenuIfFilterFails;
break;
}
}
}
if (!PassesFilterCondition && !bShowInMenu)
{
continue;
}
FText TooltipText;
if (FunctionAndUtil.Util->GetUtilityBlueprintAsset().AssetClassPath == UBlueprintGeneratedClass::StaticClass()->GetClassPathName())
{
TooltipText = FText::Format(LOCTEXT("AssetUtilTooltip", "{0}\n\n(Click to execute)"), FunctionAndUtil.FunctionData.TooltipText);
}
else if (FilterFailureMessage.IsEmpty())
{
TooltipText = FText::Format(LOCTEXT("AssetUtilTooltipFormat", "{0}\n\n(Shift-click to edit script)"), FunctionAndUtil.FunctionData.TooltipText);
}
else
{
TooltipText = FText::Format(LOCTEXT("AssetUtilTooltipWithErrorFormat", "{0}\n\n({1})\n\n(Shift-click to edit script)"), FunctionAndUtil.FunctionData.TooltipText, FText::FromString(FilterFailureMessage));
}
FMenuEntryParams MenuEntryParams;
MenuEntryParams.LabelOverride = FunctionAndUtil.FunctionData.NameText;
MenuEntryParams.ToolTipOverride = TooltipText;
MenuEntryParams.IconOverride = FSlateIcon(FAppStyle::GetAppStyleSetName(), "GraphEditor.Event_16x");
MenuEntryParams.DirectActions = FUIAction(FExecuteAction::CreateLambda([FunctionAndUtil, Selection, IsValidPropertyType]
{
if (FSlateApplication::Get().GetModifierKeys().IsShiftDown())
{
OpenEditorForUtility(FunctionAndUtil);
}
else
{
// We dont run this on the CDO, as bad things could occur!
UObject* TempObject = NewObject<UObject>(GetTransientPackage(), Cast<UObject>(FunctionAndUtil.Util->LoadUtilityAsset())->GetClass());
TempObject->AddToRoot(); // Some Blutility actions might run GC so the TempObject needs to be rooted to avoid getting destroyed
UFunction* Function = FunctionAndUtil.GetFunction();
if (Function->NumParms > 0)
{
// Create a parameter struct and fill in defaults
TSharedRef<FStructOnScope> FuncParams = MakeShared<FStructOnScope>(Function);
FProperty* FirstParamProperty = nullptr;
int32 ParameterIndex = 0;
for (TFieldIterator<FProperty> It(Function); It&& It->HasAnyPropertyFlags(CPF_Parm); ++It)
{
FString Defaults;
if (UEdGraphSchema_K2::FindFunctionParameterDefaultValue(Function, *It, Defaults))
{
It->ImportText_Direct(*Defaults, It->ContainerPtrToValuePtr<uint8>(FuncParams->GetStructMemory()), nullptr, PPF_None);
}
// Check to see if the first parameter matches the selection object type, in that case we can directly forward the selection to it
if (ParameterIndex == 0 && IsValidPropertyType(*It))
{
FirstParamProperty = *It;
}
++ParameterIndex;
}
bool bApply = true;
if (!FirstParamProperty || ParameterIndex > 1)
{
// pop up a dialog to input params to the function
TSharedRef<SWindow> Window = SNew(SWindow)
.Title(Function->GetDisplayNameText())
.ClientSize(FVector2D(400, 200))
.SupportsMinimize(false)
.SupportsMaximize(false);
TSharedPtr<SFunctionParamDialog> Dialog;
Window->SetContent(
SAssignNew(Dialog, SFunctionParamDialog, Window, FuncParams, FirstParamProperty ? FirstParamProperty->GetFName() : NAME_None)
.OkButtonText(LOCTEXT("OKButton", "OK"))
.OkButtonTooltipText(Function->GetToolTipText()));
GEditor->EditorAddModalWindow(Window);
bApply = Dialog->bOKPressed;
}
if (bApply)
{
FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "BlutilityAction", "Blutility Action"));
FEditorScriptExecutionGuard ScriptGuard;
const bool bForwardUserSelection = FirstParamProperty != nullptr;
if (bForwardUserSelection)
{
// For each user-select asset forward the selection object into the function first's parameter (if it matches)
const FString Path = FirstParamProperty->GetPathName(Function);
// Ensure we only process selection objects that are valid for this function/utility
for (const int32& SelectionIndex : FunctionAndUtil.SelectionIndices)
{
const auto SelectedAsset = Selection[SelectionIndex];
FirstParamProperty->CopySingleValue(FirstParamProperty->ContainerPtrToValuePtr<uint8>(FuncParams->GetStructMemory()), &SelectedAsset);
TempObject->ProcessEvent(Function, FuncParams->GetStructMemory());
}
}
else
{
// User is expected to manage the asset selection on its own
TempObject->ProcessEvent(Function, FuncParams->GetStructMemory());
}
}
}
else
{
FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "BlutilityAction", "Blutility Action"));
FEditorScriptExecutionGuard ScriptGuard;
TempObject->ProcessEvent(Function, nullptr);
}
TempObject->RemoveFromRoot();
}
}),
FCanExecuteAction::CreateLambda([PassesFilterCondition]()
{
return PassesFilterCondition;
})
);
GeneratedMenuEntryParams.Emplace(MenuEntryParams);
}
return GeneratedMenuEntryParams;
};
// Add a menu item for each function
if (CategoryFunctions.Num() > 0)
{
MenuBuilder.AddSubMenu(
MenuLabel,
MenuToolTip,
FNewMenuDelegate::CreateLambda([CategoryFunctions, AddFunctionEntries](FMenuBuilder& InMenuBuilder)
{
TArray<FString> CategoryNames;
CategoryFunctions.GenerateKeyArray(CategoryNames);
CategoryNames.Remove(FString());
CategoryNames.Sort();
// Add functions belong to the same category to a sub-menu
for (const FString& CategoryName : CategoryNames)
{
const TArray<FMenuEntryParams> GeneratedMenuEntries = AddFunctionEntries(CategoryFunctions.FindChecked(CategoryName));
if (GeneratedMenuEntries.Num() > 0)
{
InMenuBuilder.AddSubMenu(FText::FromString(CategoryName), FText::FromString(CategoryName),
FNewMenuDelegate::CreateLambda([GeneratedMenuEntries](FMenuBuilder& InSubMenuBuilder)
{
for (const FMenuEntryParams& MenuParams : GeneratedMenuEntries)
{
InSubMenuBuilder.AddMenuEntry(MenuParams);
}
})
);
}
}
// Non-categorized functions
const TArray<FFunctionAndUtil>* DefaultCategoryFunctionsPtr = CategoryFunctions.Find(FString());
if (DefaultCategoryFunctionsPtr)
{
for (const FMenuEntryParams& MenuParams : AddFunctionEntries(*DefaultCategoryFunctionsPtr))
{
InMenuBuilder.AddMenuEntry(MenuParams);
}
}
}),
false,
FSlateIcon(FAppStyle::GetAppStyleSetName(), IconName)
);
}
}
template<typename SelectionType>
void FBlutilityMenuExtensions::CreateBlutilityActionsMenu(FToolMenuSection& InSection, TMap<TSharedRef<FAssetActionUtilityPrototype>, TSet<int32>> Utils, const FName& MenuName, const FText& MenuLabel, const FText& MenuToolTip, TFunction<bool(const FProperty * Property)> IsValidPropertyType, const TArray<SelectionType> Selection, const FName& IconName)
{
TMap<FString, TArray<FFunctionAndUtil>> CategoryFunctions;
ExtractFunctions(Utils, CategoryFunctions);
auto AddFunctionEntries = [Selection, IsValidPropertyType](const TArray<FFunctionAndUtil>& FunctionUtils) -> TArray<FToolMenuEntry>
{
BlutilityUtil::FTextFilterExpressionContext TextFilterContext;
FTextFilterExpressionEvaluator TextFilterExpressionEvaluator(ETextFilterExpressionEvaluatorMode::Complex);
TArray<FToolMenuEntry> GeneratedMenuEntries;
for (const FFunctionAndUtil& FunctionAndUtil : FunctionUtils)
{
bool PassesFilterCondition = true;
bool bShowInMenu = true;
FString FilterFailureMessage;
if constexpr ( std::is_same_v<FAssetData, SelectionType> )
{
TArray<FAssetActionSupportCondition> Conditions = FunctionAndUtil.Util->GetAssetActionSupportConditions();
for (const FAssetActionSupportCondition& Condition : Conditions)
{
TextFilterExpressionEvaluator.SetFilterText(FText::FromString(Condition.Filter));
for (const int32& SelectionIndex : FunctionAndUtil.SelectionIndices)
{
const auto SelectedAsset = Selection[SelectionIndex];
TextFilterContext.SetAsset(&SelectedAsset);
if (!TextFilterExpressionEvaluator.TestTextFilter(TextFilterContext))
{
PassesFilterCondition = false;
FilterFailureMessage = Condition.FailureReason;
break;
}
}
if (!PassesFilterCondition)
{
bShowInMenu = Condition.bShowInMenuIfFilterFails;
break;
}
}
}
if (!PassesFilterCondition && !bShowInMenu)
{
continue;
}
FText TooltipText;
if (FunctionAndUtil.Util->GetUtilityBlueprintAsset().AssetClassPath == UBlueprintGeneratedClass::StaticClass()->GetClassPathName())
{
TooltipText = FText::Format(LOCTEXT("AssetUtilTooltip", "{0}\n\n(Click to execute)"), FunctionAndUtil.FunctionData.TooltipText);
}
else if (FilterFailureMessage.IsEmpty())
{
TooltipText = FText::Format(LOCTEXT("AssetUtilTooltipFormat", "{0}\n\n(Shift-click to edit script)"), FunctionAndUtil.FunctionData.TooltipText);
}
else
{
TooltipText = FText::Format(LOCTEXT("AssetUtilTooltipWithErrorFormat", "{0}\n\n({1})\n\n(Shift-click to edit script)"), FunctionAndUtil.FunctionData.TooltipText, FText::FromString(FilterFailureMessage));
}
FExecuteAction ExecuteAction = FExecuteAction::CreateLambda([FunctionAndUtil, Selection, IsValidPropertyType]
{
if (FSlateApplication::Get().GetModifierKeys().IsShiftDown())
{
OpenEditorForUtility(FunctionAndUtil);
}
else
{
// We dont run this on the CDO, as bad things could occur!
UObject* TempObject = NewObject<UObject>(GetTransientPackage(), Cast<UObject>(FunctionAndUtil.Util->LoadUtilityAsset())->GetClass());
TempObject->AddToRoot(); // Some Blutility actions might run GC so the TempObject needs to be rooted to avoid getting destroyed
UFunction* Function = FunctionAndUtil.GetFunction();
if (Function->NumParms > 0)
{
// Create a parameter struct and fill in defaults
TSharedRef<FStructOnScope> FuncParams = MakeShared<FStructOnScope>(Function);
FProperty* FirstParamProperty = nullptr;
int32 ParameterIndex = 0;
for (TFieldIterator<FProperty> It(Function); It&& It->HasAnyPropertyFlags(CPF_Parm); ++It)
{
FString Defaults;
if (UEdGraphSchema_K2::FindFunctionParameterDefaultValue(Function, *It, Defaults))
{
It->ImportText_Direct(*Defaults, It->ContainerPtrToValuePtr<uint8>(FuncParams->GetStructMemory()), nullptr, PPF_None);
}
// Check to see if the first parameter matches the selection object type, in that case we can directly forward the selection to it
if (ParameterIndex == 0 && IsValidPropertyType(*It))
{
FirstParamProperty = *It;
}
++ParameterIndex;
}
bool bApply = true;
if (!FirstParamProperty || ParameterIndex > 1)
{
// pop up a dialog to input params to the function
TSharedRef<SWindow> Window = SNew(SWindow)
.Title(Function->GetDisplayNameText())
.ClientSize(FVector2D(400, 200))
.SupportsMinimize(false)
.SupportsMaximize(false);
TSharedPtr<SFunctionParamDialog> Dialog;
Window->SetContent(
SAssignNew(Dialog, SFunctionParamDialog, Window, FuncParams, FirstParamProperty ? FirstParamProperty->GetFName() : NAME_None)
.OkButtonText(LOCTEXT("OKButton", "OK"))
.OkButtonTooltipText(Function->GetToolTipText()));
GEditor->EditorAddModalWindow(Window);
bApply = Dialog->bOKPressed;
}
if (bApply)
{
FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "BlutilityAction", "Blutility Action"));
FEditorScriptExecutionGuard ScriptGuard;
const bool bForwardUserSelection = FirstParamProperty != nullptr;
if (bForwardUserSelection)
{
// For each user-select asset forward the selection object into the function first's parameter (if it matches)
const FString Path = FirstParamProperty->GetPathName(Function);
// Ensure we only process selection objects that are valid for this function/utility
for (const int32& SelectionIndex : FunctionAndUtil.SelectionIndices)
{
const auto SelectedAsset = Selection[SelectionIndex];
FirstParamProperty->CopySingleValue(FirstParamProperty->ContainerPtrToValuePtr<uint8>(FuncParams->GetStructMemory()), &SelectedAsset);
TempObject->ProcessEvent(Function, FuncParams->GetStructMemory());
}
}
else
{
// User is expected to manage the asset selection on its own
TempObject->ProcessEvent(Function, FuncParams->GetStructMemory());
}
}
}
else
{
FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "BlutilityAction", "Blutility Action"));
FEditorScriptExecutionGuard ScriptGuard;
TempObject->ProcessEvent(Function, nullptr);
}
TempObject->RemoveFromRoot();
}
});
FCanExecuteAction CanExecuteAction = FCanExecuteAction::CreateLambda([PassesFilterCondition]()
{
return PassesFilterCondition;
});
FToolMenuEntry MenuEntry = FToolMenuEntry::InitMenuEntry(
FunctionAndUtil.FunctionData.Name,
FunctionAndUtil.FunctionData.NameText,
TooltipText,
FSlateIcon(FAppStyle::GetAppStyleSetName(), "GraphEditor.Event_16x"),
FUIAction(ExecuteAction, CanExecuteAction)
);
GeneratedMenuEntries.Emplace(MenuEntry);
}
return GeneratedMenuEntries;
};
// Add a menu item for each function
if (CategoryFunctions.Num() > 0)
{
InSection.AddSubMenu(
MenuName,
MenuLabel,
MenuToolTip,
FNewToolMenuDelegate::CreateLambda([CategoryFunctions, AddFunctionEntries](UToolMenu* InMenu)
{
TArray<FString> CategoryNames;
CategoryFunctions.GenerateKeyArray(CategoryNames);
CategoryNames.Remove(FString());
CategoryNames.Sort();
FToolMenuSection& UnnamedSection = InMenu->FindOrAddSection(NAME_None);
// Add functions belong to the same category to a sub-menu
for (const FString& CategoryName : CategoryNames)
{
const TArray<FToolMenuEntry> GeneratedMenuEntries = AddFunctionEntries(CategoryFunctions.FindChecked(CategoryName));
if (GeneratedMenuEntries.Num() > 0)
{
UnnamedSection.AddSubMenu(
NAME_None,
FText::FromString(CategoryName),
FText::FromString(CategoryName),
FNewToolMenuDelegate::CreateLambda([GeneratedMenuEntries](UToolMenu* InCategorySubMenu)
{
for (const FToolMenuEntry& MenuEntry : GeneratedMenuEntries)
{
InCategorySubMenu->AddMenuEntry(NAME_None, MenuEntry);
}
}
));
}
}
// Non-categorized functions
const TArray<FFunctionAndUtil>* DefaultCategoryFunctionsPtr = CategoryFunctions.Find(FString());
if (DefaultCategoryFunctionsPtr)
{
for (const FToolMenuEntry& MenuEntry : AddFunctionEntries(*DefaultCategoryFunctionsPtr))
{
InMenu->AddMenuEntry(NAME_None, MenuEntry);
}
}
}),
false,
FSlateIcon(FAppStyle::GetAppStyleSetName(), IconName)
);
}
}
UEditorUtilityExtension::UEditorUtilityExtension(const class FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
#undef LOCTEXT_NAMESPACE