Ember
Loading...
Searching...
No Matches
NavigatorTab.cpp
Go to the documentation of this file.
1#include "Tabs/NavigatorTab.h"
4#include "Core/BehaviorTree.h"
6#include "Core/Node.h"
7#include "Utils/Logger.h"
8#include <algorithm>
9#include <wx/dcbuffer.h>
10#include <wx/menu.h>
11
12namespace EmberUI {
13
14static const int INDENT_PX = 18;
15static const int CONNECTOR_X_OFFSET = 10;
16static const int TOGGLE_SIZE = 10;
17static const int BASE_LEFT_PAD = 10;
18
19// ============================================================================
20// NavigatorHierarchyView
21// ============================================================================
22
25
30
35
40
41// ============================================================================
42// NavigatorTreeListBox
43// ============================================================================
44
45NavigatorTreeListBox::NavigatorTreeListBox(NavigatorTab *owner, wxWindow *parent, wxWindowID id)
46 : wxVListBox(parent, id, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE), m_owner(owner) {
47 SetBackgroundColour(wxColour(45, 45, 50));
48 Bind(wxEVT_LEFT_DOWN, &NavigatorTreeListBox::OnLeftDown, this);
49 Bind(wxEVT_LEFT_DCLICK, &NavigatorTreeListBox::OnLeftDClick, this);
50 Bind(wxEVT_RIGHT_DOWN, &NavigatorTreeListBox::OnRightDown, this);
51}
52
53void NavigatorTreeListBox::OnDrawBackground(wxDC &dc, const wxRect &rect, size_t n) const {
54 const auto &items = m_owner->GetFlatList();
55 if (n >= items.size())
56 return;
57
58 const auto &entry = items[n];
59
60 if (entry.isHeader) {
61 dc.SetBrush(wxBrush(wxColour(38, 40, 45)));
62 dc.SetPen(*wxTRANSPARENT_PEN);
63 dc.DrawRectangle(rect);
64 return;
65 }
66
67 bool isSelected = IsSelected(n);
68 bool isCurrent = (entry.id == m_owner->GetCurrentTreeId());
69 bool isBlackboard = (entry.section == NavigatorTab::SectionType::Blackboards);
70
71 if (isSelected && isBlackboard) {
72 dc.SetBrush(wxBrush(wxColour(70, 55, 100)));
73 } else if (isSelected) {
74 dc.SetBrush(wxBrush(wxColour(55, 75, 100)));
75 } else if (isCurrent) {
76 dc.SetBrush(wxBrush(wxColour(50, 60, 75)));
77 } else if (n % 2 == 1) {
78 dc.SetBrush(wxBrush(wxColour(48, 48, 53)));
79 } else {
80 dc.SetBrush(wxBrush(wxColour(45, 45, 50)));
81 }
82 dc.SetPen(*wxTRANSPARENT_PEN);
83 dc.DrawRectangle(rect);
84
85 if (isCurrent) {
86 dc.SetBrush(wxBrush(wxColour(80, 160, 220)));
87 dc.SetPen(*wxTRANSPARENT_PEN);
88 dc.DrawRectangle(rect.x, rect.y + 4, 3, rect.height - 8);
89 }
90}
91
92void NavigatorTreeListBox::OnDrawItem(wxDC &dc, const wxRect &rect, size_t n) const {
93 const auto &items = m_owner->GetFlatList();
94 if (n >= items.size())
95 return;
96
97 const auto &entry = items[n];
98
99 if (entry.isHeader) {
100 int leftPad = BASE_LEFT_PAD;
101
102 int toggleX = leftPad;
103 int toggleY = rect.y + (rect.height / 2) - (TOGGLE_SIZE / 2);
104
105 dc.SetPen(wxPen(wxColour(180, 185, 195), 1));
106 dc.SetBrush(wxBrush(wxColour(180, 185, 195)));
107
108 if (entry.isCollapsed) {
109 wxPoint tri[3] = {wxPoint(toggleX, toggleY), wxPoint(toggleX, toggleY + TOGGLE_SIZE),
110 wxPoint(toggleX + TOGGLE_SIZE - 2, toggleY + TOGGLE_SIZE / 2)};
111 dc.DrawPolygon(3, tri);
112 } else {
113 wxPoint tri[3] = {wxPoint(toggleX, toggleY + 1), wxPoint(toggleX + TOGGLE_SIZE, toggleY + 1),
114 wxPoint(toggleX + TOGGLE_SIZE / 2, toggleY + TOGGLE_SIZE - 1)};
115 dc.DrawPolygon(3, tri);
116 }
117 leftPad += TOGGLE_SIZE + 6;
118
119 wxFont headerFont = GetFont();
120 headerFont.SetPointSize(11);
121 headerFont.SetWeight(wxFONTWEIGHT_BOLD);
122 dc.SetFont(headerFont);
123 dc.SetTextForeground(wxColour(200, 205, 215));
124
125 wxString headerText = wxString::FromUTF8(entry.displayName);
126 int textY = rect.y + (rect.height - dc.GetTextExtent(headerText).GetHeight()) / 2;
127 dc.DrawText(headerText, rect.x + leftPad, textY);
128
129 wxFont countFont = GetFont();
130 countFont.SetPointSize(9);
131 dc.SetFont(countFont);
132 dc.SetTextForeground(wxColour(130, 135, 145));
133
134 wxString countText = wxString::Format("(%d)", entry.sectionCount);
135 int countW = dc.GetTextExtent(countText).GetWidth();
136 int countX = rect.x + rect.width - countW - 10;
137 dc.DrawText(countText, countX, textY + 1);
138
139 dc.SetPen(wxPen(wxColour(60, 65, 75), 1));
140 dc.DrawLine(rect.x + 4, rect.y + rect.height - 1, rect.x + rect.width - 4, rect.y + rect.height - 1);
141
142 return;
143 }
144
145 bool isCurrent = (entry.id == m_owner->GetCurrentTreeId());
146 int depth = entry.depth;
147 int basePad = isCurrent ? BASE_LEFT_PAD + 4 : BASE_LEFT_PAD;
148 int leftPad = basePad + depth * INDENT_PX;
149
150 if (depth > 0) {
151 wxPen connPen(wxColour(80, 85, 95), 1);
152 dc.SetPen(connPen);
153
154 for (int d = 1; d < depth; ++d) {
155 if (d - 1 < static_cast<int>(entry.parentHasMore.size()) && entry.parentHasMore[d - 1]) {
156 int lineX = basePad + d * INDENT_PX - CONNECTOR_X_OFFSET;
157 dc.DrawLine(lineX, rect.y, lineX, rect.y + rect.height);
158 }
159 }
160
161 int connX = leftPad - CONNECTOR_X_OFFSET;
162 int midY = rect.y + rect.height / 2;
163 dc.DrawLine(connX, rect.y, connX, entry.isLastChild ? midY : rect.y + rect.height);
164 dc.DrawLine(connX, midY, connX + 8, midY);
165 }
166
167 if (entry.hasChildren) {
168 int toggleX = leftPad;
169 int toggleY = rect.y + (rect.height / 2) - (TOGGLE_SIZE / 2);
170
171 dc.SetPen(wxPen(wxColour(150, 155, 165), 1));
172
173 if (entry.isCollapsed) {
174 wxPoint tri[3] = {wxPoint(toggleX, toggleY), wxPoint(toggleX, toggleY + TOGGLE_SIZE),
175 wxPoint(toggleX + TOGGLE_SIZE - 2, toggleY + TOGGLE_SIZE / 2)};
176 dc.SetBrush(wxBrush(wxColour(150, 155, 165)));
177 dc.DrawPolygon(3, tri);
178 } else {
179 wxPoint tri[3] = {wxPoint(toggleX, toggleY + 1), wxPoint(toggleX + TOGGLE_SIZE, toggleY + 1),
180 wxPoint(toggleX + TOGGLE_SIZE / 2, toggleY + TOGGLE_SIZE - 1)};
181 dc.SetBrush(wxBrush(wxColour(150, 155, 165)));
182 dc.DrawPolygon(3, tri);
183 }
184 leftPad += TOGGLE_SIZE + 4;
185 } else {
186 leftPad += 4;
187 }
188
189 wxFont nameFont = GetFont();
190 nameFont.SetPointSize(10);
191 if (isCurrent)
192 nameFont.SetWeight(wxFONTWEIGHT_BOLD);
193 dc.SetFont(nameFont);
194
195 wxColour nameColour = isCurrent ? wxColour(230, 240, 255) : wxColour(210, 210, 215);
196 if (!entry.isImplemented)
197 nameColour = wxColour(140, 140, 155);
198 if (entry.section == NavigatorTab::SectionType::Blackboards)
199 nameColour = wxColour(180, 210, 255);
200 dc.SetTextForeground(nameColour);
201
202 wxString name = wxString::FromUTF8(entry.displayName);
203 int nameY = rect.y + 7;
204 dc.DrawText(name, rect.x + leftPad, nameY);
205
206 wxFont subFont = GetFont();
207 subFont.SetPointSize(8);
208 dc.SetFont(subFont);
209 dc.SetTextForeground(wxColour(110, 115, 125));
210
211 wxString info;
212 if (entry.section == NavigatorTab::SectionType::Blackboards) {
213 info = wxString::Format("%d entr%s", entry.nodeCount, entry.nodeCount == 1 ? "y" : "ies");
214 } else {
215 if (entry.nodeCount > 0) {
216 info = wxString::Format("%d node%s", entry.nodeCount, entry.nodeCount == 1 ? "" : "s");
217 }
218 if (!entry.isImplemented) {
219 if (!info.IsEmpty())
220 info += " - ";
221 info += "unimplemented";
222 }
223 if (entry.section == NavigatorTab::SectionType::OtherTrees) {
224 if (!info.IsEmpty())
225 info += " - ";
226 info += "not in main tree";
227 } else if (entry.section == NavigatorTab::SectionType::MainTree && depth > 1) {
228 if (!info.IsEmpty())
229 info += " - ";
230 info += "subtree";
231 }
232 }
233
234 if (!info.IsEmpty()) {
235 int infoY = nameY + dc.GetCharHeight() + 1;
236 dc.DrawText(info, rect.x + leftPad, infoY);
237 }
238}
239
240wxCoord NavigatorTreeListBox::OnMeasureItem(size_t n) const {
241 const auto &items = m_owner->GetFlatList();
242 if (n < items.size() && items[n].isHeader) {
243 return 32;
244 }
245 return 44;
246}
247
248void NavigatorTreeListBox::OnLeftDown(wxMouseEvent &event) {
249 int item = VirtualHitTest(event.GetPosition().y);
250 if (item == wxNOT_FOUND) {
251 event.Skip();
252 return;
253 }
254
255 const auto &items = m_owner->GetFlatList();
256 if (item < 0 || item >= static_cast<int>(items.size())) {
257 event.Skip();
258 return;
259 }
260
261 const auto &entry = items[item];
262
263 if (entry.isHeader) {
264 m_owner->ToggleSectionCollapse(entry.section);
265 return;
266 }
267
268 if (entry.section == NavigatorTab::SectionType::Blackboards) {
269 SetSelection(item);
270 Refresh();
271 m_owner->OnBlackboardClicked(entry.id);
272 return;
273 }
274
275 if (entry.hasChildren) {
276 bool isCurrent = (entry.id == m_owner->GetCurrentTreeId());
277 int toggleHitX = m_owner->GetToggleHitX(entry.depth, isCurrent);
278 int clickX = event.GetPosition().x;
279
280 if (clickX >= toggleHitX && clickX < toggleHitX + TOGGLE_SIZE + 6) {
281 m_owner->ToggleCollapse(entry.id);
282 return;
283 }
284 }
285
286 event.Skip();
287}
288
289void NavigatorTreeListBox::OnLeftDClick(wxMouseEvent &event) {
290 int item = VirtualHitTest(event.GetPosition().y);
291 if (item == wxNOT_FOUND)
292 return;
293
294 const auto &items = m_owner->GetFlatList();
295 if (item < 0 || item >= static_cast<int>(items.size()))
296 return;
297
298 const auto &entry = items[item];
299 if (entry.isHeader)
300 return;
301
302 if (entry.section == NavigatorTab::SectionType::Blackboards) {
303 m_owner->OnBlackboardClicked(entry.id);
304 return;
305 }
306
307 m_owner->DrillIntoTree(entry.id);
308}
309
310void NavigatorTreeListBox::OnRightDown(wxMouseEvent &event) {
311 int item = VirtualHitTest(event.GetPosition().y);
312 if (item == wxNOT_FOUND) {
313 event.Skip();
314 return;
315 }
316
317 const auto &items = m_owner->GetFlatList();
318 if (item < 0 || item >= static_cast<int>(items.size())) {
319 event.Skip();
320 return;
321 }
322
323 if (items[item].isHeader || items[item].section == NavigatorTab::SectionType::Blackboards) {
324 event.Skip();
325 return;
326 }
327
328 SetSelection(item);
329 Refresh();
330 m_owner->ShowTreeContextMenu(item, event.GetPosition());
331}
332
333// ============================================================================
334// SearchResultsListBox
335// ============================================================================
336
337SearchResultsListBox::SearchResultsListBox(NavigatorTab *owner, wxWindow *parent, wxWindowID id)
338 : wxVListBox(parent, id, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE), m_owner(owner) {
339 SetBackgroundColour(wxColour(45, 45, 50));
340 Bind(wxEVT_LEFT_DCLICK, &SearchResultsListBox::OnLeftDClick, this);
341 Bind(wxEVT_RIGHT_DOWN, &SearchResultsListBox::OnRightDown, this);
342}
343
344void SearchResultsListBox::OnDrawBackground(wxDC &dc, const wxRect &rect, size_t n) const {
345 bool isSelected = IsSelected(n);
346 if (isSelected) {
347 dc.SetBrush(wxBrush(wxColour(55, 75, 100)));
348 } else if (n % 2 == 1) {
349 dc.SetBrush(wxBrush(wxColour(48, 48, 53)));
350 } else {
351 dc.SetBrush(wxBrush(wxColour(45, 45, 50)));
352 }
353 dc.SetPen(*wxTRANSPARENT_PEN);
354 dc.DrawRectangle(rect);
355}
356
357void SearchResultsListBox::OnDrawItem(wxDC &dc, const wxRect &rect, size_t n) const {
358 const auto &results = m_owner->GetSearchResults();
359 if (n >= results.size())
360 return;
361
362 const auto &result = results[n];
363 int leftPad = BASE_LEFT_PAD;
364
365 wxFont nameFont = GetFont();
366 nameFont.SetPointSize(10);
367 dc.SetFont(nameFont);
368
369 if (result.type == NavigatorTab::SearchResult::Type::Tree) {
370 dc.SetTextForeground(wxColour(100, 180, 255));
371 wxString label = wxString::FromUTF8(result.treeId);
372 int nameY = rect.y + 7;
373 dc.DrawText(label, rect.x + leftPad, nameY);
374
375 wxFont subFont = GetFont();
376 subFont.SetPointSize(8);
377 dc.SetFont(subFont);
378 dc.SetTextForeground(wxColour(110, 115, 125));
379 dc.DrawText("tree", rect.x + leftPad, nameY + dc.GetCharHeight() + 1);
380 } else {
381 dc.SetTextForeground(wxColour(210, 210, 215));
382 wxString label = wxString::FromUTF8(result.nodeName);
383 int nameY = rect.y + 7;
384 dc.DrawText(label, rect.x + leftPad, nameY);
385
386 wxFont subFont = GetFont();
387 subFont.SetPointSize(8);
388 dc.SetFont(subFont);
389 dc.SetTextForeground(wxColour(110, 115, 125));
390 wxString info = "in " + wxString::FromUTF8(result.treeId);
391 dc.DrawText(info, rect.x + leftPad, nameY + dc.GetCharHeight() + 1);
392 }
393}
394
395wxCoord SearchResultsListBox::OnMeasureItem(size_t) const { return 40; }
396
397void SearchResultsListBox::OnLeftDClick(wxMouseEvent &event) {
398 int item = VirtualHitTest(event.GetPosition().y);
399 if (item == wxNOT_FOUND)
400 return;
401
402 const auto &results = m_owner->GetSearchResults();
403 if (item < 0 || item >= static_cast<int>(results.size()))
404 return;
405
406 const auto &result = results[item];
407 m_owner->ClearSearch();
408 m_owner->DrillIntoTree(result.treeId);
409
410 if (result.type == NavigatorTab::SearchResult::Type::Node) {
411 m_owner->SelectNodeById(result.nodeId);
412 }
413}
414
415void SearchResultsListBox::OnRightDown(wxMouseEvent &event) {
416 int item = VirtualHitTest(event.GetPosition().y);
417 if (item == wxNOT_FOUND)
418 return;
419
420 SetSelection(item);
421 Refresh();
422 m_owner->ShowSearchContextMenu(item, event.GetPosition());
423}
424
425// ============================================================================
426// NavigatorTab
427// ============================================================================
428
441
442NavigatorTab::NavigatorTab(wxWindow *parent, bool deferLayout) : wxPanel(parent, wxID_ANY) {
443 SetBackgroundColour(wxColour(45, 45, 50));
444 if (!deferLayout)
445 InitLayout();
446}
447
449 return new NavigatorHierarchyView(parent, cfg);
450}
451
453 wxBoxSizer *mainSizer = new wxBoxSizer(wxVERTICAL);
454
456 new wxSearchCtrl(this, ID_SEARCH_CTRL, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER);
457 m_searchCtrl->SetDescriptiveText("Search trees/nodes...");
458 m_searchCtrl->ShowSearchButton(true);
459 m_searchCtrl->ShowCancelButton(true);
460 m_searchCtrl->Bind(wxEVT_TEXT, &NavigatorTab::OnSearchTextChanged, this);
461 m_searchCtrl->Bind(wxEVT_SEARCHCTRL_SEARCH_BTN, &NavigatorTab::OnSearchEnter, this);
462 m_searchCtrl->Bind(wxEVT_TEXT_ENTER, &NavigatorTab::OnSearchEnter, this);
463 mainSizer->Add(m_searchCtrl, 0, wxEXPAND | wxALL, 4);
464
465 m_breadcrumbPanel = new wxPanel(this, wxID_ANY);
466 m_breadcrumbPanel->SetBackgroundColour(wxColour(50, 52, 58));
467 wxBoxSizer *breadSizer = new wxBoxSizer(wxHORIZONTAL);
468
469 m_backBtn = new wxButton(m_breadcrumbPanel, ID_BACK_BTN, wxString::FromUTF8("\xe2\x86\x90"), wxDefaultPosition,
470 wxDefaultSize, wxBU_EXACTFIT | wxBORDER_NONE);
471 m_backBtn->SetBackgroundColour(wxColour(50, 52, 58));
472 m_backBtn->SetForegroundColour(wxColour(140, 180, 220));
473 m_backBtn->Hide();
474
475 m_breadcrumbLabel = new wxStaticText(m_breadcrumbPanel, wxID_ANY, "Tree List");
476 m_breadcrumbLabel->SetForegroundColour(wxColour(180, 185, 195));
477 wxFont bcFont = m_breadcrumbLabel->GetFont();
478 bcFont.SetPointSize(9);
479 m_breadcrumbLabel->SetFont(bcFont);
480
481 breadSizer->AddSpacer(6);
482 breadSizer->Add(m_backBtn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4);
483 breadSizer->Add(m_breadcrumbLabel, 1, wxALIGN_CENTER_VERTICAL);
484 breadSizer->AddSpacer(6);
485 m_breadcrumbPanel->SetSizer(breadSizer);
486 mainSizer->Add(m_breadcrumbPanel, 0, wxEXPAND | wxLEFT | wxRIGHT, 2);
487
488 m_book = new wxSimplebook(this, wxID_ANY);
489
491 m_book->AddPage(m_treeListBox, "Tree List", true);
492
494 hierConfig.showSearchBar = false;
495 hierConfig.autoExpandOnLoad = true;
496 hierConfig.autoExpandDepth = 3;
497 hierConfig.syncSelectionWithCanvas = true;
498 hierConfig.panelColour = wxColour(45, 45, 50);
499 hierConfig.backgroundColour = wxColour(50, 50, 50);
501 m_book->AddPage(m_hierarchyView, "Hierarchy", false);
502
504 m_book->AddPage(m_searchListBox, "Search", false);
505
506 mainSizer->Add(m_book, 1, wxEXPAND | wxLEFT | wxRIGHT, 2);
507
508 m_statusLabel = new wxStaticText(this, wxID_ANY, "No project loaded");
509 m_statusLabel->SetForegroundColour(wxColour(120, 125, 135));
510 wxFont statusFont = m_statusLabel->GetFont();
511 statusFont.SetPointSize(8);
512 m_statusLabel->SetFont(statusFont);
513 mainSizer->Add(m_statusLabel, 0, wxEXPAND | wxALL, 6);
514
515 SetSizer(mainSizer);
516
517 m_hierarchyView->SetSelectionCallback([this](EmberCore::ITreeNode *node) { OnHierarchySelectionChanged(node); });
518 m_hierarchyView->SetContextMenuPopulator(
519 [this](EmberCore::ITreeNode *node, wxMenu &menu) { OnHierarchyContextMenu(node, menu); });
520 m_hierarchyView->SetContextMenuHandler(
521 [this](int id, EmberCore::ITreeNode *node) { OnHierarchyContextCommand(id, node); });
522 m_hierarchyView->SetNodeActivatedHandler([this](EmberCore::ITreeNode *node) { OnHierarchyNodeActivated(node); });
523}
524
525// --- Navigation ---
526
527void NavigatorTab::DrillIntoTree(const std::string &treeId) {
528 m_browsedTreeId = treeId;
529
530 auto it = m_projectTrees.find(treeId);
531 if (it != m_projectTrees.end() && it->second) {
532 if (treeId == m_currentTreeId && m_activeTreeAdapter) {
534 } else {
535 auto adapter = std::make_shared<EmberCore::DirectTreeAdapter>(it->second);
536 m_hierarchyView->SetTree(adapter);
537 }
538
541 m_hierarchyView->ApplyConfig(cfg);
542 } else if (treeId == m_currentTreeId && m_activeTreeAdapter) {
544
546 cfg.syncSelectionWithCanvas = true;
547 m_hierarchyView->ApplyConfig(cfg);
548 }
549
551 m_book->SetSelection(static_cast<int>(ViewPage::Hierarchy));
552 m_hierarchyView->Layout();
555}
556
558 m_browsedTreeId.clear();
559 m_hierarchyView->ClearTree();
561 m_book->SetSelection(static_cast<int>(ViewPage::TreeList));
564}
565
567 switch (m_currentPage) {
569 m_backBtn->Hide();
570 m_breadcrumbLabel->SetLabel("Tree List");
571 break;
573 m_backBtn->Show();
574 m_breadcrumbLabel->SetLabel("Tree List / " + wxString::FromUTF8(m_browsedTreeId));
575 break;
576 case ViewPage::Search:
577 m_backBtn->Show();
578 m_breadcrumbLabel->SetLabel("Search: " + m_searchFilter);
579 break;
580 }
581 m_breadcrumbPanel->Layout();
582}
583
585 if (m_projectTrees.empty()) {
586 m_statusLabel->SetLabel("No project loaded");
587 return;
588 }
589
591 auto it = m_treeInfoMap.find(m_browsedTreeId);
592 int count = (it != m_treeInfoMap.end()) ? it->second.nodeCount : 0;
593 wxString active = (m_browsedTreeId == m_currentTreeId) ? " (active)" : "";
594 m_statusLabel->SetLabel(wxString::Format("%s: %d nodes%s", wxString::FromUTF8(m_browsedTreeId), count, active));
595 } else if (m_currentPage == ViewPage::Search) {
596 m_statusLabel->SetLabel(wxString::Format("%zu results", m_searchResults.size()));
597 } else {
598 m_statusLabel->SetLabel(
599 wxString::Format("%zu tree%s", m_treeInfoMap.size(), m_treeInfoMap.size() == 1 ? "" : "s"));
600 }
601}
602
603// --- Tree list management ---
604
605void NavigatorTab::UpdateTreeList(const std::map<std::string, std::shared_ptr<EmberCore::BehaviorTree>> &trees,
606 const std::string &currentTreeId) {
607 m_projectTrees = trees;
608 m_currentTreeId = currentTreeId;
609 m_treeInfoMap.clear();
610
611 for (const auto &pair : trees) {
612 TreeInfo info;
613 info.id = pair.first;
614 info.nodeCount = pair.second ? static_cast<int>(pair.second->GetNodeCount()) : 0;
615 info.isImplemented = pair.second && pair.second->HasRootNode() && pair.second->GetNodeCount() > 0;
616
617 if (pair.second && pair.second->GetRootNode()) {
618 CollectSubTreeRefs(pair.second->GetRootNode(), info.subtreeRefs);
619 }
620
621 m_treeInfoMap[pair.first] = std::move(info);
622 }
623
624 m_collapsedTrees.clear();
625 for (const auto &pair : m_treeInfoMap) {
626 if (!pair.second.subtreeRefs.empty()) {
627 m_collapsedTrees.insert(pair.first);
628 }
629 }
630
633}
634
635void NavigatorTab::SetCurrentTree(const std::string &treeId) {
636 m_currentTreeId = treeId;
639}
640
642 m_projectTrees.clear();
643 m_treeInfoMap.clear();
644 m_flatList.clear();
645 m_collapsedTrees.clear();
649 m_blackboards.clear();
650 m_currentTreeId.clear();
651 m_mainTreeId.clear();
652 m_browsedTreeId.clear();
653 m_activeTreeAdapter.reset();
654 m_searchResults.clear();
655 m_treeListBox->SetItemCount(0);
656 m_treeListBox->Refresh();
657 m_searchListBox->SetItemCount(0);
658 m_searchListBox->Refresh();
659 m_hierarchyView->ClearTree();
660
663 m_book->SetSelection(0);
665 }
667}
668
669// --- Active tree (shared with canvas) ---
670
671void NavigatorTab::SetActiveTree(std::shared_ptr<EmberCore::ITreeStructure> tree, const std::string &treeId) {
672 m_activeTreeAdapter = tree;
673 m_currentTreeId = treeId;
674
678 cfg.syncSelectionWithCanvas = true;
679 m_hierarchyView->ApplyConfig(cfg);
680 }
681
684}
685
686void NavigatorTab::SelectNodeById(size_t nodeId) {
688 m_hierarchyView->SelectNodeById(nodeId);
689 }
690}
691
694 m_hierarchyView->RefreshTree();
695 }
696}
697
698void NavigatorTab::SetLayoutInvalidationCallback(std::function<void()> cb) {
699 m_hierarchyView->SetLayoutInvalidationCallback(cb);
700}
701
703 m_nodeSelectionCallback = std::move(cb);
704}
705
706void NavigatorTab::SetMainTreeId(const std::string &id) {
707 m_mainTreeId = id;
708 LOG_INFO("Navigator", "SetMainTreeId called with: '" + id + "'");
709}
710
711void NavigatorTab::SetBlackboards(const std::map<std::string, std::shared_ptr<EmberCore::Blackboard>> &bbs) {
712 m_blackboards = bbs;
714}
715
716void NavigatorTab::SetBlackboardSelectionCallback(std::function<void(const std::string &)> cb) {
717 m_blackboardSelectionCallback = std::move(cb);
718}
719
720void NavigatorTab::OnBlackboardClicked(const std::string &bbId) {
723 }
724}
725
726// --- Tree list building ---
727
728void NavigatorTab::CollectSubTreeRefs(EmberCore::Node *node, std::set<std::string> &refs) const {
729 if (!node)
730 return;
731
732 std::string subtreeRef = node->GetAttribute("__subtree_ref__", "");
733 if (!subtreeRef.empty()) {
734 refs.insert(subtreeRef);
735 }
736
737 for (size_t i = 0; i < node->GetChildCount(); ++i) {
738 CollectSubTreeRefs(node->GetChild(i), refs);
739 }
740}
741
742void NavigatorTab::CollectAllReachableSubtrees(const std::string &treeId, std::set<std::string> &reachable) const {
743 auto it = m_treeInfoMap.find(treeId);
744 if (it == m_treeInfoMap.end())
745 return;
746
747 for (const auto &ref : it->second.subtreeRefs) {
748 if (reachable.insert(ref).second) {
749 CollectAllReachableSubtrees(ref, reachable);
750 }
751 }
752}
753
755 m_flatList.clear();
756
757 if (m_treeInfoMap.empty() && m_blackboards.empty()) {
758 m_treeListBox->SetItemCount(0);
759 m_treeListBox->Refresh();
760 return;
761 }
762
763 std::set<std::string> mainTreeRefs;
764 bool hasMainTree = !m_mainTreeId.empty() && m_treeInfoMap.count(m_mainTreeId);
765
766 LOG_INFO("Navigator", "BuildFlatList: m_mainTreeId='" + m_mainTreeId +
767 "' hasMainTree=" + (hasMainTree ? "true" : "false") +
768 " treeInfoMap.size()=" + std::to_string(m_treeInfoMap.size()));
769
770 if (hasMainTree) {
772 LOG_INFO("Navigator", "BuildFlatList: mainTreeRefs.size()=" + std::to_string(mainTreeRefs.size()));
773 }
774
775 std::vector<std::string> otherTrees;
776 for (const auto &pair : m_treeInfoMap) {
777 if (hasMainTree && (pair.first == m_mainTreeId || mainTreeRefs.count(pair.first)))
778 continue;
779 otherTrees.push_back(pair.first);
780 }
781 std::sort(otherTrees.begin(), otherTrees.end());
782
783 if (hasMainTree) {
784 FlatEntry header;
785 header.id = "__section_main_tree__";
786 header.displayName = "Main Tree";
787 header.nodeCount = 0;
788 header.depth = 0;
789 header.isImplemented = true;
790 header.isLastChild = otherTrees.empty() && m_blackboards.empty();
791 header.hasChildren = true;
793 header.isHeader = true;
795 header.sectionCount = 1 + static_cast<int>(mainTreeRefs.size());
796 m_flatList.push_back(std::move(header));
797
799 std::set<std::string> visited;
800 FlattenTree(m_mainTreeId, 1, visited, true, {});
801 }
802 }
803
804 if (!otherTrees.empty()) {
805 FlatEntry header;
806 header.id = "__section_other__";
807 header.displayName = "Other Trees";
808 header.nodeCount = 0;
809 header.depth = 0;
810 header.isImplemented = true;
811 header.isLastChild = m_blackboards.empty();
812 header.hasChildren = true;
814 header.isHeader = true;
816 header.sectionCount = static_cast<int>(otherTrees.size());
817 m_flatList.push_back(std::move(header));
818
820 for (size_t i = 0; i < otherTrees.size(); ++i) {
821 const auto &id = otherTrees[i];
822 const auto &info = m_treeInfoMap[id];
823 FlatEntry e;
824 e.id = info.id;
825 e.displayName = info.id;
826 e.nodeCount = info.nodeCount;
827 e.depth = 1;
828 e.isImplemented = info.isImplemented;
829 e.isLastChild = (i == otherTrees.size() - 1);
830 e.hasChildren = false;
831 e.isCollapsed = false;
832 e.isHeader = false;
834 e.sectionCount = 0;
835 m_flatList.push_back(std::move(e));
836 }
837 }
838 }
839
840 if (!m_blackboards.empty()) {
841 FlatEntry header;
842 header.id = "__section_blackboards__";
843 header.displayName = "Blackboards";
844 header.nodeCount = 0;
845 header.depth = 0;
846 header.isImplemented = true;
847 header.isLastChild = true;
848 header.hasChildren = true;
850 header.isHeader = true;
852 header.sectionCount = static_cast<int>(m_blackboards.size());
853 m_flatList.push_back(std::move(header));
854
856 std::vector<std::string> bbIds;
857 bbIds.reserve(m_blackboards.size());
858 for (const auto &kv : m_blackboards) {
859 bbIds.push_back(kv.first);
860 }
861 std::sort(bbIds.begin(), bbIds.end());
862
863 for (size_t i = 0; i < bbIds.size(); ++i) {
864 const auto &id = bbIds[i];
865 const auto &bb = m_blackboards.at(id);
866 FlatEntry e;
867 e.id = id;
868 e.displayName = id;
869 e.nodeCount = bb ? static_cast<int>(bb->GetEntryCount()) : 0;
870 e.depth = 1;
871 e.isImplemented = true;
872 e.isLastChild = (i == bbIds.size() - 1);
873 e.hasChildren = false;
874 e.isCollapsed = false;
875 e.isHeader = false;
877 e.sectionCount = 0;
878 m_flatList.push_back(std::move(e));
879 }
880 }
881 }
882
883 m_treeListBox->SetItemCount(m_flatList.size());
884
886 if (idx >= 0) {
887 m_treeListBox->SetSelection(idx);
888 } else {
889 m_treeListBox->SetSelection(wxNOT_FOUND);
890 }
891
892 m_treeListBox->Refresh();
893}
894
895void NavigatorTab::FlattenTree(const std::string &treeId, int depth, std::set<std::string> &visited, bool isLastChild,
896 std::vector<bool> parentHasMore) {
897 if (depth > 20)
898 return;
899
900 auto it = m_treeInfoMap.find(treeId);
901 if (it == m_treeInfoMap.end())
902 return;
903
904 bool isCircular = visited.find(treeId) != visited.end();
905 visited.insert(treeId);
906
907 const TreeInfo &info = it->second;
908 bool hasKids = !isCircular && !info.subtreeRefs.empty();
909 bool collapsed = hasKids && IsCollapsed(treeId);
910
911 FlatEntry e;
912 e.id = info.id;
913 e.displayName = info.id;
914 e.nodeCount = info.nodeCount;
915 e.depth = depth;
917 e.isLastChild = isLastChild;
918 e.hasChildren = hasKids;
919 e.isCollapsed = collapsed;
920 e.isHeader = false;
922 e.sectionCount = 0;
923 e.parentHasMore = parentHasMore;
924
925 m_flatList.push_back(std::move(e));
926
927 if (hasKids && !collapsed) {
928 std::vector<std::string> sortedRefs(info.subtreeRefs.begin(), info.subtreeRefs.end());
929 std::sort(sortedRefs.begin(), sortedRefs.end());
930
931 std::vector<bool> childParentHasMore = parentHasMore;
932 childParentHasMore.push_back(!isLastChild);
933
934 for (size_t i = 0; i < sortedRefs.size(); ++i) {
935 bool childIsLast = (i == sortedRefs.size() - 1);
936 FlattenTree(sortedRefs[i], depth + 1, visited, childIsLast, childParentHasMore);
937 }
938 }
939
940 visited.erase(treeId);
941}
942
943int NavigatorTab::FindItemByTreeId(const std::string &treeId) const {
944 for (size_t i = 0; i < m_flatList.size(); ++i) {
945 if (m_flatList[i].id == treeId) {
946 return static_cast<int>(i);
947 }
948 }
949 return -1;
950}
951
952void NavigatorTab::ToggleCollapse(const std::string &treeId) {
953 size_t firstVisible = m_treeListBox->GetVisibleRowsBegin();
954 auto it = m_collapsedTrees.find(treeId);
955 if (it != m_collapsedTrees.end()) {
956 m_collapsedTrees.erase(it);
957 } else {
958 m_collapsedTrees.insert(treeId);
959 }
961 if (firstVisible < m_treeListBox->GetItemCount()) {
962 m_treeListBox->ScrollToRow(firstVisible);
963 }
964}
965
966bool NavigatorTab::IsCollapsed(const std::string &treeId) const {
967 return m_collapsedTrees.find(treeId) != m_collapsedTrees.end();
968}
969
971 size_t firstVisible = m_treeListBox->GetVisibleRowsBegin();
972 if (section == SectionType::MainTree) {
974 } else if (section == SectionType::OtherTrees) {
976 } else if (section == SectionType::Blackboards) {
978 }
980 if (firstVisible < m_treeListBox->GetItemCount()) {
981 m_treeListBox->ScrollToRow(firstVisible);
982 }
983}
984
986 if (section == SectionType::MainTree)
988 if (section == SectionType::OtherTrees)
990 if (section == SectionType::Blackboards)
992 return false;
993}
994
995int NavigatorTab::GetToggleHitX(int depth, bool isCurrent) const {
996 int basePad = isCurrent ? BASE_LEFT_PAD + 4 : BASE_LEFT_PAD;
997 return basePad + depth * INDENT_PX;
998}
999
1001 m_collapsedTrees.clear();
1002 for (const auto &pair : m_treeInfoMap) {
1003 if (!pair.second.subtreeRefs.empty()) {
1004 m_collapsedTrees.insert(pair.first);
1005 }
1006 }
1007 BuildFlatList();
1008}
1009
1014
1017 return;
1018 m_searchCtrl->ChangeValue("");
1019 m_searchFilter.Clear();
1020 m_searchResults.clear();
1021 m_searchListBox->SetItemCount(0);
1022 m_searchListBox->Refresh();
1023}
1024
1025// --- Global search ---
1026
1027void NavigatorTab::PerformGlobalSearch(const wxString &query) {
1028 m_searchResults.clear();
1029 wxString lower = query.Lower();
1030
1031 for (const auto &pair : m_treeInfoMap) {
1032 wxString treeIdLower = wxString::FromUTF8(pair.first).Lower();
1033 if (treeIdLower.Find(lower) != wxNOT_FOUND) {
1034 SearchResult r;
1036 r.treeId = pair.first;
1037 r.nodeId = 0;
1038 m_searchResults.push_back(r);
1039 }
1040 }
1041
1042 static const size_t MAX_NODE_RESULTS = 200;
1043 size_t nodeResults = 0;
1044 for (const auto &pair : m_projectTrees) {
1045 if (!pair.second || !pair.second->HasRootNode() || nodeResults >= MAX_NODE_RESULTS)
1046 continue;
1047
1048 auto adapter = std::make_shared<EmberCore::DirectTreeAdapter>(pair.second);
1049 bool limitReached = false;
1050 adapter->TraverseNodes([&](EmberCore::ITreeNode *node) {
1051 if (limitReached)
1052 return;
1053 wxString nameLower = wxString(node->GetName().c_str()).Lower();
1054 if (nameLower.Find(lower) != wxNOT_FOUND) {
1055 SearchResult r;
1057 r.treeId = pair.first;
1058 r.nodeName = node->GetName();
1059 r.nodeId = node->GetId();
1060 m_searchResults.push_back(r);
1061 ++nodeResults;
1062 if (nodeResults >= MAX_NODE_RESULTS)
1063 limitReached = true;
1064 }
1065 });
1066 }
1067
1068 m_searchListBox->SetItemCount(m_searchResults.size());
1069 m_searchListBox->Refresh();
1070}
1071
1072// --- Hierarchy hooks ---
1073
1079
1081 if (!node)
1082 return;
1083
1084 bool isActive = (!m_browsedTreeId.empty() && m_browsedTreeId == m_currentTreeId);
1085
1086 if (isActive && m_editingEnabled) {
1087 menu.Append(ID_CTX_ADD_CHILD, "Add Child Node");
1088 menu.AppendSeparator();
1089 menu.Append(ID_CTX_RENAME_NODE, "Rename");
1090 menu.Append(ID_CTX_DUPLICATE_NODE, "Duplicate");
1091 menu.AppendSeparator();
1092 menu.Append(ID_CTX_DELETE_NODE, "Delete");
1093 } else {
1094 bool canView = m_callbacks.canViewInCurrentScene ? m_callbacks.canViewInCurrentScene() : false;
1095 menu.Append(ID_CTX_OPEN_TREE, "Open this tree in current scene");
1096 menu.Enable(ID_CTX_OPEN_TREE, canView);
1097 }
1098}
1099
1101 if (id == ID_CTX_OPEN_TREE && !m_browsedTreeId.empty()) {
1104 }
1105 return;
1106 }
1107
1108 EmberCore::Node *mutableNode = GetMutableNode(node);
1109 if (!mutableNode)
1110 return;
1111
1112 switch (id) {
1113 case ID_CTX_ADD_CHILD: {
1114 auto newNode = EmberCore::NodeFactory::CreateActionNode("New Action");
1115 mutableNode->AddChild(std::move(newNode));
1117 if (m_callbacks.refreshVisualization)
1118 m_callbacks.refreshVisualization();
1119 break;
1120 }
1121 case ID_CTX_DELETE_NODE: {
1122 if (!mutableNode->GetParent())
1123 break;
1124 int result = wxMessageBox(wxString::Format("Delete '%s' and all its children?", mutableNode->GetName()),
1125 "Confirm Delete", wxYES_NO | wxICON_QUESTION, this);
1126 if (result == wxYES) {
1127 EmberCore::Node *parent = mutableNode->GetParent();
1128 if (parent) {
1129 parent->RemoveChild(mutableNode);
1130 }
1131 CallAfter([this]() {
1133 if (m_callbacks.refreshVisualization)
1134 m_callbacks.refreshVisualization();
1135 });
1136 }
1137 break;
1138 }
1139 case ID_CTX_RENAME_NODE: {
1140 wxString currentName = mutableNode->GetName();
1141 wxString newName = wxGetTextFromUser("Enter new name:", "Rename Node", currentName, this);
1142 if (!newName.IsEmpty() && newName != currentName) {
1143 mutableNode->SetName(newName.ToStdString());
1144 CallAfter([this]() {
1146 if (m_callbacks.refreshVisualization)
1147 m_callbacks.refreshVisualization();
1148 });
1149 }
1150 break;
1151 }
1152 case ID_CTX_DUPLICATE_NODE: {
1153 if (!mutableNode->GetParent())
1154 break;
1155 auto dup = mutableNode->DeepCopy();
1156 dup->SetName(mutableNode->GetName() + " Copy");
1157 mutableNode->GetParent()->AddChild(std::move(dup));
1158 wxTheApp->CallAfter([this]() {
1160 if (m_callbacks.refreshVisualization)
1161 m_callbacks.refreshVisualization();
1162 });
1163 break;
1164 }
1165 }
1166}
1167
1169 if (!node)
1170 return;
1171
1172 if (m_browsedTreeId == m_currentTreeId && m_callbacks.centerOnNode) {
1173 m_callbacks.centerOnNode(node);
1174 }
1175}
1176
1178 if (!inode)
1179 return nullptr;
1180 auto *adapter = dynamic_cast<EmberCore::NodeAdapter *>(inode);
1181 if (adapter)
1182 return adapter->GetWrappedNode();
1183 return nullptr;
1184}
1185
1186// --- Tree list context menu ---
1187
1188void NavigatorTab::ShowTreeContextMenu(int itemIndex, const wxPoint &pos) {
1189 if (itemIndex < 0 || itemIndex >= static_cast<int>(m_flatList.size()))
1190 return;
1191
1192 m_contextMenuTreeId = m_flatList[itemIndex].id;
1193
1194 bool canViewInCurrent = m_callbacks.canViewInCurrentScene ? m_callbacks.canViewInCurrentScene() : false;
1195
1196 wxMenu menu;
1197 menu.Append(ID_CTX_VIEW_CURRENT, "View in Current Scene");
1198 menu.Enable(ID_CTX_VIEW_CURRENT, canViewInCurrent);
1199 menu.Append(ID_CTX_OPEN_NEW_SCENE, "Open in New Scene");
1200 menu.Append(ID_CTX_BROWSE, "Browse Hierarchy");
1201 menu.AppendSeparator();
1202
1203 const auto &entry = m_flatList[itemIndex];
1204 if (entry.hasChildren) {
1205 if (entry.isCollapsed)
1206 menu.Append(ID_CTX_EXPAND, "Expand");
1207 else
1208 menu.Append(ID_CTX_COLLAPSE, "Collapse");
1209 }
1210
1211 m_treeListBox->PopupMenu(&menu, pos);
1212}
1213
1214void NavigatorTab::ShowSearchContextMenu(int resultIndex, const wxPoint &pos) {
1215 const auto &results = m_searchResults;
1216 if (resultIndex < 0 || resultIndex >= static_cast<int>(results.size()))
1217 return;
1218
1219 const auto &result = results[resultIndex];
1220 m_contextMenuTreeId = result.treeId;
1221
1222 bool canViewInCurrent = m_callbacks.canViewInCurrentScene ? m_callbacks.canViewInCurrentScene() : false;
1223
1224 wxMenu menu;
1225 menu.Append(ID_CTX_VIEW_CURRENT, "View in Current Scene");
1226 menu.Enable(ID_CTX_VIEW_CURRENT, canViewInCurrent);
1227 menu.Append(ID_CTX_OPEN_NEW_SCENE, "Open in New Scene");
1228 menu.Append(ID_CTX_BROWSE, "Browse Hierarchy");
1229
1230 m_searchListBox->PopupMenu(&menu, pos);
1231}
1232
1241
1243 if (m_contextMenuTreeId.empty())
1244 return;
1245 ClearSearch();
1246 if (m_callbacks.openInNewScene)
1247 m_callbacks.openInNewScene(m_contextMenuTreeId);
1248}
1249
1251 if (!m_contextMenuTreeId.empty()) {
1252 ClearSearch();
1254 }
1255}
1256
1261
1266
1267// --- Search ---
1268
1270 m_searchFilter = m_searchCtrl->GetValue();
1271
1272 if (m_searchFilter.IsEmpty()) {
1275 m_book->SetSelection(static_cast<int>(m_currentPage));
1278 }
1279 return;
1280 }
1281
1284 }
1285
1288 m_book->SetSelection(static_cast<int>(ViewPage::Search));
1291
1292 m_searchCtrl->SetFocus();
1293}
1294
1295void NavigatorTab::OnSearchEnter(wxCommandEvent &) {
1296 m_searchFilter = m_searchCtrl->GetValue();
1297 if (!m_searchFilter.IsEmpty()) {
1300 }
1303 m_book->SetSelection(static_cast<int>(ViewPage::Search));
1306 }
1307}
1308
1309// --- Breadcrumb ---
1310
1311void NavigatorTab::OnBackClicked(wxCommandEvent &) {
1313 m_searchCtrl->ChangeValue("");
1314 m_searchFilter.Clear();
1316 m_book->SetSelection(static_cast<int>(m_currentPage));
1319 } else if (m_currentPage == ViewPage::Hierarchy) {
1320 NavigateBack();
1321 }
1322}
1323
1324// --- ITab lifecycle ---
1325
1327 if (m_callbacks.onTabClosed)
1328 m_callbacks.onTabClosed();
1329}
1330
1334 if (idx >= 0) {
1335 m_treeListBox->SetSelection(idx);
1336 m_treeListBox->Refresh();
1337 }
1338 }
1339}
1340
1341} // namespace EmberUI
BehaviorTreeProjectDialog::OnProjectNameChanged BehaviorTreeProjectDialog::OnRemoveFiles wxEND_EVENT_TABLE() BehaviorTreeProjectDialog
#define LOG_INFO(category, message)
Definition Logger.h:114
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
MainFrame::OnExit EVT_MENU(wxID_ABOUT, MainFrame::OnAbout) EVT_MENU(ID_NewProject
Abstract interface for tree nodes that can be visualized.
Definition ITreeNode.h:31
virtual size_t GetId() const =0
virtual const String & GetName() const =0
Adapter class that wraps the current Node implementation to work with ITreeNode interface.
Definition NodeAdapter.h:16
Node * GetWrappedNode() const
Definition NodeAdapter.h:65
static std::unique_ptr< Node > CreateActionNode(const String &name)
Definition Node.cpp:400
Represents a node in a behavior tree structure.
Definition Node.h:20
void RemoveChild(size_t index)
Definition Node.cpp:103
void SetName(const String &name)
Definition Node.h:82
String GetAttribute(const String &name, const String &default_value="") const
Definition Node.cpp:451
Node * GetChild(size_t index) const
Definition Node.cpp:175
Node * GetParent() const
Definition Node.h:75
const String & GetName() const
Definition Node.h:81
void AddChild(std::unique_ptr< Node > child)
Definition Node.cpp:77
size_t GetChildCount() const
Definition Node.h:76
std::unique_ptr< Node > DeepCopy() const
Definition Node.cpp:149
TreeHierarchyTab subclass with context menu and node activation hooks.
NodeActivatedHandler m_activatedHandler
void OnPopulateContextMenu(EmberCore::ITreeNode *node, wxMenu &menu) override
Hook to populate context menu for a node.
ContextMenuPopulator m_contextPopulator
void OnNodeActivated(EmberCore::ITreeNode *node) override
Hook called when a node is double-clicked/activated.
ContextMenuHandler m_contextHandler
void OnContextMenuCommand(int id, EmberCore::ITreeNode *node) override
Hook to handle context menu command.
NavigatorHierarchyView(wxWindow *parent, const EmberUI::TreeHierarchyConfig &config)
Main navigator tab with tree list, hierarchy view, search, and breadcrumb navigation.
void ToggleCollapse(const std::string &treeId)
std::vector< SearchResult > m_searchResults
std::string m_contextMenuTreeId
EmberCore::Node * GetMutableNode(EmberCore::ITreeNode *inode) const
NavigatorCallbacks m_callbacks
void OnHierarchyContextCommand(int id, EmberCore::ITreeNode *node)
NavigatorHierarchyView * m_hierarchyView
wxStaticText * m_breadcrumbLabel
TreeSelectionCallback m_treeSelectionCallback
void SetBlackboards(const std::map< std::string, std::shared_ptr< EmberCore::Blackboard > > &bbs)
std::function< void(EmberCore::ITreeNode *)> m_nodeSelectionCallback
std::string m_browsedTreeId
void ShowTreeContextMenu(int itemIndex, const wxPoint &pos)
std::vector< FlatEntry > m_flatList
void OnClosed() override
Called when the tab is closed.
void SetMainTreeId(const std::string &id)
SearchResultsListBox * m_searchListBox
wxStaticText * m_statusLabel
void OnTreeListContextExpand(wxCommandEvent &event)
bool IsCollapsed(const std::string &treeId) const
void PerformGlobalSearch(const wxString &query)
std::shared_ptr< EmberCore::ITreeStructure > m_activeTreeAdapter
NavigatorTreeListBox * m_treeListBox
void OnTreeListContextBrowse(wxCommandEvent &event)
void SetCurrentTree(const std::string &treeId)
Sets the currently selected tree in the list.
void FlattenTree(const std::string &treeId, int depth, std::set< std::string > &visited, bool isLastChild, std::vector< bool > parentHasMore)
void OnHierarchySelectionChanged(EmberCore::ITreeNode *node)
bool IsSectionCollapsed(SectionType section) const
void SetNodeSelectionCallback(std::function< void(EmberCore::ITreeNode *)> cb)
void OnTreeListContextOpenNewScene(wxCommandEvent &event)
void SelectNodeById(size_t nodeId)
Selects the node with the given ID in the hierarchy.
std::map< std::string, std::shared_ptr< EmberCore::Blackboard > > m_blackboards
wxSearchCtrl * m_searchCtrl
void CollectAllReachableSubtrees(const std::string &treeId, std::set< std::string > &reachable) const
void OnBackClicked(wxCommandEvent &event)
void OnActivated() override
Called when the tab becomes active.
void OnSearchEnter(wxCommandEvent &event)
void SetActiveTree(std::shared_ptr< EmberCore::ITreeStructure > tree, const std::string &treeId)
Sets the active tree for hierarchy view and drills into it.
void OnSearchTextChanged(wxCommandEvent &event)
void OnHierarchyNodeActivated(EmberCore::ITreeNode *node)
void OnTreeListContextCollapse(wxCommandEvent &event)
void UpdateTreeList(const std::map< std::string, std::shared_ptr< EmberCore::BehaviorTree > > &trees, const std::string &currentTreeId)
Updates tree list from project trees and sets current tree.
void CollectSubTreeRefs(EmberCore::Node *node, std::set< std::string > &refs) const
void SetBlackboardSelectionCallback(std::function< void(const std::string &)> cb)
void OnBlackboardClicked(const std::string &bbId)
std::map< std::string, std::shared_ptr< EmberCore::BehaviorTree > > m_projectTrees
std::string m_currentTreeId
std::set< std::string > m_collapsedTrees
void NavigateBack()
Navigates back in breadcrumb history.
void OnHierarchyContextMenu(EmberCore::ITreeNode *node, wxMenu &menu)
void DrillIntoTree(const std::string &treeId)
Drills into a subtree (updates breadcrumb and hierarchy).
int GetToggleHitX(int depth, bool isCurrent) const
std::map< std::string, TreeInfo > m_treeInfoMap
void OnTreeListContextViewInCurrent(wxCommandEvent &event)
void ToggleSectionCollapse(SectionType section)
virtual NavigatorHierarchyView * CreateHierarchyView(wxWindow *parent, const TreeHierarchyConfig &cfg)
NavigatorTab(wxWindow *parent)
void SetLayoutInvalidationCallback(std::function< void()> cb)
std::function< void(const std::string &)> m_blackboardSelectionCallback
int FindItemByTreeId(const std::string &treeId) const
void ShowSearchContextMenu(int resultIndex, const wxPoint &pos)
wxSimplebook * m_book
Virtual list box for the tree list (main tree, other trees, blackboards).
void OnDrawBackground(wxDC &dc, const wxRect &rect, size_t n) const override
wxCoord OnMeasureItem(size_t n) const override
void OnDrawItem(wxDC &dc, const wxRect &rect, size_t n) const override
void OnLeftDown(wxMouseEvent &event)
void OnRightDown(wxMouseEvent &event)
NavigatorTreeListBox(NavigatorTab *owner, wxWindow *parent, wxWindowID id)
void OnLeftDClick(wxMouseEvent &event)
Virtual list box for search results (trees and nodes).
void OnLeftDClick(wxMouseEvent &event)
void OnRightDown(wxMouseEvent &event)
void OnDrawBackground(wxDC &dc, const wxRect &rect, size_t n) const override
void OnDrawItem(wxDC &dc, const wxRect &rect, size_t n) const override
wxCoord OnMeasureItem(size_t n) const override
SearchResultsListBox(NavigatorTab *owner, wxWindow *parent, wxWindowID id)
TreeHierarchyTab(wxWindow *parent, const TreeHierarchyConfig &config={})
Definition Panel.h:8
static const int TOGGLE_SIZE
static const int CONNECTOR_X_OFFSET
static const int INDENT_PX
static const int BASE_LEFT_PAD
wxBEGIN_EVENT_TABLE(Panel, wxPanel) EVT_SIZE(Panel
Definition Panel.cpp:8
Flattened tree list entry for display in NavigatorTreeListBox.
std::vector< bool > parentHasMore
Search result (tree or node) for display in SearchResultsListBox.
std::set< std::string > subtreeRefs
Configuration for TreeHierarchyTab appearance and behavior.
bool autoExpandOnLoad
Auto-expand tree on load.
bool syncSelectionWithCanvas
Sync selection with canvas.
bool showSearchBar
Show search bar above tree.
int autoExpandDepth
Depth to auto-expand.
wxColour backgroundColour
Tree background.
wxColour panelColour
Panel background.