Ember
Loading...
Searching...
No Matches
BehaviorTreeProjectDialog.cpp
Go to the documentation of this file.
8#include "Utils/Logger.h"
10#include <cmath>
11#include <fstream>
12#include <libxml/parser.h>
13#include <libxml/tree.h>
14#include <sstream>
15#include <wx/artprov.h>
16#include <wx/dir.h>
17#include <wx/dirdlg.h>
18#include <wx/filedlg.h>
19#include <wx/filename.h>
20#include <wx/gbsizer.h>
21#include <wx/msgdlg.h>
22#include <wx/splitter.h>
23#include <wx/statbox.h>
24#include <wx/statline.h>
25#include <wx/stdpaths.h>
26
27// Progress callback for parser validation
29 public:
30 DialogProgressCallback(wxGauge *gauge, wxStaticText *text, wxDialog *dialog)
31 : m_gauge(gauge), m_text(text), m_dialog(dialog) {}
32
33 bool OnProgress(const EmberCore::String &message, int current, int total) override {
34 if (m_gauge && m_text && m_dialog) {
35 // Update progress bar
36 if (total > 0) {
37 int percentage = (current * 100) / total;
38 m_gauge->SetValue(percentage);
39 }
40
41 // Update status text
42 m_text->SetLabelText(wxString::FromUTF8(message.c_str()));
43
44 // Force UI update
45 m_gauge->Update();
46 m_text->Update();
47 m_dialog->Layout();
48 wxYield(); // Process pending events to keep UI responsive
49 }
50 return true; // Continue parsing
51 }
52
53 private:
54 wxGauge *m_gauge;
55 wxStaticText *m_text;
56 wxDialog *m_dialog;
57};
58
59// Static member to remember last used directory across dialog instances
61
64 EVT_TEXT(ID_PROJECT_DESCRIPTION, BehaviorTreeProjectDialog::OnDescriptionChanged)
65 EVT_CHOICE(ID_PARSER_PROFILE, BehaviorTreeProjectDialog::OnProfileSelected)
67 EVT_BUTTON(ID_ADD_FOLDER,
68 BehaviorTreeProjectDialog::OnAddFolder) EVT_BUTTON(ID_REMOVE_FILES,
69 BehaviorTreeProjectDialog::OnRemoveFiles)
70 EVT_BUTTON(ID_CLEAR_FILES, BehaviorTreeProjectDialog::OnClearFiles)
71 EVT_LIST_ITEM_SELECTED(ID_RESOURCE_LIST, BehaviorTreeProjectDialog::OnResourceSelected)
72 EVT_LIST_ITEM_ACTIVATED(ID_RESOURCE_LIST, BehaviorTreeProjectDialog::OnResourceActivated)
73 EVT_BUTTON(ID_VALIDATE, BehaviorTreeProjectDialog::OnValidate)
74 EVT_BUTTON(ID_REFRESH_PREVIEW, BehaviorTreeProjectDialog::OnRefreshPreview)
75 EVT_BUTTON(ID_CREATE, BehaviorTreeProjectDialog::OnCreate)
77 EVT_BUTTON(wxID_CANCEL, BehaviorTreeProjectDialog::OnCancel)
78 EVT_TIMER(ID_PULSE_TIMER, BehaviorTreeProjectDialog::OnPulseTimer)
80
82 wxWindow *parent,
83 std::shared_ptr<EmberCore::BehaviorTreeProject> project)
84 : EmberUI::ScalableDialog(parent, wxID_ANY, project ? "Edit BehaviorTree Project" : "New BehaviorTree Project",
85 wxSize(2000, 1400)),
86 m_project(project), m_isValid(false), m_isEditMode(project != nullptr), m_validationStale(true),
87 m_lastValidatedParserProfile(""), m_resourceImageList(nullptr), m_pulseTimer(nullptr), m_pulsePhase(0.0f),
88 m_normalBg(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)), m_highlightBg(255, 250, 150),
89 m_selectedItemIndex(-1) {
90 // Create project if not editing
91 if (!m_project) {
92 m_project = std::make_shared<EmberCore::BehaviorTreeProject>("New Project");
93 }
94
95 CreateLayout();
96 LoadProfileList();
97
98 if (m_isEditMode) {
99 PopulateFromProject();
100 }
101
102 UpdateCreateButtonState();
103
104 // Start the pulse timer for field highlighting
105 m_pulseTimer = new wxTimer(this, ID_PULSE_TIMER);
106 m_pulseTimer->Start(30); // 30ms = ~33 FPS for smooth pulsing
107
108 Centre();
109}
110
113 delete m_resourceImageList;
114 }
115
116 if (m_pulseTimer) {
117 m_pulseTimer->Stop();
118 delete m_pulseTimer;
119 }
120}
121
123 // Outer vertical sizer for main content + progress bar at bottom
124 wxBoxSizer *outerSizer = new wxBoxSizer(wxVERTICAL);
125
126 // Horizontal sizer for left/right panels
127 wxBoxSizer *mainSizer = new wxBoxSizer(wxHORIZONTAL);
128
129 // Left panel - Project settings (fixed width)
130 wxPanel *leftPanel = CreateLeftPanel(this);
131 mainSizer->Add(leftPanel, 0, wxEXPAND | wxALL, 5);
132
133 // Vertical line separator
134 mainSizer->Add(new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL), 0,
135 wxEXPAND | wxTOP | wxBOTTOM, 10);
136
137 // Right panel - Tabs (expandable)
138 wxPanel *rightPanel = CreateRightPanel(this);
139 mainSizer->Add(rightPanel, 1, wxEXPAND | wxALL, 5);
140
141 // Add main content to outer sizer
142 outerSizer->Add(mainSizer, 1, wxEXPAND);
143
144 // Add separator line before progress bar
145 outerSizer->Add(new wxStaticLine(this), 0, wxEXPAND);
146
147 // Progress indicator at very bottom (hidden by default, spans full width)
148 wxBoxSizer *progressSizer = new wxBoxSizer(wxVERTICAL);
149
150 m_validationProgressText = new wxStaticText(this, wxID_ANY, "");
151 m_validationProgressText->SetForegroundColour(wxColour(150, 150, 255));
152 wxFont progressFont = m_validationProgressText->GetFont();
153 progressFont.SetPointSize(9);
154 m_validationProgressText->SetFont(progressFont);
155 progressSizer->Add(m_validationProgressText, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 8);
156
157 m_validationProgress = new wxGauge(this, wxID_ANY, 100, wxDefaultPosition, wxSize(-1, 24));
158 m_validationProgress->SetValue(0);
159 progressSizer->Add(m_validationProgress, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 8);
160
161 // Initially hidden
163 m_validationProgress->Hide();
164
165 outerSizer->Add(progressSizer, 0, wxEXPAND);
166
167 SetSizer(outerSizer);
168}
169
171 wxPanel *panel = new wxPanel(parent, wxID_ANY);
172 panel->SetMinSize(wxSize(280, -1));
173 wxBoxSizer *sizer = new wxBoxSizer(wxVERTICAL);
174
175 // Project Settings header
176 wxStaticText *header = new wxStaticText(panel, wxID_ANY, "Project Settings");
177 wxFont headerFont = header->GetFont();
178 headerFont.SetPointSize(headerFont.GetPointSize() + 2);
179 headerFont.MakeBold();
180 header->SetFont(headerFont);
181 sizer->Add(header, 0, wxALL, 10);
182
183 sizer->Add(new wxStaticLine(panel), 0, wxEXPAND | wxLEFT | wxRIGHT, 10);
184 sizer->AddSpacer(10);
185
186 // Project Name
187 wxStaticText *nameLabel = new wxStaticText(panel, wxID_ANY, "Project Name:");
188 sizer->Add(nameLabel, 0, wxLEFT | wxRIGHT, 10);
189
190 m_projectName = new wxTextCtrl(panel, ID_PROJECT_NAME, "", wxDefaultPosition, wxDefaultSize);
191 m_projectName->SetHint("Enter project name");
192 sizer->Add(m_projectName, 0, wxEXPAND | wxALL, 10);
193
194 // Description
195 wxStaticText *descLabel = new wxStaticText(panel, wxID_ANY, "Description:");
196 sizer->Add(descLabel, 0, wxLEFT | wxRIGHT, 10);
197
199 new wxTextCtrl(panel, ID_PROJECT_DESCRIPTION, "", wxDefaultPosition, wxSize(-1, 80), wxTE_MULTILINE);
200 m_projectDescription->SetHint("Optional project description");
201 sizer->Add(m_projectDescription, 0, wxEXPAND | wxALL, 10);
202
203 // Parser Profile
204 wxStaticText *profileLabel = new wxStaticText(panel, wxID_ANY, "Parser Profile:");
205 sizer->Add(profileLabel, 0, wxLEFT | wxRIGHT, 10);
206
207 m_parserProfile = new wxChoice(panel, ID_PARSER_PROFILE);
208 sizer->Add(m_parserProfile, 0, wxEXPAND | wxALL, 10);
209
210 // Spacer
211 sizer->AddStretchSpacer(1);
212
213 // Bottom buttons
214 sizer->Add(new wxStaticLine(panel), 0, wxEXPAND | wxLEFT | wxRIGHT, 10);
215 sizer->AddSpacer(10);
216
217 wxBoxSizer *buttonSizer = new wxBoxSizer(wxHORIZONTAL);
218
219 if (m_isEditMode) {
220 m_createBtn = new wxButton(panel, ID_SAVE, "Save");
221 } else {
222 m_createBtn = new wxButton(panel, ID_CREATE, "Create Project");
223 }
224 m_createBtn->SetMinSize(wxSize(120, -1));
225 buttonSizer->Add(m_createBtn, 1, wxRIGHT, 5);
226
227 m_cancelBtn = new wxButton(panel, wxID_CANCEL, "Cancel");
228 m_cancelBtn->SetMinSize(wxSize(80, -1));
229 buttonSizer->Add(m_cancelBtn, 0);
230
231 sizer->Add(buttonSizer, 0, wxEXPAND | wxALL, 10);
232
233 panel->SetSizer(sizer);
234 return panel;
235}
236
238 wxPanel *panel = new wxPanel(parent, wxID_ANY);
239 wxBoxSizer *sizer = new wxBoxSizer(wxVERTICAL);
240
241 // Notebook with tabs
242 m_notebook = new wxNotebook(panel, wxID_ANY);
243
244 m_notebook->AddPage(CreateResourcesTab(m_notebook), "Resources");
245 m_notebook->AddPage(CreateValidationTab(m_notebook), "Validation");
246 m_notebook->AddPage(CreatePreviewTab(m_notebook), "Preview");
247 m_notebook->AddPage(CreateBlackboardTab(m_notebook), "Blackboard");
248
249 sizer->Add(m_notebook, 1, wxEXPAND | wxALL, 5);
250
251 panel->SetSizer(sizer);
252 return panel;
253}
254
255wxPanel *BehaviorTreeProjectDialog::CreateResourcesTab(wxNotebook *notebook) {
256 wxPanel *panel = new wxPanel(notebook);
257 wxBoxSizer *sizer = new wxBoxSizer(wxVERTICAL);
258
259 // Header
260 wxStaticText *header =
261 new wxStaticText(panel, wxID_ANY, "Add XML files that contain BehaviorTree definitions for this project.");
262 sizer->Add(header, 0, wxALL, 10);
263
264 // Resource list with columns
265 m_resourceList = new wxListCtrl(panel, ID_RESOURCE_LIST, wxDefaultPosition, wxDefaultSize,
266 wxLC_REPORT | wxLC_SINGLE_SEL | wxBORDER_SUNKEN);
267
268 // Create image list for status icons
269 // 0: tick (valid), 1: warning (minor issues), 2: error (invalid), 3: help (references unimplemented)
270 m_resourceImageList = new wxImageList(16, 16);
271 m_resourceImageList->Add(wxArtProvider::GetBitmap(wxART_TICK_MARK, wxART_LIST, wxSize(16, 16)));
272 m_resourceImageList->Add(wxArtProvider::GetBitmap(wxART_WARNING, wxART_LIST, wxSize(16, 16)));
273 m_resourceImageList->Add(wxArtProvider::GetBitmap(wxART_ERROR, wxART_LIST, wxSize(16, 16)));
274 m_resourceImageList->Add(wxArtProvider::GetBitmap(wxART_HELP, wxART_LIST, wxSize(16, 16))); // Missing references
275 m_resourceList->SetImageList(m_resourceImageList, wxIMAGE_LIST_SMALL);
276
277 // Add columns
278 m_resourceList->InsertColumn(0, "Status", wxLIST_FORMAT_CENTER, 60);
279 m_resourceList->InsertColumn(1, "Filename", wxLIST_FORMAT_LEFT, 200);
280 m_resourceList->InsertColumn(2, "Path", wxLIST_FORMAT_LEFT, 300);
281 m_resourceList->InsertColumn(3, "Trees", wxLIST_FORMAT_CENTER, 60);
282
283 sizer->Add(m_resourceList, 1, wxEXPAND | wxALL, 10);
284
285 // Buttons row
286 wxBoxSizer *buttonSizer = new wxBoxSizer(wxHORIZONTAL);
287
288 m_addFilesBtn = new wxButton(panel, ID_ADD_FILES, "Add Files...");
289 buttonSizer->Add(m_addFilesBtn, 0, wxRIGHT, 5);
290
291 m_addFolderBtn = new wxButton(panel, ID_ADD_FOLDER, "Add Folder...");
292 buttonSizer->Add(m_addFolderBtn, 0, wxRIGHT, 5);
293
294 m_removeFilesBtn = new wxButton(panel, ID_REMOVE_FILES, "Remove Selected");
295 m_removeFilesBtn->Enable(false);
296 buttonSizer->Add(m_removeFilesBtn, 0, wxRIGHT, 5);
297
298 m_clearFilesBtn = new wxButton(panel, ID_CLEAR_FILES, "Clear All");
299 buttonSizer->Add(m_clearFilesBtn, 0, wxRIGHT, 5);
300
301 buttonSizer->AddStretchSpacer(1);
302
303 m_resourceCountLabel = new wxStaticText(panel, wxID_ANY, "0 files");
304 buttonSizer->Add(m_resourceCountLabel, 0, wxALIGN_CENTER_VERTICAL);
305
306 sizer->Add(buttonSizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10);
307
308 panel->SetSizer(sizer);
309 return panel;
310}
311
313 wxPanel *panel = new wxPanel(notebook);
314 wxBoxSizer *sizer = new wxBoxSizer(wxVERTICAL);
315
316 // Status header
317 wxBoxSizer *statusSizer = new wxBoxSizer(wxHORIZONTAL);
318
319 m_validationIcon = new wxStaticBitmap(
320 panel, wxID_ANY, wxArtProvider::GetBitmap(wxART_INFORMATION, wxART_MESSAGE_BOX, wxSize(24, 24)));
321 statusSizer->Add(m_validationIcon, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 10);
322
323 m_validationStatus = new wxStaticText(panel, wxID_ANY, "Click 'Validate' to check project resources");
324 wxFont statusFont = m_validationStatus->GetFont();
325 statusFont.MakeBold();
326 m_validationStatus->SetFont(statusFont);
327 statusSizer->Add(m_validationStatus, 1, wxALIGN_CENTER_VERTICAL);
328
329 m_validateBtn = new wxButton(panel, ID_VALIDATE, "Validate");
330 statusSizer->Add(m_validateBtn, 0, wxALIGN_CENTER_VERTICAL);
331
332 sizer->Add(statusSizer, 0, wxEXPAND | wxALL, 10);
333
334 sizer->Add(new wxStaticLine(panel), 0, wxEXPAND | wxLEFT | wxRIGHT, 10);
335
336 // Validation report
337 wxStaticText *reportLabel = new wxStaticText(panel, wxID_ANY, "Validation Report:");
338 sizer->Add(reportLabel, 0, wxLEFT | wxRIGHT | wxTOP, 10);
339
340 m_validationReport = new wxTextCtrl(panel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize,
341 wxTE_MULTILINE | wxTE_READONLY | wxTE_RICH2 | wxHSCROLL);
342 m_validationReport->SetBackgroundColour(wxColour(45, 45, 45));
343 m_validationReport->SetForegroundColour(wxColour(220, 220, 220));
344
345 // Use monospace font for report
346 wxFont monoFont(10, wxFONTFAMILY_TELETYPE, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
347 m_validationReport->SetFont(monoFont);
348
349 sizer->Add(m_validationReport, 1, wxEXPAND | wxALL, 10);
350
351 panel->SetSizer(sizer);
352 return panel;
353}
354
355wxPanel *BehaviorTreeProjectDialog::CreatePreviewTab(wxNotebook *notebook) {
356 wxPanel *panel = new wxPanel(notebook);
357 wxBoxSizer *sizer = new wxBoxSizer(wxVERTICAL);
358
359 // Header with refresh button
360 wxBoxSizer *headerSizer = new wxBoxSizer(wxHORIZONTAL);
361
362 wxStaticText *header = new wxStaticText(panel, wxID_ANY, "Project Structure & Preview:");
363 headerSizer->Add(header, 1, wxALIGN_CENTER_VERTICAL);
364
365 m_refreshPreviewBtn = new wxButton(panel, ID_REFRESH_PREVIEW, "Refresh");
366 headerSizer->Add(m_refreshPreviewBtn, 0);
367
368 sizer->Add(headerSizer, 0, wxEXPAND | wxALL, 10);
369
370 // Filter controls
371 wxStaticBox *filterBox = new wxStaticBox(panel, wxID_ANY, "Show:");
372 wxStaticBoxSizer *filterSizer = new wxStaticBoxSizer(filterBox, wxHORIZONTAL);
373
374 m_showProjectIssuesCheckBox = new wxCheckBox(panel, wxID_ANY, "Project Issues");
375 m_showFilesCheckBox = new wxCheckBox(panel, wxID_ANY, "Files");
376 m_showTreesCheckBox = new wxCheckBox(panel, wxID_ANY, "Trees");
377 m_showBlackboardsCheckBox = new wxCheckBox(panel, wxID_ANY, "Blackboards");
378 m_showErrorsCheckBox = new wxCheckBox(panel, wxID_ANY, "File Errors");
379 m_showWarningsCheckBox = new wxCheckBox(panel, wxID_ANY, "File Warnings");
380
381 // All checked by default
382 m_showProjectIssuesCheckBox->SetValue(true);
383 m_showFilesCheckBox->SetValue(true);
384 m_showTreesCheckBox->SetValue(true);
385 m_showBlackboardsCheckBox->SetValue(true);
386 m_showErrorsCheckBox->SetValue(true);
387 m_showWarningsCheckBox->SetValue(true);
388
389 // Bind filter events
396
397 filterSizer->Add(m_showProjectIssuesCheckBox, 0, wxALL, 5);
398 filterSizer->Add(m_showFilesCheckBox, 0, wxALL, 5);
399 filterSizer->Add(m_showTreesCheckBox, 0, wxALL, 5);
400 filterSizer->Add(m_showBlackboardsCheckBox, 0, wxALL, 5);
401 filterSizer->Add(m_showErrorsCheckBox, 0, wxALL, 5);
402 filterSizer->Add(m_showWarningsCheckBox, 0, wxALL, 5);
403
404 sizer->Add(filterSizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10);
405
406 // Create splitter for two views
407 wxSplitterWindow *splitter =
408 new wxSplitterWindow(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSP_3D | wxSP_LIVE_UPDATE);
409
410 // Left panel: File structure (clickable)
411 wxPanel *leftPanel = new wxPanel(splitter);
412 wxBoxSizer *leftSizer = new wxBoxSizer(wxVERTICAL);
413
414 wxStaticText *leftLabel = new wxStaticText(leftPanel, wxID_ANY, "Structure (click to view):");
415 leftLabel->SetFont(leftLabel->GetFont().Bold());
416 leftSizer->Add(leftLabel, 0, wxALL, 5);
417
419 new wxScrolledWindow(leftPanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL | wxBORDER_SUNKEN);
420 m_fileStructurePanel->SetBackgroundColour(wxColour(45, 45, 45));
421 m_fileStructurePanel->SetScrollRate(0, 10);
422
423 m_fileStructurePanel->Bind(wxEVT_PAINT, [this](wxPaintEvent &event) {
424 wxPaintDC dc(m_fileStructurePanel);
425 m_fileStructurePanel->DoPrepareDC(dc);
427 });
428
430
431 leftSizer->Add(m_fileStructurePanel, 1, wxEXPAND | wxALL, 5);
432 leftPanel->SetSizer(leftSizer);
433
434 // Right panel: XML preview with warning details at bottom
435 wxPanel *rightPanel = new wxPanel(splitter);
436 wxBoxSizer *rightSizer = new wxBoxSizer(wxVERTICAL);
437
438 wxStaticText *rightLabel = new wxStaticText(rightPanel, wxID_ANY, "XML Preview:");
439 rightLabel->SetFont(rightLabel->GetFont().Bold());
440 rightSizer->Add(rightLabel, 0, wxALL, 5);
441
442 // Create a panel to hold both XML preview and warning details
443 wxPanel *xmlContainerPanel = new wxPanel(rightPanel);
444 wxBoxSizer *xmlContainerSizer = new wxBoxSizer(wxVERTICAL);
445
447 new wxStyledTextCtrl(xmlContainerPanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_SUNKEN);
448
449 // Configure as read-only text viewer with line numbers
450 m_xmlPreviewPanel->SetReadOnly(true);
451 m_xmlPreviewPanel->SetWrapMode(wxSTC_WRAP_NONE); // No text wrapping
452
453 // Set monospace font for all styles
454 wxFont monoFont(10, wxFONTFAMILY_TELETYPE, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
455 m_xmlPreviewPanel->StyleSetFont(wxSTC_STYLE_DEFAULT, monoFont);
456
457 // Dark theme colors - set for all styles
458 m_xmlPreviewPanel->StyleSetBackground(wxSTC_STYLE_DEFAULT, wxColour(30, 30, 30));
459 m_xmlPreviewPanel->StyleSetForeground(wxSTC_STYLE_DEFAULT, wxColour(220, 220, 220));
460 m_xmlPreviewPanel->StyleClearAll(); // Apply default style to all styles
461
462 // Define custom styles for text coloring (no background change)
463 // Style 1: Warning/Error text (bright orange)
464 m_xmlPreviewPanel->StyleSetFont(1, monoFont);
465 m_xmlPreviewPanel->StyleSetBackground(1, wxColour(30, 30, 30)); // Same as default
466 m_xmlPreviewPanel->StyleSetForeground(1, wxColour(255, 140, 0)); // Bright orange
467
468 // Style 2: Tree text (bright green)
469 m_xmlPreviewPanel->StyleSetFont(2, monoFont);
470 m_xmlPreviewPanel->StyleSetBackground(2, wxColour(30, 30, 30)); // Same as default
471 m_xmlPreviewPanel->StyleSetForeground(2, wxColour(100, 255, 100)); // Bright green
472
473 // Style 3: Blackboard text (purple/magenta)
474 m_xmlPreviewPanel->StyleSetFont(3, monoFont);
475 m_xmlPreviewPanel->StyleSetBackground(3, wxColour(30, 30, 30)); // Same as default
476 m_xmlPreviewPanel->StyleSetForeground(3, wxColour(200, 130, 255)); // Purple/magenta
477
478 // Set caret (cursor) color
479 m_xmlPreviewPanel->SetCaretForeground(wxColour(220, 220, 220));
480 m_xmlPreviewPanel->SetCaretLineVisible(false); // Don't highlight current line
481
482 // Set selection colors
483 m_xmlPreviewPanel->SetSelectionMode(wxSTC_SEL_STREAM);
484 m_xmlPreviewPanel->SetSelBackground(true, wxColour(60, 90, 140)); // Blue selection background
485 m_xmlPreviewPanel->SetSelForeground(true, wxColour(255, 255, 255)); // White selection text
486
487 // Line numbers margin
488 m_xmlPreviewPanel->SetMarginType(0, wxSTC_MARGIN_NUMBER);
489 m_xmlPreviewPanel->SetMarginWidth(0, 50); // Width for line numbers
490 m_xmlPreviewPanel->StyleSetBackground(wxSTC_STYLE_LINENUMBER, wxColour(40, 40, 40));
491 m_xmlPreviewPanel->StyleSetForeground(wxSTC_STYLE_LINENUMBER, wxColour(120, 120, 120));
492
493 // Disable other margins
494 m_xmlPreviewPanel->SetMarginWidth(1, 0); // No symbol margin
495 m_xmlPreviewPanel->SetMarginWidth(2, 0); // No fold margin
496
497 // Set initial text
498 m_xmlPreviewPanel->SetText("Click an item to preview XML content");
499
500 xmlContainerSizer->Add(m_xmlPreviewPanel, 1, wxEXPAND);
501
502 // Warning details panel at the bottom (initially hidden)
503 m_warningDetailsPanel = new wxTextCtrl(xmlContainerPanel, wxID_ANY, "", wxDefaultPosition, wxSize(-1, 180),
504 wxTE_MULTILINE | wxTE_READONLY | wxTE_WORDWRAP | wxBORDER_SUNKEN);
505 m_warningDetailsPanel->SetBackgroundColour(wxColour(60, 40, 30));
506 m_warningDetailsPanel->SetForegroundColour(wxColour(255, 200, 100));
507 wxFont warningFont(9, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
508 m_warningDetailsPanel->SetFont(warningFont);
509 m_warningDetailsPanel->Hide(); // Initially hidden
510
511 xmlContainerSizer->Add(m_warningDetailsPanel, 0, wxEXPAND);
512 xmlContainerPanel->SetSizer(xmlContainerSizer);
513
514 rightSizer->Add(xmlContainerPanel, 1, wxEXPAND | wxALL, 5);
515 rightPanel->SetSizer(rightSizer);
516
517 // Split the view 25/75 (structure gets 25%, preview gets 75%)
518 splitter->SplitVertically(leftPanel, rightPanel);
519 splitter->SetSashGravity(0.25);
520 splitter->SetMinimumPaneSize(20); // Small minimum so we can control the split
521
522 // Set initial sash position to 25% of the width
523 // We'll set it after the window is shown, so bind to the size event
524 splitter->Bind(wxEVT_SIZE, [splitter](wxSizeEvent &event) {
525 int width = splitter->GetSize().GetWidth();
526 splitter->SetSashPosition(static_cast<int>(width * 0.25));
527 event.Skip();
528 });
529
530 // Make splitter non-movable by preventing sash position changes
531 splitter->Bind(wxEVT_SPLITTER_SASH_POS_CHANGING, [](wxSplitterEvent &event) {
532 event.Veto(); // Prevent sash from being moved
533 });
534
535 sizer->Add(splitter, 1, wxEXPAND | wxLEFT | wxRIGHT, 10);
536
537 // Tree count label
538 m_treeCountLabel = new wxStaticText(panel, wxID_ANY, "No trees loaded");
539 sizer->Add(m_treeCountLabel, 0, wxALL, 10);
540
541 panel->SetSizer(sizer);
542 return panel;
543}
544
546 wxPanel *panel = new wxPanel(notebook);
547 wxBoxSizer *sizer = new wxBoxSizer(wxVERTICAL);
548
549 // Header
550 wxStaticText *header = new wxStaticText(panel, wxID_ANY, "Blackboard Overview:");
551 header->SetFont(header->GetFont().Bold());
552 sizer->Add(header, 0, wxALL, 10);
553
554 // Scrollable panel for blackboard table-cards
555 m_blackboardPanel = new wxScrolledWindow(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize,
556 wxVSCROLL | wxHSCROLL | wxBORDER_SUNKEN);
557 m_blackboardPanel->SetBackgroundColour(wxColour(40, 40, 40));
558 m_blackboardPanel->SetScrollRate(10, 10);
559
560 m_blackboardPanel->Bind(wxEVT_PAINT, [this](wxPaintEvent &event) {
561 wxPaintDC dc(m_blackboardPanel);
562 m_blackboardPanel->DoPrepareDC(dc);
564 });
565
566 sizer->Add(m_blackboardPanel, 1, wxEXPAND | wxALL, 5);
567
568 panel->SetSizer(sizer);
569 return panel;
570}
571
577
579 dc.SetBackground(wxBrush(wxColour(40, 40, 40)));
580 dc.Clear();
581
583 dc.SetTextForeground(wxColour(150, 150, 150));
584 dc.SetFont(wxFont(11, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_ITALIC, wxFONTWEIGHT_NORMAL));
585 dc.DrawText("Run validation to see blackboard data", 20, 20);
586 return;
587 }
588
589 const auto &report = *m_lastValidationReport;
590
591 int x = 15;
592 int y = 10;
593 int panelWidth = m_blackboardPanel->GetClientSize().GetWidth();
594 if (panelWidth < 300)
595 panelWidth = 600;
596 int contentWidth = panelWidth - 30;
597
598 // --- Summary Box ---
599 {
600 int summaryHeight = 80;
601 dc.SetBrush(wxBrush(wxColour(50, 50, 60)));
602 dc.SetPen(wxPen(wxColour(100, 130, 200), 1));
603 dc.DrawRoundedRectangle(x, y, contentWidth, summaryHeight, 5);
604
605 dc.SetTextForeground(wxColour(130, 180, 255));
606 dc.SetFont(wxFont(11, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD));
607 dc.DrawText("Blackboard Summary", x + 10, y + 8);
608
609 // Collect counts
610 int totalBBs = 0, totalFiles = 0;
611 for (const auto &rs : report.resource_statuses) {
612 if (rs.has_blackboards) {
613 totalFiles++;
614 totalBBs += rs.blackboard_count;
615 }
616 }
617
618 dc.SetFont(wxFont(9, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL));
619 dc.SetTextForeground(wxColour(200, 200, 200));
620 dc.DrawText(wxString::Format("Total Blackboards: %d across %d file(s)", totalBBs, totalFiles), x + 15, y + 30);
621
622 if (!report.unresolved_blackboard_includes.empty()) {
623 dc.SetTextForeground(wxColour(255, 180, 50));
624 wxString unresolvedStr = "Unresolved includes: ";
625 for (size_t i = 0; i < report.unresolved_blackboard_includes.size(); ++i) {
626 if (i > 0)
627 unresolvedStr += ", ";
628 unresolvedStr += report.unresolved_blackboard_includes[i];
629 }
630 dc.DrawText(unresolvedStr, x + 15, y + 50);
631 } else {
632 dc.SetTextForeground(wxColour(100, 255, 100));
633 dc.DrawText("All blackboard includes resolved", x + 15, y + 50);
634 }
635
636 y += summaryHeight + 15;
637 }
638
639 // --- Per-file blackboard tables ---
640 // We need to re-parse files to get entry details since validation only stores IDs
641 for (const auto &rs : report.resource_statuses) {
642 if (!rs.has_blackboards)
643 continue;
644
645 // File header bar
646 wxFileName fn(wxString::FromUTF8(rs.filepath));
647 wxString filename = fn.GetFullName();
648
649 dc.SetBrush(wxBrush(wxColour(60, 50, 70)));
650 dc.SetPen(wxPen(wxColour(150, 100, 200), 1));
651 dc.DrawRoundedRectangle(x, y, contentWidth, 28, 4);
652
653 dc.SetTextForeground(wxColour(200, 160, 255));
654 dc.SetFont(wxFont(10, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD));
655 dc.DrawText(filename, x + 10, y + 5);
656
657 dc.SetTextForeground(wxColour(150, 150, 150));
658 dc.SetFont(wxFont(8, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL));
659 wxString countStr = wxString::Format("(%d blackboard(s))", rs.blackboard_count);
660 dc.DrawText(countStr, x + contentWidth - 130, y + 7);
661
662 y += 35;
663
664 // Parse the XML to get entry details
665 std::string resolvedPath = m_project->ResolveResourcePath(rs.filepath);
666 xmlDocPtr doc = xmlReadFile(resolvedPath.c_str(), nullptr, XML_PARSE_NOERROR | XML_PARSE_NOWARNING);
667 if (!doc) {
668 dc.SetTextForeground(wxColour(255, 100, 100));
669 dc.DrawText("Could not parse file for blackboard details", x + 20, y);
670 y += 25;
671 continue;
672 }
673
674 xmlNodePtr root = xmlDocGetRootElement(doc);
675 if (!root) {
676 xmlFreeDoc(doc);
677 continue;
678 }
679
680 for (xmlNodePtr child = root->children; child; child = child->next) {
681 if (child->type != XML_ELEMENT_NODE)
682 continue;
683 const char *nodeName = reinterpret_cast<const char *>(child->name);
684 if (!nodeName || strcmp(nodeName, "Blackboard") != 0)
685 continue;
686
687 // Get blackboard ID
688 std::string bbId;
689 xmlChar *idAttr = xmlGetProp(child, reinterpret_cast<const xmlChar *>("ID"));
690 if (idAttr) {
691 bbId = reinterpret_cast<const char *>(idAttr);
692 xmlFree(idAttr);
693 }
694
695 // Get includes
696 std::string includesStr;
697 xmlChar *incAttr = xmlGetProp(child, reinterpret_cast<const xmlChar *>("includes"));
698 if (incAttr) {
699 includesStr = reinterpret_cast<const char *>(incAttr);
700 xmlFree(incAttr);
701 }
702
703 // Collect entries
704 struct BBEntry {
705 std::string key, type, value;
706 };
707 std::vector<BBEntry> entries;
708 for (xmlNodePtr entryNode = child->children; entryNode; entryNode = entryNode->next) {
709 if (entryNode->type != XML_ELEMENT_NODE)
710 continue;
711 const char *eName = reinterpret_cast<const char *>(entryNode->name);
712 if (!eName || strcmp(eName, "Entry") != 0)
713 continue;
714
715 BBEntry e;
716 xmlChar *keyA = xmlGetProp(entryNode, reinterpret_cast<const xmlChar *>("key"));
717 if (keyA) {
718 e.key = reinterpret_cast<const char *>(keyA);
719 xmlFree(keyA);
720 }
721 xmlChar *typeA = xmlGetProp(entryNode, reinterpret_cast<const xmlChar *>("type"));
722 if (typeA) {
723 e.type = reinterpret_cast<const char *>(typeA);
724 xmlFree(typeA);
725 }
726 xmlChar *valA = xmlGetProp(entryNode, reinterpret_cast<const xmlChar *>("value"));
727 if (valA) {
728 e.value = reinterpret_cast<const char *>(valA);
729 xmlFree(valA);
730 }
731 entries.push_back(e);
732 }
733
734 // --- Draw blackboard card ---
735 // Card header
736 int cardHeaderH = 26;
737 dc.SetBrush(wxBrush(wxColour(70, 50, 90)));
738 dc.SetPen(wxPen(wxColour(150, 100, 200), 1));
739 dc.DrawRoundedRectangle(x + 10, y, contentWidth - 20, cardHeaderH, 3);
740
741 dc.SetTextForeground(wxColour(220, 180, 255));
742 dc.SetFont(wxFont(9, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD));
743 wxString headerText = wxString::Format("[%s] (%zu entries)", bbId, entries.size());
744 dc.DrawText(headerText, x + 20, y + 4);
745
746 if (!includesStr.empty()) {
747 dc.SetTextForeground(wxColour(180, 150, 200));
748 dc.SetFont(wxFont(8, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL));
749 wxString incText = "includes: " + wxString::FromUTF8(includesStr);
750 // Truncate if too long
751 int maxW = contentWidth - 200;
752 wxSize ext = dc.GetTextExtent(incText);
753 if (ext.GetWidth() > maxW && maxW > 0) {
754 while (dc.GetTextExtent(incText + "...").GetWidth() > maxW && incText.Length() > 10) {
755 incText = incText.Left(incText.Length() - 1);
756 }
757 incText += "...";
758 }
759 dc.DrawText(incText, x + contentWidth - 180, y + 6);
760 }
761
762 y += cardHeaderH + 2;
763
764 // Table header row
765 int rowH = 20;
766 int col1W = (contentWidth - 40) * 45 / 100; // Key: 45%
767 int col2W = (contentWidth - 40) * 25 / 100; // Type: 25%
768 int col3W = (contentWidth - 40) - col1W - col2W; // Value: rest
769 int tableX = x + 15;
770
771 dc.SetBrush(wxBrush(wxColour(55, 55, 65)));
772 dc.SetPen(wxPen(wxColour(80, 80, 100), 1));
773 dc.DrawRectangle(tableX, y, col1W, rowH);
774 dc.DrawRectangle(tableX + col1W, y, col2W, rowH);
775 dc.DrawRectangle(tableX + col1W + col2W, y, col3W, rowH);
776
777 dc.SetTextForeground(wxColour(180, 180, 220));
778 dc.SetFont(wxFont(8, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD));
779 dc.DrawText("Key", tableX + 5, y + 3);
780 dc.DrawText("Type", tableX + col1W + 5, y + 3);
781 dc.DrawText("Value", tableX + col1W + col2W + 5, y + 3);
782
783 y += rowH;
784
785 // Table rows
786 dc.SetFont(wxFont(8, wxFONTFAMILY_TELETYPE, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL));
787 for (size_t ei = 0; ei < entries.size(); ++ei) {
788 const auto &e = entries[ei];
789
790 // Alternating row colors
791 wxColour rowBg = (ei % 2 == 0) ? wxColour(45, 45, 50) : wxColour(50, 50, 55);
792 dc.SetBrush(wxBrush(rowBg));
793 dc.SetPen(wxPen(wxColour(60, 60, 70), 1));
794 dc.DrawRectangle(tableX, y, col1W, rowH);
795 dc.DrawRectangle(tableX + col1W, y, col2W, rowH);
796 dc.DrawRectangle(tableX + col1W + col2W, y, col3W, rowH);
797
798 // Truncate text helper
799 auto truncate = [&dc](const wxString &text, int maxWidth) -> wxString {
800 wxString result = text;
801 if (dc.GetTextExtent(result).GetWidth() > maxWidth && maxWidth > 0) {
802 while (dc.GetTextExtent(result + "...").GetWidth() > maxWidth && result.Length() > 1) {
803 result = result.Left(result.Length() - 1);
804 }
805 result += "...";
806 }
807 return result;
808 };
809
810 dc.SetTextForeground(wxColour(200, 200, 200));
811 dc.DrawText(truncate(wxString::FromUTF8(e.key), col1W - 10), tableX + 5, y + 3);
812
813 dc.SetTextForeground(wxColour(150, 200, 180));
814 dc.DrawText(truncate(wxString::FromUTF8(e.type), col2W - 10), tableX + col1W + 5, y + 3);
815
816 dc.SetTextForeground(wxColour(180, 180, 150));
817 dc.DrawText(truncate(wxString::FromUTF8(e.value), col3W - 10), tableX + col1W + col2W + 5, y + 3);
818
819 y += rowH;
820 }
821
822 y += 10; // Space between blackboard cards
823 }
824
825 xmlFreeDoc(doc);
826 y += 10; // Space between files
827 }
828
829 // If no blackboards found
830 if (y < 100) {
831 dc.SetTextForeground(wxColour(150, 150, 150));
832 dc.SetFont(wxFont(10, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_ITALIC, wxFONTWEIGHT_NORMAL));
833 dc.DrawText("No blackboards found in project files", 20, y);
834 y += 30;
835 }
836
837 // Set virtual size for scrolling
838 m_blackboardPanel->SetVirtualSize(panelWidth, y + 20);
839}
840
842 m_parserProfile->Clear();
843
844 auto &configManager = EmberCore::ConfigManager::GetInstance();
845 auto profileNames = configManager.GetProfileNames();
846
847 for (const auto &name : profileNames) {
848 m_parserProfile->Append(wxString::FromUTF8(name));
849 }
850
851 // Select the project's profile or the active profile
852 wxString profileToSelect = wxString::FromUTF8(m_project->GetParserProfileName());
853 int index = m_parserProfile->FindString(profileToSelect);
854 if (index == wxNOT_FOUND) {
855 profileToSelect = wxString::FromUTF8(configManager.GetActiveProfileName());
856 index = m_parserProfile->FindString(profileToSelect);
857 }
858
859 if (index != wxNOT_FOUND) {
860 m_parserProfile->SetSelection(index);
861 } else if (m_parserProfile->GetCount() > 0) {
862 m_parserProfile->SetSelection(0);
863 }
864}
865
867 m_projectName->SetValue(wxString::FromUTF8(m_project->GetName()));
868 m_projectDescription->SetValue(wxString::FromUTF8(m_project->GetDescription()));
869
870 // Set parser profile
871 wxString profileName = wxString::FromUTF8(m_project->GetParserProfileName());
872 int index = m_parserProfile->FindString(profileName);
873 if (index != wxNOT_FOUND) {
874 m_parserProfile->SetSelection(index);
875 }
876
877 // Update resource list
879
880 // Defer validation until after the dialog is visible
881 // This way the dialog opens instantly and users see the progress bar
882 CallAfter([this]() { ValidateAndUpdateUI(); });
883}
884
886 m_resourceList->DeleteAllItems();
887
888 const auto &resources = m_project->GetResources();
889
890 for (size_t i = 0; i < resources.size(); ++i) {
891 const auto &resource = resources[i];
892 wxFileName fn(wxString::FromUTF8(resource));
893
894 long item = m_resourceList->InsertItem(i, "");
895 m_resourceList->SetItem(item, 1, fn.GetFullName());
896 m_resourceList->SetItem(item, 2, fn.GetPath());
897 m_resourceList->SetItem(item, 3, "?");
898
899 // Default to unknown status
900 m_resourceList->SetItemImage(item, 1); // Warning icon
901 }
902
903 // Update count label
904 m_resourceCountLabel->SetLabel(wxString::Format("%zu file(s)", resources.size()));
905 m_clearFilesBtn->Enable(!resources.empty());
906}
907
909 m_project->SetName(m_projectName->GetValue().ToStdString());
911}
912
914 m_project->SetDescription(m_projectDescription->GetValue().ToStdString());
915}
916
918 int sel = m_parserProfile->GetSelection();
919 if (sel != wxNOT_FOUND) {
920 wxString selectedProfile = m_parserProfile->GetString(sel);
921 m_project->SetParserProfileName(selectedProfile.ToStdString());
922
923 // Mark validation as stale if profile changed
924 if (!m_lastValidatedParserProfile.IsEmpty() && m_lastValidatedParserProfile != selectedProfile) {
925 m_validationStale = true;
926
927 // Update validation status to notify user
928 m_validationStatus->SetLabel("Parser profile changed - click 'Validate' to revalidate");
929 m_validationStatus->SetForegroundColour(wxColour(255, 200, 100));
930 m_validationIcon->SetBitmap(wxArtProvider::GetBitmap(wxART_INFORMATION, wxART_MESSAGE_BOX, wxSize(24, 24)));
931 }
932 }
933}
934
935void BehaviorTreeProjectDialog::OnAddFiles(wxCommandEvent &event) {
936 // Determine the initial directory
937 wxString initialDir = s_lastUsedDirectory;
938
939 // If no last directory, try to use the project's directory or default to resources folder
940 if (initialDir.IsEmpty()) {
941 if (!m_project->GetProjectFilePath().empty()) {
942 wxFileName projectPath(wxString::FromUTF8(m_project->GetProjectFilePath()));
943 initialDir = projectPath.GetPath();
944 }
945 if (initialDir.IsEmpty()) {
946 // Default to resources folder
948
949 // Fallback to home if resources doesn't exist
950 if (initialDir.IsEmpty() || !wxFileName::DirExists(initialDir)) {
951 initialDir = wxGetHomeDir();
952 }
953 }
954 }
955
956 wxFileDialog openFileDialog(
957 this, "Add Resource Files (Tip: Use Shift+Click or Right Ctrl+Click for multi-selection)", initialDir, "",
958 "All files (*.*)|*.*|XML files (*.xml)|*.xml", wxFD_OPEN | wxFD_FILE_MUST_EXIST | wxFD_MULTIPLE);
959
960 if (openFileDialog.ShowModal() == wxID_CANCEL) {
961 return;
962 }
963
964 // Remember the directory for next time
965 s_lastUsedDirectory = openFileDialog.GetDirectory();
966
967 wxArrayString paths;
968 openFileDialog.GetPaths(paths);
969
970 int addedCount = 0;
971 int duplicateCount = 0;
972
973 for (const auto &path : paths) {
974 std::string pathStr = path.ToStdString();
975
976 // Store relative path if project has a file path
977 if (!m_project->GetProjectFilePath().empty()) {
978 pathStr = m_project->MakeRelativePath(pathStr);
979 }
980
981 // Check if already added (AddResource returns false for duplicates)
982 if (m_project->AddResource(pathStr)) {
983 addedCount++;
984 } else {
985 duplicateCount++;
986 }
987 }
988
989 // Notify user about duplicates
990 if (duplicateCount > 0) {
991 wxString msg = wxString::Format("%d file(s) added. %d file(s) were already in the project and skipped.",
992 addedCount, duplicateCount);
993 wxMessageBox(msg, "Files Added", wxOK | wxICON_INFORMATION, this);
994 }
995
998}
999
1000void BehaviorTreeProjectDialog::OnAddFolder(wxCommandEvent &event) {
1001 wxString initialDir = s_lastUsedDirectory;
1002
1003 if (initialDir.IsEmpty()) {
1004 if (!m_project->GetProjectFilePath().empty()) {
1005 wxFileName projectPath(wxString::FromUTF8(m_project->GetProjectFilePath()));
1006 initialDir = projectPath.GetPath();
1007 }
1008 if (initialDir.IsEmpty()) {
1010 if (initialDir.IsEmpty() || !wxFileName::DirExists(initialDir)) {
1011 initialDir = wxGetHomeDir();
1012 }
1013 }
1014 }
1015
1016 wxDirDialog dirDialog(this, "Select folder to add XML files recursively", initialDir,
1017 wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST);
1018
1019 if (dirDialog.ShowModal() == wxID_CANCEL)
1020 return;
1021
1022 wxString selectedDir = dirDialog.GetPath();
1023 s_lastUsedDirectory = selectedDir;
1024
1025 wxArrayString xmlFiles;
1026 wxDir::GetAllFiles(selectedDir, &xmlFiles, "*.xml", wxDIR_FILES | wxDIR_DIRS);
1027
1028 if (xmlFiles.IsEmpty()) {
1029 wxMessageBox("No XML files found in the selected folder or its subfolders.", "No Files Found",
1030 wxOK | wxICON_INFORMATION, this);
1031 return;
1032 }
1033
1034 int addedCount = 0;
1035 int duplicateCount = 0;
1036
1037 for (const auto &path : xmlFiles) {
1038 std::string pathStr = path.ToStdString();
1039
1040 if (!m_project->GetProjectFilePath().empty()) {
1041 pathStr = m_project->MakeRelativePath(pathStr);
1042 }
1043
1044 if (m_project->AddResource(pathStr)) {
1045 addedCount++;
1046 } else {
1047 duplicateCount++;
1048 }
1049 }
1050
1051 wxString msg;
1052 if (duplicateCount > 0) {
1053 msg = wxString::Format("%d XML file(s) added from folder. %d file(s) were already in the project and skipped.",
1054 addedCount, duplicateCount);
1055 } else {
1056 msg = wxString::Format("%d XML file(s) added from folder and subfolders.", addedCount);
1057 }
1058 wxMessageBox(msg, "Folder Added", wxOK | wxICON_INFORMATION, this);
1059
1062}
1063
1064void BehaviorTreeProjectDialog::OnRemoveFiles(wxCommandEvent &event) {
1065 long item = m_resourceList->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
1066 if (item == -1) {
1067 return;
1068 }
1069
1070 const auto &resources = m_project->GetResources();
1071 if (item < static_cast<long>(resources.size())) {
1072 m_project->RemoveResource(resources[item]);
1075 }
1076
1077 m_removeFilesBtn->Enable(false);
1078}
1079
1080void BehaviorTreeProjectDialog::OnClearFiles(wxCommandEvent &event) {
1081 int result = wxMessageBox("Remove all files from the project?", "Confirm", wxYES_NO | wxICON_QUESTION, this);
1082 if (result == wxYES) {
1083 m_project->ClearResources();
1086 }
1087}
1088
1089void BehaviorTreeProjectDialog::OnResourceSelected(wxListEvent &event) { m_removeFilesBtn->Enable(true); }
1090
1092 // Double-click on resource: switch to Preview tab and show the file
1093 long item = event.GetIndex();
1094 const auto &resources = m_project->GetResources();
1095
1096 if (item >= static_cast<long>(resources.size())) {
1097 return;
1098 }
1099
1100 std::string resourcePath = resources[item];
1101
1102 // Switch to Preview tab (index 2: Resources=0, Validation=1, Preview=2)
1103 if (m_notebook) {
1104 m_notebook->SetSelection(2);
1105 }
1106
1107 // Find this file in the clickable items and select it
1108 for (size_t i = 0; i < m_clickableItems.size(); ++i) {
1109 const auto &clickItem = m_clickableItems[i];
1110 if (clickItem.type == ClickableItem::File && clickItem.filepath == resourcePath) {
1111 // Found it! Select and load preview
1112 m_selectedItemIndex = static_cast<int>(i);
1113
1114 // Smart scroll: Scroll the structure panel to show the selected item
1116 wxRect itemRect = clickItem.rect;
1117 int scrollUnitY;
1118 m_fileStructurePanel->GetScrollPixelsPerUnit(nullptr, &scrollUnitY);
1119
1120 if (scrollUnitY > 0) {
1121 // Get visible area
1122 wxSize clientSize = m_fileStructurePanel->GetClientSize();
1123 int viewStartX, viewStartY;
1124 m_fileStructurePanel->GetViewStart(&viewStartX, &viewStartY);
1125 int visibleTop = viewStartY * scrollUnitY;
1126 int visibleBottom = visibleTop + clientSize.GetHeight();
1127
1128 // Check if item is not fully visible
1129 if (itemRect.GetTop() < visibleTop || itemRect.GetBottom() > visibleBottom) {
1130 // Scroll to center the item
1131 int targetY = itemRect.GetTop() - (clientSize.GetHeight() / 2) + (itemRect.GetHeight() / 2);
1132 if (targetY < 0)
1133 targetY = 0;
1134 m_fileStructurePanel->Scroll(0, targetY / scrollUnitY);
1135 }
1136 }
1137 }
1138
1139 m_fileStructurePanel->Refresh();
1140 LoadXMLPreview(clickItem.filepath);
1141 m_warningDetailsPanel->Hide();
1142 m_warningDetailsPanel->GetParent()->Layout();
1143 return;
1144 }
1145 }
1146
1147 // If not found in clickable items, just load the XML directly
1148 LoadXMLPreview(m_project->ResolveResourcePath(resourcePath));
1149 m_warningDetailsPanel->Hide();
1150 m_warningDetailsPanel->GetParent()->Layout();
1151}
1152
1154
1156
1158 // Redraw the preview with new filters
1160 m_fileStructurePanel->Refresh();
1161 }
1162}
1163
1165 wxString currentProfile = m_parserProfile->GetStringSelection();
1166
1167 // Show progress UI
1168 m_validationProgress->Show();
1170 m_validationProgress->SetValue(0);
1171 m_validationProgressText->SetLabelText("Initializing parser...");
1172 m_validateBtn->Enable(false);
1173 Layout();
1174 Update();
1175 wxYield();
1176
1178
1179 // Create parser with selected profile
1180 std::string profileName = currentProfile.ToStdString();
1181 auto &configManager = EmberCore::ConfigManager::GetInstance();
1182 auto profile = configManager.GetProfile(profileName);
1183
1184 EmberCore::ParserConfig parserConfig;
1185 if (profile) {
1186 parserConfig = profile->GetConfig();
1187 } else {
1188 // Fallback to default profile
1189 LOG_WARNING("BehaviorTreeProjectDialog", "Failed to load parser profile, using default config");
1190 }
1191
1192 // Create parser instance
1193 auto parser = std::make_unique<EmberCore::LibXMLBehaviorTreeParser>(parserConfig);
1194
1195 // Set progress callback
1197 parser->SetProgressCallback(&progressCallback);
1198
1199 // Run parser-based validation
1200 report = m_project->ValidateWithParser(parser.get());
1201
1202 // Store result
1203 m_lastValidationReport = std::make_unique<EmberCore::ProjectValidationReport>(report);
1204 m_validationStale = false;
1205 m_lastValidatedParserProfile = currentProfile;
1206
1207 // Build a map of which files reference unimplemented trees
1208 std::set<EmberCore::String> files_with_unimpl_refs;
1209 for (const auto &tree_id : report.unimplemented_trees) {
1210 auto it = report.tree_statuses.find(tree_id);
1211 if (it != report.tree_statuses.end()) {
1212 for (const auto &file : it->second.referenced_in_files) {
1213 files_with_unimpl_refs.insert(file);
1214 }
1215 }
1216 }
1217
1218 // Update resource list with status
1219 const auto &resources = m_project->GetResources();
1220 for (size_t i = 0; i < report.resource_statuses.size() && i < resources.size(); ++i) {
1221 const auto &status = report.resource_statuses[i];
1222
1223 int imageIndex;
1224 if (!status.IsValid()) {
1225 // File has errors (invalid XML, not found, etc.)
1226 imageIndex = status.exists ? 1 : 2; // warning or error
1227 } else if (files_with_unimpl_refs.find(resources[i]) != files_with_unimpl_refs.end()) {
1228 // File is valid but references unimplemented trees
1229 imageIndex = 3; // help/question icon
1230 } else {
1231 // File is valid and all references are implemented
1232 imageIndex = 0; // tick
1233 }
1234
1235 m_resourceList->SetItemImage(i, imageIndex);
1236 m_resourceList->SetItem(i, 3, wxString::Format("%d", status.tree_count));
1237 }
1238
1239 // Update validation status
1240 if (report.is_valid) {
1241 if (!report.unimplemented_trees.empty()) {
1242 m_validationStatus->SetLabel("Validation passed with warnings");
1243 m_validationStatus->SetForegroundColour(wxColour(255, 180, 50));
1244 m_validationIcon->SetBitmap(wxArtProvider::GetBitmap(wxART_WARNING, wxART_MESSAGE_BOX, wxSize(24, 24)));
1245 } else {
1246 m_validationStatus->SetLabel("Validation passed");
1247 m_validationStatus->SetForegroundColour(wxColour(100, 200, 100));
1248 m_validationIcon->SetBitmap(wxArtProvider::GetBitmap(wxART_TICK_MARK, wxART_MESSAGE_BOX, wxSize(24, 24)));
1249 }
1250 } else {
1251 m_validationStatus->SetLabel("Validation failed - see report for details");
1252 m_validationStatus->SetForegroundColour(wxColour(255, 100, 100));
1253 m_validationIcon->SetBitmap(wxArtProvider::GetBitmap(wxART_ERROR, wxART_MESSAGE_BOX, wxSize(24, 24)));
1254 }
1255
1256 // Generate and display report with color coding
1258
1259 m_isValid = report.is_valid;
1261
1262 // Update preview
1264
1265 // Update blackboard tab
1267
1268 // Hide progress UI
1269 m_validationProgress->Hide();
1271 m_validateBtn->Enable(true);
1272 Layout();
1273}
1274
1276 m_validationReport->Clear();
1277
1278 // Define colors
1279 wxColour headerColor(100, 200, 255); // Light blue for headers
1280 wxColour successColor(100, 255, 100); // Green for success/OK
1281 wxColour warningColor(255, 200, 50); // Yellow/orange for warnings
1282 wxColour errorColor(255, 100, 100); // Red for errors
1283 wxColour infoColor(180, 180, 180); // Light gray for info
1284 wxColour defaultColor(220, 220, 220); // Default text color
1285
1286 // Helper lambda to append colored text
1287 auto AppendColored = [this](const wxString &text, const wxColour &color) {
1288 wxTextAttr style;
1289 style.SetTextColour(color);
1290 m_validationReport->SetDefaultStyle(style);
1291 m_validationReport->AppendText(text);
1292 };
1293
1294 // Header
1295 AppendColored("=== BehaviorTree Project Validation Report ===\n\n", headerColor);
1296
1297 // File count
1298 AppendColored(wxString::Format("Files: %zu XML files\n\n", report.resource_statuses.size()), infoColor);
1299
1300 // Valid files section
1301 int valid_count = report.GetValidFileCount();
1302 AppendColored(wxString::Format("VALID FILES (%d):\n", valid_count), successColor);
1303 for (const auto &status : report.resource_statuses) {
1304 if (status.IsValid()) {
1305 AppendColored(" [OK] ", successColor);
1306 AppendColored(wxString::FromUTF8(status.filepath.c_str()), defaultColor);
1307 AppendColored(wxString::Format(" (%d trees)\n", status.tree_count), infoColor);
1308 }
1309 }
1310 AppendColored("\n", defaultColor);
1311
1312 // Invalid files section
1313 int invalid_count = static_cast<int>(report.resource_statuses.size()) - valid_count;
1314 if (invalid_count > 0) {
1315 AppendColored(wxString::Format("INVALID FILES (%d):\n", invalid_count), errorColor);
1316 for (const auto &status : report.resource_statuses) {
1317 if (!status.IsValid()) {
1318 AppendColored(" [ERROR] ", errorColor);
1319 AppendColored(wxString::FromUTF8(status.filepath.c_str()) + "\n", defaultColor);
1320 for (const auto &err : status.errors) {
1321 AppendColored(" - ", errorColor);
1322 AppendColored(wxString::FromUTF8(err.c_str()) + "\n", defaultColor);
1323 }
1324 }
1325 }
1326 AppendColored("\n", defaultColor);
1327 }
1328
1329 // Warnings section
1330 if (!report.warnings.empty() || !report.unimplemented_trees.empty()) {
1331 AppendColored(wxString::Format("WARNINGS (%d):\n",
1332 static_cast<int>(report.warnings.size() + report.unimplemented_trees.size())),
1333 warningColor);
1334 for (const auto &warn : report.warnings) {
1335 AppendColored(" [WARN] ", warningColor);
1336 AppendColored(wxString::FromUTF8(warn.c_str()) + "\n", defaultColor);
1337 }
1338 for (const auto &tree : report.unimplemented_trees) {
1339 AppendColored(" [WARN] ", warningColor);
1340 AppendColored("Tree '", defaultColor);
1341 AppendColored(wxString::FromUTF8(tree.c_str()), warningColor);
1342 AppendColored("' is referenced but not implemented\n", defaultColor);
1343 }
1344 AppendColored("\n", defaultColor);
1345 }
1346
1347 // Errors section
1348 if (!report.errors.empty() || !report.circular_references.empty()) {
1349 AppendColored(wxString::Format("ERRORS (%d):\n",
1350 static_cast<int>(report.errors.size() + report.circular_references.size())),
1351 errorColor);
1352 for (const auto &err : report.errors) {
1353 AppendColored(" [ERROR] ", errorColor);
1354 AppendColored(wxString::FromUTF8(err.c_str()) + "\n", defaultColor);
1355 }
1356 for (const auto &ref : report.circular_references) {
1357 AppendColored(" [ERROR] ", errorColor);
1358 AppendColored("Circular reference detected: ", defaultColor);
1359 AppendColored(wxString::FromUTF8(ref.c_str()) + "\n", errorColor);
1360 }
1361 AppendColored("\n", defaultColor);
1362 }
1363
1364 // Tree reference map
1365 AppendColored("TREE REFERENCE MAP:\n", headerColor);
1366 for (const auto &pair : report.tree_statuses) {
1367 const auto &id = pair.first;
1368 const auto &status = pair.second;
1369 if (status.is_implemented) {
1370 AppendColored(" [OK] ", successColor);
1371 AppendColored(wxString::FromUTF8(id.c_str()), defaultColor);
1372 AppendColored(" (implemented in ", infoColor);
1373 AppendColored(wxString::FromUTF8(status.defined_in_file.c_str()), infoColor);
1374 AppendColored(")\n", infoColor);
1375 } else {
1376 AppendColored(" [!!] ", warningColor);
1377 AppendColored(wxString::FromUTF8(id.c_str()), warningColor);
1378 AppendColored(" (not implemented)\n", defaultColor);
1379 }
1380 for (const auto &ref_file : status.referenced_in_files) {
1381 AppendColored(" <- referenced by ", infoColor);
1382 AppendColored(wxString::FromUTF8(ref_file.c_str()) + "\n", defaultColor);
1383 }
1384 }
1385}
1386
1388 // Reset selection when updating preview
1390
1392 m_treeCountLabel->SetLabel("No validation data - click Validate first");
1393 m_fileStructurePanel->Refresh();
1394 return;
1395 }
1396
1397 // Count implemented trees
1398 int implementedTrees = 0;
1399 for (const auto &pair : m_lastValidationReport->tree_statuses) {
1400 if (pair.second.is_implemented) {
1401 implementedTrees++;
1402 }
1403 }
1404
1405 // Count blackboards
1406 int totalBlackboards = 0;
1407 for (const auto &rs : m_lastValidationReport->resource_statuses) {
1408 totalBlackboards += rs.blackboard_count;
1409 }
1410
1411 // Update count label
1412 m_treeCountLabel->SetLabel(wxString::Format("%d tree(s), %d blackboard(s), %zu unimplemented ref(s)",
1413 implementedTrees, totalBlackboards,
1414 m_lastValidationReport->unimplemented_trees.size()));
1415
1416 // Trigger repaint
1417 m_fileStructurePanel->Refresh();
1418}
1419
1421 // Clear background
1422 dc.SetBackground(wxBrush(wxColour(45, 45, 45)));
1423 dc.Clear();
1424
1425 // Clear clickable items
1426 m_clickableItems.clear();
1427
1429 // Draw "No data" message
1430 dc.SetTextForeground(wxColour(150, 150, 150));
1431 wxFont font(12, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
1432 dc.SetFont(font);
1433 dc.DrawText("Click 'Validate' to see structure", 20, 20);
1434 return;
1435 }
1436
1437 // Group trees by file
1438 std::map<std::string, std::vector<std::string>> treesByFile;
1439 for (const auto &pair : m_lastValidationReport->tree_statuses) {
1440 const auto &status = pair.second;
1441 if (status.is_implemented) {
1442 treesByFile[status.defined_in_file].push_back(status.tree_id);
1443 }
1444 }
1445
1446 // Ensure blackboard-only files also appear in the treesByFile map
1447 // (with an empty tree list) so they get drawn in the main loop
1448 for (const auto &rs : m_lastValidationReport->resource_statuses) {
1449 if (rs.has_blackboards && treesByFile.find(rs.filepath) == treesByFile.end()) {
1450 treesByFile[rs.filepath] = {}; // Empty tree list, but file will be drawn
1451 }
1452 }
1453
1454 // Drawing parameters
1455 int fileBoxWidth = 250;
1456 int fileBoxHeight = 40;
1457 int treeBoxWidth = 200;
1458 int treeBoxHeight = 35;
1459 int spacing = 20;
1460 int leftMargin = 30;
1461 int topMargin = 30;
1462
1463 wxFont titleFont(10, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD);
1464 wxFont normalFont(9, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
1465
1466 int currentY = topMargin;
1467
1468 // Get filter states
1469 bool showProjectIssues = !m_showProjectIssuesCheckBox || m_showProjectIssuesCheckBox->GetValue();
1470 bool showFiles = !m_showFilesCheckBox || m_showFilesCheckBox->GetValue();
1471 bool showTrees = !m_showTreesCheckBox || m_showTreesCheckBox->GetValue();
1472 bool showBlackboards = !m_showBlackboardsCheckBox || m_showBlackboardsCheckBox->GetValue();
1473 bool showErrors = !m_showErrorsCheckBox || m_showErrorsCheckBox->GetValue();
1474 bool showWarnings = !m_showWarningsCheckBox || m_showWarningsCheckBox->GetValue();
1475
1476 // Build map of which files reference which unimplemented trees
1477 std::map<std::string, std::vector<std::string>> fileToUnimplementedRefs;
1478 for (const auto &unimp : m_lastValidationReport->unimplemented_trees) {
1479 // Find which files reference this tree
1480 for (const auto &resource : m_lastValidationReport->resource_statuses) {
1481 for (const auto &ref : resource.subtree_refs) {
1482 if (ref == unimp) {
1483 fileToUnimplementedRefs[resource.filepath].push_back(unimp);
1484 break;
1485 }
1486 }
1487 }
1488 }
1489
1490 // Section 1: Draw project-level ERRORS (critical issues)
1491 if (showProjectIssues && !m_lastValidationReport->errors.empty()) {
1492 dc.SetFont(titleFont);
1493 dc.SetTextForeground(wxColour(255, 100, 100));
1494 dc.DrawText("❌ Project Errors:", leftMargin, currentY);
1495 currentY += 30;
1496
1497 for (const auto &error : m_lastValidationReport->errors) {
1498 int errorX = leftMargin + 20;
1499
1500 // Draw error box
1501 dc.SetBrush(wxBrush(wxColour(80, 40, 40)));
1502 dc.SetPen(wxPen(wxColour(255, 100, 100), 2));
1503 dc.DrawRoundedRectangle(errorX, currentY, treeBoxWidth, treeBoxHeight, 5);
1504
1505 // Draw error icon (X)
1506 dc.SetBrush(wxBrush(wxColour(255, 100, 100)));
1507 dc.SetPen(wxPen(wxColour(200, 80, 80), 2));
1508 dc.DrawLine(errorX + 10, currentY + 10, errorX + 20, currentY + 20);
1509 dc.DrawLine(errorX + 20, currentY + 10, errorX + 10, currentY + 20);
1510
1511 // Draw error text (truncate if needed)
1512 dc.SetFont(normalFont);
1513 dc.SetTextForeground(wxColour(255, 150, 150));
1514 wxString errorText = wxString::FromUTF8(error);
1515 int maxTextWidth = treeBoxWidth - 40;
1516 wxSize textExtent = dc.GetTextExtent(errorText);
1517 if (textExtent.GetWidth() > maxTextWidth) {
1518 while (!errorText.IsEmpty() && dc.GetTextExtent(errorText + "...").GetWidth() > maxTextWidth) {
1519 errorText = errorText.Left(errorText.Length() - 1);
1520 }
1521 errorText += "...";
1522 }
1523 dc.DrawText(errorText, errorX + 30, currentY + 10);
1524
1525 // Track as clickable item
1526 ClickableItem item;
1527 item.type = ClickableItem::Warning; // Reuse warning type for now
1528 item.rect = wxRect(errorX, currentY, treeBoxWidth, treeBoxHeight);
1529 item.identifier = error;
1530 item.filepath = "";
1531 item.line_number = -1;
1532
1533 int itemIndex = m_clickableItems.size();
1534 m_clickableItems.push_back(item);
1535
1536 if (itemIndex == m_selectedItemIndex) {
1537 dc.SetBrush(*wxTRANSPARENT_BRUSH);
1538 dc.SetPen(wxPen(wxColour(0, 255, 255), 4)); // Bright cyan, thicker
1539 dc.DrawRoundedRectangle(errorX - 2, currentY - 2, treeBoxWidth + 4, treeBoxHeight + 4, 5);
1540 }
1541
1542 currentY += treeBoxHeight + 10;
1543 }
1544 currentY += spacing;
1545 }
1546
1547 // Section 2: Draw circular dependencies
1548 if (showProjectIssues && !m_lastValidationReport->circular_references.empty()) {
1549 dc.SetFont(titleFont);
1550 dc.SetTextForeground(wxColour(255, 150, 100));
1551 dc.DrawText("🔄 Circular Dependencies:", leftMargin, currentY);
1552 currentY += 30;
1553
1554 for (const auto &circular : m_lastValidationReport->circular_references) {
1555 int circX = leftMargin + 20;
1556
1557 // Draw warning box
1558 dc.SetBrush(wxBrush(wxColour(80, 50, 40)));
1559 dc.SetPen(wxPen(wxColour(255, 150, 100), 2));
1560 dc.DrawRoundedRectangle(circX, currentY, treeBoxWidth, treeBoxHeight, 5);
1561
1562 // Draw circular icon
1563 dc.SetBrush(*wxTRANSPARENT_BRUSH);
1564 dc.SetPen(wxPen(wxColour(255, 150, 100), 2));
1565 dc.DrawCircle(circX + 15, currentY + 17, 8);
1566 dc.SetBrush(wxBrush(wxColour(255, 150, 100)));
1567 wxPoint circArrow[3] = {wxPoint(circX + 23, currentY + 14), wxPoint(circX + 23, currentY + 20),
1568 wxPoint(circX + 27, currentY + 17)};
1569 dc.DrawPolygon(3, circArrow);
1570
1571 // Draw circular ref text
1572 dc.SetFont(normalFont);
1573 dc.SetTextForeground(wxColour(255, 180, 120));
1574 wxString circText = wxString::FromUTF8(circular);
1575 int maxTextWidth = treeBoxWidth - 40;
1576 wxSize textExtent = dc.GetTextExtent(circText);
1577 if (textExtent.GetWidth() > maxTextWidth) {
1578 while (!circText.IsEmpty() && dc.GetTextExtent(circText + "...").GetWidth() > maxTextWidth) {
1579 circText = circText.Left(circText.Length() - 1);
1580 }
1581 circText += "...";
1582 }
1583 dc.DrawText(circText, circX + 30, currentY + 10);
1584
1585 // Track as clickable
1586 ClickableItem item;
1588 item.rect = wxRect(circX, currentY, treeBoxWidth, treeBoxHeight);
1589 item.identifier = circular;
1590 item.filepath = "";
1591 item.line_number = -1;
1592
1593 int itemIndex = m_clickableItems.size();
1594 m_clickableItems.push_back(item);
1595
1596 if (itemIndex == m_selectedItemIndex) {
1597 dc.SetBrush(*wxTRANSPARENT_BRUSH);
1598 dc.SetPen(wxPen(wxColour(0, 255, 255), 4)); // Bright cyan, thicker
1599 dc.DrawRoundedRectangle(circX - 2, currentY - 2, treeBoxWidth + 4, treeBoxHeight + 4, 5);
1600 }
1601
1602 currentY += treeBoxHeight + 10;
1603 }
1604 currentY += spacing;
1605 }
1606
1607 // Section 3: Draw unimplemented references (NOW SHOWN UNDER EACH FILE INSTEAD)
1608 // Disabled - unimplemented refs are now shown as warnings under their corresponding files
1609 if (false && !m_lastValidationReport->unimplemented_trees.empty()) {
1610 // Draw section header
1611 dc.SetFont(titleFont);
1612 dc.SetTextForeground(wxColour(255, 200, 100));
1613 dc.DrawText("Unimplemented References:", leftMargin, currentY);
1614 currentY += 30;
1615
1616 // Draw each unimplemented reference
1617 for (const auto &treeId : m_lastValidationReport->unimplemented_trees) {
1618 int treeX = leftMargin + 20;
1619
1620 // Draw warning box
1621 dc.SetBrush(wxBrush(wxColour(80, 60, 40)));
1622 dc.SetPen(wxPen(wxColour(255, 180, 0), 2));
1623 dc.DrawRoundedRectangle(treeX, currentY, treeBoxWidth, treeBoxHeight, 5);
1624
1625 // Draw warning icon (triangle with !)
1626 dc.SetBrush(wxBrush(wxColour(255, 180, 0)));
1627 dc.SetPen(wxPen(wxColour(200, 140, 0), 1));
1628 wxPoint warnIcon[3] = {wxPoint(treeX + 15, currentY + 10), wxPoint(treeX + 10, currentY + 25),
1629 wxPoint(treeX + 20, currentY + 25)};
1630 dc.DrawPolygon(3, warnIcon);
1631 dc.SetTextForeground(wxColour(50, 30, 0));
1632 dc.DrawText("!", treeX + 13, currentY + 11);
1633
1634 // Draw tree ID (truncate if too long)
1635 dc.SetFont(normalFont);
1636 dc.SetTextForeground(wxColour(255, 200, 100));
1637 wxString treeIdText = wxString::FromUTF8(treeId);
1638
1639 // Measure text and truncate if necessary to fit in box
1640 int maxTextWidth = treeBoxWidth - 40; // Account for icon space and margins
1641 wxSize textExtent = dc.GetTextExtent(treeIdText);
1642 if (textExtent.GetWidth() > maxTextWidth) {
1643 // Truncate with ellipsis
1644 while (!treeIdText.IsEmpty() && dc.GetTextExtent(treeIdText + "...").GetWidth() > maxTextWidth) {
1645 treeIdText = treeIdText.Left(treeIdText.Length() - 1);
1646 }
1647 treeIdText += "...";
1648 }
1649
1650 dc.DrawText(treeIdText, treeX + 30, currentY + 10);
1651
1652 // Find which file references this tree
1653 std::string referenceFile;
1654 for (const auto &resource : m_lastValidationReport->resource_statuses) {
1655 for (const auto &ref : resource.subtree_refs) {
1656 if (ref == treeId) {
1657 referenceFile = resource.filepath;
1658 break;
1659 }
1660 }
1661 if (!referenceFile.empty())
1662 break;
1663 }
1664
1665 // Track as clickable item
1666 ClickableItem item;
1668 item.rect = wxRect(treeX, currentY, treeBoxWidth, treeBoxHeight);
1669 item.identifier = treeId;
1670 item.filepath = referenceFile;
1671 item.line_number = -1; // We'll search for it when clicked
1672
1673 int itemIndex = m_clickableItems.size();
1674 m_clickableItems.push_back(item);
1675
1676 // Draw selection highlight if this item is selected
1677 if (itemIndex == m_selectedItemIndex) {
1678 dc.SetBrush(*wxTRANSPARENT_BRUSH);
1679 dc.SetPen(wxPen(wxColour(0, 255, 255), 4)); // Bright cyan, thicker // Bright blue border
1680 dc.DrawRoundedRectangle(treeX - 2, currentY - 2, treeBoxWidth + 4, treeBoxHeight + 4, 5);
1681 }
1682
1683 currentY += treeBoxHeight + 10;
1684 }
1685
1686 currentY += spacing * 2; // Extra spacing after warnings
1687 }
1688
1689 // Section 4: Draw files with errors but no trees
1690 std::set<std::string> filesWithTreesOrErrors;
1691
1692 // First, check if there are any files with errors/warnings/unimplemented refs that don't have trees
1693 bool hasFilesWithIssuesButNoTrees = false;
1694 if (showFiles || showErrors || showWarnings) {
1695 for (const auto &resource : m_lastValidationReport->resource_statuses) {
1696 bool hasIssues = (!resource.errors.empty() && showErrors) || (!resource.warnings.empty() && showWarnings) ||
1697 (!fileToUnimplementedRefs[resource.filepath].empty() && showWarnings);
1698 if (hasIssues && treesByFile.find(resource.filepath) == treesByFile.end()) {
1699 hasFilesWithIssuesButNoTrees = true;
1700 break;
1701 }
1702 }
1703 }
1704
1705 // Draw section header if there are files with issues
1706 if (hasFilesWithIssuesButNoTrees) {
1707 dc.SetFont(titleFont);
1708 dc.SetTextForeground(wxColour(255, 150, 100));
1709 dc.DrawText("Files with Issues:", leftMargin, currentY);
1710 currentY += 30;
1711 }
1712
1713 for (const auto &resource : m_lastValidationReport->resource_statuses) {
1714 // Check if this file has any items that should be shown based on filters
1715 bool hasVisibleErrors = showErrors && !resource.errors.empty();
1716 bool hasVisibleWarnings =
1717 showWarnings && (!resource.warnings.empty() || !fileToUnimplementedRefs[resource.filepath].empty());
1718
1719 if (hasVisibleErrors || hasVisibleWarnings) {
1720 // Check if this file is NOT in treesByFile (no valid trees)
1721 if (treesByFile.find(resource.filepath) == treesByFile.end()) {
1722 filesWithTreesOrErrors.insert(resource.filepath);
1723
1724 wxFileName fn(wxString::FromUTF8(resource.filepath));
1725 wxSize textExtent; // Declare here for use throughout this section
1726
1727 // Only draw file box if showFiles is true
1728 if (showFiles) {
1729 // Draw file box
1730 dc.SetBrush(wxBrush(wxColour(70, 70, 90)));
1731 dc.SetPen(wxPen(wxColour(100, 100, 150), 2));
1732 dc.DrawRoundedRectangle(leftMargin, currentY, fileBoxWidth, fileBoxHeight, 5);
1733
1734 // Draw file icon
1735 dc.SetBrush(wxBrush(wxColour(100, 150, 255)));
1736 dc.SetPen(wxPen(wxColour(80, 120, 200), 1));
1737 dc.DrawRectangle(leftMargin + 10, currentY + 10, 20, 20);
1738
1739 // Draw file name (truncate if too long)
1740 dc.SetFont(titleFont);
1741 dc.SetTextForeground(wxColour(200, 200, 255));
1742 wxString fileName = fn.GetFullName();
1743 int maxFileTextWidth = fileBoxWidth - 50;
1744 textExtent = dc.GetTextExtent(fileName);
1745 if (textExtent.GetWidth() > maxFileTextWidth) {
1746 while (!fileName.IsEmpty() &&
1747 dc.GetTextExtent(fileName + "...").GetWidth() > maxFileTextWidth) {
1748 fileName = fileName.Left(fileName.Length() - 1);
1749 }
1750 fileName += "...";
1751 }
1752 dc.DrawText(fileName, leftMargin + 40, currentY + 12);
1753
1754 // Track file as clickable
1755 ClickableItem fileItem;
1756 fileItem.type = ClickableItem::File;
1757 fileItem.rect = wxRect(leftMargin, currentY, fileBoxWidth, fileBoxHeight);
1758 fileItem.identifier = resource.filepath;
1759 fileItem.filepath = resource.filepath;
1760 fileItem.line_number = -1;
1761
1762 int fileItemIndex = m_clickableItems.size();
1763 m_clickableItems.push_back(fileItem);
1764
1765 if (fileItemIndex == m_selectedItemIndex) {
1766 dc.SetBrush(*wxTRANSPARENT_BRUSH);
1767 dc.SetPen(wxPen(wxColour(0, 255, 255), 4)); // Bright cyan, thicker
1768 dc.DrawRoundedRectangle(leftMargin - 2, currentY - 2, fileBoxWidth + 4, fileBoxHeight + 4, 5);
1769 }
1770
1771 currentY += fileBoxHeight + spacing;
1772 }
1773
1774 // Draw errors for this file
1775 if (showErrors) {
1776 for (const auto &error : resource.errors) {
1777 int errorX = leftMargin + 40;
1778
1779 dc.SetBrush(wxBrush(wxColour(80, 40, 40)));
1780 dc.SetPen(wxPen(wxColour(255, 100, 100), 2));
1781 dc.DrawRoundedRectangle(errorX, currentY, treeBoxWidth, treeBoxHeight, 5);
1782
1783 dc.SetBrush(wxBrush(wxColour(255, 100, 100)));
1784 dc.SetPen(wxPen(wxColour(200, 80, 80), 2));
1785 dc.DrawLine(errorX + 10, currentY + 10, errorX + 20, currentY + 20);
1786 dc.DrawLine(errorX + 20, currentY + 10, errorX + 10, currentY + 20);
1787
1788 dc.SetFont(normalFont);
1789 dc.SetTextForeground(wxColour(255, 150, 150));
1790 wxString errorText = wxString::FromUTF8(error);
1791 int maxTextWidth = treeBoxWidth - 40;
1792 textExtent = dc.GetTextExtent(errorText);
1793 if (textExtent.GetWidth() > maxTextWidth) {
1794 while (!errorText.IsEmpty() &&
1795 dc.GetTextExtent(errorText + "...").GetWidth() > maxTextWidth) {
1796 errorText = errorText.Left(errorText.Length() - 1);
1797 }
1798 errorText += "...";
1799 }
1800 dc.DrawText(errorText, errorX + 30, currentY + 10);
1801
1802 ClickableItem errorItem;
1803 errorItem.type = ClickableItem::Warning;
1804 errorItem.rect = wxRect(errorX, currentY, treeBoxWidth, treeBoxHeight);
1805 errorItem.identifier = error;
1806 errorItem.filepath = resource.filepath;
1807 errorItem.line_number = -1;
1808
1809 int errorItemIndex = m_clickableItems.size();
1810 m_clickableItems.push_back(errorItem);
1811
1812 if (errorItemIndex == m_selectedItemIndex) {
1813 dc.SetBrush(*wxTRANSPARENT_BRUSH);
1814 dc.SetPen(wxPen(wxColour(0, 255, 255), 4)); // Bright cyan, thicker
1815 dc.DrawRoundedRectangle(errorX - 2, currentY - 2, treeBoxWidth + 4, treeBoxHeight + 4, 5);
1816 }
1817
1818 currentY += treeBoxHeight + 10;
1819 }
1820 }
1821
1822 // Draw warnings for this file
1823 if (showWarnings) {
1824 for (const auto &warning : resource.warnings) {
1825 int warnX = leftMargin + 40;
1826
1827 dc.SetBrush(wxBrush(wxColour(80, 60, 40)));
1828 dc.SetPen(wxPen(wxColour(255, 180, 0), 2));
1829 dc.DrawRoundedRectangle(warnX, currentY, treeBoxWidth, treeBoxHeight, 5);
1830
1831 dc.SetBrush(wxBrush(wxColour(255, 180, 0)));
1832 dc.SetPen(wxPen(wxColour(200, 140, 0), 1));
1833 wxPoint warnIcon[3] = {wxPoint(warnX + 15, currentY + 10), wxPoint(warnX + 10, currentY + 25),
1834 wxPoint(warnX + 20, currentY + 25)};
1835 dc.DrawPolygon(3, warnIcon);
1836 dc.SetTextForeground(wxColour(50, 30, 0));
1837 dc.DrawText("!", warnX + 13, currentY + 11);
1838
1839 dc.SetFont(normalFont);
1840 dc.SetTextForeground(wxColour(255, 200, 100));
1841 wxString warnText = wxString::FromUTF8(warning);
1842 int maxTextWidth = treeBoxWidth - 40;
1843 textExtent = dc.GetTextExtent(warnText);
1844 if (textExtent.GetWidth() > maxTextWidth) {
1845 while (!warnText.IsEmpty() &&
1846 dc.GetTextExtent(warnText + "...").GetWidth() > maxTextWidth) {
1847 warnText = warnText.Left(warnText.Length() - 1);
1848 }
1849 warnText += "...";
1850 }
1851 dc.DrawText(warnText, warnX + 30, currentY + 10);
1852
1853 ClickableItem warnItem;
1854 warnItem.type = ClickableItem::Warning;
1855 warnItem.rect = wxRect(warnX, currentY, treeBoxWidth, treeBoxHeight);
1856 warnItem.identifier = warning;
1857 warnItem.filepath = resource.filepath;
1858 warnItem.line_number = -1;
1859
1860 int warnItemIndex = m_clickableItems.size();
1861 m_clickableItems.push_back(warnItem);
1862
1863 if (warnItemIndex == m_selectedItemIndex) {
1864 dc.SetBrush(*wxTRANSPARENT_BRUSH);
1865 dc.SetPen(wxPen(wxColour(0, 255, 255), 4)); // Bright cyan, thicker
1866 dc.DrawRoundedRectangle(warnX - 2, currentY - 2, treeBoxWidth + 4, treeBoxHeight + 4, 5);
1867 }
1868
1869 currentY += treeBoxHeight + 10;
1870 }
1871 }
1872
1873 // Draw unimplemented references for this file
1874 if (showWarnings && fileToUnimplementedRefs.count(resource.filepath) > 0) {
1875 for (const auto &unimpRef : fileToUnimplementedRefs[resource.filepath]) {
1876 int warnX = leftMargin + 40;
1877
1878 dc.SetBrush(wxBrush(wxColour(80, 60, 40)));
1879 dc.SetPen(wxPen(wxColour(255, 180, 0), 2));
1880 dc.DrawRoundedRectangle(warnX, currentY, treeBoxWidth, treeBoxHeight, 5);
1881
1882 dc.SetBrush(wxBrush(wxColour(255, 180, 0)));
1883 dc.SetPen(wxPen(wxColour(200, 140, 0), 1));
1884 wxPoint warnIcon[3] = {wxPoint(warnX + 15, currentY + 10), wxPoint(warnX + 10, currentY + 25),
1885 wxPoint(warnX + 20, currentY + 25)};
1886 dc.DrawPolygon(3, warnIcon);
1887 dc.SetTextForeground(wxColour(50, 30, 0));
1888 dc.DrawText("!", warnX + 13, currentY + 11);
1889
1890 dc.SetFont(normalFont);
1891 dc.SetTextForeground(wxColour(255, 200, 100));
1892 wxString warnText = "Missing reference: " + wxString::FromUTF8(unimpRef);
1893 int maxTextWidth = treeBoxWidth - 40;
1894 textExtent = dc.GetTextExtent(warnText);
1895 if (textExtent.GetWidth() > maxTextWidth) {
1896 while (!warnText.IsEmpty() &&
1897 dc.GetTextExtent(warnText + "...").GetWidth() > maxTextWidth) {
1898 warnText = warnText.Left(warnText.Length() - 1);
1899 }
1900 warnText += "...";
1901 }
1902 dc.DrawText(warnText, warnX + 30, currentY + 10);
1903
1904 ClickableItem warnItem;
1905 warnItem.type = ClickableItem::Warning;
1906 warnItem.rect = wxRect(warnX, currentY, treeBoxWidth, treeBoxHeight);
1907 warnItem.identifier = unimpRef;
1908 warnItem.filepath = resource.filepath;
1909 warnItem.line_number = -1;
1910
1911 int warnItemIndex = m_clickableItems.size();
1912 m_clickableItems.push_back(warnItem);
1913
1914 if (warnItemIndex == m_selectedItemIndex) {
1915 dc.SetBrush(*wxTRANSPARENT_BRUSH);
1916 dc.SetPen(wxPen(wxColour(0, 255, 255), 4)); // Bright cyan, thicker
1917 dc.DrawRoundedRectangle(warnX - 2, currentY - 2, treeBoxWidth + 4, treeBoxHeight + 4, 5);
1918 }
1919
1920 currentY += treeBoxHeight + 10;
1921 }
1922 }
1923
1924 currentY += spacing;
1925 }
1926 }
1927 }
1928
1929 // Draw section header for implemented trees
1930 if ((showFiles || showTrees) && !treesByFile.empty()) {
1931 dc.SetFont(titleFont);
1932 dc.SetTextForeground(wxColour(150, 200, 255));
1933 dc.DrawText("Implemented Trees:", leftMargin, currentY);
1934 currentY += 30;
1935 }
1936
1937 // Draw each file and its trees
1938 for (const auto &pair : treesByFile) {
1939 // Check if this file has anything visible based on filters
1940 const EmberCore::ResourceValidationStatus *resourceStatus = nullptr;
1941 for (const auto &resource : m_lastValidationReport->resource_statuses) {
1942 if (resource.filepath == pair.first) {
1943 resourceStatus = &resource;
1944 break;
1945 }
1946 }
1947
1948 bool hasVisibleErrors = showErrors && resourceStatus && !resourceStatus->errors.empty();
1949 bool hasVisibleWarnings = showWarnings && resourceStatus &&
1950 (!resourceStatus->warnings.empty() || !fileToUnimplementedRefs[pair.first].empty());
1951 bool hasVisibleTrees = showTrees && !pair.second.empty();
1952 bool hasVisibleBlackboards = showBlackboards && resourceStatus && !resourceStatus->blackboard_ids.empty();
1953
1954 // Skip this file entirely if nothing should be visible
1955 if (!showFiles && !hasVisibleErrors && !hasVisibleWarnings && !hasVisibleTrees && !hasVisibleBlackboards) {
1956 continue;
1957 }
1958 wxFileName fn(wxString::FromUTF8(pair.first));
1959 wxSize textExtent; // Declare here for use throughout this file's section
1960
1961 // Only draw file box if showFiles is true
1962 if (showFiles) {
1963 // Draw file box
1964 dc.SetBrush(wxBrush(wxColour(70, 70, 90)));
1965 dc.SetPen(wxPen(wxColour(100, 100, 150), 2));
1966 dc.DrawRoundedRectangle(leftMargin, currentY, fileBoxWidth, fileBoxHeight, 5);
1967
1968 // Draw file icon
1969 dc.SetBrush(wxBrush(wxColour(100, 150, 255)));
1970 dc.SetPen(wxPen(wxColour(80, 120, 200), 1));
1971 dc.DrawRectangle(leftMargin + 10, currentY + 10, 20, 20);
1972
1973 // Draw file name (truncate if too long)
1974 dc.SetFont(titleFont);
1975 dc.SetTextForeground(wxColour(200, 200, 255));
1976 wxString fileName = fn.GetFullName();
1977
1978 // Measure text and truncate if necessary to fit in box
1979 int maxFileTextWidth = fileBoxWidth - 50; // Account for icon space and margins
1980 textExtent = dc.GetTextExtent(fileName);
1981 if (textExtent.GetWidth() > maxFileTextWidth) {
1982 // Truncate with ellipsis
1983 while (!fileName.IsEmpty() && dc.GetTextExtent(fileName + "...").GetWidth() > maxFileTextWidth) {
1984 fileName = fileName.Left(fileName.Length() - 1);
1985 }
1986 fileName += "...";
1987 }
1988
1989 dc.DrawText(fileName, leftMargin + 40, currentY + 12);
1990
1991 // Track file as clickable
1992 ClickableItem fileItem;
1993 fileItem.type = ClickableItem::File;
1994 fileItem.rect = wxRect(leftMargin, currentY, fileBoxWidth, fileBoxHeight);
1995 fileItem.identifier = pair.first;
1996 fileItem.filepath = pair.first;
1997 fileItem.line_number = -1;
1998
1999 int fileItemIndex = m_clickableItems.size();
2000 m_clickableItems.push_back(fileItem);
2001
2002 // Draw selection highlight if this item is selected
2003 if (fileItemIndex == m_selectedItemIndex) {
2004 dc.SetBrush(*wxTRANSPARENT_BRUSH);
2005 dc.SetPen(wxPen(wxColour(0, 255, 255), 4)); // Bright cyan, thicker // Bright blue border
2006 dc.DrawRoundedRectangle(leftMargin - 2, currentY - 2, fileBoxWidth + 4, fileBoxHeight + 4, 5);
2007 }
2008
2009 currentY += fileBoxHeight + spacing;
2010 }
2011
2012 // Draw file-level errors (if any)
2013 if (showErrors && resourceStatus && !resourceStatus->errors.empty()) {
2014 for (const auto &error : resourceStatus->errors) {
2015 int errorX = leftMargin + 40;
2016
2017 // Draw error box
2018 dc.SetBrush(wxBrush(wxColour(80, 40, 40)));
2019 dc.SetPen(wxPen(wxColour(255, 100, 100), 2));
2020 dc.DrawRoundedRectangle(errorX, currentY, treeBoxWidth, treeBoxHeight, 5);
2021
2022 // Draw error icon (X)
2023 dc.SetBrush(wxBrush(wxColour(255, 100, 100)));
2024 dc.SetPen(wxPen(wxColour(200, 80, 80), 2));
2025 dc.DrawLine(errorX + 10, currentY + 10, errorX + 20, currentY + 20);
2026 dc.DrawLine(errorX + 20, currentY + 10, errorX + 10, currentY + 20);
2027
2028 // Draw error text
2029 dc.SetFont(normalFont);
2030 dc.SetTextForeground(wxColour(255, 150, 150));
2031 wxString errorText = wxString::FromUTF8(error);
2032 int maxTextWidth = treeBoxWidth - 40;
2033 wxSize textExtent = dc.GetTextExtent(errorText);
2034 if (textExtent.GetWidth() > maxTextWidth) {
2035 while (!errorText.IsEmpty() && dc.GetTextExtent(errorText + "...").GetWidth() > maxTextWidth) {
2036 errorText = errorText.Left(errorText.Length() - 1);
2037 }
2038 errorText += "...";
2039 }
2040 dc.DrawText(errorText, errorX + 30, currentY + 10);
2041
2042 // Track as clickable
2043 ClickableItem errorItem;
2044 errorItem.type = ClickableItem::Warning;
2045 errorItem.rect = wxRect(errorX, currentY, treeBoxWidth, treeBoxHeight);
2046 errorItem.identifier = error;
2047 errorItem.filepath = pair.first;
2048 errorItem.line_number = -1;
2049
2050 int errorItemIndex = m_clickableItems.size();
2051 m_clickableItems.push_back(errorItem);
2052
2053 if (errorItemIndex == m_selectedItemIndex) {
2054 dc.SetBrush(*wxTRANSPARENT_BRUSH);
2055 dc.SetPen(wxPen(wxColour(0, 255, 255), 4)); // Bright cyan, thicker
2056 dc.DrawRoundedRectangle(errorX - 2, currentY - 2, treeBoxWidth + 4, treeBoxHeight + 4, 5);
2057 }
2058
2059 currentY += treeBoxHeight + 10;
2060 }
2061 }
2062
2063 // Draw file-level warnings (if any)
2064 if (showWarnings && resourceStatus && !resourceStatus->warnings.empty()) {
2065 for (const auto &warning : resourceStatus->warnings) {
2066 int warnX = leftMargin + 40;
2067
2068 // Draw warning box
2069 dc.SetBrush(wxBrush(wxColour(80, 60, 40)));
2070 dc.SetPen(wxPen(wxColour(255, 180, 0), 2));
2071 dc.DrawRoundedRectangle(warnX, currentY, treeBoxWidth, treeBoxHeight, 5);
2072
2073 // Draw warning icon (triangle with !)
2074 dc.SetBrush(wxBrush(wxColour(255, 180, 0)));
2075 dc.SetPen(wxPen(wxColour(200, 140, 0), 1));
2076 wxPoint warnIcon[3] = {wxPoint(warnX + 15, currentY + 10), wxPoint(warnX + 10, currentY + 25),
2077 wxPoint(warnX + 20, currentY + 25)};
2078 dc.DrawPolygon(3, warnIcon);
2079 dc.SetTextForeground(wxColour(50, 30, 0));
2080 dc.DrawText("!", warnX + 13, currentY + 11);
2081
2082 // Draw warning text
2083 dc.SetFont(normalFont);
2084 dc.SetTextForeground(wxColour(255, 200, 100));
2085 wxString warnText = wxString::FromUTF8(warning);
2086 int maxTextWidth = treeBoxWidth - 40;
2087 wxSize textExtent = dc.GetTextExtent(warnText);
2088 if (textExtent.GetWidth() > maxTextWidth) {
2089 while (!warnText.IsEmpty() && dc.GetTextExtent(warnText + "...").GetWidth() > maxTextWidth) {
2090 warnText = warnText.Left(warnText.Length() - 1);
2091 }
2092 warnText += "...";
2093 }
2094 dc.DrawText(warnText, warnX + 30, currentY + 10);
2095
2096 // Track as clickable
2097 ClickableItem warnItem;
2098 warnItem.type = ClickableItem::Warning;
2099 warnItem.rect = wxRect(warnX, currentY, treeBoxWidth, treeBoxHeight);
2100 warnItem.identifier = warning;
2101 warnItem.filepath = pair.first;
2102 warnItem.line_number = -1;
2103
2104 int warnItemIndex = m_clickableItems.size();
2105 m_clickableItems.push_back(warnItem);
2106
2107 if (warnItemIndex == m_selectedItemIndex) {
2108 dc.SetBrush(*wxTRANSPARENT_BRUSH);
2109 dc.SetPen(wxPen(wxColour(0, 255, 255), 4)); // Bright cyan, thicker
2110 dc.DrawRoundedRectangle(warnX - 2, currentY - 2, treeBoxWidth + 4, treeBoxHeight + 4, 5);
2111 }
2112
2113 currentY += treeBoxHeight + 10;
2114 }
2115 }
2116
2117 // Draw unimplemented references for this file
2118 if (showWarnings && fileToUnimplementedRefs.count(pair.first) > 0) {
2119 for (const auto &unimpRef : fileToUnimplementedRefs[pair.first]) {
2120 int warnX = leftMargin + 40;
2121
2122 dc.SetBrush(wxBrush(wxColour(80, 60, 40)));
2123 dc.SetPen(wxPen(wxColour(255, 180, 0), 2));
2124 dc.DrawRoundedRectangle(warnX, currentY, treeBoxWidth, treeBoxHeight, 5);
2125
2126 dc.SetBrush(wxBrush(wxColour(255, 180, 0)));
2127 dc.SetPen(wxPen(wxColour(200, 140, 0), 1));
2128 wxPoint warnIcon[3] = {wxPoint(warnX + 15, currentY + 10), wxPoint(warnX + 10, currentY + 25),
2129 wxPoint(warnX + 20, currentY + 25)};
2130 dc.DrawPolygon(3, warnIcon);
2131 dc.SetTextForeground(wxColour(50, 30, 0));
2132 dc.DrawText("!", warnX + 13, currentY + 11);
2133
2134 dc.SetFont(normalFont);
2135 dc.SetTextForeground(wxColour(255, 200, 100));
2136 wxString warnText = "Missing reference: " + wxString::FromUTF8(unimpRef);
2137 int maxTextWidth = treeBoxWidth - 40;
2138 wxSize textExtent = dc.GetTextExtent(warnText);
2139 if (textExtent.GetWidth() > maxTextWidth) {
2140 while (!warnText.IsEmpty() && dc.GetTextExtent(warnText + "...").GetWidth() > maxTextWidth) {
2141 warnText = warnText.Left(warnText.Length() - 1);
2142 }
2143 warnText += "...";
2144 }
2145 dc.DrawText(warnText, warnX + 30, currentY + 10);
2146
2147 ClickableItem warnItem;
2148 warnItem.type = ClickableItem::Warning;
2149 warnItem.rect = wxRect(warnX, currentY, treeBoxWidth, treeBoxHeight);
2150 warnItem.identifier = unimpRef;
2151 warnItem.filepath = pair.first;
2152 warnItem.line_number = -1;
2153
2154 int warnItemIndex = m_clickableItems.size();
2155 m_clickableItems.push_back(warnItem);
2156
2157 if (warnItemIndex == m_selectedItemIndex) {
2158 dc.SetBrush(*wxTRANSPARENT_BRUSH);
2159 dc.SetPen(wxPen(wxColour(0, 255, 255), 4)); // Bright cyan, thicker
2160 dc.DrawRoundedRectangle(warnX - 2, currentY - 2, treeBoxWidth + 4, treeBoxHeight + 4, 5);
2161 }
2162
2163 currentY += treeBoxHeight + 10;
2164 }
2165 }
2166
2167 // Draw trees in this file
2168 if (showTrees) {
2169 for (const auto &treeId : pair.second) {
2170 // Indent trees under their file
2171 int treeX = leftMargin + 40;
2172
2173 // Draw tree box
2174 dc.SetBrush(wxBrush(wxColour(60, 80, 60)));
2175 dc.SetPen(wxPen(wxColour(100, 180, 100), 2));
2176 dc.DrawRoundedRectangle(treeX, currentY, treeBoxWidth, treeBoxHeight, 5);
2177
2178 // Draw tree icon (simple triangle)
2179 dc.SetBrush(wxBrush(wxColour(80, 200, 100)));
2180 dc.SetPen(wxPen(wxColour(60, 160, 80), 1));
2181 wxPoint treeIcon[3] = {wxPoint(treeX + 15, currentY + 10), wxPoint(treeX + 10, currentY + 25),
2182 wxPoint(treeX + 20, currentY + 25)};
2183 dc.DrawPolygon(3, treeIcon);
2184
2185 // Draw tree ID (truncate if too long)
2186 dc.SetFont(normalFont);
2187 dc.SetTextForeground(wxColour(150, 255, 150));
2188 wxString treeIdText = wxString::FromUTF8(treeId);
2189
2190 // Measure text and truncate if necessary to fit in box
2191 int maxTextWidth = treeBoxWidth - 40; // Account for icon space and margins
2192 wxSize textExtent = dc.GetTextExtent(treeIdText);
2193 if (textExtent.GetWidth() > maxTextWidth) {
2194 // Truncate with ellipsis
2195 while (!treeIdText.IsEmpty() && dc.GetTextExtent(treeIdText + "...").GetWidth() > maxTextWidth) {
2196 treeIdText = treeIdText.Left(treeIdText.Length() - 1);
2197 }
2198 treeIdText += "...";
2199 }
2200
2201 dc.DrawText(treeIdText, treeX + 30, currentY + 10);
2202
2203 // Track tree as clickable
2204 ClickableItem treeItem;
2205 treeItem.type = ClickableItem::Tree;
2206 treeItem.rect = wxRect(treeX, currentY, treeBoxWidth, treeBoxHeight);
2207 treeItem.identifier = treeId;
2208 treeItem.filepath = pair.first;
2209 treeItem.line_number = -1;
2210
2211 int treeItemIndex = m_clickableItems.size();
2212 m_clickableItems.push_back(treeItem);
2213
2214 // Draw selection highlight if this item is selected
2215 if (treeItemIndex == m_selectedItemIndex) {
2216 dc.SetBrush(*wxTRANSPARENT_BRUSH);
2217 dc.SetPen(wxPen(wxColour(0, 255, 255), 4)); // Bright cyan, thicker // Bright blue border
2218 dc.DrawRoundedRectangle(treeX - 2, currentY - 2, treeBoxWidth + 4, treeBoxHeight + 4, 5);
2219 }
2220
2221 currentY += treeBoxHeight + 10;
2222 }
2223 }
2224
2225 // Draw blackboards in this file
2226 if (showBlackboards && resourceStatus && !resourceStatus->blackboard_ids.empty()) {
2227 for (const auto &bbId : resourceStatus->blackboard_ids) {
2228 int bbX = leftMargin + 40;
2229
2230 // Draw blackboard box (purple/magenta theme)
2231 dc.SetBrush(wxBrush(wxColour(70, 50, 80)));
2232 dc.SetPen(wxPen(wxColour(160, 100, 200), 2));
2233 dc.DrawRoundedRectangle(bbX, currentY, treeBoxWidth, treeBoxHeight, 5);
2234
2235 // Draw blackboard icon (small grid)
2236 dc.SetBrush(wxBrush(wxColour(180, 120, 230)));
2237 dc.SetPen(wxPen(wxColour(140, 90, 190), 1));
2238 dc.DrawRectangle(bbX + 10, currentY + 10, 16, 16);
2239 dc.DrawLine(bbX + 10, currentY + 18, bbX + 26, currentY + 18);
2240 dc.DrawLine(bbX + 18, currentY + 10, bbX + 18, currentY + 26);
2241
2242 // Draw blackboard ID (truncate if too long)
2243 dc.SetFont(normalFont);
2244 dc.SetTextForeground(wxColour(200, 160, 255));
2245 wxString bbText = wxString::FromUTF8(bbId);
2246
2247 int maxTextWidth = treeBoxWidth - 40;
2248 wxSize bbTextExtent = dc.GetTextExtent(bbText);
2249 if (bbTextExtent.GetWidth() > maxTextWidth) {
2250 while (!bbText.IsEmpty() && dc.GetTextExtent(bbText + "...").GetWidth() > maxTextWidth) {
2251 bbText = bbText.Left(bbText.Length() - 1);
2252 }
2253 bbText += "...";
2254 }
2255
2256 dc.DrawText(bbText, bbX + 30, currentY + 10);
2257
2258 // Track blackboard as clickable
2259 ClickableItem bbItem;
2261 bbItem.rect = wxRect(bbX, currentY, treeBoxWidth, treeBoxHeight);
2262 bbItem.identifier = bbId;
2263 bbItem.filepath = pair.first;
2264 bbItem.line_number = -1;
2265
2266 int bbItemIndex = m_clickableItems.size();
2267 m_clickableItems.push_back(bbItem);
2268
2269 // Draw selection highlight if this item is selected
2270 if (bbItemIndex == m_selectedItemIndex) {
2271 dc.SetBrush(*wxTRANSPARENT_BRUSH);
2272 dc.SetPen(wxPen(wxColour(0, 255, 255), 4));
2273 dc.DrawRoundedRectangle(bbX - 2, currentY - 2, treeBoxWidth + 4, treeBoxHeight + 4, 5);
2274 }
2275
2276 currentY += treeBoxHeight + 10;
2277 }
2278 }
2279
2280 currentY += spacing;
2281 }
2282
2283 // Set virtual size for scrolling
2284 m_fileStructurePanel->SetVirtualSize(fileBoxWidth + leftMargin + 50, currentY + 20);
2285}
2286
2288 wxString name = m_projectName->GetValue().Trim();
2289 bool canCreate = !name.IsEmpty() && IsValidProjectName(name) && m_project->GetResourceCount() > 0;
2290
2291 // For create mode, we allow creating even if validation fails
2292 // The user will see warnings but can still proceed
2293 m_createBtn->Enable(canCreate);
2294}
2295
2296void BehaviorTreeProjectDialog::OnCreate(wxCommandEvent &event) {
2297 // Ensure we have a valid name
2298 wxString name = m_projectName->GetValue().Trim();
2299 if (name.IsEmpty()) {
2300 wxMessageBox("Please enter a project name.", "Error", wxOK | wxICON_ERROR, this);
2301 m_projectName->SetFocus();
2302 return;
2303 }
2304
2305 // Validate project name (only letters and numbers)
2306 if (!IsValidProjectName(name)) {
2307 wxMessageBox("Project name can only contain letters (a-z, A-Z) and numbers (0-9).\n"
2308 "No spaces, special characters, or symbols are allowed.",
2309 "Invalid Project Name", wxOK | wxICON_ERROR, this);
2310 m_projectName->SetFocus();
2311 return;
2312 }
2313
2314 // Ensure we have resources
2315 if (m_project->GetResourceCount() == 0) {
2316 wxMessageBox("Please add at least one XML file to the project.", "Error", wxOK | wxICON_ERROR, this);
2317 return;
2318 }
2319
2320 // Run validation if not done yet
2323 }
2324
2325 // Warn about validation issues but allow proceeding
2326 if (!m_isValid) {
2327 int result = wxMessageBox("The project has validation issues. Do you want to create it anyway?\n\n"
2328 "You can still work with the project, but some features may not work correctly.",
2329 "Validation Warning", wxYES_NO | wxICON_WARNING, this);
2330
2331 if (result != wxYES) {
2332 return;
2333 }
2334 }
2335
2336 // Note: Unimplemented references will be reported by MainFrame after project loads
2337 // No need to show duplicate warning here
2338
2339 EndModal(wxID_OK);
2340}
2341
2342void BehaviorTreeProjectDialog::OnSave(wxCommandEvent &event) {
2343 // Same as create for now
2344 OnCreate(event);
2345}
2346
2347void BehaviorTreeProjectDialog::OnCancel(wxCommandEvent &event) { EndModal(wxID_CANCEL); }
2348
2349bool BehaviorTreeProjectDialog::IsValidProjectName(const wxString &name) const {
2350 if (name.IsEmpty()) {
2351 return false;
2352 }
2353
2354 // Only letters (a-z, A-Z) and numbers (0-9) allowed
2355 for (size_t i = 0; i < name.Length(); ++i) {
2356 wxChar ch = name[i];
2357 if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9'))) {
2358 return false;
2359 }
2360 }
2361
2362 return true;
2363}
2364
2366 // Increment phase for smooth pulsing animation
2367 m_pulsePhase += 0.05f;
2368 if (m_pulsePhase > 1.0f) {
2369 m_pulsePhase = 0.0f;
2370 }
2371
2373}
2374
2376 // Calculate pulse intensity using sine wave for smooth animation
2377 float t = (std::sin(m_pulsePhase * 2.0f * 3.14159f) + 1.0f) * 0.5f;
2378
2379 // Check project name field
2380 if (m_projectName) {
2381 wxString name = m_projectName->GetValue().Trim();
2382 if (!name.IsEmpty() && !IsValidProjectName(name)) {
2383 // Invalid - pulse yellow
2384 wxColour pulseColor = InterpolateColor(m_normalBg, m_highlightBg, t);
2385 m_projectName->SetBackgroundColour(pulseColor);
2386 m_projectName->Refresh();
2387 } else {
2388 // Valid or empty - use normal background
2389 m_projectName->SetBackgroundColour(m_normalBg);
2390 m_projectName->Refresh();
2391 }
2392 }
2393}
2394
2395wxColour BehaviorTreeProjectDialog::InterpolateColor(const wxColour &color1, const wxColour &color2, float t) {
2396 // Clamp t to [0, 1]
2397 if (t < 0.0f)
2398 t = 0.0f;
2399 if (t > 1.0f)
2400 t = 1.0f;
2401
2402 // Linear interpolation of RGB components
2403 unsigned char r = static_cast<unsigned char>(color1.Red() + t * (color2.Red() - color1.Red()));
2404 unsigned char g = static_cast<unsigned char>(color1.Green() + t * (color2.Green() - color1.Green()));
2405 unsigned char b = static_cast<unsigned char>(color1.Blue() + t * (color2.Blue() - color1.Blue()));
2406
2407 return wxColour(r, g, b);
2408}
2409
2411 // Convert click position to scrolled position
2412 int scrollX, scrollY;
2413 m_fileStructurePanel->GetViewStart(&scrollX, &scrollY);
2414 int scrollUnitY;
2415 m_fileStructurePanel->GetScrollPixelsPerUnit(nullptr, &scrollUnitY);
2416
2417 wxPoint clickPos = event.GetPosition();
2418 clickPos.y += scrollY * scrollUnitY; // Adjust for scroll position
2419
2420 // Find which item was clicked
2421 for (size_t i = 0; i < m_clickableItems.size(); ++i) {
2422 const auto &item = m_clickableItems[i];
2423 if (item.rect.Contains(clickPos)) {
2424 // Update selected item and refresh to show highlight
2425 m_selectedItemIndex = static_cast<int>(i);
2426
2427 // Smart scroll: If clicking on item that's partially visible, scroll to center it
2428 int scrollUnitY;
2429 m_fileStructurePanel->GetScrollPixelsPerUnit(nullptr, &scrollUnitY);
2430
2431 if (scrollUnitY > 0) {
2432 wxSize clientSize = m_fileStructurePanel->GetClientSize();
2433 int viewStartX, viewStartY;
2434 m_fileStructurePanel->GetViewStart(&viewStartX, &viewStartY);
2435 int visibleTop = viewStartY * scrollUnitY;
2436 int visibleBottom = visibleTop + clientSize.GetHeight();
2437
2438 // Only auto-scroll if item is near the edges (top 20% or bottom 20% of visible area)
2439 int topThreshold = visibleTop + clientSize.GetHeight() / 5;
2440 int bottomThreshold = visibleBottom - clientSize.GetHeight() / 5;
2441
2442 if (item.rect.GetTop() < topThreshold || item.rect.GetBottom() > bottomThreshold) {
2443 // Scroll to center the item
2444 int targetY = item.rect.GetTop() - (clientSize.GetHeight() / 2) + (item.rect.GetHeight() / 2);
2445 if (targetY < 0)
2446 targetY = 0;
2447 m_fileStructurePanel->Scroll(0, targetY / scrollUnitY);
2448 }
2449 }
2450
2451 m_fileStructurePanel->Refresh();
2452
2453 switch (item.type) {
2455 m_warningDetailsPanel->Hide(); // Hide warning details
2456 m_warningDetailsPanel->GetParent()->Layout();
2457 LoadXMLPreview(item.filepath);
2458 break;
2459
2461 m_warningDetailsPanel->Hide(); // Hide warning details
2462 m_warningDetailsPanel->GetParent()->Layout();
2463 LoadTreePreview(item.filepath, item.identifier);
2464 break;
2465
2467 m_warningDetailsPanel->Hide(); // Hide warning details
2468 m_warningDetailsPanel->GetParent()->Layout();
2469 LoadBlackboardPreview(item.filepath, item.identifier);
2470 break;
2471
2473 // Show warning details panel with comprehensive information
2474 {
2475 wxString warningDetails;
2476
2477 // Detect type of issue based on identifier content
2478 bool isUnimplementedRef = false;
2479 bool isCircularRef = false;
2480 bool isProjectError = false;
2481
2482 // Check if it's an unimplemented reference
2483 for (const auto &unimp : m_lastValidationReport->unimplemented_trees) {
2484 if (unimp == item.identifier) {
2485 isUnimplementedRef = true;
2486 break;
2487 }
2488 }
2489
2490 // Check if it's a circular reference
2491 if (!isUnimplementedRef) {
2492 for (const auto &circ : m_lastValidationReport->circular_references) {
2493 if (circ == item.identifier) {
2494 isCircularRef = true;
2495 break;
2496 }
2497 }
2498 }
2499
2500 // Check if it's a project-level error
2501 if (!isUnimplementedRef && !isCircularRef) {
2502 for (const auto &err : m_lastValidationReport->errors) {
2503 if (err == item.identifier) {
2504 isProjectError = true;
2505 break;
2506 }
2507 }
2508 }
2509
2510 // Check if it's a blackboard-related warning
2511 bool isBlackboardInclude = false;
2512 bool isBlackboardDuplicate = false;
2513 bool isBlackboardValidation = false;
2514 std::string bbIncludeName;
2515
2516 if (item.identifier.find("Missing blackboard include: ") == 0) {
2517 isBlackboardInclude = true;
2518 bbIncludeName = item.identifier.substr(28); // length of "Missing blackboard include: "
2519 } else if (item.identifier.find("Duplicate blackboard ID") != std::string::npos) {
2520 isBlackboardDuplicate = true;
2521 } else if (item.identifier.find("Blackboard") != std::string::npos &&
2522 (item.identifier.find("missing") != std::string::npos ||
2523 item.identifier.find("duplicate entry key") != std::string::npos ||
2524 item.identifier.find("unsupported type") != std::string::npos ||
2525 item.identifier.find("malformed includes") != std::string::npos)) {
2526 isBlackboardValidation = true;
2527 }
2528
2529 if (isUnimplementedRef) {
2530 warningDetails << "UNIMPLEMENTED REFERENCE WARNING\n\n";
2531 warningDetails << "Issue: SubTree reference not found\n";
2532 warningDetails << "Tree ID: " << item.identifier << "\n\n";
2533 } else if (isCircularRef) {
2534 warningDetails << "CIRCULAR DEPENDENCY ERROR\n\n";
2535 warningDetails << "Issue: Circular reference detected\n";
2536 warningDetails << "Chain: " << item.identifier << "\n\n";
2537 } else if (isProjectError) {
2538 warningDetails << "PROJECT ERROR\n\n";
2539 warningDetails << "Issue: " << item.identifier << "\n\n";
2540 } else if (isBlackboardInclude) {
2541 warningDetails << "MISSING BLACKBOARD INCLUDE\n\n";
2542 warningDetails << "Issue: Blackboard includes reference not found\n";
2543 warningDetails << "Missing ID: " << bbIncludeName << "\n\n";
2544 } else if (isBlackboardDuplicate) {
2545 warningDetails << "DUPLICATE BLACKBOARD ID\n\n";
2546 warningDetails << "Issue: " << item.identifier << "\n\n";
2547 } else if (isBlackboardValidation) {
2548 warningDetails << "BLACKBOARD VALIDATION ERROR\n\n";
2549 warningDetails << "Issue: " << item.identifier << "\n\n";
2550 } else {
2551 // File-level error or warning
2552 bool isError = item.identifier.find("Error") != std::string::npos ||
2553 item.identifier.find("error") != std::string::npos ||
2554 item.identifier.find("ERROR") != std::string::npos;
2555
2556 if (isError) {
2557 warningDetails << "FILE ERROR\n\n";
2558 } else {
2559 warningDetails << "FILE WARNING\n\n";
2560 }
2561 warningDetails << "Issue: " << item.identifier << "\n\n";
2562 }
2563
2564 if (!item.filepath.empty()) {
2565 wxFileName fn(wxString::FromUTF8(item.filepath));
2566 warningDetails << "File: " << fn.GetFullName() << "\n";
2567 warningDetails << "Path: " << item.filepath << "\n\n";
2568
2569 if (isUnimplementedRef) {
2570 warningDetails << "Description: The file references a SubTree with ID \"" << item.identifier
2571 << "\" ";
2572 warningDetails
2573 << "but no BehaviorTree with this ID exists in any of the project's XML files. ";
2574 warningDetails << "This will cause a runtime error if this tree is executed.\n\n";
2575 warningDetails << "Solution: Create a BehaviorTree with ID=\"" << item.identifier << "\" ";
2576 warningDetails << "in one of your XML files, or fix the SubTree reference if it's a typo.";
2577 } else if (isCircularRef) {
2578 warningDetails << "Description: This represents a circular dependency chain where trees "
2579 "reference each other ";
2580 warningDetails << "in a loop. This will cause infinite recursion at runtime.\n\n";
2581 warningDetails << "Solution: Review the SubTree references in your files and break the "
2582 "circular dependency ";
2583 warningDetails << "by restructuring your tree hierarchy.";
2584 } else if (isBlackboardInclude) {
2585 warningDetails
2586 << "Description: A Blackboard in this file uses includes=\"{...}\" referencing \""
2587 << bbIncludeName << "\" ";
2588 warningDetails
2589 << "but no Blackboard with this ID exists in any of the project's XML files. ";
2590 warningDetails << "At runtime, the blackboard will be missing the included entries.\n\n";
2591 warningDetails << "Solution: Create a Blackboard with ID=\"" << bbIncludeName << "\" ";
2592 warningDetails << "in one of your XML files, or fix the includes reference if it's a typo.";
2593 } else if (isBlackboardDuplicate) {
2594 warningDetails
2595 << "Description: Multiple Blackboards share the same ID across the project. ";
2596 warningDetails << "This can cause unpredictable behavior as one definition may override "
2597 "the other.\n\n";
2598 warningDetails << "Solution: Rename one of the duplicate Blackboard IDs to be unique, or "
2599 "merge them into a single definition.";
2600 } else if (isBlackboardValidation) {
2601 if (item.identifier.find("missing required") != std::string::npos) {
2602 warningDetails << "Description: A Blackboard or Entry element is missing a required "
2603 "XML attribute. ";
2604 warningDetails << "All Blackboards must have an 'ID' attribute, and all Entries must "
2605 "have 'key' and 'type' attributes.\n\n";
2606 warningDetails << "Solution: Add the missing attribute to the XML element. Check the "
2607 "syntax carefully.";
2608 } else if (item.identifier.find("duplicate entry key") != std::string::npos) {
2609 warningDetails
2610 << "Description: A Blackboard has multiple entries with the same key name. ";
2611 warningDetails << "This can cause one entry to overwrite another.\n\n";
2612 warningDetails << "Solution: Rename one of the duplicate entry keys to be unique, or "
2613 "remove the duplicate.";
2614 } else if (item.identifier.find("unsupported type") != std::string::npos) {
2615 warningDetails << "Description: A Blackboard entry uses a data type that is not "
2616 "recognized by the parser. ";
2617 warningDetails << "This could be a typo or an unsupported type.\n\n";
2618 warningDetails
2619 << "Solution: Check the type name for typos (e.g., 'strng' should be 'string'). ";
2620 warningDetails << "Supported types include: bool, int, float, string, vec(...), "
2621 "map(...), set(...), and custom types starting with uppercase.";
2622 } else if (item.identifier.find("malformed includes") != std::string::npos) {
2623 warningDetails << "Description: A Blackboard's 'includes' attribute doesn't follow the "
2624 "correct syntax. ";
2625 warningDetails << "The includes must be in the format: includes=\"{ID1 ID2 ID3}\" with "
2626 "braces.\n\n";
2627 warningDetails << "Solution: Fix the includes syntax to use curly braces around the "
2628 "IDs: includes=\"{BLACKBOARD_ID1 BLACKBOARD_ID2}\".";
2629 } else {
2630 warningDetails << "Description: A blackboard validation issue was detected.\n\n";
2631 warningDetails << "Solution: Review the blackboard definition and fix any syntax or "
2632 "structural issues.";
2633 }
2634 } else {
2635 warningDetails << "Description: This file has a validation issue that needs attention. ";
2636 warningDetails << "Review the file content and fix any syntax errors, missing attributes, "
2637 "or structural problems.\n\n";
2638 warningDetails << "Solution: Open the file and examine the highlighted area for issues. ";
2639 warningDetails << "Common problems include malformed XML, missing required attributes, or "
2640 "invalid node types.";
2641 }
2642
2643 m_warningDetailsPanel->SetValue(warningDetails);
2644 m_warningDetailsPanel->Show();
2645 m_warningDetailsPanel->GetParent()->Layout();
2646
2647 // Load the file and try to find the problematic line
2648 std::ifstream file(item.filepath);
2649 if (file.is_open()) {
2650 std::string content((std::istreambuf_iterator<char>(file)),
2651 std::istreambuf_iterator<char>());
2652 file.close();
2653
2654 int lineNumber = -1;
2655
2656 if (isUnimplementedRef) {
2657 // Search for SubTree reference with this ID
2658 std::string searchStr = "SubTree ID=\"" + item.identifier + "\"";
2659 size_t pos = content.find(searchStr);
2660
2661 if (pos == std::string::npos) {
2662 // Try with single quotes
2663 searchStr = "SubTree ID='" + item.identifier + "'";
2664 pos = content.find(searchStr);
2665 }
2666
2667 if (pos != std::string::npos) {
2668 // Count line number
2669 lineNumber = 1;
2670 for (size_t i = 0; i < pos; ++i) {
2671 if (content[i] == '\n') {
2672 lineNumber++;
2673 }
2674 }
2675 }
2676 } else if (isBlackboardInclude && !bbIncludeName.empty()) {
2677 // Search for the includes attribute containing the missing reference
2678 size_t pos = content.find(bbIncludeName);
2679 if (pos != std::string::npos) {
2680 // Find the Blackboard line containing this include
2681 lineNumber = 1;
2682 for (size_t i = 0; i < pos; ++i) {
2683 if (content[i] == '\n') {
2684 lineNumber++;
2685 }
2686 }
2687 }
2688 } else if (isBlackboardDuplicate) {
2689 // Extract the BB ID from the warning text
2690 std::string dupId = item.identifier;
2691 // "Duplicate blackboard ID within file: X" or "Duplicate blackboard ID in project: X"
2692 size_t colonPos = dupId.rfind(": ");
2693 if (colonPos != std::string::npos) {
2694 std::string bbId = dupId.substr(colonPos + 2);
2695 std::string searchStr = "Blackboard ID=\"" + bbId + "\"";
2696 size_t pos = content.find(searchStr);
2697 if (pos == std::string::npos) {
2698 searchStr = "Blackboard ID='" + bbId + "'";
2699 pos = content.find(searchStr);
2700 }
2701 if (pos != std::string::npos) {
2702 lineNumber = 1;
2703 for (size_t i = 0; i < pos; ++i) {
2704 if (content[i] == '\n') {
2705 lineNumber++;
2706 }
2707 }
2708 }
2709 }
2710 } else {
2711 // Try to parse other blackboard-related errors
2712 std::string identifier = item.identifier;
2713
2714 // "Blackboard element missing required 'ID' attribute"
2715 if (identifier.find("Blackboard element missing") != std::string::npos) {
2716 // Find first <Blackboard without an ID attribute
2717 size_t pos = content.find("<Blackboard");
2718 while (pos != std::string::npos) {
2719 size_t lineEnd = content.find('>', pos);
2720 if (lineEnd != std::string::npos) {
2721 std::string tag = content.substr(pos, lineEnd - pos + 1);
2722 if (tag.find("ID=") == std::string::npos) {
2723 // Found Blackboard without ID
2724 lineNumber = 1;
2725 for (size_t i = 0; i < pos; ++i) {
2726 if (content[i] == '\n')
2727 lineNumber++;
2728 }
2729 break;
2730 }
2731 }
2732 pos = content.find("<Blackboard", pos + 1);
2733 }
2734 }
2735 // "Blackboard 'X' has Entry missing required 'key' attribute"
2736 else if (identifier.find("has Entry missing required 'key'") != std::string::npos) {
2737 size_t quoteStart = identifier.find('\'');
2738 size_t quoteEnd = identifier.find('\'', quoteStart + 1);
2739 if (quoteStart != std::string::npos && quoteEnd != std::string::npos) {
2740 std::string bbId = identifier.substr(quoteStart + 1, quoteEnd - quoteStart - 1);
2741 // Find the Blackboard with this ID
2742 std::string searchStr = "Blackboard ID=\"" + bbId + "\"";
2743 size_t bbPos = content.find(searchStr);
2744 if (bbPos == std::string::npos) {
2745 searchStr = "Blackboard ID='" + bbId + "'";
2746 bbPos = content.find(searchStr);
2747 }
2748 if (bbPos != std::string::npos) {
2749 // Find <Entry without key attribute after this position
2750 size_t entryPos = content.find("<Entry", bbPos);
2751 while (entryPos != std::string::npos) {
2752 size_t lineEnd = content.find('>', entryPos);
2753 if (lineEnd != std::string::npos) {
2754 std::string tag = content.substr(entryPos, lineEnd - entryPos + 1);
2755 if (tag.find("key=") == std::string::npos) {
2756 lineNumber = 1;
2757 for (size_t i = 0; i < entryPos; ++i) {
2758 if (content[i] == '\n')
2759 lineNumber++;
2760 }
2761 break;
2762 }
2763 }
2764 entryPos = content.find("<Entry", entryPos + 1);
2765 // Stop at next Blackboard
2766 size_t nextBB = content.find("<Blackboard", bbPos + 1);
2767 if (nextBB != std::string::npos && entryPos > nextBB)
2768 break;
2769 }
2770 }
2771 }
2772 }
2773 // "Blackboard 'X' entry 'Y' missing required 'type' attribute"
2774 // "Blackboard 'X' has duplicate entry key: Y"
2775 // "Blackboard 'X' entry 'Y' has unsupported type: Z"
2776 else if (identifier.find("entry '") != std::string::npos ||
2777 identifier.find("entry key: ") != std::string::npos) {
2778 // Extract entry key
2779 std::string entryKey;
2780 size_t keyStart = identifier.find("entry '");
2781 if (keyStart != std::string::npos) {
2782 keyStart += 7; // length of "entry '"
2783 size_t keyEnd = identifier.find('\'', keyStart);
2784 if (keyEnd != std::string::npos) {
2785 entryKey = identifier.substr(keyStart, keyEnd - keyStart);
2786 }
2787 } else {
2788 keyStart = identifier.find("entry key: ");
2789 if (keyStart != std::string::npos) {
2790 entryKey = identifier.substr(keyStart + 11); // length of "entry key: "
2791 }
2792 }
2793
2794 if (!entryKey.empty()) {
2795 // Search for Entry with this key
2796 std::string searchStr = "Entry key=\"" + entryKey + "\"";
2797 size_t pos = content.find(searchStr);
2798 if (pos == std::string::npos) {
2799 searchStr = "Entry key='" + entryKey + "'";
2800 pos = content.find(searchStr);
2801 }
2802 if (pos != std::string::npos) {
2803 lineNumber = 1;
2804 for (size_t i = 0; i < pos; ++i) {
2805 if (content[i] == '\n')
2806 lineNumber++;
2807 }
2808 }
2809 }
2810 }
2811 // "Blackboard 'X' has malformed includes syntax"
2812 else if (identifier.find("has malformed includes syntax") != std::string::npos) {
2813 size_t quoteStart = identifier.find('\'');
2814 size_t quoteEnd = identifier.find('\'', quoteStart + 1);
2815 if (quoteStart != std::string::npos && quoteEnd != std::string::npos) {
2816 std::string bbId = identifier.substr(quoteStart + 1, quoteEnd - quoteStart - 1);
2817 std::string searchStr = "Blackboard ID=\"" + bbId + "\"";
2818 size_t pos = content.find(searchStr);
2819 if (pos == std::string::npos) {
2820 searchStr = "Blackboard ID='" + bbId + "'";
2821 pos = content.find(searchStr);
2822 }
2823 if (pos != std::string::npos) {
2824 lineNumber = 1;
2825 for (size_t i = 0; i < pos; ++i) {
2826 if (content[i] == '\n')
2827 lineNumber++;
2828 }
2829 }
2830 }
2831 }
2832 }
2833
2834 if (lineNumber > 0) {
2835 LoadXMLPreview(item.filepath, lineNumber);
2836 } else {
2837 LoadXMLPreview(item.filepath);
2838 }
2839 } else {
2840 m_xmlPreviewPanel->SetReadOnly(false);
2841 m_xmlPreviewPanel->SetText(
2842 wxString::Format("Error: Could not open file '%s'", item.filepath));
2843 m_xmlPreviewPanel->SetReadOnly(true);
2844 }
2845 } else {
2846 // No specific file - handle different issue types
2847 if (isUnimplementedRef) {
2848 warningDetails << "Location: Unknown file\n\n";
2849 warningDetails << "Description: The SubTree ID \"" << item.identifier
2850 << "\" is referenced ";
2851 warningDetails << "somewhere in the project but no BehaviorTree with this ID exists. ";
2852 warningDetails << "The specific file reference could not be determined.\n\n";
2853 warningDetails << "Solution: Search your XML files for SubTree nodes with ID=\""
2854 << item.identifier << "\" ";
2855 warningDetails << "and either implement the tree or fix the reference.";
2856 } else if (isCircularRef) {
2857 warningDetails << "Location: Multiple files\n\n";
2858 warningDetails << "Description: A circular dependency has been detected in your project. ";
2859 warningDetails << "The dependency chain is: " << item.identifier << "\n\n";
2860 warningDetails
2861 << "Solution: Review the files involved in this chain and restructure your trees ";
2862 warningDetails << "to eliminate the circular reference.";
2863 } else if (isProjectError) {
2864 warningDetails << "Location: Project level\n\n";
2865 warningDetails << "Description: This is a project-wide issue that affects the entire "
2866 "project structure.\n\n";
2867 warningDetails << "Solution: Review your project configuration and ensure all required "
2868 "settings are valid.";
2869 } else {
2870 warningDetails << "Location: Unknown\n\n";
2871 warningDetails << "Description: A validation issue was detected but its specific location "
2872 "could not be determined.\n\n";
2873 warningDetails << "Solution: Review the validation report tab for more details.";
2874 }
2875
2876 m_warningDetailsPanel->SetValue(warningDetails);
2877 m_warningDetailsPanel->Show();
2878 m_warningDetailsPanel->GetParent()->Layout();
2879
2880 // Show appropriate message in XML preview
2881 m_xmlPreviewPanel->SetReadOnly(false);
2882 if (isCircularRef) {
2883 m_xmlPreviewPanel->SetText(wxString::Format(
2884 "Circular Dependency Detected\n\n"
2885 "Chain: %s\n\n"
2886 "This indicates that trees reference each other in a loop, which will cause\n"
2887 "infinite recursion at runtime. Review the files in your project to identify\n"
2888 "and break the circular reference chain.",
2889 item.identifier));
2890 } else if (isProjectError) {
2891 m_xmlPreviewPanel->SetText(
2892 wxString::Format("Project Error\n\n"
2893 "%s\n\n"
2894 "This is a project-level error. Review your project settings\n"
2895 "and configuration to resolve this issue.",
2896 item.identifier));
2897 } else {
2898 m_xmlPreviewPanel->SetText(
2899 wxString::Format("Validation Issue\n\n"
2900 "%s\n\n"
2901 "No specific file reference found.\n"
2902 "Check the validation report tab for more details.",
2903 item.identifier));
2904 }
2905 m_xmlPreviewPanel->SetReadOnly(true);
2906 }
2907 }
2908 break;
2909 }
2910 return;
2911 }
2912 }
2913}
2914
2915void BehaviorTreeProjectDialog::LoadXMLPreview(const std::string &filepath, int highlightLine) {
2916 // Read the XML file
2917 std::ifstream file(filepath);
2918 if (!file.is_open()) {
2919 m_xmlPreviewPanel->SetReadOnly(false);
2920 m_xmlPreviewPanel->SetText(wxString::Format("Error: Could not open file '%s'", filepath));
2921 m_xmlPreviewPanel->SetReadOnly(true);
2922 return;
2923 }
2924
2925 std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
2926 file.close();
2927
2928 // Set the content
2929 m_xmlPreviewPanel->SetReadOnly(false);
2930 m_xmlPreviewPanel->SetText(wxString::FromUTF8(content));
2931
2932 // Reset all text to default style (style 0)
2933 m_xmlPreviewPanel->StartStyling(0);
2934 m_xmlPreviewPanel->SetStyling(m_xmlPreviewPanel->GetLength(), 0);
2935
2936 // If we need to highlight a specific line
2937 if (highlightLine > 0) {
2938 // Count to the line
2939 long pos = 0;
2940 int currentLine = 1;
2941 for (size_t i = 0; i < content.size() && currentLine < highlightLine; ++i) {
2942 if (content[i] == '\n') {
2943 currentLine++;
2944 }
2945 pos++;
2946 }
2947
2948 // Find end of line
2949 long endPos = pos;
2950 while (endPos < (long)content.size() && content[endPos] != '\n') {
2951 endPos++;
2952 }
2953
2954 // Highlight the line with warning style (style 1)
2955 m_xmlPreviewPanel->StartStyling(pos);
2956 m_xmlPreviewPanel->SetStyling(endPos - pos, 1);
2957
2958 // Center the view on the highlighted line using SetFirstVisibleLine
2959 // This explicitly sets which line appears at the TOP of the viewport
2960 int lineNum = m_xmlPreviewPanel->LineFromPosition(pos);
2961 int linesOnScreen = m_xmlPreviewPanel->LinesOnScreen();
2962 int firstLine = lineNum - (linesOnScreen / 2);
2963 if (firstLine < 0)
2964 firstLine = 0;
2965
2966 m_xmlPreviewPanel->SetFirstVisibleLine(firstLine);
2967
2968 // Clear any selection (no caret highlighting)
2969 m_xmlPreviewPanel->SetSelection(0, 0);
2970 } else {
2971 // No highlight line - move to beginning of file
2972 m_xmlPreviewPanel->SetFirstVisibleLine(0);
2973 }
2974
2975 // Clear any selection (no caret highlighting)
2976 m_xmlPreviewPanel->SetSelection(0, 0);
2977 m_xmlPreviewPanel->SetReadOnly(true);
2978}
2979
2980void BehaviorTreeProjectDialog::LoadTreePreview(const std::string &filepath, const std::string &treeId) {
2981 // Read the XML file
2982 std::ifstream file(filepath);
2983 if (!file.is_open()) {
2984 m_xmlPreviewPanel->SetReadOnly(false);
2985 m_xmlPreviewPanel->SetText(wxString::Format("Error: Could not open file '%s'", filepath));
2986 m_xmlPreviewPanel->SetReadOnly(true);
2987 return;
2988 }
2989
2990 std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
2991 file.close();
2992
2993 // Set the entire file content
2994 m_xmlPreviewPanel->SetReadOnly(false);
2995 m_xmlPreviewPanel->SetText(wxString::FromUTF8(content));
2996
2997 // Reset all text to default style (style 0)
2998 m_xmlPreviewPanel->StartStyling(0);
2999 m_xmlPreviewPanel->SetStyling(m_xmlPreviewPanel->GetLength(), 0);
3000
3001 // Find the BehaviorTree with this ID
3002 std::string searchStr = "BehaviorTree ID=\"" + treeId + "\"";
3003 size_t treeStartPos = content.find(searchStr);
3004
3005 if (treeStartPos == std::string::npos) {
3006 // Try with single quotes
3007 searchStr = "BehaviorTree ID='" + treeId + "'";
3008 treeStartPos = content.find(searchStr);
3009 }
3010
3011 if (treeStartPos != std::string::npos) {
3012 // Find the start of the line
3013 size_t lineStartPos = treeStartPos;
3014 while (lineStartPos > 0 && content[lineStartPos - 1] != '\n') {
3015 lineStartPos--;
3016 }
3017
3018 // Find the closing tag
3019 size_t treeEndPos = content.find("</BehaviorTree>", treeStartPos);
3020 if (treeEndPos != std::string::npos) {
3021 // Find the end of the closing tag line
3022 treeEndPos = content.find('>', treeEndPos);
3023 if (treeEndPos != std::string::npos) {
3024 treeEndPos++; // Include the '>'
3025
3026 // Highlight this tree section with green style (style 2)
3027 m_xmlPreviewPanel->StartStyling(lineStartPos);
3028 m_xmlPreviewPanel->SetStyling(treeEndPos - lineStartPos, 2);
3029
3030 // Center the view on the start of the highlighted tree
3031 // Use SetFirstVisibleLine to explicitly position the viewport
3032 int treeLineNum = m_xmlPreviewPanel->LineFromPosition(lineStartPos);
3033 int treeEndLineNum = m_xmlPreviewPanel->LineFromPosition(treeEndPos);
3034 int linesOnScreen = m_xmlPreviewPanel->LinesOnScreen();
3035
3036 // Calculate how many lines the tree spans
3037 int treeLines = treeEndLineNum - treeLineNum + 1;
3038
3039 int firstLine;
3040 if (treeLines <= linesOnScreen) {
3041 // Tree fits on screen: center it vertically
3042 int middleLine = treeLineNum + (treeLines / 2);
3043 firstLine = middleLine - (linesOnScreen / 2);
3044 } else {
3045 // Tree is bigger than viewport: show from the start with a small margin
3046 firstLine = treeLineNum - 2;
3047 }
3048 if (firstLine < 0)
3049 firstLine = 0;
3050
3051 m_xmlPreviewPanel->SetFirstVisibleLine(firstLine);
3052
3053 // Clear any selection (no caret highlighting)
3054 m_xmlPreviewPanel->SetSelection(0, 0);
3055 m_xmlPreviewPanel->SetReadOnly(true);
3056 return;
3057 }
3058 }
3059 }
3060
3061 // If we couldn't find the tree, move to beginning
3062 m_xmlPreviewPanel->SetFirstVisibleLine(0);
3063
3064 // Clear any selection (no caret highlighting)
3065 m_xmlPreviewPanel->SetSelection(0, 0);
3066 m_xmlPreviewPanel->SetReadOnly(true);
3067}
3068
3069void BehaviorTreeProjectDialog::LoadBlackboardPreview(const std::string &filepath, const std::string &blackboardId) {
3070 // Read the XML file
3071 std::string resolvedPath = m_project->ResolveResourcePath(filepath);
3072 std::ifstream file(resolvedPath);
3073 if (!file.is_open()) {
3074 m_xmlPreviewPanel->SetReadOnly(false);
3075 m_xmlPreviewPanel->SetText(wxString::Format("Error: Could not open file '%s'", resolvedPath));
3076 m_xmlPreviewPanel->SetReadOnly(true);
3077 return;
3078 }
3079
3080 std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
3081 file.close();
3082
3083 // Set the entire file content
3084 m_xmlPreviewPanel->SetReadOnly(false);
3085 m_xmlPreviewPanel->SetText(wxString::FromUTF8(content));
3086
3087 // Reset all text to default style (style 0)
3088 m_xmlPreviewPanel->StartStyling(0);
3089 m_xmlPreviewPanel->SetStyling(m_xmlPreviewPanel->GetLength(), 0);
3090
3091 // Find the Blackboard with this ID
3092 std::string searchStr = "Blackboard ID=\"" + blackboardId + "\"";
3093 size_t bbStartPos = content.find(searchStr);
3094
3095 if (bbStartPos == std::string::npos) {
3096 // Try with single quotes
3097 searchStr = "Blackboard ID='" + blackboardId + "'";
3098 bbStartPos = content.find(searchStr);
3099 }
3100
3101 if (bbStartPos != std::string::npos) {
3102 // Find the start of the line
3103 size_t lineStartPos = bbStartPos;
3104 while (lineStartPos > 0 && content[lineStartPos - 1] != '\n') {
3105 lineStartPos--;
3106 }
3107
3108 // Find the closing tag
3109 size_t bbEndPos = content.find("</Blackboard>", bbStartPos);
3110 if (bbEndPos != std::string::npos) {
3111 // Find the end of the closing tag line
3112 bbEndPos = content.find('>', bbEndPos);
3113 if (bbEndPos != std::string::npos) {
3114 bbEndPos++; // Include the '>'
3115
3116 // Highlight this blackboard section with purple style (style 3)
3117 m_xmlPreviewPanel->StartStyling(lineStartPos);
3118 m_xmlPreviewPanel->SetStyling(bbEndPos - lineStartPos, 3);
3119
3120 // Center the view on the start of the highlighted blackboard
3121 int bbLineNum = m_xmlPreviewPanel->LineFromPosition(lineStartPos);
3122 int bbEndLineNum = m_xmlPreviewPanel->LineFromPosition(bbEndPos);
3123 int linesOnScreen = m_xmlPreviewPanel->LinesOnScreen();
3124
3125 int bbLines = bbEndLineNum - bbLineNum + 1;
3126
3127 int firstLine;
3128 if (bbLines <= linesOnScreen) {
3129 int middleLine = bbLineNum + (bbLines / 2);
3130 firstLine = middleLine - (linesOnScreen / 2);
3131 } else {
3132 firstLine = bbLineNum - 2;
3133 }
3134 if (firstLine < 0)
3135 firstLine = 0;
3136
3137 m_xmlPreviewPanel->SetFirstVisibleLine(firstLine);
3138
3139 // Clear any selection
3140 m_xmlPreviewPanel->SetSelection(0, 0);
3141 m_xmlPreviewPanel->SetReadOnly(true);
3142 return;
3143 }
3144 }
3145 }
3146
3147 // If we couldn't find the blackboard, move to beginning
3148 m_xmlPreviewPanel->SetFirstVisibleLine(0);
3149 m_xmlPreviewPanel->SetSelection(0, 0);
3150 m_xmlPreviewPanel->SetReadOnly(true);
3151}
BehaviorTreeProjectDialog::OnProjectNameChanged BehaviorTreeProjectDialog::OnRemoveFiles wxEND_EVENT_TABLE() BehaviorTreeProjectDialog
BehaviorTreeProjectDialog::OnProjectNameChanged EVT_BUTTON(ID_ADD_FOLDER, BehaviorTreeProjectDialog::OnAddFolder) EVT_BUTTON(ID_REMOVE_FILES
wxBEGIN_EVENT_TABLE(BehaviorTreeProjectDialog, EmberUI::ScalableDialog) EVT_TEXT(ID_PROJECT_NAME
#define LOG_WARNING(category, message)
Definition Logger.h:115
MainFrame::OnExit MainFrame::OnNewProject MainFrame::OnCloseProject MainFrame::OnToggleMaximize MainFrame::OnPreviousScene MainFrame::OnPreferences MainFrame::OnEditorTool EVT_BUTTON(ID_MonitorTool, MainFrame::OnMonitorTool) EVT_PAINT(MainFrame
Definition MainFrame.cpp:63
Centralized resource path management for EmberForge.
Dialog for creating and configuring BehaviorTree projects.
void OnResourceSelected(wxListEvent &event)
void OnProfileSelected(wxCommandEvent &event)
void LoadTreePreview(const std::string &filepath, const std::string &treeId)
void OnClearFiles(wxCommandEvent &event)
wxPanel * CreatePreviewTab(wxNotebook *notebook)
wxPanel * CreateValidationTab(wxNotebook *notebook)
void OnValidate(wxCommandEvent &event)
wxColour InterpolateColor(const wxColour &color1, const wxColour &color2, float t)
wxPanel * CreateBlackboardTab(wxNotebook *notebook)
void OnDescriptionChanged(wxCommandEvent &event)
wxPanel * CreateRightPanel(wxWindow *parent)
void OnCancel(wxCommandEvent &event)
void OnSave(wxCommandEvent &event)
void OnPulseTimer(wxTimerEvent &event)
void OnProjectNameChanged(wxCommandEvent &event)
std::vector< ClickableItem > m_clickableItems
void OnStructurePanelClick(wxMouseEvent &event)
void OnResourceActivated(wxListEvent &event)
void OnRefreshPreview(wxCommandEvent &event)
void LoadBlackboardPreview(const std::string &filepath, const std::string &blackboardId)
std::unique_ptr< EmberCore::ProjectValidationReport > m_lastValidationReport
void GenerateColoredValidationReport(const EmberCore::ProjectValidationReport &report)
void LoadXMLPreview(const std::string &filepath, int highlightLine=-1)
void OnCreate(wxCommandEvent &event)
void OnPreviewFilterChanged(wxCommandEvent &event)
std::shared_ptr< EmberCore::BehaviorTreeProject > m_project
void OnAddFolder(wxCommandEvent &event)
wxPanel * CreateLeftPanel(wxWindow *parent)
bool IsValidProjectName(const wxString &name) const
wxPanel * CreateResourcesTab(wxNotebook *notebook)
void OnAddFiles(wxCommandEvent &event)
void OnRemoveFiles(wxCommandEvent &event)
bool OnProgress(const EmberCore::String &message, int current, int total) override
Called to report progress during parsing.
DialogProgressCallback(wxGauge *gauge, wxStaticText *text, wxDialog *dialog)
static ConfigManager & GetInstance()
Callback interface for reporting parsing progress.
Configuration for XML parser behavior and element/attribute mappings.
static wxString GetResourcesDir()
Get the base resources directory.
DPI-aware dialog base class for scalable layouts.
Main types header for EmberCore.
std::string String
Framework-agnostic string type.
Definition String.h:14
Definition Panel.h:8
Complete validation report for a project.
std::vector< String > warnings
Project-level warnings.
std::vector< ResourceValidationStatus > resource_statuses
Status for each resource.
std::vector< String > unimplemented_trees
Trees referenced but not implemented.
std::vector< String > errors
Project-level errors.
bool is_valid
Overall validation status.
std::vector< String > circular_references
Circular reference chains detected.
std::map< String, TreeImplementationStatus > tree_statuses
Status for each tree.
Resource validation status for a single file.
std::vector< String > blackboard_ids
IDs of blackboards found in the file.
std::vector< String > errors
Validation errors for this file.
std::vector< String > warnings
Validation warnings for this file.