Ember
Loading...
Searching...
No Matches
Extending Ember

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:

// In NodeWidget.cpp
wxColour NodeWidget::GetNodeColor() const {
switch (m_node->GetType()) {
case NodeType::Action:
return wxColour(76, 175, 80); // Green
case NodeType::Condition:
return wxColour(255, 193, 7); // Yellow
case NodeType::Control:
return wxColour(33, 150, 243); // Blue
case NodeType::Decorator:
return wxColour(156, 39, 176); // Purple
// Add custom types here
default:
return wxColour(158, 158, 158); // Grey
}
}

Creating Custom Tabs

Step 1: Implement ITab Interface

// include/Tabs/MyCustomTab.h
#pragma once
#include "Tabs/ITab.h"
#include <wx/wx.h>
namespace EmberForge {
class MyCustomTab : public ITab {
public:
explicit MyCustomTab(wxWindow* parent);
virtual ~MyCustomTab();
// ITab interface
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;
// Add your UI components here
};
} // namespace EmberForge
Interface for tab-based UI components in the application.
Definition ITab.h:7

Step 2: Implement the Tab

// src/Tabs/MyCustomTab.cpp
#include "Tabs/MyCustomTab.h"
namespace EmberForge {
MyCustomTab::MyCustomTab(wxWindow* parent) {
m_panel = new wxPanel(parent, wxID_ANY);
CreateLayout();
SetupEventHandlers();
}
MyCustomTab::~MyCustomTab() {
// Cleanup handled by wxWidgets
}
void MyCustomTab::CreateLayout() {
auto* sizer = new wxBoxSizer(wxVERTICAL);
// Add your UI components
auto* label = new wxStaticText(m_panel, wxID_ANY, "Custom Tab Content");
sizer->Add(label, 0, wxALL, 10);
m_panel->SetSizer(sizer);
}
void MyCustomTab::Initialize() {
// Called when tab is first shown
}
void MyCustomTab::Refresh() {
// Called when tab needs to update its content
m_panel->Refresh();
}
void MyCustomTab::OnActivated() {
// Called when tab becomes active
}
void MyCustomTab::OnDeactivated() {
// Called when tab loses focus
}
void MyCustomTab::OnClosed() {
// Called when tab is about to close
}
wxBitmap MyCustomTab::GetIcon() const {
// Return a 16x16 icon for the tab
return wxNullBitmap;
}
void MyCustomTab::SetupEventHandlers() {
// Bind event handlers here
}
} // namespace EmberForge

Step 3: Register with TabFactory

// In TabFactory.cpp, add to CreateTab():
ITabPtr TabFactory::CreateTab(const wxString& type, wxWindow* parent) {
if (type == "FileExplorer") {
return std::make_shared<FileExplorerTab>(parent);
}
// ... existing tabs ...
// Add your custom tab
if (type == "MyCustom") {
return std::make_shared<MyCustomTab>(parent);
}
return nullptr;
}
std::unique_ptr< ITab > ITabPtr
Definition ITab.h:54

Creating Custom Panels

Step 1: Derive from Panel or SidePanel

// include/Panels/MyCustomPanel.h
#pragma once
namespace EmberForge {
class MyCustomPanel : public SidePanel {
public:
MyCustomPanel(wxWindow* parent,
wxWindowID id = wxID_ANY,
PanelDescriptor* descriptor = nullptr);
virtual ~MyCustomPanel();
// IPanel interface
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);
};
} // namespace EmberForge

Step 2: Implement the Panel

// src/Panels/MyCustomPanel.cpp
#include "Panels/MyCustomPanel.h"
namespace EmberForge {
MyCustomPanel::MyCustomPanel(wxWindow* parent, wxWindowID id,
PanelDescriptor* descriptor)
: SidePanel(parent, id, descriptor) {
DoCreateLayout(); // Use non-virtual helper
}
MyCustomPanel::~MyCustomPanel() = default;
void MyCustomPanel::CreateLayout() {
DoCreateLayout(); // Delegate to base
// Add custom tabs
auto myTab = std::make_shared<MyCustomTab>(GetNotebook());
AddTab(myTab);
}
void MyCustomPanel::OnCustomAction(wxCommandEvent& event) {
// Handle custom actions
}
} // namespace EmberForge

Creating Custom Dialogs

Dialog Template

// include/Dialogs/MyCustomDialog.h
#pragma once
#include <wx/dialog.h>
namespace EmberForge {
class MyCustomDialog : public wxDialog {
public:
explicit MyCustomDialog(wxWindow* parent);
// Accessors for dialog results
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();
};
} // namespace EmberForge

Dialog Implementation

// src/Dialogs/MyCustomDialog.cpp
#include "Dialogs/MyCustomDialog.h"
namespace EmberForge {
wxBEGIN_EVENT_TABLE(MyCustomDialog, wxDialog)
EVT_BUTTON(wxID_OK, MyCustomDialog::OnOK)
EVT_BUTTON(wxID_CANCEL, MyCustomDialog::OnCancel)
MyCustomDialog::MyCustomDialog(wxWindow* parent)
: wxDialog(parent, wxID_ANY, "Custom Dialog",
wxDefaultPosition, wxSize(400, 200)) {
CreateControls();
Centre();
}
void MyCustomDialog::CreateControls() {
auto* sizer = new wxBoxSizer(wxVERTICAL);
// Add input control
m_inputCtrl = new wxTextCtrl(this, wxID_ANY);
sizer->Add(m_inputCtrl, 0, wxEXPAND | wxALL, 10);
// Add buttons
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);
}
} // namespace EmberForge
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

// Load from file
profile.LoadFromFile("my_profile.json");
// Register with ConfigManager
manager.AddProfile(profile);
manager.SaveProfiles();
static ConfigManager & GetInstance()
A named parser configuration profile with metadata.
bool LoadFromFile(const String &filepath)

Using in Parser

// Get profile
auto profile = manager.GetProfile("MyCustomFormat");
// Apply to parser
parser.SetConfig(profile.GetConfig());
// Parse file
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

// Override in TreeVisualization or create custom renderer
void TreeVisualization::DrawCustomNode(wxDC& dc, NodeWidget* node) {
// Custom drawing logic
wxRect bounds = node->GetBounds();
// Draw background
dc.SetBrush(wxBrush(node->GetBackgroundColor()));
dc.SetPen(wxPen(node->GetBorderColor(), 2));
dc.DrawRoundedRectangle(bounds, 5);
// Draw icon
if (node->HasIcon()) {
dc.DrawBitmap(node->GetIcon(), bounds.GetTopLeft());
}
// Draw text
dc.DrawLabel(node->GetLabel(), bounds, wxALIGN_CENTER);
}

Custom Connection Styles

// In TreeVisualization
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

// Create unit tests for core functionality
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 {
// Load data...
} 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)
Definition Logger.h:116

See Also

  • System Architecture - System architecture
  • UI Components - UI component details
  • EmberForge::ITab - Tab interface reference
  • EmberForge::Panel - Panel base class reference