Ember
Loading...
Searching...
No Matches
MainPanel.cpp
Go to the documentation of this file.
1#include "Panels/MainPanel.h"
2#include "Core/Node.h"
7#include "Utils/Logger.h"
9#include <wx/artprov.h>
10#include <wx/filename.h>
11#include <wx/menu.h>
12#include <wx/statline.h>
13#include <wx/textdlg.h>
14
15wxBEGIN_EVENT_TABLE(MainPanel, EmberUI::Panel) EVT_AUINOTEBOOK_PAGE_CHANGED(ID_SCENE_NOTEBOOK, MainPanel::OnTabChanged)
16 EVT_AUINOTEBOOK_PAGE_CLOSE(ID_SCENE_NOTEBOOK, MainPanel::OnTabClosed)
17 EVT_BUTTON(ID_NEW_SCENE_BUTTON, MainPanel::OnNewSceneButton)
18 EVT_BUTTON(ID_OVERLAY_BUTTON, MainPanel::OnOverlayButton) wxEND_EVENT_TABLE()
19
20 MainPanel::MainPanel(wxWindow *parent)
21 : EmberUI::Panel(parent, "MainPanel"), m_activeSceneIndex(-1), m_nextSceneNumber(1) {
22 SetTitle("Scene");
23 SetPanelType("Scene");
24 SetBackgroundColour(wxColour(50, 50, 50)); // Dark like most editors
25 DoCreateLayout(); // Non-virtual: safe to call from constructor
26}
27
29 DoCreateLayout(); // Virtual dispatch works when called at runtime (e.g. from Panel::Initialize)
30}
31
33 wxBoxSizer *sizer = new wxBoxSizer(wxVERTICAL);
34
36
37 m_book = new wxSimplebook(this, wxID_ANY);
38
40 m_book->AddPage(m_welcomePanel, "Welcome");
41
42 auto *scenePage = new wxPanel(m_book, wxID_ANY);
43 scenePage->SetBackgroundColour(wxColour(50, 50, 50));
44 auto *sceneSizer = new wxBoxSizer(wxVERTICAL);
45
46 m_sceneNotebook = new wxAuiNotebook(scenePage, ID_SCENE_NOTEBOOK, wxDefaultPosition, wxDefaultSize,
47 wxAUI_NB_DEFAULT_STYLE | wxAUI_NB_CLOSE_ON_ACTIVE_TAB);
48 m_sceneNotebook->SetBackgroundColour(wxColour(50, 50, 50));
49 m_sceneNotebook->SetArtProvider(new EmberForge::CustomTabArt());
50
51 sceneSizer->Add(m_sceneNotebook, 1, wxEXPAND);
52 scenePage->SetSizer(sceneSizer);
53
54 m_book->AddPage(scenePage, "Scenes");
55
56 sizer->Add(m_toolbar, 0, wxEXPAND | wxALL, 2);
57 sizer->Add(m_book, 1, wxEXPAND | wxALL, 5);
58
59 SetSizer(sizer);
60
62}
63
65 const wxColour bgColour(42, 42, 42);
66 const wxColour btnNormal(60, 60, 60);
67 const wxColour btnHover(80, 80, 80);
68 const wxColour btnPressed(45, 45, 45);
69 const wxColour entryNormal(50, 50, 50);
70 const wxColour entryHover(65, 65, 65);
71 const wxColour entryPressed(40, 40, 40);
72
73 auto *panel = new wxPanel(m_book, wxID_ANY);
74 panel->SetBackgroundColour(bgColour);
75
76 auto *rootSizer = new wxBoxSizer(wxVERTICAL);
77 rootSizer->AddStretchSpacer(1);
78
79 auto *centerSizer = new wxBoxSizer(wxVERTICAL);
80
81 // --- Action Buttons ---
82 auto bindButtonHighlight = [=](wxButton *btn) {
83 btn->Bind(wxEVT_ENTER_WINDOW, [btn, btnHover](wxMouseEvent &e) {
84 btn->SetBackgroundColour(btnHover);
85 btn->Refresh();
86 e.Skip();
87 });
88 btn->Bind(wxEVT_LEAVE_WINDOW, [btn, btnNormal](wxMouseEvent &e) {
89 btn->SetBackgroundColour(btnNormal);
90 btn->Refresh();
91 e.Skip();
92 });
93 btn->Bind(wxEVT_LEFT_DOWN, [btn, btnPressed](wxMouseEvent &e) {
94 btn->SetBackgroundColour(btnPressed);
95 btn->Refresh();
96 e.Skip();
97 });
98 btn->Bind(wxEVT_LEFT_UP, [btn, btnHover](wxMouseEvent &e) {
99 btn->SetBackgroundColour(btnHover);
100 btn->Refresh();
101 e.Skip();
102 });
103 };
104
105 auto makeButton = [&](wxPanel *parent, const wxString &label) -> wxButton * {
106 auto *btn = new wxButton(parent, wxID_ANY, label, wxDefaultPosition, wxSize(220, 36));
107 btn->SetBackgroundColour(btnNormal);
108 btn->SetForegroundColour(wxColour(210, 210, 210));
109 btn->SetFont(wxFont(11, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL));
110 btn->SetCursor(wxCursor(wxCURSOR_HAND));
111 bindButtonHighlight(btn);
112 return btn;
113 };
114
115 auto *newProjectBtn = makeButton(panel, "New Project");
116 centerSizer->Add(newProjectBtn, 0, wxALIGN_CENTER_HORIZONTAL);
117
118 auto *openProjectBtn = makeButton(panel, "Open Project");
119 centerSizer->Add(openProjectBtn, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, 6);
120
121 auto *docsBtn = makeButton(panel, "Documentation");
122 centerSizer->Add(docsBtn, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, 6);
123
124 newProjectBtn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) {
126 m_welcomeActionCallback("new_project", "");
127 });
128 openProjectBtn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) {
130 m_welcomeActionCallback("open_project", "");
131 });
132 docsBtn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) {
134 m_welcomeActionCallback("docs", "");
135 });
136
137 centerSizer->AddSpacer(30);
138
139 // --- Recent Projects ---
140 auto *recentHeader = new wxStaticText(panel, wxID_ANY, "Recent Projects");
141 recentHeader->SetForegroundColour(wxColour(180, 180, 180));
142 recentHeader->SetFont(wxFont(12, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD));
143 centerSizer->Add(recentHeader, 0, wxALIGN_CENTER_HORIZONTAL);
144
145 auto *line = new wxStaticLine(panel, wxID_ANY, wxDefaultPosition, wxSize(300, 1));
146 centerSizer->Add(line, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP | wxBOTTOM, 6);
147
148 const auto &recentProjects = EmberCore::ProjectManager::GetInstance().GetRecentProjects();
149
150 if (recentProjects.empty()) {
151 auto *emptyLabel = new wxStaticText(panel, wxID_ANY, "No recent projects");
152 emptyLabel->SetForegroundColour(wxColour(100, 100, 100));
153 emptyLabel->SetFont(wxFont(10, wxFONTFAMILY_SWISS, wxFONTSTYLE_ITALIC, wxFONTWEIGHT_NORMAL));
154 centerSizer->Add(emptyLabel, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, 4);
155 } else {
156 auto bindEntryHighlight = [=](wxPanel *ep) {
157 ep->Bind(wxEVT_ENTER_WINDOW, [ep, entryHover](wxMouseEvent &e) {
158 ep->SetBackgroundColour(entryHover);
159 ep->Refresh();
160 e.Skip();
161 });
162 ep->Bind(wxEVT_LEAVE_WINDOW, [ep, entryNormal](wxMouseEvent &e) {
163 ep->SetBackgroundColour(entryNormal);
164 ep->Refresh();
165 e.Skip();
166 });
167 ep->Bind(wxEVT_LEFT_DOWN, [ep, entryPressed](wxMouseEvent &e) {
168 ep->SetBackgroundColour(entryPressed);
169 ep->Refresh();
170 e.Skip();
171 });
172 ep->Bind(wxEVT_LEFT_UP, [ep, entryHover](wxMouseEvent &e) {
173 ep->SetBackgroundColour(entryHover);
174 ep->Refresh();
175 e.Skip();
176 });
177 };
178
179 for (const auto &projectPath : recentProjects) {
180 wxFileName fn(wxString::FromUTF8(projectPath));
181 wxString projectName = fn.GetName();
182 wxString displayPath = fn.GetFullPath();
183
184 auto *entryPanel = new wxPanel(panel, wxID_ANY);
185 entryPanel->SetBackgroundColour(entryNormal);
186 entryPanel->SetMinSize(wxSize(400, -1));
187
188 auto *entrySizer = new wxBoxSizer(wxVERTICAL);
189
190 auto *nameLabel = new wxStaticText(entryPanel, wxID_ANY, projectName);
191 nameLabel->SetForegroundColour(wxColour(100, 180, 255));
192 nameLabel->SetFont(wxFont(11, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL));
193 entrySizer->Add(nameLabel, 0, wxLEFT | wxTOP, 6);
194
195 auto *pathLabel = new wxStaticText(entryPanel, wxID_ANY, displayPath);
196 pathLabel->SetForegroundColour(wxColour(120, 120, 120));
197 pathLabel->SetFont(wxFont(8, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL));
198 entrySizer->Add(pathLabel, 0, wxLEFT | wxBOTTOM, 6);
199
200 entryPanel->SetSizer(entrySizer);
201
202 std::string path = projectPath;
203 entryPanel->SetCursor(wxCursor(wxCURSOR_HAND));
204 nameLabel->SetCursor(wxCursor(wxCURSOR_HAND));
205 pathLabel->SetCursor(wxCursor(wxCURSOR_HAND));
206
207 bindEntryHighlight(entryPanel);
208
209 auto clickHandler = [this, path](wxMouseEvent &e) {
211 m_welcomeActionCallback("open_recent", path);
212 e.Skip();
213 };
214 entryPanel->Bind(wxEVT_LEFT_UP, clickHandler);
215 nameLabel->Bind(wxEVT_LEFT_UP, clickHandler);
216 pathLabel->Bind(wxEVT_LEFT_UP, clickHandler);
217
218 // Forward mouse enter/leave from child labels so the panel highlights properly
219 nameLabel->Bind(wxEVT_ENTER_WINDOW, [entryPanel, entryHover](wxMouseEvent &e) {
220 entryPanel->SetBackgroundColour(entryHover);
221 entryPanel->Refresh();
222 e.Skip();
223 });
224 nameLabel->Bind(wxEVT_LEAVE_WINDOW, [entryPanel, entryNormal](wxMouseEvent &e) {
225 wxPoint pos = entryPanel->ScreenToClient(wxGetMousePosition());
226 if (!entryPanel->GetRect().Contains(entryPanel->GetPosition() + pos))
227 entryPanel->SetBackgroundColour(entryNormal);
228 entryPanel->Refresh();
229 e.Skip();
230 });
231 pathLabel->Bind(wxEVT_ENTER_WINDOW, [entryPanel, entryHover](wxMouseEvent &e) {
232 entryPanel->SetBackgroundColour(entryHover);
233 entryPanel->Refresh();
234 e.Skip();
235 });
236 pathLabel->Bind(wxEVT_LEAVE_WINDOW, [entryPanel, entryNormal](wxMouseEvent &e) {
237 wxPoint pos = entryPanel->ScreenToClient(wxGetMousePosition());
238 if (!entryPanel->GetRect().Contains(entryPanel->GetPosition() + pos))
239 entryPanel->SetBackgroundColour(entryNormal);
240 entryPanel->Refresh();
241 e.Skip();
242 });
243
244 centerSizer->Add(entryPanel, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, 4);
245 }
246 }
247
248 rootSizer->Add(centerSizer, 0, wxALIGN_CENTER_HORIZONTAL);
249 rootSizer->AddStretchSpacer(1);
250
251 // Version label anchored bottom-right
252 auto *versionLabel = new wxStaticText(panel, wxID_ANY, "v1.0.0");
253 versionLabel->SetForegroundColour(wxColour(100, 100, 100));
254 versionLabel->SetFont(wxFont(9, wxFONTFAMILY_SWISS, wxFONTSTYLE_ITALIC, wxFONTWEIGHT_NORMAL));
255 rootSizer->Add(versionLabel, 0, wxALIGN_RIGHT | wxRIGHT | wxBOTTOM, 10);
256
257 panel->SetSizer(rootSizer);
258 return panel;
259}
260
262 if (m_book) {
263 m_book->SetSelection(0);
264 }
265 if (m_toolbar) {
266 m_toolbar->Hide();
267 Layout();
268 }
269}
270
272 if (m_book) {
273 m_book->SetSelection(1);
274 }
275 if (m_toolbar) {
276 m_toolbar->Show();
277 Layout();
278 }
279}
280
282 DoCreateToolbar(); // Virtual dispatch works when called at runtime
283}
284
286 m_toolbar =
287 new wxToolBar(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTB_HORIZONTAL | wxTB_FLAT | wxTB_NODIVIDER);
288 m_toolbar->SetBackgroundColour(wxColour(60, 60, 60));
289 m_toolbar->SetToolBitmapSize(wxSize(22, 22));
290
291 wxString resPath = EmberForge::ResourcePath::GetDir("buttons");
292
293 wxBitmap newNormal(resPath + "new_scene/NewScene_Normal_22.png", wxBITMAP_TYPE_PNG);
294 wxBitmap newHovered(resPath + "new_scene/NewScene_Hovered_22.png", wxBITMAP_TYPE_PNG);
295 wxBitmap newPressed(resPath + "new_scene/NewScene_Pressed_22.png", wxBITMAP_TYPE_PNG);
296 wxBitmap newDisabled(resPath + "new_scene/NewScene_Disabled_22.png", wxBITMAP_TYPE_PNG);
297
298 auto *newSceneBtn = new wxBitmapButton(m_toolbar, ID_NEW_SCENE_BUTTON, newNormal, wxDefaultPosition, wxSize(32, 32),
299 wxBORDER_NONE | wxBU_EXACTFIT);
300 newSceneBtn->SetBitmapHover(newHovered);
301 newSceneBtn->SetBitmapPressed(newPressed);
302 newSceneBtn->SetBitmapDisabled(newDisabled);
303 newSceneBtn->SetToolTip("Create a new empty behavior tree scene");
304 m_toolbar->AddControl(newSceneBtn);
305
306 wxBitmap ovNormal(resPath + "overlay/Overlay_Normal_22.png", wxBITMAP_TYPE_PNG);
307 wxBitmap ovHovered(resPath + "overlay/Overlay_Hovered_22.png", wxBITMAP_TYPE_PNG);
308 wxBitmap ovPressed(resPath + "overlay/Overlay_Pressed_22.png", wxBITMAP_TYPE_PNG);
309 wxBitmap ovDisabled(resPath + "overlay/Overlay_Disabled_22.png", wxBITMAP_TYPE_PNG);
310
311 auto *overlayBtn = new wxBitmapButton(m_toolbar, ID_OVERLAY_BUTTON, ovPressed, wxDefaultPosition, wxSize(32, 32),
312 wxBORDER_NONE | wxBU_EXACTFIT);
313 overlayBtn->SetBitmapHover(ovHovered);
314 overlayBtn->SetBitmapPressed(ovPressed);
315 overlayBtn->SetBitmapDisabled(ovDisabled);
316 overlayBtn->SetToolTip("Overlay settings");
317 m_toolbar->AddControl(overlayBtn);
318
319 m_toolbar->Realize();
320
322}
323
324int MainPanel::AddScene(std::unique_ptr<IScene> scene) {
325 if (!scene)
326 return -1;
327
328 // Check scene limit from preferences
330 int maxScenes = prefs.GetMainPanelSettings().maxScenes;
331
332 if (maxScenes > 0 && static_cast<int>(m_scenes.size()) >= maxScenes) {
333 wxMessageBox(
334 wxString::Format("Maximum number of scenes (%d) reached.\nClose some scenes before creating new ones.",
335 maxScenes),
336 "Scene Limit Reached", wxOK | wxICON_WARNING);
337 return -1;
338 }
339
340 // Add the scene's panel to the notebook
341 int index = m_sceneNotebook->GetPageCount();
342 m_sceneNotebook->AddPage(scene->GetPanel(), scene->GetTitle(), true); // true = make active
343
344 // Store the scene
345 m_scenes.push_back(std::move(scene));
346
347 // If this is the first scene, make it active
348 if (m_scenes.size() == 1) {
350 m_scenes[0]->OnActivated();
351 } else {
352 // Update active scene index to the new scene
353 m_activeSceneIndex = index;
354 m_scenes[m_activeSceneIndex]->OnActivated();
355 }
356
357 return index;
358}
359
360int MainPanel::InsertScene(int insertIdx, std::unique_ptr<IScene> scene) {
361 if (!scene)
362 return -1;
363
364 if (insertIdx < 0)
365 insertIdx = 0;
366 if (insertIdx > static_cast<int>(m_scenes.size()))
367 insertIdx = static_cast<int>(m_scenes.size());
368
369 m_sceneNotebook->InsertPage(insertIdx, scene->GetPanel(), scene->GetTitle(), false);
370 m_scenes.insert(m_scenes.begin() + insertIdx, std::move(scene));
371
372 if (m_activeSceneIndex >= insertIdx)
374
375 return insertIdx;
376}
377
378bool MainPanel::RemoveScene(int index, bool force) {
379 if (index < 0 || index >= static_cast<int>(m_scenes.size())) {
380 return false;
381 }
382
383 if (!force && m_scenes[index]->HasUnsavedChanges()) {
384 wxMessageDialog dialog(
385 this, wxString::Format("Scene '%s' has unsaved changes. Close anyway?", m_scenes[index]->GetTitle()),
386 "Unsaved Changes", wxYES_NO | wxICON_QUESTION);
387 if (dialog.ShowModal() != wxID_YES) {
388 return false;
389 }
390 }
391
392 std::unique_ptr<IScene> sceneToRemove = std::move(m_scenes[index]);
393
394 if (index == m_activeSceneIndex) {
395 sceneToRemove->OnDeactivated();
396 }
397
398 m_scenes.erase(m_scenes.begin() + index);
399
400 if (m_scenes.empty()) {
402 } else if (index == m_activeSceneIndex) {
403 if (index >= static_cast<int>(m_scenes.size())) {
404 m_activeSceneIndex = static_cast<int>(m_scenes.size()) - 1;
405 } else {
406 m_activeSceneIndex = index;
407 }
408 if (m_activeSceneIndex >= 0) {
409 m_scenes[m_activeSceneIndex]->OnActivated();
410 }
411 } else if (index < m_activeSceneIndex) {
413 }
414
415 // Destroy the scene object FIRST while its child widgets are still alive,
416 // so the destructor can safely clear callbacks on those widgets.
417 sceneToRemove.reset();
418
419 // Now remove the notebook page, which destroys the (already-cleaned-up) widgets.
420 m_sceneNotebook->DeletePage(index);
421
422 return true;
423}
424
426 if (m_activeSceneIndex >= 0 && m_activeSceneIndex < static_cast<int>(m_scenes.size())) {
427 return m_scenes[m_activeSceneIndex].get();
428 }
429 return nullptr;
430}
431
432IScene *MainPanel::GetScene(int index) const {
433 if (index >= 0 && index < static_cast<int>(m_scenes.size())) {
434 return m_scenes[index].get();
435 }
436 return nullptr;
437}
438
440 if (index < 0 || index >= static_cast<int>(m_scenes.size())) {
441 return false;
442 }
443
444 if (index == m_activeSceneIndex) {
445 return true; // Already active
446 }
447
448 // Deactivate current scene
449 if (m_activeSceneIndex >= 0 && m_activeSceneIndex < static_cast<int>(m_scenes.size())) {
450 m_scenes[m_activeSceneIndex]->OnDeactivated();
451 }
452
453 // Set new active scene
454 m_activeSceneIndex = index;
455 m_sceneNotebook->SetSelection(index);
456 m_scenes[m_activeSceneIndex]->OnActivated();
457
458 return true;
459}
460
462 // Create a new empty behavior tree scene
463 wxString title = GenerateSceneTitle();
464 auto newScene = std::make_unique<BehaviorTreeScene>(m_sceneNotebook, title);
465
466 // Add the scene to the panel
467 int index = AddScene(std::move(newScene));
468
469 // Only log if scene was successfully added
470 if (index != -1) {
471 LOG_INFO("MainPanel", "Created new scene: " + std::string(title.mb_str()));
472 }
473
474 return index;
475}
476
478 wxString title;
479 bool titleExists = false;
480
481 do {
482 title = wxString::Format("Scene %d", m_nextSceneNumber);
483 titleExists = false;
484
485 // Check if title already exists
486 for (const auto &scene : m_scenes) {
487 if (scene->GetTitle() == title) {
488 titleExists = true;
489 break;
490 }
491 }
492
493 if (titleExists) {
494 const_cast<MainPanel *>(this)->m_nextSceneNumber++;
495 }
496 } while (titleExists);
497
498 const_cast<MainPanel *>(this)->m_nextSceneNumber++;
499 return title;
500}
501
502bool MainPanel::SceneTitleExists(const wxString &title) const {
503 for (const auto &scene : m_scenes) {
504 if (scene->GetTitle() == title) {
505 return true;
506 }
507 }
508 return false;
509}
510
511int MainPanel::CreateNewScene(const wxString &customTitle) {
512 // Create a new empty behavior tree scene with custom title
513 auto newScene = std::make_unique<BehaviorTreeScene>(m_sceneNotebook, customTitle);
514
515 // Add the scene to the panel
516 int index = AddScene(std::move(newScene));
517
518 // Only log if scene was successfully added
519 if (index != -1) {
520 LOG_INFO("MainPanel", "Created new scene: " + std::string(customTitle.mb_str()));
521 }
522
523 return index;
524}
525
526void MainPanel::UpdateActiveSceneTitle(const wxString &newTitle) {
527 if (m_activeSceneIndex < 0 || m_activeSceneIndex >= static_cast<int>(m_scenes.size())) {
528 return;
529 }
530
531 // Update the scene's internal title
532 auto *behaviorTreeScene = dynamic_cast<BehaviorTreeScene *>(m_scenes[m_activeSceneIndex].get());
533 if (behaviorTreeScene) {
534 behaviorTreeScene->SetTitle(newTitle);
535 }
536
537 // Update the notebook tab text
538 m_sceneNotebook->SetPageText(m_activeSceneIndex, newTitle);
539
540 LOG_INFO("MainPanel", "Updated scene title to: " + std::string(newTitle.mb_str()));
541}
542
543void MainPanel::OnTabChanged(wxAuiNotebookEvent &event) {
544 if (m_isClosingTab) {
545 event.Skip();
546 return;
547 }
548
549 int newIndex = event.GetSelection();
550
551 // Deactivate current scene
552 if (m_activeSceneIndex >= 0 && m_activeSceneIndex < static_cast<int>(m_scenes.size())) {
553 m_scenes[m_activeSceneIndex]->OnDeactivated();
554 }
555
556 // Activate new scene
557 m_activeSceneIndex = newIndex;
558 IScene *newScene = nullptr;
559 if (m_activeSceneIndex >= 0 && m_activeSceneIndex < static_cast<int>(m_scenes.size())) {
560 m_scenes[m_activeSceneIndex]->OnActivated();
561 newScene = m_scenes[m_activeSceneIndex].get();
562 }
563
564 // Notify listeners about scene change
567 }
568
569 event.Skip();
570}
571
572void MainPanel::OnNewSceneButton(wxCommandEvent &event) {
573 // Show dialog to get scene name from user
574 wxTextEntryDialog dialog(this, "Enter a name for the new scene:", "New Scene", GenerateSceneTitle());
575
576 if (dialog.ShowModal() == wxID_OK) {
577 wxString sceneName = dialog.GetValue().Trim();
578
579 // Validate the scene name
580 if (sceneName.IsEmpty()) {
581 wxMessageBox("Scene name cannot be empty!", "Invalid Name", wxOK | wxICON_WARNING, this);
582 return;
583 }
584
585 // Check if name already exists
586 if (SceneTitleExists(sceneName)) {
587 wxMessageBox(
588 wxString::Format("A scene named '%s' already exists!\nPlease choose a different name.", sceneName),
589 "Duplicate Name", wxOK | wxICON_WARNING, this);
590 return;
591 }
592
593 // Create the scene with the custom name
594 CreateNewScene(sceneName);
595 }
596}
597
598void MainPanel::OnTabClosed(wxAuiNotebookEvent &event) {
599 int index = event.GetSelection();
600
601 if (index < 0 || index >= static_cast<int>(m_scenes.size())) {
602 wxLogError("Invalid tab close index: %d (have %zu scenes)", index, m_scenes.size());
603 event.Veto();
604 return;
605 }
606
607 if (!m_scenes[index]->IsClosable()) {
608 event.Veto();
609 return;
610 }
611
613 auto closeConfirmMode = prefs.GetMainPanelSettings().closeConfirmation;
614
615 bool shouldAsk = false;
616 wxString message;
617
619 shouldAsk = true;
620 if (m_scenes[index]->HasUnsavedChanges()) {
621 message = wxString::Format("Scene '%s' has unsaved changes. Close anyway?", m_scenes[index]->GetTitle());
622 } else {
623 message = wxString::Format("Close scene '%s'?", m_scenes[index]->GetTitle());
624 }
626 if (m_scenes[index]->HasUnsavedChanges()) {
627 shouldAsk = true;
628 message = wxString::Format("Scene '%s' has unsaved changes. Close anyway?", m_scenes[index]->GetTitle());
629 }
630 }
631
632 if (shouldAsk) {
633 wxMessageDialog dialog(this, message, "Close Scene", wxYES_NO | wxICON_QUESTION);
634 if (dialog.ShowModal() != wxID_YES) {
635 event.Veto();
636 return;
637 }
638 }
639
640 m_isClosingTab = true;
641
642 std::unique_ptr<IScene> sceneToCleanup = std::move(m_scenes[index]);
643
644 if (index == m_activeSceneIndex) {
645 sceneToCleanup->OnDeactivated();
646 }
647
648 m_scenes.erase(m_scenes.begin() + index);
649
650 if (m_scenes.empty()) {
652 } else if (index == m_activeSceneIndex) {
653 if (index >= static_cast<int>(m_scenes.size())) {
654 m_activeSceneIndex = static_cast<int>(m_scenes.size()) - 1;
655 } else {
656 m_activeSceneIndex = index;
657 }
658 } else if (index < m_activeSceneIndex) {
660 }
661
662 sceneToCleanup.reset();
663
664 m_isClosingTab = false;
665
666 if (m_activeSceneIndex >= 0 && m_activeSceneIndex < static_cast<int>(m_scenes.size())) {
667 m_scenes[m_activeSceneIndex]->OnActivated();
670 }
671 } else if (m_sceneChangedCallback) {
672 m_sceneChangedCallback(nullptr, -1);
673 }
674}
675
676void MainPanel::ApplyCanvasBackgroundColor(const wxColour &color) {
677 // Apply to all existing scenes
678 for (auto &scene : m_scenes) {
679 if (scene) {
680 BehaviorTreeScene *btScene = dynamic_cast<BehaviorTreeScene *>(scene.get());
681 if (btScene) {
682 btScene->SetCanvasBackgroundColor(color);
683 }
684 }
685 }
686}
687
689 if (m_scenes.empty() || !m_sceneNotebook)
690 return;
691
692 int currentSelection = m_sceneNotebook->GetSelection();
693 int nextSelection = (currentSelection + 1) % m_sceneNotebook->GetPageCount();
694 m_sceneNotebook->SetSelection(nextSelection);
695}
696
698 if (m_scenes.empty() || !m_sceneNotebook)
699 return;
700
701 int currentSelection = m_sceneNotebook->GetSelection();
702 int pageCount = m_sceneNotebook->GetPageCount();
703 int prevSelection = (currentSelection - 1 + pageCount) % pageCount;
704 m_sceneNotebook->SetSelection(prevSelection);
705}
706
707void MainPanel::OnOverlayButton(wxCommandEvent &) {
709 auto &mainPanel = prefs.GetMainPanelSettings();
710 auto &btView = prefs.GetBehaviorTreeViewSettings();
711
712 wxMenu menu;
713
714 menu.Append(ID_OVERLAY_SHOW_ALL, "Show All");
715 menu.Append(ID_OVERLAY_HIDE_ALL, "Hide All");
716 menu.AppendSeparator();
717
718 menu.AppendCheckItem(ID_OVERLAY_GRID, "Show Grid")->Check(mainPanel.showGrid);
719 menu.AppendCheckItem(ID_OVERLAY_MINIMAP, "Show Minimap")->Check(mainPanel.showMinimap);
720 menu.AppendCheckItem(ID_OVERLAY_BREADCRUMB, "Show Breadcrumb")->Check(mainPanel.showCanvasBreadcrumb);
721 menu.AppendSeparator();
722
723 menu.AppendCheckItem(ID_OVERLAY_COORDINATE, "Show Coordinate Info")->Check(btView.showCoordinateInfo);
724 menu.AppendCheckItem(ID_OVERLAY_SELECTED_NODE, "Show Selected Node Info")->Check(btView.showSelectedNodeInfo);
725 menu.AppendCheckItem(ID_OVERLAY_TREE_INFO, "Show Tree Info")->Check(btView.showTreeInfo);
726 menu.AppendCheckItem(ID_OVERLAY_FPS, "Show FPS Counter")->Check(btView.showFPS);
727 menu.AppendCheckItem(ID_OVERLAY_CONTROLS_HELP, "Show Controls Help")->Check(btView.showControlsHelp);
728
729 menu.Bind(wxEVT_MENU, &MainPanel::OnOverlayMenuItem, this);
730 PopupMenu(&menu);
731}
732
733void MainPanel::OnOverlayMenuItem(wxCommandEvent &event) {
735 auto &prefs = prefMgr.GetPreferences();
736 auto &mp = prefs.GetMainPanelSettings();
737 auto &btv = prefs.GetBehaviorTreeViewSettings();
738
739 int id = event.GetId();
740
741 if (id == ID_OVERLAY_SHOW_ALL || id == ID_OVERLAY_HIDE_ALL) {
742 bool val = (id == ID_OVERLAY_SHOW_ALL);
743 mp.showGrid = val;
744 mp.showMinimap = val;
745 mp.showCanvasBreadcrumb = val;
746 btv.showCoordinateInfo = val;
747 btv.showSelectedNodeInfo = val;
748 btv.showTreeInfo = val;
749 btv.showFPS = val;
750 btv.showControlsHelp = val;
751 } else if (id == ID_OVERLAY_GRID) {
752 mp.showGrid = !mp.showGrid;
753 } else if (id == ID_OVERLAY_MINIMAP) {
754 mp.showMinimap = !mp.showMinimap;
755 } else if (id == ID_OVERLAY_BREADCRUMB) {
756 mp.showCanvasBreadcrumb = !mp.showCanvasBreadcrumb;
757 } else if (id == ID_OVERLAY_COORDINATE) {
758 btv.showCoordinateInfo = !btv.showCoordinateInfo;
759 } else if (id == ID_OVERLAY_SELECTED_NODE) {
760 btv.showSelectedNodeInfo = !btv.showSelectedNodeInfo;
761 } else if (id == ID_OVERLAY_TREE_INFO) {
762 btv.showTreeInfo = !btv.showTreeInfo;
763 } else if (id == ID_OVERLAY_FPS) {
764 btv.showFPS = !btv.showFPS;
765 } else if (id == ID_OVERLAY_CONTROLS_HELP) {
766 btv.showControlsHelp = !btv.showControlsHelp;
767 }
768
771
772 std::string configPath = EmberForge::AppPreferences::GetDefaultConfigPath();
773 prefs.SaveToFile(configPath);
774}
775
777 IScene *scene = GetActiveScene();
778 if (!scene)
779 return;
780
781 auto *btScene = dynamic_cast<BehaviorTreeScene *>(scene);
782 if (!btScene)
783 return;
784
785 auto *canvas = btScene->GetTreeVisualization();
786 if (!canvas)
787 return;
788
790 const auto &mp = prefs.GetMainPanelSettings();
791
792 canvas->SetShowGrid(mp.showGrid);
793 canvas->SetShowMinimap(mp.showMinimap);
794 canvas->SetShowBreadcrumb(mp.showCanvasBreadcrumb);
795 canvas->Refresh();
796}
797
799 if (!m_toolbar)
800 return;
801
802 auto *win = m_toolbar->FindWindow(ID_OVERLAY_BUTTON);
803 if (!win)
804 return;
805
806 auto *btn = dynamic_cast<wxBitmapButton *>(win);
807 if (!btn)
808 return;
809
811 const auto &mp = prefs.GetMainPanelSettings();
812 const auto &btv = prefs.GetBehaviorTreeViewSettings();
813
814 bool anyOn = mp.showGrid || mp.showMinimap || mp.showCanvasBreadcrumb || btv.showCoordinateInfo ||
815 btv.showSelectedNodeInfo || btv.showTreeInfo || btv.showFPS || btv.showControlsHelp;
816
817 wxString resPath = EmberForge::ResourcePath::GetDir("buttons");
818 if (anyOn) {
819 btn->SetBitmap(wxBitmap(resPath + "overlay/Overlay_Pressed_22.png", wxBITMAP_TYPE_PNG));
820 } else {
821 btn->SetBitmap(wxBitmap(resPath + "overlay/Overlay_Normal_22.png", wxBITMAP_TYPE_PNG));
822 }
823}
BehaviorTreeProjectDialog::OnProjectNameChanged BehaviorTreeProjectDialog::OnRemoveFiles wxEND_EVENT_TABLE() BehaviorTreeProjectDialog
MainPanel::OnTabChanged EVT_BUTTON(ID_OVERLAY_BUTTON, MainPanel::OnOverlayButton) wxEND_EVENT_TABLE() MainPanel
Definition MainPanel.cpp:18
wxBEGIN_EVENT_TABLE(MainPanel, EmberUI::Panel) EVT_AUINOTEBOOK_PAGE_CHANGED(ID_SCENE_NOTEBOOK
#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
Centralized resource path management for EmberForge.
Scene implementation for behavior tree visualization.
EmberForge::ForgeTreeCanvas * GetTreeVisualization() const
void SetCanvasBackgroundColor(const wxColour &color)
Set the canvas background color.
static ProjectManager & GetInstance()
const std::vector< String > & GetRecentProjects() const
static AppPreferencesManager & GetInstance()
MainPanelSettings & GetMainPanelSettings()
static EmberCore::String GetDefaultConfigPath()
Custom tab art provider that uses the application's accent color.
static wxString GetDir(const wxString &relativeDir)
Get the full path to a resource directory.
Base class for all panels with layout, theme, and state management.
Definition Panel.h:11
bool HasUnsavedChanges() const override
Returns true if there are unsaved changes.
Definition Panel.h:44
Interface for scene-based UI components (e.g., views or screens).
Definition IScene.h:7
Central panel for managing and displaying behavior tree scenes.
Definition MainPanel.h:43
bool SetActiveScene(int index)
Set the active scene by index.
void UpdateActiveSceneTitle(const wxString &newTitle)
Update the title of the active scene.
void DoCreateLayout()
Definition MainPanel.cpp:32
std::vector< std::unique_ptr< IScene > > m_scenes
Definition MainPanel.h:190
@ ID_OVERLAY_HIDE_ALL
Definition MainPanel.h:154
@ ID_OVERLAY_MINIMAP
Definition MainPanel.h:156
@ ID_OVERLAY_BUTTON
Definition MainPanel.h:152
@ ID_OVERLAY_COORDINATE
Definition MainPanel.h:158
@ ID_OVERLAY_SELECTED_NODE
Definition MainPanel.h:159
@ ID_OVERLAY_GRID
Definition MainPanel.h:155
@ ID_OVERLAY_TREE_INFO
Definition MainPanel.h:160
@ ID_SCENE_NOTEBOOK
Definition MainPanel.h:151
@ ID_NEW_SCENE_BUTTON
Definition MainPanel.h:150
@ ID_OVERLAY_FPS
Definition MainPanel.h:161
@ ID_OVERLAY_BREADCRUMB
Definition MainPanel.h:157
@ ID_OVERLAY_SHOW_ALL
Definition MainPanel.h:153
@ ID_OVERLAY_CONTROLS_HELP
Definition MainPanel.h:162
wxPanel * m_welcomePanel
Definition MainPanel.h:187
void DoCreateToolbar()
void OnNewSceneButton(wxCommandEvent &event)
void ShowScenes()
void CreateToolbar()
int m_nextSceneNumber
Definition MainPanel.h:192
bool m_isClosingTab
Definition MainPanel.h:193
wxAuiNotebook * m_sceneNotebook
Definition MainPanel.h:188
int m_activeSceneIndex
Definition MainPanel.h:191
SceneChangedCallback m_sceneChangedCallback
Definition MainPanel.h:194
wxString GetTitle() const override
Returns the display title of the panel.
Definition MainPanel.h:48
void UpdateOverlayButtonState()
int AddScene(std::unique_ptr< IScene > scene)
Add a new scene to the panel.
void SwitchToNextScene()
Switch to the next scene (cyclic)
wxSimplebook * m_book
Definition MainPanel.h:186
void ShowWelcome()
MainPanel(wxWindow *parent)
int InsertScene(int index, std::unique_ptr< IScene > scene)
Insert a scene at a specific position.
void CreateLayout() override
Hook: creates the panel layout. Override to customize.
Definition MainPanel.cpp:28
void OnOverlayMenuItem(wxCommandEvent &event)
bool RemoveScene(int index, bool force=false)
Remove a scene by index.
void OnTabChanged(wxAuiNotebookEvent &event)
wxString GenerateSceneTitle() const
void OnTabClosed(wxAuiNotebookEvent &event)
IScene * GetActiveScene() const
Get the currently active scene.
void ApplyOverlayToActiveCanvas()
wxPanel * CreateWelcomePanel()
Definition MainPanel.cpp:64
bool SceneTitleExists(const wxString &title) const
void ApplyCanvasBackgroundColor(const wxColour &color)
Apply canvas background color to all scenes.
IScene * GetScene(int index) const
Get a scene by index.
void SwitchToPreviousScene()
Switch to the previous scene (cyclic)
WelcomeActionCallback m_welcomeActionCallback
Definition MainPanel.h:195
wxToolBar * m_toolbar
Definition MainPanel.h:185
int CreateNewScene()
Create a new empty behavior tree scene.
void OnOverlayButton(wxCommandEvent &event)
Definition Panel.h:8