Creating new toolbar buttons

2021/01 02 13:01

If you have created a custom tool or window for display within the editor, you probably need some way to let the user make it appear. The easiest way to do this is to create a toolbar customization that adds a new toolbar button, and have it display your window when clicked. Create a new engine module by following the previous recipe, as we’ll need it to initialize our toolbar customization.

How to do it…

  1. Inside of the Chapter_10Editor folder, create a new header file, CookbookCommands.h, and insert the following class declaration:
#pragma once
#include "Commands.h"
#include "EditorStyleSet.h"


class FCookbookCommands : public TCommands<FCookbookCommands>
{
public:
FCookbookCommands()
: TCommands<FCookbookCommands>(
FName(TEXT("UE4_Cookbook")),
FText::FromString("Cookbook Commands"),
NAME_None,
FEditorStyle::GetStyleSetName())
{
};

virtual void RegisterCommands() override;

TSharedPtr<FUICommandInfo> MyButton;

TSharedPtr<FUICommandInfo> MyMenuButton;
};
  1. Implement the new class by placing the following in the .cpp file:
#include "CookbookCommands.h"
#include "Chapter_10Editor.h"
#include "Commands.h"


void FCookbookCommands::RegisterCommands()
{
#define LOCTEXT_NAMESPACE ""
UI_COMMAND(MyButton, "Cookbook", "Demo Cookbook Toolbar Command", EUserInterfaceActionType::Button, FInputGesture());
UI_COMMAND(MyMenuButton, "Cookbook", "Demo Cookbook Toolbar Command", EUserInterfaceActionType::Button, FInputGesture());
#undef LOCTEXT_NAMESPACE
}

  1. Next, we will need to update our module class (Chapter_10Editor.h) to the following:
#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"


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

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

void MyButton_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);
}

};

void AddToolbarExtension(FToolBarBuilder &builder)
{

FSlateIcon IconBrush =
FSlateIcon(FEditorStyle::GetStyleSetName(),
"LevelEditor.ViewOptions",
"LevelEditor.ViewOptions.Small"); builder.AddToolBarButton(FCookbookCommands::Get()
.MyButton, NAME_None, FText::FromString("My Button"),
FText::FromString("Click me to display a message"),
IconBrush, NAME_None);

};
};

Be sure to #include the header file for your command class as well.

  1. We now need to implement StartupModule and ShutdownModule:
#include "Chapter_10Editor.h" 
#include "Modules/ModuleManager.h"
#include "Modules/ModuleInterface.h"
#include "LevelEditor.h"
#include "SlateBasics.h"
#include "MultiBoxExtender.h"
#include "CookbookCommands.h"

IMPLEMENT_GAME_MODULE(FChapter_10EditorModule, Chapter_10Editor)

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" );

Extension = ToolbarExtender->AddToolBarExtension("Compile", EExtensionHook::Before, CommandList, FToolBarExtensionDelegate::CreateRaw(this, &FChapter_10EditorModule::AddToolbarExtension));


LevelEditorModule.GetToolBarExtensibilityManager()->AddExtender(ToolbarExtender);


}

void FChapter_10EditorModule::ShutdownModule()
{

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

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

}
  1. Regenerate your project piles if needed, compile your project from Visual Studio and start the editor.
  2. Verify that there’s a new button on the toolbar in the main level editor, which can be clicked on to open a new window:

How it works…

Unreal’s editor UI is based on the concept of commands. Commands are a design pattern that allows looser coupling between the UI and the actions that it needs to perform.

To create a class that contains a set of commands, it is necessary to inherit from TCommands.

TCommands is a template class that leverages the Curiously Recurring Template Pattern (CRTP). The CRTP is used commonly throughout Slate UI code as a means of creating compile-time polymorphism.

In the initializer list for FCookbookCommands constructor, we invoke the parent class constructor, passing in a number of parameters:

  • The first parameter is the name of the command set, and is a simple FName.
  • The second parameter is a tooltip/human readable string, and, as such, uses FText so that it can support localization if necessary.
  • If there’s a parent group of commands, the third parameter contains the name of the group. Otherwise, it contains NAME_None.
  • The final parameter for the constructor is the Slate Style set that contains any command icons that the command set will be using.

The RegisterCommands() function allows TCommands-derived classes to create any command objects that they require. The resulting FUICommandInfo instances that are returned from that function are stored inside the Commands class as members so that UI elements or functions can be bound to the commands.

This is why we have the member variable TSharedPtr<FUICommandInfo> MyButton.

In the implementation for the class, we simply need to create our commands in RegisterCommands.

The UI_COMMAND macro that was used to create an instance of FUICommandInfo expects a localization namespace to be defined, even if it is just an empty default namespace. As a result, we need to enclose our UI_COMMAND calls with #defines to set a valid value for LOCTEXT_NAMESPACE, even if we don’t intend to use localization.

The actual UI_COMMAND macro takes a number of parameters:

  • The first parameter is the variable to store the FUICommandInfo in
  • The second parameter is a human-readable name for the command
  • The third parameter is a description for the command
  • The fourth parameter is EUserInterfaceActionType

This enumeration essentially specifies what sort of button is being created. It supports Button, ToggleButton, RadioButton, and Check as valid types.

