Ember
Loading...
Searching...
No Matches
TreeHierarchyTab.cpp
Go to the documentation of this file.
2#include <algorithm>
3#include <functional>
4
5namespace EmberUI {
6
24
26 m_mainSizer = new wxBoxSizer(wxVERTICAL);
27
28 m_searchCtrl = new wxSearchCtrl(this, ID_SEARCH_CTRL, "", wxDefaultPosition, wxDefaultSize);
29 m_searchCtrl->SetDescriptiveText("Search nodes...");
30 m_searchCtrl->ShowSearchButton(true);
31 m_searchCtrl->ShowCancelButton(true);
32 if (!m_config.showSearchBar) {
33 m_searchCtrl->Hide();
34 }
35
36 long treeStyle = wxTR_HAS_BUTTONS | wxTR_MULTIPLE;
37 if (!m_config.showTreeLines) {
38 treeStyle |= wxTR_NO_LINES;
39 }
40
41 m_treeCtrl = new wxTreeCtrl(this, ID_TREE_CTRL, wxDefaultPosition, wxDefaultSize, treeStyle);
42 m_treeCtrl->SetBackgroundColour(m_config.backgroundColour);
43 m_treeCtrl->SetForegroundColour(m_config.foregroundColour);
44 m_treeCtrl->SetIndent(m_config.treeIndentationSize);
45
46 wxFont treeFont = m_treeCtrl->GetFont();
47 treeFont.SetPointSize(m_config.fontSize);
48 m_treeCtrl->SetFont(treeFont);
49
50 m_mainSizer->Add(m_searchCtrl, 0, wxLEFT | wxRIGHT | wxEXPAND, 10);
51 m_mainSizer->AddSpacer(5);
52 m_mainSizer->Add(m_treeCtrl, 1, wxLEFT | wxRIGHT | wxEXPAND, 10);
53 m_mainSizer->AddSpacer(10);
54
55 SetSizer(m_mainSizer);
56}
57
59 m_config = config;
60
61 if (m_searchCtrl) {
62 m_searchCtrl->Show(m_config.showSearchBar);
63 }
64
65 if (m_treeCtrl) {
66 m_treeCtrl->SetBackgroundColour(m_config.backgroundColour);
67 m_treeCtrl->SetForegroundColour(m_config.foregroundColour);
68 m_treeCtrl->SetIndent(m_config.treeIndentationSize);
69 wxFont f = m_treeCtrl->GetFont();
70 f.SetPointSize(m_config.fontSize);
71 m_treeCtrl->SetFont(f);
72 }
73
74 Layout();
75}
76
77// --- Tree management ---
78
79void TreeHierarchyTab::SetTree(std::shared_ptr<EmberCore::ITreeStructure> tree) {
80 m_tree = std::move(tree);
81 m_searchFilter.Clear();
82 if (m_searchCtrl) {
83 m_searchCtrl->ChangeValue("");
84 }
86}
87
89 m_tree.reset();
90 m_treeCtrl->DeleteAllItems();
91 m_lastSelectedItem = wxTreeItemId();
92}
93
95 m_lastSelectedItem = wxTreeItemId();
96 m_isRefreshing = true;
98 m_isRefreshing = false;
99}
100
102 m_treeCtrl->Freeze();
103 m_treeCtrl->DeleteAllItems();
104
105 if (!m_tree || !m_tree->HasRootNode()) {
106 m_treeCtrl->Thaw();
107 return;
108 }
109
110 EmberCore::ITreeNode *root = m_tree->GetRootNode();
111 wxTreeItemId rootItem = m_treeCtrl->AddRoot(GetNodeDisplayText(root));
112 m_treeCtrl->SetItemData(rootItem, new TreeNodeItemData(root));
113
114 if (!m_searchFilter.IsEmpty()) {
115 AddFilteredNodes(rootItem, root);
116 } else {
117 AddChildrenToItem(rootItem, root);
118 }
119
120 if (m_isRefreshing) {
121 ExpandTreeByVisibility(rootItem);
122 } else if (m_config.autoExpandOnLoad || !m_searchFilter.IsEmpty()) {
123 ExpandTreeToDepth(rootItem, m_config.autoExpandDepth);
124 }
125
126 m_treeCtrl->UnselectAll();
127 m_treeCtrl->Thaw();
128}
129
130void TreeHierarchyTab::AddFilteredNodes(const wxTreeItemId &parentItem, EmberCore::ITreeNode *node) {
131 if (!node)
132 return;
133
134 for (size_t i = 0; i < node->GetChildCount(); ++i) {
135 EmberCore::ITreeNode *child = node->GetChild(i);
136 if (!child)
137 continue;
138
139 wxString nodeName = wxString(child->GetName().c_str()).Lower();
140 wxString searchLower = m_searchFilter.Lower();
141 if (nodeName.Find(searchLower) == wxNOT_FOUND)
142 continue;
143
144 wxTreeItemId childItem = m_treeCtrl->AppendItem(parentItem, GetNodeDisplayText(child));
145 m_treeCtrl->SetItemData(childItem, new TreeNodeItemData(child));
146
147 AddFilteredNodes(childItem, child);
148 }
149}
150
151void TreeHierarchyTab::AddChildrenToItem(const wxTreeItemId &parentItem, EmberCore::ITreeNode *node) {
152 if (!node)
153 return;
154
155 for (size_t i = 0; i < node->GetChildCount(); ++i) {
156 EmberCore::ITreeNode *child = node->GetChild(i);
157 if (!child)
158 continue;
159
160 wxTreeItemId childItem = m_treeCtrl->AppendItem(parentItem, GetNodeDisplayText(child));
161 m_treeCtrl->SetItemData(childItem, new TreeNodeItemData(child));
162
163 if (child->HasChildren()) {
164 m_treeCtrl->AppendItem(childItem, "", -1, -1, new DummyTreeItemData());
165 }
166 }
167}
168
169void TreeHierarchyTab::ExpandTreeToDepth(const wxTreeItemId &item, int remainingDepth) {
170 if (remainingDepth <= 0 || !item.IsOk())
171 return;
172
173 if (!m_treeCtrl->ItemHasChildren(item))
174 return;
175
176 wxTreeItemIdValue checkCookie;
177 wxTreeItemId firstChild = m_treeCtrl->GetFirstChild(item, checkCookie);
178 if (firstChild.IsOk()) {
179 wxTreeItemData *data = m_treeCtrl->GetItemData(firstChild);
180 if (dynamic_cast<DummyTreeItemData *>(data)) {
182 if (node) {
183 m_treeCtrl->DeleteChildren(item);
184 AddChildrenToItem(item, node);
185 }
186 }
187 }
188
189 m_treeCtrl->Expand(item);
190
191 wxTreeItemIdValue cookie;
192 wxTreeItemId child = m_treeCtrl->GetFirstChild(item, cookie);
193 while (child.IsOk()) {
194 ExpandTreeToDepth(child, remainingDepth - 1);
195 child = m_treeCtrl->GetNextChild(item, cookie);
196 }
197}
198
199void TreeHierarchyTab::ExpandTreeByVisibility(const wxTreeItemId &item) {
200 if (!item.IsOk() || !m_treeCtrl->ItemHasChildren(item))
201 return;
202
204 if (!node || !node->AreChildrenVisible())
205 return;
206
207 wxTreeItemIdValue checkCookie;
208 wxTreeItemId firstChild = m_treeCtrl->GetFirstChild(item, checkCookie);
209 if (firstChild.IsOk()) {
210 wxTreeItemData *data = m_treeCtrl->GetItemData(firstChild);
211 if (dynamic_cast<DummyTreeItemData *>(data)) {
212 m_treeCtrl->DeleteChildren(item);
213 AddChildrenToItem(item, node);
214 }
215 }
216
217 m_treeCtrl->Expand(item);
218
219 wxTreeItemIdValue cookie;
220 wxTreeItemId child = m_treeCtrl->GetFirstChild(item, cookie);
221 while (child.IsOk()) {
223 child = m_treeCtrl->GetNextChild(item, cookie);
224 }
225}
226
228
230
231// --- Selection ---
232
234 wxArrayTreeItemIds selections;
235 size_t count = m_treeCtrl->GetSelections(selections);
236 if (count > 0) {
237 return GetNodeFromItem(selections[0]);
238 }
239 return nullptr;
240}
241
243 if (!m_config.syncSelectionWithCanvas)
244 return;
245
246 wxTreeItemId item = FindItemById(nodeId);
247
248 if (!item.IsOk()) {
249 EmberCore::ITreeNode *node = m_tree ? m_tree->FindNodeById(nodeId) : nullptr;
250 if (node) {
251 item = ExpandPathToNode(node);
252 }
253 }
254
255 if (item.IsOk()) {
256 m_treeCtrl->UnselectAll();
257 m_treeCtrl->SelectItem(item);
259 }
260}
261
262// --- Virtual hooks (default implementations) ---
263
265 if (!node)
266 return "Unknown Node";
267 return node->GetName();
268}
269
275
277 if (node) {
278 node->SetChildrenVisible(true);
279 }
280}
281
283 if (node) {
284 node->SetChildrenVisible(false);
285 }
286}
287
288// --- Utility ---
289
291 if (!item.IsOk())
292 return nullptr;
293
294 TreeNodeItemData *data = dynamic_cast<TreeNodeItemData *>(m_treeCtrl->GetItemData(item));
295 if (!data)
296 return nullptr;
297
298 return data->node;
299}
300
301wxTreeItemId TreeHierarchyTab::FindItemById(size_t nodeId, const wxTreeItemId &start) {
302 wxTreeItemId searchStart = start.IsOk() ? start : m_treeCtrl->GetRootItem();
303 if (!searchStart.IsOk())
304 return wxTreeItemId();
305
306 EmberCore::ITreeNode *currentNode = GetNodeFromItem(searchStart);
307 if (currentNode && currentNode->GetId() == nodeId) {
308 return searchStart;
309 }
310
311 wxTreeItemIdValue cookie;
312 wxTreeItemId child = m_treeCtrl->GetFirstChild(searchStart, cookie);
313 while (child.IsOk()) {
314 wxTreeItemId found = FindItemById(nodeId, child);
315 if (found.IsOk())
316 return found;
317 child = m_treeCtrl->GetNextChild(searchStart, cookie);
318 }
319
320 return wxTreeItemId();
321}
322
324 if (!node || !m_tree || !m_tree->HasRootNode())
325 return wxTreeItemId();
326
327 // Build path from root to target
328 std::vector<EmberCore::ITreeNode *> path;
329 EmberCore::ITreeNode *current = node;
330 while (current) {
331 path.push_back(current);
332 current = current->GetParent();
333 }
334 std::reverse(path.begin(), path.end());
335
336 wxTreeItemId currentItem = m_treeCtrl->GetRootItem();
337 if (!currentItem.IsOk())
338 return wxTreeItemId();
339
340 EmberCore::ITreeNode *rootNode = GetNodeFromItem(currentItem);
341 if (!rootNode || rootNode->GetId() != path[0]->GetId())
342 return wxTreeItemId();
343
344 for (size_t i = 1; i < path.size(); ++i) {
345 // Perform lazy loading if needed
346 wxTreeItemIdValue checkCookie;
347 wxTreeItemId firstChild = m_treeCtrl->GetFirstChild(currentItem, checkCookie);
348 if (firstChild.IsOk() && dynamic_cast<DummyTreeItemData *>(m_treeCtrl->GetItemData(firstChild))) {
349 EmberCore::ITreeNode *parentNode = GetNodeFromItem(currentItem);
350 if (parentNode) {
351 m_treeCtrl->DeleteChildren(currentItem);
352 AddChildrenToItem(currentItem, parentNode);
353 }
354 }
355
356 if (!m_treeCtrl->IsExpanded(currentItem) && m_treeCtrl->ItemHasChildren(currentItem)) {
357 m_treeCtrl->Expand(currentItem);
358 }
359
360 wxTreeItemIdValue cookie;
361 wxTreeItemId child = m_treeCtrl->GetFirstChild(currentItem, cookie);
362 bool found = false;
363 while (child.IsOk()) {
364 EmberCore::ITreeNode *childNode = GetNodeFromItem(child);
365 if (childNode && childNode->GetId() == path[i]->GetId()) {
366 currentItem = child;
367 found = true;
368 break;
369 }
370 child = m_treeCtrl->GetNextChild(currentItem, cookie);
371 }
372 if (!found)
373 return wxTreeItemId();
374 }
375
376 return currentItem;
377}
378
379void TreeHierarchyTab::ScrollToItemHorizontally(const wxTreeItemId &item) {
380 if (!item.IsOk())
381 return;
382
383 m_treeCtrl->EnsureVisible(item);
384
385 wxRect itemRect;
386 if (!m_treeCtrl->GetBoundingRect(item, itemRect, true))
387 return;
388
389 int clientWidth = m_treeCtrl->GetClientSize().GetWidth();
390 int margin = 8;
391
392 if (itemRect.x < margin) {
393 int pos = m_treeCtrl->GetScrollPos(wxHORIZONTAL);
394 m_treeCtrl->SetScrollPos(wxHORIZONTAL, std::max(0, pos - (margin - itemRect.x)), true);
395 m_treeCtrl->Refresh();
396 } else if (itemRect.x > clientWidth - 100) {
397 int pos = m_treeCtrl->GetScrollPos(wxHORIZONTAL);
398 m_treeCtrl->SetScrollPos(wxHORIZONTAL, pos + (itemRect.x - margin), true);
399 m_treeCtrl->Refresh();
400 }
401}
402
403void TreeHierarchyTab::SelectRange(const wxTreeItemId &from, const wxTreeItemId &to) {
404 if (!from.IsOk() || !to.IsOk())
405 return;
406
407 std::vector<wxTreeItemId> allItems;
408 std::function<void(const wxTreeItemId &)> collectItems = [&](const wxTreeItemId &item) {
409 if (!item.IsOk())
410 return;
411 allItems.push_back(item);
412 if (m_treeCtrl->IsExpanded(item)) {
413 wxTreeItemIdValue cookie;
414 wxTreeItemId child = m_treeCtrl->GetFirstChild(item, cookie);
415 while (child.IsOk()) {
416 collectItems(child);
417 child = m_treeCtrl->GetNextChild(item, cookie);
418 }
419 }
420 };
421
422 wxTreeItemId root = m_treeCtrl->GetRootItem();
423 if (root.IsOk()) {
424 collectItems(root);
425 }
426
427 int fromIdx = -1, toIdx = -1;
428 for (size_t i = 0; i < allItems.size(); ++i) {
429 if (allItems[i] == from)
430 fromIdx = static_cast<int>(i);
431 if (allItems[i] == to)
432 toIdx = static_cast<int>(i);
433 }
434
435 if (fromIdx < 0 || toIdx < 0)
436 return;
437
438 if (fromIdx > toIdx)
439 std::swap(fromIdx, toIdx);
440
441 m_treeCtrl->UnselectAll();
442 for (int i = fromIdx; i <= toIdx; ++i) {
443 m_treeCtrl->SelectItem(allItems[i], true);
444 }
445
447}
448
449// --- Event handlers ---
450
452 wxTreeItemId item = event.GetItem();
454 if (!node) {
455 event.Skip();
456 return;
457 }
458
459 if (!m_isRefreshing)
460 OnNodeExpanding(node);
461
462 wxTreeItemIdValue cookie;
463 wxTreeItemId firstChild = m_treeCtrl->GetFirstChild(item, cookie);
464 if (firstChild.IsOk()) {
465 wxTreeItemData *data = m_treeCtrl->GetItemData(firstChild);
466 if (dynamic_cast<DummyTreeItemData *>(data)) {
467 m_treeCtrl->Freeze();
468 m_treeCtrl->DeleteChildren(item);
469 AddChildrenToItem(item, node);
470 m_treeCtrl->Thaw();
471 }
472 }
473
476
477 event.Skip();
478}
479
481 EmberCore::ITreeNode *node = GetNodeFromItem(event.GetItem());
482
483 if (!m_isRefreshing)
484 OnNodeCollapsing(node);
485
488
489 event.Skip();
490}
491
493 wxTreeItemId selectedItem = event.GetItem();
494 if (!selectedItem.IsOk()) {
495 OnSelectionChanged(nullptr);
496 event.Skip();
497 return;
498 }
499
500 EmberCore::ITreeNode *node = GetNodeFromItem(selectedItem);
501 if (node) {
503 if (m_config.syncSelectionWithCanvas) {
504 ScrollToItemHorizontally(selectedItem);
505 }
506 }
507 OnSelectionChanged(node);
508 event.Skip();
509}
510
512 EmberCore::ITreeNode *node = GetNodeFromItem(event.GetItem());
513 if (!node)
514 return;
515
516 m_treeCtrl->SelectItem(event.GetItem());
517
518 wxMenu contextMenu;
519 OnPopulateContextMenu(node, contextMenu);
520
521 if (contextMenu.GetMenuItemCount() > 0) {
522 contextMenu.Bind(wxEVT_COMMAND_MENU_SELECTED,
523 [this, node](wxCommandEvent &cmd) { OnContextMenuCommand(cmd.GetId(), node); });
524 PopupMenu(&contextMenu);
525 }
526}
527
529 EmberCore::ITreeNode *node = GetNodeFromItem(event.GetItem());
530 if (node) {
531 OnNodeActivated(node);
532 }
533}
534
535void TreeHierarchyTab::OnSearchTextChanged(wxCommandEvent &event) {
536 if (m_config.liveSearch) {
537 m_searchFilter = event.GetString();
538 PopulateTree();
539 }
540}
541
542void TreeHierarchyTab::OnSearchEnter(wxCommandEvent &event) {
543 m_searchFilter = m_searchCtrl->GetValue();
544 PopulateTree();
545}
546
547} // namespace EmberUI
BehaviorTreeProjectDialog::OnProjectNameChanged BehaviorTreeProjectDialog::OnRemoveFiles wxEND_EVENT_TABLE() BehaviorTreeProjectDialog
Abstract interface for tree nodes that can be visualized.
Definition ITreeNode.h:31
virtual size_t GetId() const =0
virtual size_t GetChildCount() const =0
virtual ITreeNode * GetParent() const =0
virtual bool AreChildrenVisible() const =0
virtual bool HasChildren() const
Definition ITreeNode.h:73
virtual void SetChildrenVisible(bool visible)=0
virtual const String & GetName() const =0
virtual ITreeNode * GetChild(size_t index) const =0
virtual void SetVisualState(VisualState state)=0
Base class for hierarchy tabs operating on ITreeNode.
EmberCore::ITreeNode * GetSelectedNode() const
Returns the currently selected node, or nullptr.
void SelectNodeById(size_t nodeId)
Selects the tree item with the given node ID.
wxTreeItemId ExpandPathToNode(EmberCore::ITreeNode *node)
Expands path from root to node and returns its item ID.
virtual void OnSelectionChanged(EmberCore::ITreeNode *node)
Called when selection changes; subclasses may override.
virtual void OnNodeActivated(EmberCore::ITreeNode *node)
Hook called when a node is double-clicked/activated.
SelectionCallback m_selectionCallback
void AddFilteredNodes(const wxTreeItemId &parentItem, EmberCore::ITreeNode *node)
void ExpandTreeToDepth(const wxTreeItemId &item, int remainingDepth)
virtual void OnContextMenuCommand(int id, EmberCore::ITreeNode *node)
Hook to handle context menu command.
void CollapseAll()
Collapses all tree items.
virtual wxString GetNodeDisplayText(EmberCore::ITreeNode *node) const
Returns display text for a node; subclasses may override.
virtual void OnNodeExpanding(EmberCore::ITreeNode *node)
Called when a node is expanding; used for lazy loading.
void RefreshTree()
Rebuilds the tree from the current structure.
void ScrollToItemHorizontally(const wxTreeItemId &item)
Scrolls horizontally so the item is visible.
void OnTreeItemCollapsing(wxTreeEvent &event)
void ClearTree()
Clears the tree and removes all items.
EmberCore::ITreeNode * GetNodeFromItem(const wxTreeItemId &item) const
Extracts ITreeNode from a tree item (or nullptr if DummyTreeItemData).
void ApplyConfig(const TreeHierarchyConfig &config)
Applies new configuration and refreshes the tree.
void OnTreeItemRightClick(wxTreeEvent &event)
void OnTreeItemExpanding(wxTreeEvent &event)
virtual void OnNodeCollapsing(EmberCore::ITreeNode *node)
Called when a node is collapsing.
LayoutInvalidationCallback m_layoutInvalidationCb
void OnSearchTextChanged(wxCommandEvent &event)
void ExpandTreeByVisibility(const wxTreeItemId &item)
wxTreeItemId FindItemById(size_t nodeId, const wxTreeItemId &start=wxTreeItemId())
Finds tree item by node ID, optionally starting from given item.
virtual void OnPopulateContextMenu(EmberCore::ITreeNode *node, wxMenu &menu)
Hook to populate context menu for a node.
std::shared_ptr< EmberCore::ITreeStructure > m_tree
TreeHierarchyConfig m_config
void ExpandAll()
Expands all tree items.
void OnSearchEnter(wxCommandEvent &event)
void AddChildrenToItem(const wxTreeItemId &parentItem, EmberCore::ITreeNode *node)
void OnTreeItemActivated(wxTreeEvent &event)
void SelectRange(const wxTreeItemId &from, const wxTreeItemId &to)
Selects a range of items from from to to.
void OnTreeSelectionChanged(wxTreeEvent &event)
void SetTree(std::shared_ptr< EmberCore::ITreeStructure > tree)
Sets the tree structure to display.
Standard layout constants for consistent UI spacing and sizing.
Definition Panel.h:8
wxBEGIN_EVENT_TABLE(Panel, wxPanel) EVT_SIZE(Panel
Definition Panel.cpp:8
Placeholder tree item data for lazy loading (children not yet populated).
Configuration for TreeHierarchyTab appearance and behavior.
wxTreeItemData storing a pointer to ITreeNode.
EmberCore::ITreeNode * node