Creating custom context menu entries for Assets

2021/01 02 13:01

Custom Asset types commonly have special functions you wish to be able to perform on them. For example, converting images into sprites is an option you wouldn’t want to add to any other Asset type. You can create custom context menu entries for specific Asset types to make those functions accessible to users.

How to do it…

  1. From the Chapter_10Editor folder, create two new files called MyCustomAssetActions.h and MyCustomAssetActions.cpp.
  2. Return to your project file and update your Visual Studio project. Once finished, open up the project in Visual Studio.
  3. Open MyCustomAssetActions.h and use the following code:
#pragma once
#include "AssetTypeActions_Base.h"
#include "Editor/MainFrame/Public/Interfaces/IMainFrameModule.h"

class CHAPTER_10EDITOR_API FMyCustomAssetActions : public FAssetTypeActions_Base
{
public:

virtual bool HasActions(const TArray<UObject*>& InObjects)
const override;

virtual void GetActions(const TArray<UObject*>& InObjects,
FMenuBuilder& MenuBuilder) override;

virtual FText GetName() const override;

virtual UClass* GetSupportedClass() const override;

virtual FColor GetTypeColor() const override;

virtual uint32 GetCategories() override;

void MyCustomAssetContext_Clicked()
{
TSharedRef<SWindow> CookbookWindow = SNew(SWindow)
.Title(FText::FromString(TEXT("Cookbook Window")))
.ClientSize(FVector2D(800, 400))
.SupportsMaximize(false)
.SupportsMinimize(false);

IMainFrameModule& MainFrameModule =
FModuleManager::LoadModuleChecked<IMainFrameModule>
(TEXT("MainFrame"));

if (MainFrameModule.GetParentWindow().IsValid())
{
FSlateApplication::Get().AddWindowAsNativeChild(CookbookWindow,
MainFrameModule.GetParentWindow().ToSharedRef());
}
else
{
FSlateApplication::Get().AddWindow(CookbookWindow);
}

};
};

  1. Open MyCustomAssetActions.cpp and add the following code:
#include "MyCustomAssetActions.h"
#include "Chapter_10Editor.h"
#include "MyCustomAsset.h"

bool FMyCustomAssetActions::HasActions(const TArray<UObject*>& InObjects) const
{
return true;
}

void FMyCustomAssetActions::GetActions(const TArray<UObject*>& InObjects, FMenuBuilder& MenuBuilder)
{
MenuBuilder.AddMenuEntry(
FText::FromString("CustomAssetAction"),
FText::FromString("Action from Cookbook Recipe"),
FSlateIcon(FEditorStyle::GetStyleSetName(),
"LevelEditor.ViewOptions"),
FUIAction(
FExecuteAction::CreateRaw(this,
&FMyCustomAssetActions::MyCustomAssetContext_Clicked),
FCanExecuteAction()
));
}

uint32 FMyCustomAssetActions::GetCategories()
{
return EAssetTypeCategories::Misc;
}

FText FMyCustomAssetActions::GetName() const
{
return FText::FromString(TEXT("My Custom Asset"));
}

UClass* FMyCustomAssetActions::GetSupportedClass() const
{
return UMyCustomAsset::StaticClass();
}

FColor FMyCustomAssetActions::GetTypeColor() const
{
return FColor::Emerald;
}

  1. Open up the Chapter_10Editor.h file and add the following property to the class:
#pragma once

#include "Engine.h"
#include "Modules/ModuleInterface.h"
#include "Modules/ModuleManager.h"
#include "UnrealEd.h"
#include "CookbookCommands.h"
#include "Editor/MainFrame/Public/Interfaces/IMainFrameModule.h"
#include "Developer/AssetTools/Public/IAssetTypeActions.h"

class FChapter_10EditorModule: public IModuleInterface
{
virtual void StartupModule() override;
virtual void ShutdownModule() override;

TArray< TSharedPtr<IAssetTypeActions> > CreatedAssetTypeActions;

TSharedPtr<FExtender> ToolbarExtender;
TSharedPtr<const FExtensionBase> Extension;

Don’t forget to add the #include for IAssetTypeActions.h.

