{"id":3098,"date":"2021-01-02T13:57:19","date_gmt":"2021-01-02T05:57:19","guid":{"rendered":"http:\/\/blog.coolcoding.cn\/?p=3098"},"modified":"2021-01-02T19:43:41","modified_gmt":"2021-01-02T11:43:41","slug":"creating-custom-context-menu-entries-for-assets","status":"publish","type":"post","link":"https:\/\/blog.coolcoding.cn\/?p=3098","title":{"rendered":"Creating custom context menu entries for Assets"},"content":{"rendered":"\n<p> 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&#8217;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. <\/p>\n\n\n\n<h1 class=\"wp-block-heading\">How to do it&#8230;<\/h1>\n\n\n\n<ol><li>From the&nbsp;Chapter_10Editor&nbsp;folder, create two new files called&nbsp;MyCustomAssetActions.h&nbsp;and&nbsp;MyCustomAssetActions.cpp.<\/li><li>Return to your project file and update your Visual Studio project. Once finished, open up the project in Visual Studio.<\/li><li>Open&nbsp;MyCustomAssetActions.h&nbsp;and use the following code:<\/li><\/ol>\n\n\n\n<pre class=\"wp-block-preformatted\">#pragma once<br>#include \"AssetTypeActions_Base.h\"<br>#include \"Editor\/MainFrame\/Public\/Interfaces\/IMainFrameModule.h\"<br><br>class CHAPTER_10EDITOR_API FMyCustomAssetActions : public FAssetTypeActions_Base<br>{<br>public:<br>    <br>    virtual bool HasActions(const TArray&lt;UObject*&gt;&amp; InObjects)<br>    const override;<br><br>    virtual void GetActions(const TArray&lt;UObject*&gt;&amp; InObjects,<br>    FMenuBuilder&amp; MenuBuilder) override;<br><br>    virtual FText GetName() const override;<br><br>    virtual UClass* GetSupportedClass() const override;<br><br>    virtual FColor GetTypeColor() const override;<br><br>    virtual uint32 GetCategories() override;<br><br>    void MyCustomAssetContext_Clicked()<br>    {<br>        TSharedRef&lt;SWindow&gt; CookbookWindow = SNew(SWindow)<br>            .Title(FText::FromString(TEXT(\"Cookbook Window\")))<br>            .ClientSize(FVector2D(800, 400))<br>            .SupportsMaximize(false)<br>            .SupportsMinimize(false);<br><br>        IMainFrameModule&amp; MainFrameModule = <br>        FModuleManager::LoadModuleChecked&lt;IMainFrameModule&gt;<br>        (TEXT(\"MainFrame\"));<br><br>        if (MainFrameModule.GetParentWindow().IsValid())<br>        {<br>            FSlateApplication::Get().AddWindowAsNativeChild(CookbookWindow, <br>            MainFrameModule.GetParentWindow().ToSharedRef());<br>        }<br>        else<br>        {<br>            FSlateApplication::Get().AddWindow(CookbookWindow);<br>        }<br><br>    };<br>};<br><br><\/pre>\n\n\n\n<ol><li>Open&nbsp;MyCustomAssetActions.cpp&nbsp;and add the following code:<\/li><\/ol>\n\n\n\n<pre class=\"wp-block-preformatted\">#include \"MyCustomAssetActions.h\"<br>#include \"Chapter_10Editor.h\"<br>#include \"MyCustomAsset.h\"<br><br>bool FMyCustomAssetActions::HasActions(const TArray&lt;UObject*&gt;&amp; InObjects) const<br>{<br>  return true;<br>}<br><br>void FMyCustomAssetActions::GetActions(const TArray&lt;UObject*&gt;&amp; InObjects, FMenuBuilder&amp; MenuBuilder)<br>{<br>  MenuBuilder.AddMenuEntry(<br>    FText::FromString(\"CustomAssetAction\"),<br>    FText::FromString(\"Action from Cookbook Recipe\"),<br>    FSlateIcon(FEditorStyle::GetStyleSetName(),<br>    \"LevelEditor.ViewOptions\"),<br>    FUIAction(<br>      FExecuteAction::CreateRaw(this, <br>      &amp;FMyCustomAssetActions::MyCustomAssetContext_Clicked),<br>      FCanExecuteAction()<br>      ));<br>}<br><br>uint32 FMyCustomAssetActions::GetCategories()<br>{<br>  return EAssetTypeCategories::Misc;<br>}<br><br>FText FMyCustomAssetActions::GetName() const<br>{<br>  return FText::FromString(TEXT(\"My Custom Asset\"));<br>}<br><br>UClass* FMyCustomAssetActions::GetSupportedClass() const<br>{<br>  return UMyCustomAsset::StaticClass();<br>}<br><br>FColor FMyCustomAssetActions::GetTypeColor() const<br>{<br>  return FColor::Emerald;<br>}<br><br><\/pre>\n\n\n\n<ol><li>Open up the&nbsp;Chapter_10Editor.h&nbsp;file and add the following property to the class:<\/li><\/ol>\n\n\n\n<pre class=\"wp-block-preformatted\">#pragma once<br><br>#include \"Engine.h\"<br>#include \"Modules\/ModuleInterface.h\"<br>#include \"Modules\/ModuleManager.h\"<br>#include \"UnrealEd.h\"<br>#include \"CookbookCommands.h\"<br>#include \"Editor\/MainFrame\/Public\/Interfaces\/IMainFrameModule.h\"<br><strong>#include \"Developer\/AssetTools\/Public\/IAssetTypeActions.h\"<\/strong><br><br> class FChapter_10EditorModule: public IModuleInterface <br>{ <br>    virtual void StartupModule() override;<br>    virtual void ShutdownModule() override;<br><br><strong>    TArray&lt; TSharedPtr&lt;IAssetTypeActions&gt; &gt; CreatedAssetTypeActions;<\/strong><br><br>    TSharedPtr&lt;FExtender&gt; ToolbarExtender;<br>    TSharedPtr&lt;const FExtensionBase&gt; Extension;<br><br><\/pre>\n\n\n\n<p>Don&#8217;t forget to add the&nbsp;#include&nbsp;for&nbsp;IAssetTypeActions.h.<\/p>\n\n\n\n<ol><li>Within your editor module (Chapter_10Editor.cpp), add the following code to the&nbsp;StartupModule()&nbsp;function:<\/li><\/ol>\n\n\n\n<pre class=\"wp-block-preformatted\"><strong>#include \"Developer\/AssetTools\/Public\/IAssetTools.h\"<\/strong><br><strong>#include \"Developer\/AssetTools\/Public\/AssetToolsModule.h\"<\/strong><br><strong>#include \"MyCustomAssetActions.h\"<\/strong><br>\/\/ ...<br><br>void FChapter_10EditorModule::StartupModule()<br>{<br>    <br>    FCookbookCommands::Register();<br>   <br>    TSharedPtr&lt;FUICommandList&gt; CommandList = MakeShareable(new FUICommandList());<br>    <br>    CommandList-&gt;MapAction(FCookbookCommands::Get().MyButton, FExecuteAction::CreateRaw(this, &amp;FChapter_10EditorModule::MyButton_Clicked), FCanExecuteAction());<br>    <br>    <br>    ToolbarExtender = MakeShareable(new FExtender());<br><br>    FLevelEditorModule&amp; LevelEditorModule = FModuleManager::LoadModuleChecked&lt;FLevelEditorModule&gt;(\"LevelEditor\");<br><br><strong>    IAssetTools&amp; AssetTools = <br>    FModuleManager::LoadModuleChecked&lt;FAssetToolsModule&gt;<br>    (\"AssetTools\").Get();<\/strong><br><br><strong>    auto Actions = MakeShareable(new FMyCustomAssetActions);<\/strong><br><strong>    AssetTools.RegisterAssetTypeActions(Actions);<\/strong><br><strong>    CreatedAssetTypeActions.Add(Actions);<\/strong><br>    <br>}<\/pre>\n\n\n\n<ol><li>Add the following code inside the module&#8217;s&nbsp;ShutdownModule()&nbsp;function:<\/li><\/ol>\n\n\n\n<pre class=\"wp-block-preformatted\">void FChapter_10EditorModule::ShutdownModule()<br>{<br>    <br>    ToolbarExtender-&gt;RemoveExtension(Extension.ToSharedRef());<br>    <br>    Extension.Reset();<br>    ToolbarExtender.Reset();<br><br><strong>IAssetTools&amp; AssetTools = FModuleManager::LoadModuleChecked&lt;FAssetToolsModule&gt;(\"Asset Tools\").Get(); <\/strong><br><br><strong> for (auto Action : CreatedAssetTypeActions)<\/strong><br><strong> {<\/strong><br><strong> AssetTools.UnregisterAssetTypeActions(Action.ToSharedRef());<\/strong><br><strong> }<\/strong><br>    <br>}<\/pre>\n\n\n\n<ol><li>Compile your project and launch the editor.<\/li><li>Create an instance of your custom Asset inside the&nbsp;Content Browser&nbsp;by right-clicking and selecting&nbsp;Miscellaneous | My Custom Asset.<\/li><li>Right-click on your new asset to see our custom command in the context menu:<\/li><\/ol>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/learning.oreilly.com\/library\/view\/unreal-engine-4x\/9781789809503\/assets\/ae13de6c-8e5e-4f38-8a0e-06ad7da16286.png\" alt=\"\"\/><\/figure>\n\n\n\n<ol><li>Select the\u00a0CustomAssetAction\u00a0command to display a new blank editor window.<\/li><\/ol>\n\n\n\n<h1 class=\"wp-block-heading\">How it works&#8230;<\/h1>\n\n\n\n<p>The base class for all asset type-specific context menu commands is&nbsp;FAssetTypeActions_Base, so we need to inherit from that class.<\/p>\n\n\n\n<p>FAssetTypeActions_Base&nbsp;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&nbsp;IAssetTypeActions.h.<\/p>\n\n\n\n<p>We also declare a function that we bind to our custom context menu entry.<\/p>\n\n\n\n<p>IAssetTypeActions::HasActions ( const TArray&lt;UObject*&gt;&amp; InObjects )&nbsp;is the function that&#8217;s called by the engine code to see if our&nbsp;AssetTypeActions&nbsp;class contains any actions that can be applied to the selected objects.<\/p>\n\n\n\n<p>IAssetTypeActions::GetActions(const TArray&lt;UObject*&gt;&amp; InObjects, class FMenuBuilder&amp; MenuBuilder)&nbsp;is called if the&nbsp;HasActions&nbsp;function returns&nbsp;true. It calls functions on&nbsp;MenuBuilder&nbsp;to create the menu options for the actions that we provide.<\/p>\n\n\n\n<p>IAssetTypeActions::GetName()&nbsp;returns the name of this class.<\/p>\n\n\n\n<p>IAssetTypeActions::GetSupportedClass()&nbsp;returns an instance of&nbsp;UClass&nbsp;that our actions class supports.<\/p>\n\n\n\n<p>IAssetTypeActions::GetTypeColor()&nbsp;returns the color associated with this class and actions.<\/p>\n\n\n\n<p>IAssetTypeActions::GetCategories()&nbsp;returns a category that&#8217;s appropriate for the asset. This is used to change the category under which the actions show up in the context menu.<\/p>\n\n\n\n<p>Our overridden implementation of&nbsp;HasActions&nbsp;simply returns&nbsp;true&nbsp;under all circumstances, and relies on filtering based on the results of&nbsp;GetSupportedClass.<\/p>\n\n\n\n<p>Inside the implementation of&nbsp;GetActions, we can call some functions on the&nbsp;MenuBuilder&nbsp;object that we are given as a function parameter. The&nbsp;MenuBuilder&nbsp;is passed as a reference, so any changes that are made by our function will persist after it returns.<\/p>\n\n\n\n<p>AddMenuEntry&nbsp;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&nbsp;FText&nbsp;so that it can be localized should you wish. For the sake of simplicity, we construct&nbsp;FText&nbsp;from a string literal and don&#8217;t concern ourselves with multiple language support.<\/p>\n\n\n\n<p>The second parameter is also&nbsp;FText, which we construct by calling&nbsp;FText::FromString. This parameter is the text that&#8217;s displayed in a tooltip if the user hovers over our command for more than a small period of time.<\/p>\n\n\n\n<p>The next parameter is&nbsp;FSlateIcon&nbsp;for the command, which is constructed from the&nbsp;LevelEditor.ViewOptions&nbsp;icon within the editor style set.<\/p>\n\n\n\n<p>The last parameter to this function is an&nbsp;FUIAction&nbsp;instance. The&nbsp;FUIAction&nbsp;is a wrapper around a delegate binding, so we use&nbsp;FExecuteAction::CreateRaw&nbsp;to bind the command to the&nbsp;MyCustomAsset_Clicked&nbsp;function on this very instance of&nbsp;FMyCustomAssetActions.<\/p>\n\n\n\n<p>This means that when the menu entry is clicked, our&nbsp;MyCustomAssetContext_Clicked&nbsp;function will be run.<\/p>\n\n\n\n<p>Our implementation of&nbsp;GetName&nbsp;returns the name of our Asset type. This string will be used on the thumbnail for our Asset if we don&#8217;t set one ourselves, apart from being used in the title of the menu section that our custom Assets will be placed in.<\/p>\n\n\n\n<p>As you&#8217;d expect, the implementation of&nbsp;GetSupportedClass&nbsp;returns&nbsp;UMyCustomAsset::StaticClass(), as this is the Asset type we want our actions to operate on.<\/p>\n\n\n\n<p>GetTypeColor()&nbsp;returns the color that will be used for color coding in&nbsp;Content Browser&nbsp;\u2013&nbsp;the color is used in the bar at the bottom of the asset thumbnail. I&#8217;ve used Emerald here, but any arbitrary color will work.<\/p>\n\n\n\n<p>The real workhorse of this recipe is the&nbsp;MyCustomAssetContext_Clicked()&nbsp;function.<\/p>\n\n\n\n<p>The first thing that this function does is create a new instance of&nbsp;SWindow.<\/p>\n\n\n\n<p>SWindow&nbsp;is the Slate Window&nbsp;\u2013&nbsp;a class from the Slate UI framework.<\/p>\n\n\n\n<p>Slate Widgets are created using the&nbsp;SNew&nbsp;function, which returns an instance of the widget requested.<\/p>\n\n\n\n<p>Slate uses the&nbsp;builder&nbsp;design pattern, which means that all of the functions that are&nbsp;<strong>chained<\/strong>&nbsp;after&nbsp;SNew,&nbsp;return a reference to the object that was being operated on.<\/p>\n\n\n\n<p>In this function, we create our new&nbsp;SWindow, then set the window title, its client size or area, and whether it can be maximized or minimized.<\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>We do this using the&nbsp;IMainFrameModule&nbsp;class. It&#8217;s a module, so we use the&nbsp;Module Manager&nbsp;to load it.<\/p>\n\n\n\n<p>LoadModuleChecked&nbsp;will assert if we can&#8217;t load the module, so we don&#8217;t need to check it.<\/p>\n\n\n\n<p>If the module was loaded, we check that we have a valid parent window. If that window is valid, then we use&nbsp;FSlateApplication::AddWindowAsNativeChild&nbsp;to add our window as a child of the top-level parent window.<\/p>\n\n\n\n<p>If we don&#8217;t have a top-level parent, the function uses&nbsp;AddWindow&nbsp;to add the new window without parenting it to another window within the hierarchy.<\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>As a result, we place our code into the&nbsp;StartupModule&nbsp;and&nbsp;ShutdownModule&nbsp;functions.<\/p>\n\n\n\n<p>Inside&nbsp;StartupModule, we load the Asset Tools module using&nbsp;Module Manager.<\/p>\n\n\n\n<p>With the module loaded, we create a new shared pointer that references an instance of our custom Asset actions class.<\/p>\n\n\n\n<p>All we need to do then is call&nbsp;AssetModule.RegisterAssetTypeActions&nbsp;and pass in an instance of our actions class.<\/p>\n\n\n\n<p>We then need to store a reference to that&nbsp;Actions&nbsp;instance so that we can unregister it later.<\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>Within&nbsp;ShutdownModule, we again retrieve an instance of the Asset Tools module.<\/p>\n\n\n\n<p>Using a range-based for loop, we iterate over the array of&nbsp;Actions&nbsp;instances that we populated earlier and call&nbsp;UnregisterAssetTypeActions, passing&nbsp;in our&nbsp;Actions&nbsp;class so it can be unregistered.<\/p>\n\n\n\n<p>With our class registered, the editor has been instructed to ask our registered class if it can handle assets that are right-clicked on.<\/p>\n\n\n\n<p>If the asset is of the Custom Asset class, then its&nbsp;StaticClass&nbsp;will match the one returned by&nbsp;GetSupportedClass. The editor will then call&nbsp;GetActions, and display the menu with the alterations made by our implementation of that function.<\/p>\n\n\n\n<p>When the&nbsp;CustomAssetAction&nbsp;button is clicked, our custom&nbsp;MyCustomAssetContext_Clicked&nbsp;function will be called via the delegate&nbsp;that we created.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Custom Asset types commonly have special functions you  [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[27,1],"tags":[26],"_links":{"self":[{"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=\/wp\/v2\/posts\/3098"}],"collection":[{"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=3098"}],"version-history":[{"count":1,"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=\/wp\/v2\/posts\/3098\/revisions"}],"predecessor-version":[{"id":3104,"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=\/wp\/v2\/posts\/3098\/revisions\/3104"}],"wp:attachment":[{"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=3098"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=3098"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=3098"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}