Painting Editors¶
Introduction¶
ILIAD provides several painting Editors in order to paint on Unreal Engine Textures) in different environments :
Texture Editor : Edit a Unreal Engine
UTexture2D
asset in a seperate window.Viewport Texture Editor : Edit a Unreal Engine
UTexture2D
asset directly in Unreal Engine’s Main Windows and 3D ViewportFlipbook Editor : Edit a Unreal Engine
UFlipbook
asset and its underlyingUTexture2D
assets in a seperate window.
Architecture¶
FAssetEditorToolkit
in Standalone mode : opens a seperate window (for example, a Blueprint Editor)FModeToolkit
: opens in the main windows as a tool to edit the main viewport (for example the MeshPaint Editor).
Iliad Editor Toolkits are :
- Texture Editor and Flipbook Editor use a Asset Editor Toolkit
in standalone mode.
- Viewport Texture Editor use a Mode Toolkit
FOdysseyAssetEditorToolkit
and FOdysseyModeToolkit
.

Let’s describe the role of each of those classes :
FOdysseyEditor
: base class for all “Editor” classes, where the editors needs to store their dataFOdysseyEditorGUI
: base class for all “EditorGUI” classes, where all editors defines the general structure/shortcuts/menu/tabs of their GUI.FOdysseyEditorTab
: base class for all “EditorTab” classes, defines feature specific GUI and its behaviour/shortcuts/menu. A tab is basically a subwindow of the Editor.
FOdysseyTexture2DEditor needs a TextureDetails panel, to display specific texture options
FOdysseyFlipbookEditor also needs a TextureDetails panel, but displaying the currently selected texture
FOdysseyFlipbookEditor needs a timeline
FOdysseyViewportDrawingEditor : needs everything FOdysseyTexture2DEditor has but needs to arrange the GUI differently
FOdysseyPainterEditor
beside the other Painting Editors.FOdysseyPainterEditor
is just a base class for every Painting Editors that provides everything a Painting Editor will need to paint on a pixel block, and everything that is meant to be generic and available for every Painting Editor.For the same reasons, ILIAD provides a FOdysseyTextureEditor
beside its FOdysseyTexture2DEditor
so that FOdysseyTextureEditor
implements every aspect applying to any kind of textures, and FOdysseyTexture2DEditor
implements only aspects specific to Texture2D texture types.
The full Painting Editors architecture is then the following :

