Overview
Ember is designed to be extensible. This guide covers how to add new functionality including custom node types, new tabs, panels, and parser profiles.
Adding Custom Node Types
Registering Node Types
Node types are defined in the XML's TreeNodesModel section:
<TreeNodesModel>
<Action ID="MyCustomAction">
<input_port name="target" type="string"/>
<output_port name="result" type="bool"/>
</Action>
<Condition ID="MyCustomCondition">
<input_port name="value" type="int"/>
</Condition>
<Control ID="MyCustomControl">
<input_port name="threshold" type="int" default="3"/>
</Control>
</TreeNodesModel>
Node Categories
| Category | Base Type | Children | Typical Use |
| Action | Leaf | None | Perform tasks |
| Condition | Leaf | None | Check state |
| Control | Internal | Multiple | Control flow |
| Decorator | Internal | One | Modify behavior |
| SubTree | Reference | None | Modularity |
Custom Node Visualization
To customize how a node type appears in the editor, modify NodeWidget:
wxColour NodeWidget::GetNodeColor() const {
switch (m_node->GetType()) {
case NodeType::Action:
return wxColour(76, 175, 80);
case NodeType::Condition:
return wxColour(255, 193, 7);
case NodeType::Control:
return wxColour(33, 150, 243);
case NodeType::Decorator:
return wxColour(156, 39, 176);
default:
return wxColour(158, 158, 158);
}
}
Creating Custom Tabs
Step 1: Implement ITab Interface
#pragma once
#include "Tabs/ITab.h"
#include <wx/wx.h>
class MyCustomTab :
public ITab {
public:
explicit MyCustomTab(wxWindow* parent);
virtual ~MyCustomTab();
wxWindow* GetWidget() override { return m_panel; }
wxString GetTitle() const override { return "My Tab"; }
wxString GetTabType() const override { return "MyCustom"; }
wxBitmap GetIcon() const override;
void Initialize() override;
void Refresh() override;
void OnActivated() override;
void OnDeactivated() override;
void OnClosed() override;
bool IsValid() const override { return m_panel != nullptr; }
bool CanClose() const override { return true; }
bool CanMove() const override { return true; }
private:
void CreateLayout();
void SetupEventHandlers();
wxPanel* m_panel = nullptr;
};
}
Interface for tab-based UI components in the application.
Step 2: Implement the Tab
#include "Tabs/MyCustomTab.h"
MyCustomTab::MyCustomTab(wxWindow* parent) {
m_panel = new wxPanel(parent, wxID_ANY);
CreateLayout();
SetupEventHandlers();
}
MyCustomTab::~MyCustomTab() {
}
void MyCustomTab::CreateLayout() {
auto* sizer = new wxBoxSizer(wxVERTICAL);
auto* label = new wxStaticText(m_panel, wxID_ANY, "Custom Tab Content");
sizer->Add(label, 0, wxALL, 10);
m_panel->SetSizer(sizer);
}
void MyCustomTab::Initialize() {
}
void MyCustomTab::Refresh() {
m_panel->Refresh();
}
void MyCustomTab::OnActivated() {
}
void MyCustomTab::OnDeactivated() {
}
void MyCustomTab::OnClosed() {
}
wxBitmap MyCustomTab::GetIcon() const {
return wxNullBitmap;
}
void MyCustomTab::SetupEventHandlers() {
}
}
Step 3: Register with TabFactory
ITabPtr TabFactory::CreateTab(
const wxString& type, wxWindow* parent) {
if (type == "FileExplorer") {
return std::make_shared<FileExplorerTab>(parent);
}
if (type == "MyCustom") {
return std::make_shared<MyCustomTab>(parent);
}
return nullptr;
}
std::unique_ptr< ITab > ITabPtr
Creating Custom Panels
Step 1: Derive from Panel or SidePanel
#pragma once
public:
MyCustomPanel(wxWindow* parent,
wxWindowID id = wxID_ANY,
PanelDescriptor* descriptor = nullptr);
virtual ~MyCustomPanel();
wxString GetTitle() const override { return "Custom"; }
wxString GetPanelType() const override { return "Custom"; }
protected:
void CreateLayout() override;
bool ShouldShowToolbar() const override { return true; }
private:
void OnCustomAction(wxCommandEvent& event);
};
}
Step 2: Implement the Panel
#include "Panels/MyCustomPanel.h"
MyCustomPanel::MyCustomPanel(wxWindow* parent, wxWindowID id,
PanelDescriptor* descriptor)
DoCreateLayout();
}
MyCustomPanel::~MyCustomPanel() = default;
void MyCustomPanel::CreateLayout() {
DoCreateLayout();
auto myTab = std::make_shared<MyCustomTab>(GetNotebook());
AddTab(myTab);
}
void MyCustomPanel::OnCustomAction(wxCommandEvent& event) {
}
}
Creating Custom Dialogs
Dialog Template
#pragma once
#include <wx/dialog.h>
class MyCustomDialog : public wxDialog {
public:
explicit MyCustomDialog(wxWindow* parent);
wxString GetResult() const { return m_result; }
private:
void CreateControls();
void OnOK(wxCommandEvent& event);
void OnCancel(wxCommandEvent& event);
wxString m_result;
wxTextCtrl* m_inputCtrl = nullptr;
wxDECLARE_EVENT_TABLE();
};
}
Dialog Implementation
#include "Dialogs/MyCustomDialog.h"
MyCustomDialog::MyCustomDialog(wxWindow* parent)
: wxDialog(parent, wxID_ANY, "Custom Dialog",
wxDefaultPosition, wxSize(400, 200)) {
CreateControls();
Centre();
}
void MyCustomDialog::CreateControls() {
auto* sizer = new wxBoxSizer(wxVERTICAL);
m_inputCtrl = new wxTextCtrl(this, wxID_ANY);
sizer->Add(m_inputCtrl, 0, wxEXPAND | wxALL, 10);
auto* buttonSizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL);
sizer->Add(buttonSizer, 0, wxEXPAND | wxALL, 10);
SetSizer(sizer);
}
void MyCustomDialog::OnOK(wxCommandEvent& event) {
m_result = m_inputCtrl->GetValue();
EndModal(wxID_OK);
}
void MyCustomDialog::OnCancel(wxCommandEvent& event) {
EndModal(wxID_CANCEL);
}
}
BehaviorTreeProjectDialog::OnProjectNameChanged BehaviorTreeProjectDialog::OnRemoveFiles wxEND_EVENT_TABLE() BehaviorTreeProjectDialog
wxBEGIN_EVENT_TABLE(LogTab, wxPanel) EVT_CHOICE(ID_LEVEL_FILTER
LogTab::OnLevelFilterChanged LogTab::OnCategoryFilterChanged LogTab::OnAutoScrollToggled EVT_BUTTON(ID_PAUSE_BTN, LogTab::OnPauseToggled) EVT_CHECKBOX(ID_CONSOLE_CHECK
Creating Custom Parser Profiles
Profile JSON Format
{
"name": "MyCustomFormat",
"description": "Parser profile for my custom XML format",
"version": 1,
"config": {
"root_element": "BehaviorTrees",
"tree_element": "Tree",
"tree_id_attribute": "name",
"node_id_attribute": "type",
"preserve_whitespace": false,
"validate_structure": true
}
}
Loading Custom Profile
manager.AddProfile(profile);
manager.SaveProfiles();
static ConfigManager & GetInstance()
A named parser configuration profile with metadata.
bool LoadFromFile(const String &filepath)
Using in Parser
auto profile = manager.GetProfile("MyCustomFormat");
auto result = parser.ParseFile("my_tree.xml");
Thread-safe XML parser using libxml2 for behavior tree files.
void SetConfig(const ParserConfig &config)
ParserConfig & GetConfig()
Adding Custom Visualization
Custom Node Renderer
void TreeVisualization::DrawCustomNode(wxDC& dc, NodeWidget* node) {
wxRect bounds = node->GetBounds();
dc.SetBrush(wxBrush(node->GetBackgroundColor()));
dc.SetPen(wxPen(node->GetBorderColor(), 2));
dc.DrawRoundedRectangle(bounds, 5);
if (node->HasIcon()) {
dc.DrawBitmap(node->GetIcon(), bounds.GetTopLeft());
}
dc.DrawLabel(node->GetLabel(), bounds, wxALIGN_CENTER);
}
Custom Connection Styles
void TreeVisualization::DrawConnection(wxDC& dc,
const wxPoint& from,
const wxPoint& to) {
switch (m_connectionStyle) {
case ConnectionStyle::Straight:
dc.DrawLine(from, to);
break;
case ConnectionStyle::Bezier:
DrawBezierCurve(dc, from, to);
break;
case ConnectionStyle::Orthogonal:
DrawOrthogonalPath(dc, from, to);
break;
}
}
Best Practices
Follow Existing Patterns
- Study existing implementations before creating new ones
- Use the same naming conventions
- Follow the IPanel/ITab/IScene interface patterns
Document Your Extensions
class MyExtension { ... };
Test Your Extensions
TEST(MyCustomTab, InitializesCorrectly) {
wxFrame* frame = new wxFrame(nullptr, wxID_ANY, "Test");
MyCustomTab tab(frame);
EXPECT_TRUE(tab.IsValid());
EXPECT_EQ(tab.GetTitle(), "My Tab");
frame->Destroy();
}
Handle Errors Gracefully
void MyCustomTab::LoadData(const wxString& path) {
try {
} catch (const std::exception& e) {
LOG_ERROR(
"MyCustomTab",
"Failed to load: " + std::string(e.what()));
wxMessageBox("Failed to load data", "Error", wxICON_ERROR);
}
}
#define LOG_ERROR(category, message)
See Also
- System Architecture - System architecture
- UI Components - UI component details
- EmberForge::ITab - Tab interface reference
- EmberForge::Panel - Panel base class reference