Buttons are simple generic buttons. A toggle button stores on and off states. The radio button is similar to a toggle, but is grouped with other radio buttons, and only one can be enabled at a time. Lastly, the checkbox displays a read-only checkbox that’s adjacent to the button.

The last parameter for UI_COMMAND is the input chord, or the combination of keys that are required to activate the command.

This parameter is primarily useful for defining key combinations for hotkeys linked to the command in question, rather than buttons. As a result, we use an empty InputGesture.

So, we now have a set of commands, but we haven’t told the engine we want to add the set to the commands that show on the toolbar. We also haven’t set up what actually happens when the button is clicked. To do this, we need to perform some initialization when our module begins, so we place some code into the StartupModule/ShutdownModule functions.

Inside StartupModule, we call the static Register function on the commands class that we defined earlier.

We then create a shared pointer to a list of commands using the MakeShareable function.

In the command list, we use MapAction to create a mapping, or association, between the UICommandInfo object, which we set as a member of the FCookbookCommands, and the actual function we want to execute when the command is invoked.

You’ll note that we don’t explicitly set anything regarding what could be used to invoke the command here.

To perform this mapping, we call the MapAction function. The first parameter to MapAction is a FUICommandInfo object, which we can retrieve from FCookbookCommands by using its static Get() method to retrieve the instance.

FCookbookCommands is implemented as a singleton – a class with a single instance that exists throughout the application. You’ll see the pattern in most places – there’s a static Get() method available in the engine.

The second parameter of the MapAction function is a delegate bound to the function to be invoked when the command is executed.

Because Chapter_10EditorModule is a raw C++ class rather than a UObject, and we want to invoke a member function rather than a static function, we use CreateRaw to create a new delegate that’s bound to a raw C++ member function.

CreateRaw expects a pointer to the object instance, and a function reference to the function to invoke on that pointer.

The third parameter for MapAction is a delegate to call to test if the action can be executed. Because we want the command to be executable all the time, we can use a simple predefined delegate that always returns true.

With an association created between our command and the action it should call, we now need to actually tell the extension system that we want to add new commands to the toolbar.

We can do this via the FExtender class, which can be used to extend menus, context menus, or toolbars.

We initially create an instance of FExtender as a shared pointer so that our extensions are uninitialized when the module is shut down.

We then call AddToolBarExtension on our new extender, storing the results in a shared pointer so that we can remove it on module uninitialization.

First argument of AddToolBarExtension is the name of the extension point where we want to add our extension.

To find where we want to place our extension, we first need to turn on the display of extension points within the editor UI.

To do so, open Editor Preferences in the Edit menu within the editor:

Open General | Miscellaneous and select Display UIExtension Points:

Restart the editor, and you should see green text overlaid on the Editor UI, as shown in the following screenshot:

Green text overlaying the Editor UI

The green text indicates UIExtensionPoint, and the text’s value is the string we should provide to the AddToolBarExtension function.

We’re going to add our extension to the Compile extension point in this recipe, but of course, you could use any other extension point you wish.

It’s important to note that adding a toolbar extension to a menu extension point will fail silently, and vice versa.

The second parameter to AddToolBarExtension is a location anchor relative to the extension point that’s specified. We’ve selected FExtensionHook::Before, so our icon will be displayed before the compile point.

The next parameter is our command list that contains mapped actions.

Finally, the last parameter is a delegate that is responsible for actually adding UI controls to the toolbar at the extension point and the anchor that we specified earlier.

The delegate is bound to a function that has the form void (*func) (FToolBarBuilder and builder). In this instance, it is a function called AddToolbarExtension, which is defined in our module class.

When the function is invoked, calling commands on the builder that adds UI elements will apply those elements to the location in the UI we specified.

Lastly, we need to load the level editor module within this function so that we can add our extender to the main toolbar within the level editor.

As usual, we can use ModuleManager to load a module and return a reference to it.

With that reference in hand, we can get the Toolbar Extensibility Manager for the module, and tell it to add our Extender.

While this may seem cumbersome at first, the intention is to allow you to apply the same toolbar extension to multiple toolbars in different modules, if you would like to create a consistent UI layout between different editor windows.

The counterpart to initializing our extension, of course, is removing it when our module is unloaded. To do that, we remove our extension from the extender, then null the shared pointers for both Extender and extension, thus reclaiming their memory allocation.

The AddToolBarExtension function within the editor module is the one that is responsible for actually adding UI elements to the toolbar that can invoke our commands.

It does this by calling functions on the FToolBarBuilder instance that’s passed in as a function parameter.

First, we retrieve an appropriate icon for our new toolbar button using the FSlateIcon constructor. Then, with the icon loaded, we invoke AddToolBarButton on the builder instance.

AddToolbarButton has a number of parameters. The first parameter is the command to bind to – you’ll notice it’s the same MyButton member that we accessed earlier when binding the action to the command. The second parameter is an override for the extension hook we specified earlier, but we don’t want to override that, so we can use NAME_None. The third parameter is a label override for the new button that we create. Parameter four is a tooltip for the new button. The second to last parameter is the button’s icon, and the last parameter is a name that’s used to refer to this button element for highlighting support if you wish to use the in-editor tutorial framework.