Customizations¶
FOdysseyFlipbookEditor
inherits FOdysseyTexture2DEditor
to inherit all its options concerning Texture2D edition.FOdysseyFlipbookEditor
needs to switch from one Texture to another when moving the cursor in its timeline.Tabs¶
Flipbook Editor
which needs to add a Timeline tab.First of all, let’s create an empty tab named FOdysseyFlipbookEditorTimelineTab
in Source/Editor/OdysseyFlipbookEditor/FlipbookEditor/FOdysseyFlipbookEditorTimelineTab.h
and Source/Editor/OdysseyFlipbookEditor/FlipbookEditor/FOdysseyFlipbookEditorTimelineTab.cpp
.
FOdysseyFlipbookEditorTimelineTab.h
#include "OdysseyEditorTab.h"
class FOdysseyFlipbookEditor;
class ODYSSEYFLIPBOOKEDITOR_API FOdysseyFlipbookEditorTimelineTab :
public FOdysseyEditorTab
{
public:
// Construction / Destruction
virtual ~FOdysseyFlipbookEditorTimelineTab();
FOdysseyFlipbookEditorTimelineTab(FOdysseyFlipbookEditor* iEditor);
protected:
// FOdysseyEditorTab interface
virtual TSharedPtr CreateWidget() override;
private:
FOdysseyFlipbookEditor* mEditor;
};
FOdysseyFlipbookEditorTimelineTab.cpp
#include "OdysseyFlipbookEditorTimelineTab.h"
#include "OdysseyFlipbookEditor.h"
#define LOCTEXT_NAMESPACE "OdysseyFlipbookEditorTimelineTab"
/////////////////////////////////////////////////////
// FOdysseyFlipbookEditorTimelineTab
//--------------------------------------------------------------------------------------
//----------------------------------------------------------- Construction / Destruction
FOdysseyFlipbookEditorTimelineTab::~FOdysseyFlipbookEditorTimelineTab()
{
}
FOdysseyFlipbookEditorTimelineTab::FOdysseyFlipbookEditorTimelineTab(FOdysseyFlipbookEditor* iEditor)
: FOdysseyEditorTab(TEXT("OdysseyFlipbookEditor_Timeline"),
LOCTEXT( "OdysseyFlipbookEditorTimelineTab", "Timeline" ),
FSlateIcon( "OdysseyStyle", "FlipbookEditor.Layers16" )) //TODO: Timeline Icon
, mEditor(iEditor)
{
}
//--------------------------------------------------------------------------------------
//--------------------------------------------------- FOdysseyFlipbookEditorTab interface
TSharedPtr
FOdysseyFlipbookEditorTimelineTab::CreateWidget()
{
return SNew(SNullWidget);
}
#undef LOCTEXT_NAMESPACE
CreateWidget()
returns a Slate Widget (a null widget for now) which defines what the content of the Tab.FOdysseyFlipbookEditorTimelineTab
should return a SOdysseyFlipbookTimelineView
(which is a Slate Widget defined somewhere else) and define its options and behaviour.CreateWidget()
TSharedPtr
FOdysseyFlipbookEditorTimelineTab::CreateWidget()
{
return SNew(SOdysseyFlipbookTimelineView).SomeOption(anOptionValue);
}
FOdysseyFlipbookEditorGUI
.CreateTab()
method and add your tab as member (adding a getter is also recommended but not mandatory) :OdysseyFlipbookEditorGUI.h
class ODYSSEYFLIPBOOKEDITOR_API FOdysseyFlipbookEditorGUI :
public FOdysseyTexture2DEditorGUI
{
public:
// Construction / Destruction
virtual ~FOdysseyFlipbookEditorGUI();
FOdysseyFlipbookEditorGUI(FOdysseyFlipbookEditor* iEditor);
protected:
//Init
virtual void CreateTabs() override;
public:
// Getters
TSharedPtr& GetTimelineTab();
protected:
//Tabs
TSharedPtr mTimelineTab;
};
In OdysseyFlipbookEditorGUI.cpp
you will need to add your tab in CreateTab()
using one of these macros :
ODYSSEY_ADD_TAB
: Adds a new Tab classODYSSEY_SET_TAB
: Replaces a Tab class with another Tab class (usually used to replace a Tab class with an modified version of the same tab)
ODYSSEY_ADD_TAB
void
FOdysseyFlipbookEditorGUI::CreateTabs()
{
FOdysseyTexture2DEditorGUI::CreateTabs();
//ADD NEW TABS
ODYSSEY_ADD_TAB(mTimelineTab, FOdysseyFlipbookEditorTimelineTab, mEditor);
}
Flipbook Editor
.CreateWidget()
returns, adding shortcuts, etc…Shortcuts¶
FUICommandInfo
objects, which will called Shortcut Identifiers
in this documentation.Create a Shortcut¶
Define the module in which to write it
OdysseyPainterEditor
.OdysseyFlipbookEditor
.Create the shortcut identifier
FOdyssey***EditorCommands
.MyShortcut
indentifier in OdysseyPainterEditorCommands
.OdysseyPainterEditorCommands.h
class FOdysseyPainterEditorCommands
: public TCommands
{
public:
// Default constructor.
FOdysseyPainterEditorCommands();
public:
// TCommands interface
virtual void RegisterCommands() override;
public:
/** My COmmand Identifier */
TSharedPtr MyShortcut;
};
OdysseyPainterEditorCommands.cpp
// IDDN FR.001.250001.004.S.X.2019.000.00000
// ILIAD is subject to copyright laws and is the legal and intellectual property of Praxinos,Inc
#include "Models/OdysseyPainterEditorCommands.h"
#include "OdysseyStyleSet.h"
#include "OdysseyEditorCommandsMacro.h"
#define LOCTEXT_NAMESPACE "OdysseyPainterEditorCommands"
namespace
{
const FName MyShortcutCategory = "My Shortcut Category";
}
FOdysseyPainterEditorCommands::FOdysseyPainterEditorCommands()
: TCommands( "IliadPainterEditor", NSLOCTEXT( "Contexts", "IliadPainterEditor", "Iliad Painter Editor" ), NAME_None, FOdysseyStyle::GetStyleSetName() )
{
AddBundle(MyShortcutCategory, LOCTEXT("MyShortcutCategory", "My Shortcut Category"));
}
void
FOdysseyPainterEditorCommands::RegisterCommands()
{
// Adding MyShortcut indentifier into the MyShortcutCategory
UI_CMD( MyShortcut, MyShortcutCategory, "My Shortcut", "My Shortcut", EUserInterfaceActionType::Button, FInputChord( EKeys::F8 ) );
}
#undef LOCTEXT_NAMESPACE
UI_CMD
, which is an Iliad specific Macro created to simplify shortcut categories management.MyShortcut
identifier and a MyShortcutCategory
are created, the category with AddBundle
is registered and then the shortcut is added into the category with UI_CMD
.Once the FOdyssey***EditorCommands
class is created, you need to ensure that the shortcuts are registerd in FOdyssey***EditorModule
.
This will ensure that all commands will be available at Unreal Engine startup and displayed in Unreal Engine’s Editor Preferences
.
FOdysseyPainterEditorModule.h
#include "Modules/ModuleManager.h"
class FOdysseyPainterEditorModule
: public IModuleInterface
{
public:
// IModuleInterface interface
virtual void StartupModule() override;
virtual void ShutdownModule() override;
private:
//Commands
void RegisterCommands();
void UnregisterCommands();
};
FOdysseyPainterEditorModule.cpp
#include "OdysseyPainterEditorModule.h"
#include "Models/OdysseyPainterEditorCommands.h"
#define LOCTEXT_NAMESPACE "OdysseyPainterEditorModule"
void
FOdysseyPainterEditorModule::StartupModule()
{
RegisterCommands();
}
void
FOdysseyPainterEditorModule::ShutdownModule()
{
UnregisterCommands();
}
void
FOdysseyPainterEditorModule::RegisterCommands()
{
FOdysseyPainterEditorCommands::Register();
}
void
FOdysseyPainterEditorModule::UnregisterCommands()
{
FOdysseyPainterEditorCommands::Unregister();
}
IMPLEMENT_MODULE( FOdysseyPainterEditorModule, OdysseyPainterEditor );
#undef LOCTEXT_NAMESPACE
Bind a Shortcut¶
Odyssey***Editor
: Your shortcut is global to your editor and does not rely on any GUIOdyssey***EditorGUI
: Your short is global to your editor and does rely on a GUIOdyssey***Editor***Tab
: Your short is specific to a Tab of your editor
Once the file is chosen, you need to bind it to a function or method in the BindShortcut()
method.
Let’s continue the MyShortcut
example, and say MyShortcut
is a global shortcut relying on a GUI, so it needs to be bound in OdysseyPaintEditorGUI
:
FOdysseyPainterEditorGUI.h
#include "OdysseyEditorGUI.h"
class FOdysseyPainterEditor;
class ODYSSEYPAINTEREDITOR_API FOdysseyPainterEditorGUI :
public FOdysseyEditorGUI
{
public:
// Construction / Destruction
virtual ~FOdysseyPainterEditorGUI();
FOdysseyPainterEditorGUI(FOdysseyPainterEditor* iEditor);
public:
// FOdysseyEditorGUI overrides
virtual void BindShortcuts(FBaseToolkit* iToolkit);
protected:
// Shortcuts
virtual void MyShortcutMethod();
private:
FOdysseyPainterEditor* mEditor;
};
FOdysseyPainterEditorGUI.h
#include "OdysseyPainterEditorGUI.h"
#include "OdysseyPainterEditor.h"
#include "Models/OdysseyPainterEditorCommands.h"
#define LOCTEXT_NAMESPACE "OdysseyPainterEditorGUI"
// Construction / Destruction
FOdysseyPainterEditorGUI::~FOdysseyPainterEditorGUI()
{
}
FOdysseyPainterEditorGUI::FOdysseyPainterEditorGUI(FOdysseyPainterEditor* iEditor)
: FOdysseyEditorGUI(iEditor)
, mEditor(iEditor)
{
}
// FOdysseyEditorGUI overrides
void
FOdysseyPainterEditorGUI::BindShortcuts(FBaseToolkit* iToolkit)
{
FOdysseyEditorGUI::BindShortcuts(iToolkit);
//---
// Retrive the CommandList in which to bind MyShortcut to MyShortcutMethod
const TSharedRef& toolkitCommands = iToolkit->GetToolkitCommands();
// Retrieve the FOdysseyPainterEditorCommands which contains MyShortcut identifier
const FOdysseyPainterEditorCommands& painterEditorCommands = FOdysseyPainterEditorCommands::Get();
// A simple macro to simplify binding a shortcut identifier to a method in the commandList
#define MAP_ACTION(action, ...) toolkitCommands->MapAction( action, FExecuteAction::CreateSP( this, &FOdysseyPainterEditorGUI::__VA_ARGS__ ), FCanExecuteAction() );
//Binding MyShortcut identifier to MyShortcutMethod in the commandList
MAP_ACTION(painterEditorCommands.MyShortcut, MyShortcutMethod)
#undef MAP_ACTION
}
// Shortcuts
void
FOdysseyPainterEditorGUI::MyShortcutMethod()
{
//Here is your shortcut code
}
#undef LOCTEXT_NAMESPACE
That’s it. For more information on how shortcuts work and on commandLists, see Shortcuts.
Menu Entries¶
Odyssey***Editor
: Your menu entry is global to your editor and does not rely on any GUI for its actionOdyssey***EditorGUI
: Your menu entry is global to your editor and does rely on a GUI for its actionOdyssey***Editor***Tab
: Your menu entry is specific to a Tab of your editor
ExtendMenu()
method.FOdysseyPainterEditorGUI
as an example :FOdysseyPainterEditorGUI.h
#include "OdysseyEditorGUI.h"
class FOdysseyPainterEditor;
class ODYSSEYPAINTEREDITOR_API FOdysseyPainterEditorGUI :
public FOdysseyEditorGUI
{
public:
// Construction / Destruction
virtual ~FOdysseyPainterEditorGUI();
FOdysseyPainterEditorGUI(FOdysseyPainterEditor* iEditor);
public:
// Menu And Toolbar
virtual void ExtendMenu(FName iMenuName) override;
private:
FOdysseyPainterEditor* mEditor;
};
FOdysseyPainterEditorGUI.cpp
#include "OdysseyPainterEditorGUI.h"
#include "OdysseyPainterEditor.h"
#include "ToolMenus.h"
#define LOCTEXT_NAMESPACE "OdysseyPainterEditorGUI"
// Construction / Destruction
FOdysseyPainterEditorGUI::~FOdysseyPainterEditorGUI()
{
}
FOdysseyPainterEditorGUI::FOdysseyPainterEditorGUI(FOdysseyPainterEditor* iEditor)
: FOdysseyEditorGUI(iEditor)
, mEditor(iEditor)
{
}
// Menu and Toolbar
void
FOdysseyPainterEditorGUI::ExtendMenu(FName iMenuName)
{
// iMenuName provides the root menu name
// Extend your menu here
}
#undef LOCTEXT_NAMESPACE
FUICommandInfo
(see Shortcuts) or directly on a given function.Here are som examples, taking place in FOdysseyPainterEditorGUI
:
Retrieving an existing menu
void
FOdysseyPainterEditorGUI::ExtendMenu(FToolMenuOwner iOwner, FName iMenuName)
{
// Retrieve the "File" Menu from it's ID
FName fileMenuID = *(iMenuName.ToString() + TEXT(".File"));
UToolMenu* menu = UToolMenus::Get()->FindMenu(fileMenuID);
}
Creating a new menu
void
FOdysseyPainterEditorGUI::ExtendMenu(FToolMenuOwner iOwner, FName iMenuName)
{
// Create an ID of the new menu
FName myMenuID = *(iMenuName.ToString() + TEXT(".MyMenu"));
// If the menu is not yet created, then you need to create it
if (!UToolMenus::Get()->IsMenuRegistered(myMenuID))
{
// Retrieve the parent menu, here it is the main menu
UToolMenu* mainMenu = UToolMenus::Get()->FindMenu(iMenuName);
// Add "MyMenu" to the main menu
mainMenu->AddSubMenu(
iOwner, //The Owner
NAME_None, //The name of the section to create in the submenu, usually none, as section are created explicitly later
myMenuID, //The ID of the menu
LOCTEXT("MyMenu", "My Menu"), //The name to display for this menu
LOCTEXT("MyMenu_ToolTip", "Open my menu") //The Tooltip to display for this menu
);
}
// Retrieve the new menu
UToolMenu* myMenu = UToolMenus::Get()->FindMenu(myMenuID);
}
Retrieve a Section
void
FOdysseyPainterEditorGUI::ExtendMenu(FToolMenuOwner iOwner, FName iMenuName)
{
// Retrieve the "File" Menu from it's ID
FName fileMenuID = *(iMenuName.ToString() + TEXT(".File"));
UToolMenu* menu = UToolMenus::Get()->FindMenu(fileMenuID);
// Retrieve the "FileLoadAndSave" section of the "File" menu
FToolMenuSection* section = menu->FindSection("FileLoadAndSave");
}
Add a Section
void
FOdysseyPainterEditorGUI::ExtendMenu(FToolMenuOwner iOwner, FName iMenuName)
{
// Retrieve the "File" Menu from it's ID
FName fileMenuID = *(iMenuName.ToString() + TEXT(".File"));
UToolMenu* menu = UToolMenus::Get()->FindMenu(fileMenuID);
// Add a "MySection" section to the "File" menu
FToolMenuSection& section = menu->AddSection("MySection", LOCTEXT("OdysseyPainter", "My Section"));
}
Add a Section relative to another Section
void
FOdysseyPainterEditorGUI::ExtendMenu(FToolMenuOwner iOwner, FName iMenuName)
{
// Retrieve the "File" Menu from it's ID
FName fileMenuID = *(iMenuName.ToString() + TEXT(".File"));
UToolMenu* menu = UToolMenus::Get()->FindMenu(fileMenuID);
// Add a "MySection" section before the "FileLoadAndSave" section to the "File" menu
FToolMenuSection& section = menu->AddSection(
"MySection",
LOCTEXT("OdysseyPainter","My Section")
FToolMenuInsert("FileLoadAndSave", EToolMenuInsertType::Before)
);
// Add a "MySection" section after the "FileLoadAndSave" section to the "File" menu
FToolMenuSection& section = menu->AddSection(
"MySection",
LOCTEXT("OdysseyPainter","My Section")
FToolMenuInsert("FileLoadAndSave", EToolMenuInsertType::After)
);
// Add a "MySection" section as first section to the "File" menu
FToolMenuSection& section = menu->AddSection(
"MySection",
LOCTEXT("OdysseyPainter","My Section")
FToolMenuInsert("FileLoadAndSave", EToolMenuInsertType::First)
);
}
Add a FUICommand entry to a menu
void
FOdysseyPainterEditorGUI::ExtendMenu(FToolMenuOwner iOwner, FName iMenuName)
{
// Retrieve the "File" Menu from it's ID
FName fileMenuID = *(iMenuName.ToString() + TEXT(".File"));
UToolMenu* menu = UToolMenus::Get()->FindMenu(fileMenuID);
// Retrieve the "Help" section of the "File" menu
FToolMenuSection* section = menu->FindSection("Help");
// Add an "About ILIAD" Entry relying on the "AboutIliad" command from "FOdysseyPainterEditorCommands"
section->AddMenuEntry(FOdysseyPainterEditorCommands::Get().AboutIliad);
//Alternatively, you can also override the name, tooltip and icon of the command for this entry
section->AddMenuEntry(
FOdysseyPainterEditorCommands::Get().AboutIliad
, LOCTEXT("AboutIliad", "About Iliad")
, LOCTEXT("AboutIliad_Tooltip", "To get more information about the plugin, the team that created it, etc.")
, FSlateIcon("OdysseyStyle", "OdysseyLogo.Iliad16")
, NAME_None
);
}
Add a function entry to a menu
void
FOdysseyPainterEditorGUI::ExtendMenu(FToolMenuOwner iOwner, FName iMenuName)
{
// Retrieve the "File" Menu from it's ID
FName fileMenuID = *(iMenuName.ToString() + TEXT(".File"));
UToolMenu* menu = UToolMenus::Get()->FindMenu(fileMenuID);
// Retrieve the "Help" section of the "File" menu
FToolMenuSection* section = menu->FindSection("Help");
// Add an "MyEntry" entry relying on a function
section->AddMenuEntry(
"MyEntry", //An ID for the entry
LOCTEXT("MyEntry", "My Entry"), //A Label to display
LOCTEXT("MyEntry_Tooltip", "My Entry Tooltip"), //A Tooltip to display
FSlateIcon("OdysseyStyle", "OdysseyLogo.Iliad16"), //An Icon to Display
FExecuteAction::CreateLambda([]()
{
//The action to execute
}
),
EUserInterfaceActionType::Button, //The Action Type of the entry (usually Button, but there is athoer usefull action types)
NAME_None //An ID for a tutorial to rely on (usually not needed)
);
// Alternatively, you can define even more options for the action, like when it should be executable, when its checkbox should be checked, is it visible, etc....
section->AddMenuEntry(
"MyEntry", //An ID for the entry
LOCTEXT("MyEntry", "My Entry"), //A Label to display
LOCTEXT("MyEntry_Tooltip", "My Entry Tooltip"), //A Tooltip to display
FSlateIcon("OdysseyStyle", "OdysseyLogo.Iliad16"), //An Icon to Display
FUIAction(
FExecuteAction::CreateLambda([]()
{
//The action to execute
}
),
FCanExecuteAction::CreateLambda([]()
{
return false; //action is greyed out
}
),
FIsActionChecked::CreateLambda([]()
{
return true; //entry is checked
}
),
FIsActionButtonVisible::CreateLambda([]()
{
return true; //entry is visible
}
)
),
EUserInterfaceActionType::Button, //The Action Type of the entry (usually Button, but there is athoer usefull action types)
NAME_None //An ID for a tutorial to rely on (usually not needed)
);
}
Add entries dynamically under some conditions
void
FOdysseyPainterEditorGUI::ExtendMenu(FToolMenuOwner iOwner, FName iMenuName)
{
// Retrieve the "File" Menu from it's ID
FName fileMenuID = *(iMenuName.ToString() + TEXT(".File"));
UToolMenu* menu = UToolMenus::Get()->FindMenu(fileMenuID);
// Retrieve the "Help" section of the "File" menu
FToolMenuSection* section = menu->FindSection("Help");
// Add an "About ILIAD" entry, only if the current paint color is RED (255, 0, 0)
section->AddDynamicEntry("AboutIliad", FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& iSection)
{
if (mEditor->PaintColor() == ::ULIS::FColor::RGB(255, 0, 0))
{
iSection.AddMenuEntry(FOdysseyPainterEditorCommands::Get().AboutIliad);
}
}));
}