  1. Within your editor module (Chapter_10Editor.cpp), add the following code to the StartupModule() function:
#include "Developer/AssetTools/Public/IAssetTools.h"
#include "Developer/AssetTools/Public/AssetToolsModule.h"
#include "MyCustomAssetActions.h"
// ...

void FChapter_10EditorModule::StartupModule()
{

FCookbookCommands::Register();

TSharedPtr<FUICommandList> CommandList = MakeShareable(new FUICommandList());

CommandList->MapAction(FCookbookCommands::Get().MyButton, FExecuteAction::CreateRaw(this, &FChapter_10EditorModule::MyButton_Clicked), FCanExecuteAction());


ToolbarExtender = MakeShareable(new FExtender());

FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");

IAssetTools& AssetTools =
FModuleManager::LoadModuleChecked<FAssetToolsModule>
("AssetTools").Get();


auto Actions = MakeShareable(new FMyCustomAssetActions);
AssetTools.RegisterAssetTypeActions(Actions);
CreatedAssetTypeActions.Add(Actions);

}
  1. Add the following code inside the module’s ShutdownModule() function:
void FChapter_10EditorModule::ShutdownModule()
{

ToolbarExtender->RemoveExtension(Extension.ToSharedRef());

Extension.Reset();
ToolbarExtender.Reset();

IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("Asset Tools").Get();

for (auto Action : CreatedAssetTypeActions)
{
AssetTools.UnregisterAssetTypeActions(Action.ToSharedRef());
}

}
  1. Compile your project and launch the editor.
  2. Create an instance of your custom Asset inside the Content Browser by right-clicking and selecting Miscellaneous | My Custom Asset.
  3. Right-click on your new asset to see our custom command in the context menu:
  1. Select the CustomAssetAction command to display a new blank editor window.

How it works…

The base class for all asset type-specific context menu commands is FAssetTypeActions_Base, so we need to inherit from that class.

FAssetTypeActions_Base is an abstract class that defines a number of virtual functions that allow us to extend the context menu. The interface that contains the original information for these virtual functions can be found in IAssetTypeActions.h.

We also declare a function that we bind to our custom context menu entry.

IAssetTypeActions::HasActions ( const TArray<UObject*>& InObjects ) is the function that’s called by the engine code to see if our AssetTypeActions class contains any actions that can be applied to the selected objects.

IAssetTypeActions::GetActions(const TArray<UObject*>& InObjects, class FMenuBuilder& MenuBuilder) is called if the HasActions function returns true. It calls functions on MenuBuilder to create the menu options for the actions that we provide.

IAssetTypeActions::GetName() returns the name of this class.

IAssetTypeActions::GetSupportedClass() returns an instance of UClass that our actions class supports.

IAssetTypeActions::GetTypeColor() returns the color associated with this class and actions.

IAssetTypeActions::GetCategories() returns a category that’s appropriate for the asset. This is used to change the category under which the actions show up in the context menu.

Our overridden implementation of HasActions simply returns true under all circumstances, and relies on filtering based on the results of GetSupportedClass.

Inside the implementation of GetActions, we can call some functions on the MenuBuilder object that we are given as a function parameter. The MenuBuilder is passed as a reference, so any changes that are made by our function will persist after it returns.

AddMenuEntry has a number of parameters. The first parameter is the name of the action itself. This is the name that will be visible within the context menu. The name is an FText so that it can be localized should you wish. For the sake of simplicity, we construct FText from a string literal and don’t concern ourselves with multiple language support.

The second parameter is also FText, which we construct by calling FText::FromString. This parameter is the text that’s displayed in a tooltip if the user hovers over our command for more than a small period of time.

The next parameter is FSlateIcon for the command, which is constructed from the LevelEditor.ViewOptions icon within the editor style set.

The last parameter to this function is an FUIAction instance. The FUIAction is a wrapper around a delegate binding, so we use FExecuteAction::CreateRaw to bind the command to the MyCustomAsset_Clicked function on this very instance of FMyCustomAssetActions.

This means that when the menu entry is clicked, our MyCustomAssetContext_Clicked function will be run.

Our implementation of GetName returns the name of our Asset type. This string will be used on the thumbnail for our Asset if we don’t set one ourselves, apart from being used in the title of the menu section that our custom Assets will be placed in.

As you’d expect, the implementation of GetSupportedClass returns UMyCustomAsset::StaticClass(), as this is the Asset type we want our actions to operate on.

GetTypeColor() returns the color that will be used for color coding in Content Browser – the color is used in the bar at the bottom of the asset thumbnail. I’ve used Emerald here, but any arbitrary color will work.

The real workhorse of this recipe is the MyCustomAssetContext_Clicked() function.

The first thing that this function does is create a new instance of SWindow.

SWindow is the Slate Window – a class from the Slate UI framework.

Slate Widgets are created using the SNew function, which returns an instance of the widget requested.

Slate uses the builder design pattern, which means that all of the functions that are chained after SNew, return a reference to the object that was being operated on.

In this function, we create our new SWindow, then set the window title, its client size or area, and whether it can be maximized or minimized.

With our new Window ready, we need to get a reference to the root window for the editor so that we can add our window to the hierarchy and get it displayed.

We do this using the IMainFrameModule class. It’s a module, so we use the Module Manager to load it.

LoadModuleChecked will assert if we can’t load the module, so we don’t need to check it.

If the module was loaded, we check that we have a valid parent window. If that window is valid, then we use FSlateApplication::AddWindowAsNativeChild to add our window as a child of the top-level parent window.

If we don’t have a top-level parent, the function uses AddWindow to add the new window without parenting it to another window within the hierarchy.

So, now we have a class that will display custom actions on our custom Asset type, but what we actually need to do is tell the engine that it should ask our class to handle custom actions for the type. To do that, we need to register our class with the Asset Tools module.

The best way to do this is to register our class when our editor module is loaded, and unregister it when it is shut down.

As a result, we place our code into the StartupModule and ShutdownModule functions.

Inside StartupModule, we load the Asset Tools module using Module Manager.

With the module loaded, we create a new shared pointer that references an instance of our custom Asset actions class.

All we need to do then is call AssetModule.RegisterAssetTypeActions and pass in an instance of our actions class.

We then need to store a reference to that Actions instance so that we can unregister it later.

The sample code for this recipe uses an array of all the created asset actions in case we want to add custom actions for other classes as well.

Within ShutdownModule, we again retrieve an instance of the Asset Tools module.

Using a range-based for loop, we iterate over the array of Actions instances that we populated earlier and call UnregisterAssetTypeActions, passing in our Actions class so it can be unregistered.

With our class registered, the editor has been instructed to ask our registered class if it can handle assets that are right-clicked on.

If the asset is of the Custom Asset class, then its StaticClass will match the one returned by GetSupportedClass. The editor will then call GetActions, and display the menu with the alterations made by our implementation of that function.

When the CustomAssetAction button is clicked, our custom MyCustomAssetContext_Clicked function will be called via the delegate that we created.