gui.cpp

main 2915 lines · 78.7 KB Raw
Alex Alex Commit Map Editor 18/10/2026 23:40
1//////////////////////////////////////////////////////////////////////
2// This file is part of Remere's Map Editor
3//////////////////////////////////////////////////////////////////////
4// Remere's Map Editor is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Remere's Map Editor is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <http://www.gnu.org/licenses/>.
16//////////////////////////////////////////////////////////////////////
17
18/*
19 * TASK: Refresh Custom Palette After Selection to Doodad Conversion POSTPHONED IGNORE UNTIL THIS DOES NOT SAY IGNORE :D
20 * --------------------------------------------------------------
21 *
22 * Current Behavior:
23 * - Selection to Doodad menu item creates a doodad brush from selected items
24 * - Brush is saved as XML but not immediately visible in palette
25 *
26 * Required Changes:
27 * 1. Palette Update
28 * - After doodad creation, refresh custom brushes palette
29 * - New doodad should appear at the top of custom brushes list
30 * - Maintain proper sorting/grouping of brushes
31 *
32 * Implementation Steps:
33 * 1. Add Palette Refresh Function
34 * - Create method in GUI class to reload custom brushes
35 * - Clear existing custom brush entries
36 * - Reload all XML brush definitions
37 * - Sort brushes according to current palette settings
38 *
39 * 2. Update Selection to Doodad Handler
40 * - After successful doodad creation and XML save
41 * - Call palette refresh function
42 * - Select newly created brush in palette
43 *
44 * 3. XML Integration
45 * - Ensure proper XML file naming for new doodad
46 * - Maintain brush metadata for palette organization
47 * - Handle file paths consistently
48 *
49 * Technical Requirements:
50 * - Access to palette window through g_gui
51 * - Proper brush management in BrushManager
52 * - XML file handling for brush definitions
53 * - Maintain undo/redo compatibility
54 *
55 * Files to Modify:
56 * - gui.cpp/h: Add refresh function
57 * - palette_window.cpp: Update brush list
58 * - doodad_brush.cpp: XML handling
59 *
60 * Related Code:
61 * - Selection to Doodad conversion (main_menubar.cpp)
62 * - Palette management (palette_window.cpp)
63 * - Brush XML handling (brush.cpp)
64 */
65
66/*
67 * TASK: Refresh Custom Palette After Selection to Doodad Conversion
68 * --------------------------------------------------------------
69 *
70 * IMPLEMENTATION OPTIONS:
71 *
72 * 1. QUICK REFRESH (Simple but Inefficient)
73 * ---------------------------------------
74 * - Use existing PaletteWindow::InvalidateContents()
75 * - Call g_gui.RefreshPalettes() after doodad creation
76 * Code:
77 * ```cpp
78 * PaletteWindow* palette = dynamic_cast<PaletteWindow*>(g_gui.GetPalette());
79 * if(palette) {
80 * palette->InvalidateContents();
81 * palette->SelectPage(TILESET_DOODAD);
82 * }
83 * g_gui.RefreshPalettes();
84 * ```
85 * Pros: Simple, uses existing methods
86 * Cons: Reloads entire palette, inefficient
87 *
88 * 2. TARGETED REFRESH (Balanced)
89 * ---------------------------
90 * - Add refresh method to BrushPalettePanel
91 * - Only reload doodad brushes section
92 * - Update specific palette page
93 * Code:
94 * ```cpp
95 * class BrushPalettePanel {
96 * void ReloadDoodadBrushes() {
97 * // Clear and reload only doodad brushes
98 * LoadDoodadBrushes();
99 * Refresh();
100 * }
101 * };
102 * ```
103 * Pros: More efficient, better UX
104 * Cons: Requires new methods in palette classes
105 *
106 * 3. REACTIVE SYSTEM (Complex but Robust)
107 * -----------------------------------
108 * - Implement observer pattern for brush changes
109 * - Palettes subscribe to brush manager updates
110 * - Auto-refresh when brushes change
111 * Code:
112 * ```cpp
113 * class BrushManager {
114 * std::vector<BrushObserver*> observers;
115 * void NotifyBrushChange(BrushCategory category) {
116 * for(auto* observer : observers) {
117 * observer->OnBrushesChanged(category);
118 * }
119 * }
120 * };
121 * ```
122 * Pros: Clean architecture, handles all brush changes
123 * Cons: Major refactoring required
124 *
125 * 4. HYBRID APPROACH (Recommended)
126 * -----------------------------
127 * - Use existing GUI methods but add targeted refresh
128 * - Add doodad-specific refresh to PaletteWindow
129 * - Maintain current architecture while improving efficiency
130 * Code:
131 * ```cpp
132 * class PaletteWindow {
133 * void RefreshDoodadPalette() {
134 * // Refresh only doodad page
135 * if(brush_panel) {
136 * brush_panel->LoadDoodadBrushes();
137 * SelectPage(TILESET_DOODAD);
138 * }
139 * }
140 * };
141 * ```
142 * Pros:
143 * - Balance of efficiency and simplicity
144 * - Minimal changes to existing code
145 * - Maintains current architecture
146 * Cons:
147 * - Still requires some new methods
148 * - Not as elegant as full reactive system
149 */
150#include "main.h"
151
152#include <wx/display.h>
153
154#include "gui.h"
155#include "main_menubar.h"
156
157#include "editor.h"
158#include "brush.h"
159#include "map.h"
160#include "sprites.h"
161#include "materials.h"
162#include "doodad_brush.h"
163#include "spawn_brush.h"
164
165#include "common_windows.h"
166#include "result_window.h"
167#include "map_summary_window.h"
168#include "minimap_window.h"
169#include "palette_window.h"
170#include "map_display.h"
171#include "application.h"
172#include "welcome_dialog.h"
173
174#include "live_client.h"
175#include "live_tab.h"
176#include "live_server.h"
177#include "dark_mode_manager.h"
178#include <wx/regex.h>
179
180#ifdef __WXOSX__
181 #include <AGL/agl.h>
182#endif
183
184const wxEventType EVT_UPDATE_MENUS = wxNewEventType();
185
186// Global GUI instance
187GUI g_gui;
188
189// GUI class implementation
190GUI::GUI() :
191 aui_manager(nullptr),
192 root(nullptr),
193 minimap_enabled(false),
194 mode(DRAWING_MODE),
195 pasting(false),
196 hotkeys_enabled(true),
197 search_result_window(nullptr),
198 map_summary_window(nullptr),
199 loaded_version(CLIENT_VERSION_NONE),
200 secondary_map(nullptr),
201 minimap(nullptr),
202 has_last_search(false),
203 last_search_itemid(0),
204 last_search_on_selection(false),
205 last_ignored_ids_enabled(false),
206 creature_spawntime(0),
207 gem(nullptr),
208 doodad_buffer_map(std::make_unique<BaseMap>()),
209 house_brush(nullptr),
210 house_exit_brush(nullptr),
211 waypoint_brush(nullptr),
212 optional_brush(nullptr),
213 eraser(nullptr),
214 spawn_brush(nullptr),
215 normal_door_brush(nullptr),
216 locked_door_brush(nullptr),
217 magic_door_brush(nullptr),
218 quest_door_brush(nullptr),
219 hatch_door_brush(nullptr),
220 normal_door_alt_brush(nullptr),
221 archway_door_brush(nullptr),
222 window_door_brush(nullptr),
223 pz_brush(nullptr),
224 rook_brush(nullptr),
225 nolog_brush(nullptr),
226 pvp_brush(nullptr),
227 OGLContext(nullptr),
228 current_brush(nullptr),
229 previous_brush(nullptr),
230 brush_shape(BRUSHSHAPE_SQUARE),
231 brush_size(0),
232 brush_variation(0),
233 draw_locked_doors(false),
234 use_custom_thickness(false),
235 custom_thickness_mod(0.0),
236 progressBar(nullptr),
237 progressFrom(0),
238 progressTo(0),
239 currentProgress(0),
240 winDisabler(nullptr),
241 disabled_counter(0),
242 last_autosave(time(nullptr)),
243 last_autosave_check(time(nullptr))
244{
245}
246
247GUI::~GUI() {
248 // Close all editors before deleting other resources
249 if (tabbook) {
250 CloseAllEditors();
251 }
252
253 // Clean up brushes
254 CleanupBrushes();
255
256 // Close any remaining detached/dockable views
257 for (auto& view_pair : detached_views) {
258 for (wxFrame* frame : view_pair.second) {
259 frame->Close(true);
260 }
261 }
262 detached_views.clear();
263
264 for (auto& view_pair : dockable_views) {
265 for (MapWindow* window : view_pair.second) {
266 if (aui_manager && aui_manager->GetPane(window).IsOk()) {
267 aui_manager->DetachPane(window);
268 window->Destroy();
269 }
270 }
271 }
272 dockable_views.clear();
273
274 // doodad_buffer_map and OGLContext are now managed by unique_ptr
275
276 // NOTE: aui_manager is deleted by MainFrame's destructor
277}
278
279void GUI::CleanupBrushes() {
280 // The GUI doesn't own the brushes, they're owned by g_brushes
281 // Just nullify the pointers to avoid dangling references
282 house_brush = nullptr;
283 house_exit_brush = nullptr;
284 waypoint_brush = nullptr;
285 optional_brush = nullptr;
286 eraser = nullptr;
287 spawn_brush = nullptr;
288 normal_door_brush = nullptr;
289 locked_door_brush = nullptr;
290 magic_door_brush = nullptr;
291 quest_door_brush = nullptr;
292 hatch_door_brush = nullptr;
293 normal_door_alt_brush = nullptr;
294 archway_door_brush = nullptr;
295 window_door_brush = nullptr;
296 pz_brush = nullptr;
297 rook_brush = nullptr;
298 nolog_brush = nullptr;
299 pvp_brush = nullptr;
300
301 current_brush = nullptr;
302 previous_brush = nullptr;
303}
304
305wxGLContext* GUI::GetGLContext(wxGLCanvas* win) {
306 if (!OGLContext) {
307#ifdef __WXOSX__
308 /*
309 wxGLContext(AGLPixelFormat fmt, wxGLCanvas *win,
310 const wxPalette& WXUNUSED(palette),
311 const wxGLContext *other
312 );
313 */
314 OGLContext = std::unique_ptr<wxGLContext>(new wxGLContext(win, nullptr));
315#else
316 OGLContext = std::unique_ptr<wxGLContext>(newd wxGLContext(win));
317#endif
318 }
319
320 return OGLContext.get();
321}
322
323wxString GUI::GetDataDirectory() {
324 std::string cfg_str = g_settings.getString(Config::DATA_DIRECTORY);
325 if (!cfg_str.empty()) {
326 FileName dir;
327 dir.Assign(wxstr(cfg_str));
328 wxString path;
329 if (dir.DirExists()) {
330 path = dir.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR);
331 return path;
332 }
333 }
334
335 // Silently reset directory
336 FileName exec_directory;
337 try {
338 exec_directory = dynamic_cast<wxStandardPaths&>(wxStandardPaths::Get()).GetExecutablePath();
339 } catch (const std::bad_cast) {
340 throw; // Crash application (this should never happend anyways...)
341 }
342
343 exec_directory.AppendDir("data");
344 return exec_directory.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR);
345}
346
347wxString GUI::GetExecDirectory() {
348 // Silently reset directory
349 FileName exec_directory;
350 try {
351 exec_directory = dynamic_cast<wxStandardPaths&>(wxStandardPaths::Get()).GetExecutablePath();
352 } catch (const std::bad_cast) {
353 wxLogError("Could not fetch executable directory.");
354 }
355 return exec_directory.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR);
356}
357
358wxString GUI::GetLocalDataDirectory() {
359 if (g_settings.getInteger(Config::INDIRECTORY_INSTALLATION)) {
360 FileName dir = GetDataDirectory();
361 dir.AppendDir("user");
362 dir.AppendDir("data");
363 dir.Mkdir(0755, wxPATH_MKDIR_FULL);
364 return dir.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR);
365 ;
366 } else {
367 FileName dir = dynamic_cast<wxStandardPaths&>(wxStandardPaths::Get()).GetUserDataDir();
368#ifdef __WINDOWS__
369 dir.AppendDir("Remere's Map Editor");
370#else
371 dir.AppendDir(".rme");
372#endif
373 dir.AppendDir("data");
374 dir.Mkdir(0755, wxPATH_MKDIR_FULL);
375 return dir.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR);
376 }
377}
378
379wxString GUI::GetLocalDirectory() {
380 if (g_settings.getInteger(Config::INDIRECTORY_INSTALLATION)) {
381 FileName dir = GetDataDirectory();
382 dir.AppendDir("user");
383 dir.Mkdir(0755, wxPATH_MKDIR_FULL);
384 return dir.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR);
385 ;
386 } else {
387 FileName dir = dynamic_cast<wxStandardPaths&>(wxStandardPaths::Get()).GetUserDataDir();
388#ifdef __WINDOWS__
389 dir.AppendDir("Remere's Map Editor");
390#else
391 dir.AppendDir(".rme");
392#endif
393 dir.Mkdir(0755, wxPATH_MKDIR_FULL);
394 return dir.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR);
395 }
396}
397
398wxString GUI::GetExtensionsDirectory() {
399 std::string cfg_str = g_settings.getString(Config::EXTENSIONS_DIRECTORY);
400 if (!cfg_str.empty()) {
401 FileName dir;
402 dir.Assign(wxstr(cfg_str));
403 wxString path;
404 if (dir.DirExists()) {
405 path = dir.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR);
406 return path;
407 }
408 }
409
410 // Silently reset directory
411 FileName local_directory = GetLocalDirectory();
412 local_directory.AppendDir("extensions");
413 local_directory.Mkdir(0755, wxPATH_MKDIR_FULL);
414 return local_directory.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR);
415}
416
417void GUI::discoverDataDirectory(const wxString& existentFile) {
418 wxString currentDir = wxGetCwd();
419 wxString execDir = GetExecDirectory();
420
421 wxString possiblePaths[] = {
422 execDir,
423 currentDir + "/",
424
425 // these are used usually when running from build directories
426 execDir + "/../",
427 execDir + "/../../",
428 execDir + "/../../../",
429 currentDir + "/../",
430 };
431
432 bool found = false;
433 for (const wxString& path : possiblePaths) {
434 if (wxFileName(path + "data/" + existentFile).FileExists()) {
435 m_dataDirectory = path + "data/";
436 found = true;
437 break;
438 }
439 }
440
441 if (!found) {
442 wxLogError(wxString() + "Could not find data directory.\n");
443 }
444}
445
446bool GUI::LoadVersion(ClientVersionID version, wxString& error, wxArrayString& warnings, bool force) {
447 if (ClientVersion::get(version) == nullptr) {
448 error = "Unsupported client version! (8)";
449 return false;
450 }
451
452 if (version != loaded_version || force) {
453 if (getLoadedVersion() != nullptr) {
454 // There is another version loaded right now, save window layout
455 g_gui.SavePerspective();
456 }
457
458 // Disable all rendering so the data is not accessed while reloading
459 UnnamedRenderingLock();
460 DestroyPalettes();
461 DestroyMinimap();
462
463 // Destroy the previous version
464 UnloadVersion();
465
466 loaded_version = version;
467 if (!getLoadedVersion()->hasValidPaths()) {
468 if (!getLoadedVersion()->loadValidPaths()) {
469 error = "Couldn't load relevant asset files";
470 loaded_version = CLIENT_VERSION_NONE;
471 return false;
472 }
473 }
474
475 bool ret = LoadDataFiles(error, warnings);
476 if (ret) {
477 g_gui.LoadPerspective();
478 } else {
479 loaded_version = CLIENT_VERSION_NONE;
480 }
481
482 return ret;
483 }
484 return true;
485}
486
487void GUI::EnableHotkeys() {
488 hotkeys_enabled = true;
489}
490
491void GUI::DisableHotkeys() {
492 hotkeys_enabled = false;
493}
494
495bool GUI::AreHotkeysEnabled() const {
496 return hotkeys_enabled;
497}
498
499ClientVersionID GUI::GetCurrentVersionID() const {
500 if (loaded_version != CLIENT_VERSION_NONE) {
501 return getLoadedVersion()->getID();
502 }
503 return CLIENT_VERSION_NONE;
504}
505
506const ClientVersion& GUI::GetCurrentVersion() const {
507 assert(loaded_version);
508 return *getLoadedVersion();
509}
510
511void GUI::CycleTab(bool forward) {
512 tabbook->CycleTab(forward);
513}
514
515bool GUI::LoadDataFiles(wxString& error, wxArrayString& warnings) {
516 minimap_enabled = false;
517 FileName data_path = getLoadedVersion()->getDataPath();
518 FileName client_path = getLoadedVersion()->getClientPath();
519 FileName extension_path = GetExtensionsDirectory();
520
521 FileName exec_directory;
522 try {
523 exec_directory = dynamic_cast<wxStandardPaths&>(wxStandardPaths::Get()).GetExecutablePath();
524 } catch (std::bad_cast&) {
525 error = "Couldn't establish working directory...";
526 return false;
527 }
528
529 g_gui.gfx.client_version = getLoadedVersion();
530
531 if (!g_gui.gfx.loadOTFI(client_path.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR), error, warnings)) {
532 error = "Couldn't load otfi file: " + error;
533 g_gui.DestroyLoadBar();
534 UnloadVersion();
535 return false;
536 }
537
538 g_gui.CreateLoadBar("Loading asset files");
539 g_gui.SetLoadDone(0, "Loading metadata file...");
540
541 wxFileName metadata_path = g_gui.gfx.getMetadataFileName();
542 if (!g_gui.gfx.loadSpriteMetadata(metadata_path, error, warnings)) {
543 error = "Couldn't load metadata: " + error;
544 g_gui.DestroyLoadBar();
545 UnloadVersion();
546 return false;
547 }
548
549 g_gui.SetLoadDone(10, "Loading sprites file...");
550
551 wxFileName sprites_path = g_gui.gfx.getSpritesFileName();
552 if (!g_gui.gfx.loadSpriteData(sprites_path.GetFullPath(), error, warnings)) {
553 error = "Couldn't load sprites: " + error;
554 g_gui.DestroyLoadBar();
555 UnloadVersion();
556 return false;
557 }
558
559 g_gui.SetLoadDone(20, "Loading items.otb file...");
560 if (!g_items.loadFromOtb(wxString(data_path.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR) + "items.otb"), error, warnings)) {
561 error = "Couldn't load items.otb: " + error;
562 g_gui.DestroyLoadBar();
563 UnloadVersion();
564 return false;
565 }
566
567 g_gui.SetLoadDone(30, "Loading items.xml ...");
568 if (!g_items.loadFromGameXml(wxString(data_path.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR) + "items.xml"), error, warnings)) {
569 warnings.push_back("Couldn't load items.xml: " + error);
570 }
571
572 g_gui.SetLoadDone(45, "Loading creatures.xml ...");
573 if (!g_creatures.loadFromXML(wxString(data_path.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR) + "creatures.xml"), true, error, warnings)) {
574 warnings.push_back("Couldn't load creatures.xml: " + error);
575 }
576
577 g_gui.SetLoadDone(45, "Loading user creatures.xml ...");
578 {
579 FileName cdb = getLoadedVersion()->getLocalDataPath();
580 cdb.SetFullName("creatures.xml");
581 wxString nerr;
582 wxArrayString nwarn;
583 g_creatures.loadFromXML(cdb, false, nerr, nwarn);
584 }
585
586 g_gui.SetLoadDone(50, "Loading materials.xml ...");
587 if (!g_materials.loadMaterials(wxString(data_path.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR) + "materials.xml"), error, warnings)) {
588 warnings.push_back("Couldn't load materials.xml: " + error);
589 }
590
591 g_gui.SetLoadDone(60, "Loading collections.xml ...");
592 if (!g_materials.loadMaterials(wxString(data_path.GetPath(wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR) + "collections.xml"), error, warnings)) {
593 warnings.push_back("Couldn't load collections.xml: " + error);
594 }
595
596 g_gui.SetLoadDone(70, "Loading extensions...");
597 if (!g_materials.loadExtensions(extension_path, error, warnings)) {
598 // warnings.push_back("Couldn't load extensions: " + error);
599 }
600
601 g_gui.SetLoadDone(70, "Finishing...");
602 g_brushes.init();
603 g_materials.createOtherTileset();
604
605 g_gui.DestroyLoadBar();
606 return true;
607}
608
609void GUI::UnloadVersion() {
610 UnnamedRenderingLock();
611 gfx.clear();
612 current_brush = nullptr;
613 previous_brush = nullptr;
614
615 house_brush = nullptr;
616 house_exit_brush = nullptr;
617 waypoint_brush = nullptr;
618 optional_brush = nullptr;
619 eraser = nullptr;
620 normal_door_brush = nullptr;
621 locked_door_brush = nullptr;
622 magic_door_brush = nullptr;
623 quest_door_brush = nullptr;
624 hatch_door_brush = nullptr;
625 window_door_brush = nullptr;
626
627 if (loaded_version != CLIENT_VERSION_NONE) {
628 // Close all detached and dockable views
629 for (auto it = detached_views.begin(); it != detached_views.end();) {
630 auto editor_it = it;
631 ++it; // Advance iterator before we potentially erase elements
632
633 // Close all views for this editor
634 CloseDetachedViews(editor_it->first);
635 }
636
637 // g_gui.UnloadVersion();
638 g_materials.clear();
639 g_brushes.clear();
640 g_items.clear();
641 gfx.clear();
642
643 FileName cdb = getLoadedVersion()->getLocalDataPath();
644 cdb.SetFullName("creatures.xml");
645 g_creatures.saveToXML(cdb);
646 g_creatures.clear();
647
648 loaded_version = CLIENT_VERSION_NONE;
649 }
650}
651
652void GUI::SaveCurrentMap(FileName filename, bool showdialog) {
653 MapTab* mapTab = GetCurrentMapTab();
654 if (mapTab) {
655 Editor* editor = mapTab->GetEditor();
656 if (editor) {
657 editor->saveMap(filename, showdialog);
658
659 const std::string& filename = editor->map.getFilename();
660 const Position& position = mapTab->GetScreenCenterPosition();
661 std::ostringstream stream;
662 stream << position;
663 g_settings.setString(Config::RECENT_EDITED_MAP_PATH, filename);
664 g_settings.setString(Config::RECENT_EDITED_MAP_POSITION, stream.str());
665 }
666 }
667
668 UpdateTitle();
669 root->UpdateMenubar();
670 root->Refresh();
671}
672
673bool GUI::IsEditorOpen() const {
674 return tabbook != nullptr && GetCurrentMapTab();
675}
676
677double GUI::GetCurrentZoom() {
678 MapTab* tab = GetCurrentMapTab();
679 if (tab) {
680 return tab->GetCanvas()->GetZoom();
681 }
682 return 1.0;
683}
684
685void GUI::SetCurrentZoom(double zoom) {
686 MapTab* tab = GetCurrentMapTab();
687 if (tab) {
688 tab->GetCanvas()->SetZoom(zoom);
689 }
690}
691
692void GUI::FitViewToMap() {
693 for (int index = 0; index < tabbook->GetTabCount(); ++index) {
694 if (auto* tab = dynamic_cast<MapTab*>(tabbook->GetTab(index))) {
695 tab->GetView()->FitToMap();
696 }
697 }
698}
699
700void GUI::FitViewToMap(MapTab* mt) {
701 for (int index = 0; index < tabbook->GetTabCount(); ++index) {
702 if (auto* tab = dynamic_cast<MapTab*>(tabbook->GetTab(index))) {
703 if (tab->HasSameReference(mt)) {
704 tab->GetView()->FitToMap();
705 }
706 }
707 }
708}
709
710bool GUI::NewMap() {
711 FinishWelcomeDialog();
712
713 Editor* editor;
714 try {
715 editor = newd Editor(copybuffer);
716 } catch (std::runtime_error& e) {
717 PopupDialog(root, "Error!", wxString(e.what(), wxConvUTF8), wxOK);
718 return false;
719 }
720
721 auto* mapTab = newd MapTab(tabbook, editor);
722 mapTab->OnSwitchEditorMode(mode);
723 editor->map.clearChanges();
724
725 SetStatusText("Created new map");
726 UpdateTitle();
727 RefreshPalettes();
728 root->UpdateMenubar();
729 root->Refresh();
730
731 return true;
732}
733
734void GUI::OpenMap() {
735 wxString wildcard = g_settings.getInteger(Config::USE_OTGZ) != 0 ? MAP_LOAD_FILE_WILDCARD_OTGZ : MAP_LOAD_FILE_WILDCARD;
736 wxFileDialog dialog(root, "Open map file", wxEmptyString, wxEmptyString, wildcard, wxFD_OPEN | wxFD_FILE_MUST_EXIST);
737
738 if (dialog.ShowModal() == wxID_OK) {
739 LoadMap(dialog.GetPath());
740 }
741}
742
743void GUI::SaveMap() {
744 if (!IsEditorOpen()) {
745 return;
746 }
747
748 if (GetCurrentMap().hasFile()) {
749 SaveCurrentMap(true);
750 } else {
751 wxString wildcard = g_settings.getInteger(Config::USE_OTGZ) != 0 ? MAP_SAVE_FILE_WILDCARD_OTGZ : MAP_SAVE_FILE_WILDCARD;
752 wxFileDialog dialog(root, "Save...", wxEmptyString, wxEmptyString, wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
753
754 if (dialog.ShowModal() == wxID_OK) {
755 SaveCurrentMap(dialog.GetPath(), true);
756 }
757 }
758}
759
760void GUI::SaveMapAs() {
761 if (!IsEditorOpen()) {
762 return;
763 }
764
765 wxString wildcard = g_settings.getInteger(Config::USE_OTGZ) != 0 ? MAP_SAVE_FILE_WILDCARD_OTGZ : MAP_SAVE_FILE_WILDCARD;
766 wxFileDialog dialog(root, "Save As...", "", "", wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
767
768 if (dialog.ShowModal() == wxID_OK) {
769 SaveCurrentMap(dialog.GetPath(), true);
770 UpdateTitle();
771 root->menu_bar->AddRecentFile(dialog.GetPath());
772 root->UpdateMenubar();
773 }
774}
775
776bool GUI::LoadMap(const FileName& fileName) {
777 FinishWelcomeDialog();
778
779 if (GetCurrentEditor() && !GetCurrentMap().hasChanged() && !GetCurrentMap().hasFile()) {
780 g_gui.CloseCurrentEditor();
781 }
782
783 Editor* editor;
784 try {
785 editor = newd Editor(copybuffer, fileName);
786 } catch (std::runtime_error& e) {
787 PopupDialog(root, "Error!", wxString(e.what(), wxConvUTF8), wxOK);
788 return false;
789 }
790
791 auto* mapTab = newd MapTab(tabbook, editor);
792 mapTab->OnSwitchEditorMode(mode);
793
794 root->AddRecentFile(fileName);
795
796 mapTab->GetView()->FitToMap();
797 UpdateTitle();
798 ListDialog("Map loader errors", mapTab->GetMap()->getWarnings());
799 root->DoQueryImportCreatures();
800
801 FitViewToMap(mapTab);
802 root->UpdateMenubar();
803
804 // Pre-cache the entire minimap for smooth performance
805 if (minimap && IsMinimapVisible()) {
806 minimap->PreCacheEntireMap();
807 }
808
809 std::string path = g_settings.getString(Config::RECENT_EDITED_MAP_PATH);
810 if (!path.empty()) {
811 FileName file(path);
812 if (file == fileName) {
813 std::istringstream stream(g_settings.getString(Config::RECENT_EDITED_MAP_POSITION));
814 Position position;
815 stream >> position;
816 mapTab->SetScreenCenterPosition(position);
817 }
818 }
819 return true;
820}
821
822Editor* GUI::GetCurrentEditor() {
823 MapTab* mapTab = GetCurrentMapTab();
824 if (mapTab) {
825 return mapTab->GetEditor();
826 }
827 return nullptr;
828}
829
830EditorTab* GUI::GetTab(int idx) {
831 return tabbook->GetTab(idx);
832}
833
834int GUI::GetTabCount() const {
835 return tabbook->GetTabCount();
836}
837
838EditorTab* GUI::GetCurrentTab() {
839 return tabbook->GetCurrentTab();
840}
841
842MapTab* GUI::GetCurrentMapTab() const {
843 if (tabbook && tabbook->GetTabCount() > 0) {
844 EditorTab* editorTab = tabbook->GetCurrentTab();
845 auto* mapTab = dynamic_cast<MapTab*>(editorTab);
846 return mapTab;
847 }
848 return nullptr;
849}
850
851Map& GUI::GetCurrentMap() {
852 Editor* editor = GetCurrentEditor();
853 ASSERT(editor);
854 return editor->map;
855}
856
857int GUI::GetOpenMapCount() {
858 std::set<Map*> open_maps;
859
860 for (int i = 0; i < tabbook->GetTabCount(); ++i) {
861 auto* tab = dynamic_cast<MapTab*>(tabbook->GetTab(i));
862 if (tab) {
863 open_maps.insert(open_maps.begin(), tab->GetMap());
864 }
865 }
866
867 return static_cast<int>(open_maps.size());
868}
869
870bool GUI::ShouldSave() {
871 const Map& map = GetCurrentMap();
872 if (map.hasChanged()) {
873 if (map.getTileCount() == 0) {
874 Editor* editor = GetCurrentEditor();
875 ASSERT(editor);
876 return editor->actionQueue->canUndo();
877 }
878 return true;
879 }
880 return false;
881}
882
883void GUI::AddPendingCanvasEvent(wxEvent& event) {
884 MapTab* mapTab = GetCurrentMapTab();
885 if (mapTab) {
886 mapTab->GetCanvas()->GetEventHandler()->AddPendingEvent(event);
887 }
888}
889
890void GUI::CloseCurrentEditor() {
891 MapTab* mapTab = GetCurrentMapTab();
892 if (mapTab) {
893 // Check if the map has detached views and warn the user
894 if (HasDetachedViews(mapTab->GetEditor())) {
895 wxString message = "This map has one or more detached views open.\n";
896 message += "You must close all detached views before closing the map.";
897
898 int choice = wxMessageBox(
899 message,
900 "Detached Views Open",
901 wxOK | wxCANCEL | wxICON_EXCLAMATION
902 );
903
904 if (choice == wxOK) {
905 // User chose to close detached views
906 CloseDetachedViews(mapTab->GetEditor());
907 } else {
908 // User canceled operation
909 return;
910 }
911 }
912 }
913
914 RefreshPalettes();
915 tabbook->DeleteTab(tabbook->GetSelection());
916 root->UpdateMenubar();
917}
918
919bool GUI::CloseLiveEditors(LiveSocket* sock) {
920 for (int i = 0; i < tabbook->GetTabCount(); ++i) {
921 auto* mapTab = dynamic_cast<MapTab*>(tabbook->GetTab(i));
922 if (mapTab) {
923 Editor* editor = mapTab->GetEditor();
924 if (editor->GetLiveClient() == sock) {
925 tabbook->DeleteTab(i--);
926 }
927 }
928 auto* liveLogTab = dynamic_cast<LiveLogTab*>(tabbook->GetTab(i));
929 if (liveLogTab) {
930 if (liveLogTab->GetSocket() == sock) {
931 liveLogTab->Disconnect();
932 tabbook->DeleteTab(i--);
933 }
934 }
935 }
936 root->UpdateMenubar();
937 return true;
938}
939
940bool GUI::CloseAllEditors() {
941 for (int i = 0; i < tabbook->GetTabCount(); ++i) {
942 auto* mapTab = dynamic_cast<MapTab*>(tabbook->GetTab(i));
943 if (mapTab) {
944 // Check if the map has detached views and warn the user
945 if (HasDetachedViews(mapTab->GetEditor())) {
946 tabbook->SetFocusedTab(i);
947
948 wxString message = "This map has one or more detached views open.\n";
949 message += "You must close all detached views before closing the map.";
950
951 int choice = wxMessageBox(
952 message,
953 "Detached Views Open",
954 wxOK | wxCANCEL | wxICON_EXCLAMATION
955 );
956
957 if (choice == wxOK) {
958 // User chose to close detached views
959 CloseDetachedViews(mapTab->GetEditor());
960 } else {
961 // User canceled operation
962 return false;
963 }
964 }
965
966 if (mapTab->IsUniqueReference() && mapTab->GetMap() && mapTab->GetMap()->hasChanged()) {
967 tabbook->SetFocusedTab(i);
968 if (!root->DoQuerySave(false)) {
969 return false;
970 } else {
971 RefreshPalettes();
972 tabbook->DeleteTab(i--);
973 }
974 } else {
975 tabbook->DeleteTab(i--);
976 }
977 }
978 }
979 if (root) {
980 root->UpdateMenubar();
981 }
982 return true;
983}
984
985void GUI::NewMapView() {
986 MapTab* mapTab = GetCurrentMapTab();
987 if (mapTab) {
988 auto* newMapTab = newd MapTab(mapTab);
989 newMapTab->OnSwitchEditorMode(mode);
990
991 SetStatusText("Created new view");
992 UpdateTitle();
993 RefreshPalettes();
994 root->UpdateMenubar();
995 root->Refresh();
996 }
997}
998
999void GUI::NewDetachedMapView() {
1000 MapTab* mapTab = GetCurrentMapTab();
1001 if (mapTab) {
1002 // Display options dialog for the type of view
1003 wxArrayString choices;
1004 choices.Add("Detached Window (Can be moved to another monitor)");
1005 choices.Add("Always-on-top Window (Will stay on top of other windows)");
1006 choices.Add("Dockable Panel (Can be attached like palette/minimap)");
1007
1008 wxSingleChoiceDialog dialog(root, "Select type of view:", "Map View Options", choices);
1009
1010 if (dialog.ShowModal() == wxID_OK) {
1011 int selection = dialog.GetSelection();
1012
1013 if (selection == 0 || selection == 1) {
1014 // Create a standalone top-level window
1015 wxFrame* detachedFrame = newd wxFrame(root, wxID_ANY, "Detached Map View",
1016 wxDefaultPosition, wxSize(800, 600),
1017 wxDEFAULT_FRAME_STYLE | wxRESIZE_BORDER | wxMAXIMIZE_BOX | wxMINIMIZE_BOX);
1018
1019 // Create a map window in the new frame
1020 MapWindow* newMapWindow = newd MapWindow(detachedFrame, *mapTab->GetEditor());
1021
1022 // Set up a basic sizer for the frame
1023 wxSizer* sizer = newd wxBoxSizer(wxVERTICAL);
1024 sizer->Add(newMapWindow, 1, wxEXPAND);
1025 detachedFrame->SetSizer(sizer);
1026
1027 // Initialize the map window to match current map view
1028 newMapWindow->FitToMap();
1029
1030 // Set the center position to match the current view
1031 Position pos = mapTab->GetScreenCenterPosition();
1032 newMapWindow->SetScreenCenterPosition(pos);
1033
1034 // Configure the map window based on current editor mode
1035 if (mode == SELECTION_MODE) {
1036 newMapWindow->GetCanvas()->EnterSelectionMode();
1037 } else {
1038 newMapWindow->GetCanvas()->EnterDrawingMode();
1039 }
1040
1041 // Add a small toolbar for common functions
1042 wxToolBar* toolbar = new wxToolBar(detachedFrame, wxID_ANY);
1043 wxButton* syncButton = new wxButton(toolbar, wxID_ANY, "Sync View");
1044 wxCheckBox* pinCheckbox = new wxCheckBox(toolbar, wxID_ANY, "Keep on Top");
1045 wxCheckBox* keepOpenCheckbox = new wxCheckBox(toolbar, wxID_ANY, "Keep Open");
1046 toolbar->AddControl(syncButton);
1047 toolbar->AddSeparator();
1048 toolbar->AddControl(pinCheckbox);
1049 toolbar->AddSeparator();
1050 toolbar->AddControl(keepOpenCheckbox);
1051 toolbar->Realize();
1052
1053 // Add the toolbar to the frame
1054 sizer->Insert(0, toolbar, 0, wxEXPAND);
1055
1056 // Bind toolbar events
1057 syncButton->Bind(wxEVT_BUTTON, [=](wxCommandEvent& event) {
1058 // Sync button clicked
1059 MapTab* currentTab = GetCurrentMapTab();
1060 if (currentTab) {
1061 Position mainPos = currentTab->GetScreenCenterPosition();
1062 newMapWindow->SetScreenCenterPosition(mainPos);
1063 }
1064 });
1065
1066 pinCheckbox->Bind(wxEVT_CHECKBOX, [=](wxCommandEvent& event) {
1067 // Pin checkbox toggled
1068 bool checked = pinCheckbox->GetValue();
1069 if (checked) {
1070 detachedFrame->SetWindowStyleFlag(detachedFrame->GetWindowStyleFlag() | wxSTAY_ON_TOP);
1071 } else {
1072 detachedFrame->SetWindowStyleFlag(detachedFrame->GetWindowStyleFlag() & ~wxSTAY_ON_TOP);
1073 }
1074
1075 // Need to update the window for the style change to take effect
1076 detachedFrame->Refresh();
1077 });
1078
1079 // Create a client data object to store the keep-open flag
1080 // This avoids a memory leak from the heap-allocated bool
1081 struct WindowData : public wxClientData {
1082 bool keepOpen = false;
1083 };
1084 WindowData* windowData = new WindowData();
1085 detachedFrame->SetClientObject(windowData);
1086
1087 // Default close behavior with flag check
1088 detachedFrame->Bind(wxEVT_CLOSE_WINDOW, [detachedFrame](wxCloseEvent& event) {
1089 WindowData* data = static_cast<WindowData*>(detachedFrame->GetClientObject());
1090 if (data && data->keepOpen && event.CanVeto()) {
1091 // Minimize instead of closing
1092 event.Veto();
1093 detachedFrame->Iconize(true);
1094 } else {
1095 // Regular close
1096 detachedFrame->Destroy();
1097 }
1098 });
1099
1100 // Keep Open checkbox handler - updates the flag in client data
1101 keepOpenCheckbox->Bind(wxEVT_CHECKBOX, [detachedFrame](wxCommandEvent& event) {
1102 wxCheckBox* cb = static_cast<wxCheckBox*>(event.GetEventObject());
1103 WindowData* data = static_cast<WindowData*>(detachedFrame->GetClientObject());
1104 if (data) {
1105 data->keepOpen = cb->GetValue();
1106 }
1107 });
1108
1109 // Add context menu for quick floor navigation
1110 newMapWindow->Bind(wxEVT_RIGHT_DOWN, [=](wxMouseEvent& event) {
1111 wxMenu popupMenu;
1112
1113 // Floor navigation submenu
1114 wxMenu* floorMenu = new wxMenu();
1115 for (int floor = 0; floor <= 15; floor++) {
1116 wxMenuItem* floorItem = floorMenu->Append(wxID_ANY, wxString::Format("Floor %d", floor));
1117
1118 floorMenu->Bind(wxEVT_COMMAND_MENU_SELECTED, [=](wxCommandEvent& event) {
1119 newMapWindow->GetCanvas()->ChangeFloor(floor);
1120 }, floorItem->GetId());
1121 }
1122 popupMenu.Append(wxID_ANY, "Go to Floor", floorMenu);
1123
1124 // Show popup menu
1125 newMapWindow->PopupMenu(&popupMenu);
1126 });
1127
1128 // Title should include map name
1129 detachedFrame->SetTitle(wxString::Format("Detached View: %s", wxstr(mapTab->GetEditor()->map.getName())));
1130
1131 // If user selected always-on-top, enable that flag
1132 if (selection == 1) {
1133 detachedFrame->SetWindowStyleFlag(detachedFrame->GetWindowStyleFlag() | wxSTAY_ON_TOP);
1134 detachedFrame->SetTitle(wxString::Format("Always-on-top View: %s", wxstr(mapTab->GetEditor()->map.getName())));
1135 pinCheckbox->SetValue(true);
1136 }
1137
1138 // Register the detached view
1139 RegisterDetachedView(mapTab->GetEditor(), detachedFrame);
1140
1141 // Add cleanup to close handler to remove the frame from our registry
1142 detachedFrame->Bind(wxEVT_DESTROY, [=](wxWindowDestroyEvent& event) {
1143 UnregisterDetachedView(mapTab->GetEditor(), detachedFrame);
1144 });
1145
1146 // Show the window
1147 detachedFrame->Show();
1148
1149 SetStatusText(selection == 0 ? "Created new detached view" : "Created new always-on-top view");
1150 }
1151 else if (selection == 2) {
1152 // Create a MapWindow as a dockable panel
1153 MapWindow* newMapWindow = newd MapWindow(root, *mapTab->GetEditor());
1154
1155 // Add the window to the AUI manager
1156 wxAuiPaneInfo paneInfo;
1157 paneInfo.Caption("Map View")
1158 .Float()
1159 .Floatable(true)
1160 .Dockable(true)
1161 .Movable(true)
1162 .Resizable(true)
1163 .MinSize(400, 300)
1164 .BestSize(640, 480);
1165
1166 aui_manager->AddPane(newMapWindow, paneInfo);
1167 aui_manager->Update();
1168
1169 // Initialize the map window to match current map view
1170 newMapWindow->FitToMap();
1171
1172 // Set the center position to match the current view
1173 Position pos = mapTab->GetScreenCenterPosition();
1174 newMapWindow->SetScreenCenterPosition(pos);
1175
1176 // Configure the map window based on current editor mode
1177 if (mode == SELECTION_MODE) {
1178 newMapWindow->GetCanvas()->EnterSelectionMode();
1179 } else {
1180 newMapWindow->GetCanvas()->EnterDrawingMode();
1181 }
1182
1183 // Register the dockable view
1184 RegisterDockableView(mapTab->GetEditor(), newMapWindow);
1185
1186 // Bind cleanup event to remove the window from our registry when destroyed
1187 newMapWindow->Bind(wxEVT_DESTROY, [=](wxWindowDestroyEvent& event) {
1188 UnregisterDockableView(mapTab->GetEditor(), newMapWindow);
1189 event.Skip();
1190 });
1191
1192 SetStatusText("Created new dockable map view");
1193 }
1194 }
1195 }
1196}
1197
1198void GUI::LoadPerspective() {
1199 if (!IsVersionLoaded()) {
1200 if (g_settings.getInteger(Config::WINDOW_MAXIMIZED)) {
1201 root->Maximize();
1202 } else {
1203 root->SetSize(wxSize(
1204 g_settings.getInteger(Config::WINDOW_WIDTH),
1205 g_settings.getInteger(Config::WINDOW_HEIGHT)
1206 ));
1207 }
1208 } else {
1209 std::string tmp;
1210 std::string layout = g_settings.getString(Config::PALETTE_LAYOUT);
1211
1212 std::vector<std::string> palette_list;
1213 for (char c : layout) {
1214 if (c == '|') {
1215 palette_list.push_back(tmp);
1216 tmp.clear();
1217 } else {
1218 tmp.push_back(c);
1219 }
1220 }
1221
1222 if (!tmp.empty()) {
1223 palette_list.push_back(tmp);
1224 }
1225
1226 for (const std::string& name : palette_list) {
1227 PaletteWindow* palette = CreatePalette();
1228
1229 wxAuiPaneInfo& info = aui_manager->GetPane(palette);
1230 aui_manager->LoadPaneInfo(wxstr(name), info);
1231
1232 if (info.IsFloatable()) {
1233 bool offscreen = true;
1234 for (uint32_t index = 0; index < wxDisplay::GetCount(); ++index) {
1235 wxDisplay display(index);
1236 wxRect rect = display.GetClientArea();
1237 if (rect.Contains(info.floating_pos)) {
1238 offscreen = false;
1239 break;
1240 }
1241 }
1242
1243 if (offscreen) {
1244 info.Dock();
1245 }
1246 }
1247 }
1248
1249 if (g_settings.getInteger(Config::MINIMAP_VISIBLE)) {
1250 if (!minimap) {
1251 wxAuiPaneInfo info;
1252
1253 const wxString& data = wxstr(g_settings.getString(Config::MINIMAP_LAYOUT));
1254 aui_manager->LoadPaneInfo(data, info);
1255
1256 minimap = newd MinimapWindow(root);
1257 // Ensure the minimap is always dockable regardless of saved state
1258 info.Dockable(true).Resizable(true).MinSize(300, 200);
1259
1260 aui_manager->AddPane(minimap, info);
1261 } else {
1262 wxAuiPaneInfo& info = aui_manager->GetPane(minimap);
1263
1264 const wxString& data = wxstr(g_settings.getString(Config::MINIMAP_LAYOUT));
1265 aui_manager->LoadPaneInfo(data, info);
1266
1267 // Ensure the minimap is always dockable regardless of saved state
1268 info.Dockable(true).Resizable(true).MinSize(300, 200);
1269 }
1270
1271 wxAuiPaneInfo& info = aui_manager->GetPane(minimap);
1272 if (info.IsFloatable()) {
1273 bool offscreen = true;
1274 for (uint32_t index = 0; index < wxDisplay::GetCount(); ++index) {
1275 wxDisplay display(index);
1276 wxRect rect = display.GetClientArea();
1277 if (rect.Contains(info.floating_pos)) {
1278 offscreen = false;
1279 break;
1280 }
1281 }
1282
1283 if (offscreen) {
1284 info.Dock();
1285 }
1286 }
1287 }
1288
1289 aui_manager->Update();
1290 root->UpdateMenubar();
1291 }
1292
1293 root->GetAuiToolBar()->LoadPerspective();
1294}
1295
1296void GUI::SavePerspective() {
1297 g_settings.setInteger(Config::WINDOW_MAXIMIZED, root->IsMaximized());
1298 g_settings.setInteger(Config::WINDOW_WIDTH, root->GetSize().GetWidth());
1299 g_settings.setInteger(Config::WINDOW_HEIGHT, root->GetSize().GetHeight());
1300
1301 g_settings.setInteger(Config::MINIMAP_VISIBLE, minimap ? 1 : 0);
1302
1303 wxString pinfo;
1304 for (auto& palette : palettes) {
1305 if (aui_manager->GetPane(palette).IsShown()) {
1306 pinfo << aui_manager->SavePaneInfo(aui_manager->GetPane(palette)) << "|";
1307 }
1308 }
1309 g_settings.setString(Config::PALETTE_LAYOUT, nstr(pinfo));
1310
1311 if (minimap) {
1312 wxString s = aui_manager->SavePaneInfo(aui_manager->GetPane(minimap));
1313 g_settings.setString(Config::MINIMAP_LAYOUT, nstr(s));
1314 }
1315
1316 root->GetAuiToolBar()->SavePerspective();
1317}
1318
1319void GUI::HideSearchWindow() {
1320 if (search_result_window) {
1321 aui_manager->GetPane(search_result_window).Show(false);
1322 aui_manager->Update();
1323 }
1324}
1325
1326SearchResultWindow* GUI::GetSearchWindow() {
1327 return search_result_window;
1328}
1329
1330SearchResultWindow* GUI::ShowSearchWindow() {
1331 if (search_result_window == nullptr) {
1332 search_result_window = newd SearchResultWindow(root);
1333 aui_manager->AddPane(search_result_window, wxAuiPaneInfo().Caption("Search Results"));
1334 } else {
1335 aui_manager->GetPane(search_result_window).Show();
1336 }
1337 aui_manager->Update();
1338 return search_result_window;
1339}
1340
1341void GUI::HideMapSummaryWindow() {
1342 if (map_summary_window) {
1343 aui_manager->GetPane(map_summary_window).Show(false);
1344 aui_manager->Update();
1345 }
1346}
1347
1348MapSummaryWindow* GUI::GetMapSummaryWindow() {
1349 return map_summary_window;
1350}
1351
1352MapSummaryWindow* GUI::ShowMapSummaryWindow() {
1353 if (map_summary_window == nullptr) {
1354 map_summary_window = newd MapSummaryWindow(root);
1355 aui_manager->AddPane(map_summary_window, wxAuiPaneInfo().Caption("Map Summary"));
1356 } else {
1357 aui_manager->GetPane(map_summary_window).Show();
1358 }
1359 aui_manager->Update();
1360 return map_summary_window;
1361}
1362
1363//=============================================================================
1364// Palette Window Interface implementation
1365
1366PaletteWindow* GUI::GetPalette() const {
1367 if (palettes.empty()) {
1368 return nullptr;
1369 }
1370 return palettes.front();
1371}
1372
1373PaletteWindow* GUI::NewPalette() {
1374 return CreatePalette();
1375}
1376
1377void GUI::RefreshPalettes(Map* m, bool usedefault) {
1378 for (auto& palette : palettes) {
1379 palette->OnUpdate(m ? m : (usedefault ? (IsEditorOpen() ? &GetCurrentMap() : nullptr) : nullptr));
1380 }
1381 SelectBrush();
1382}
1383
1384void GUI::RefreshOtherPalettes(PaletteWindow* p) {
1385 for (auto& palette : palettes) {
1386 if (palette != p) {
1387 palette->OnUpdate(IsEditorOpen() ? &GetCurrentMap() : nullptr);
1388 }
1389 }
1390 SelectBrush();
1391}
1392
1393PaletteWindow* GUI::CreatePalette() {
1394 if (!IsVersionLoaded()) {
1395 return nullptr;
1396 }
1397
1398 auto* palette = new PaletteWindow(root, g_materials.tilesets);
1399
1400 // Add the palette with resizable properties
1401 wxAuiPaneInfo paneInfo;
1402 paneInfo.Caption("Palette")
1403 .TopDockable(false)
1404 .BottomDockable(false)
1405 .Resizable(true) // Allow resizing
1406 .MinSize(225, 250) // Minimum size
1407 .BestSize(230, 400); // Default size
1408
1409 aui_manager->AddPane(palette, paneInfo);
1410 aui_manager->Update();
1411
1412 // Make us the active palette
1413 palettes.push_front(palette);
1414 // Select brush from this palette
1415 SelectBrushInternal(palette->GetSelectedBrush());
1416 // fix for blank house list on f5 or new palette
1417 palette->OnUpdate(IsEditorOpen() ? &GetCurrentMap() : nullptr);
1418 return palette;
1419}
1420
1421void GUI::ActivatePalette(PaletteWindow* p) {
1422 palettes.erase(std::find(palettes.begin(), palettes.end(), p));
1423 palettes.push_front(p);
1424}
1425
1426void GUI::DestroyPalettes() {
1427 for (auto palette : palettes) {
1428 aui_manager->DetachPane(palette);
1429 palette->Destroy();
1430 palette = nullptr;
1431 }
1432 palettes.clear();
1433 aui_manager->Update();
1434}
1435
1436void GUI::RebuildPalettes() {
1437 // Completely recreate palettes to include/exclude tileset editing buttons
1438 if (!palettes.empty()) {
1439 // Remember which palette was active
1440 Map* current_map = IsEditorOpen() ? &GetCurrentMap() : nullptr;
1441
1442 // Destroy all palettes
1443 DestroyPalettes();
1444
1445 // Recreate palette
1446 CreatePalette();
1447
1448 // Force update
1449 RefreshPalettes(current_map);
1450 }
1451
1452 // Update the AUI manager
1453 aui_manager->Update();
1454}
1455
1456void GUI::ShowPalette() {
1457 if (palettes.empty()) {
1458 return;
1459 }
1460
1461 for (auto& palette : palettes) {
1462 if (aui_manager->GetPane(palette).IsShown()) {
1463 return;
1464 }
1465 }
1466
1467 aui_manager->GetPane(palettes.front()).Show(true);
1468 aui_manager->Update();
1469}
1470
1471void GUI::SelectPalettePage(PaletteType pt) {
1472 if (palettes.empty()) {
1473 CreatePalette();
1474 }
1475 PaletteWindow* p = GetPalette();
1476 if (!p) {
1477 return;
1478 }
1479
1480 ShowPalette();
1481 p->SelectPage(pt);
1482 aui_manager->Update();
1483 SelectBrushInternal(p->GetSelectedBrush());
1484}
1485
1486//=============================================================================
1487// Minimap Window Interface Implementation
1488
1489void GUI::CreateMinimap() {
1490 if (!IsVersionLoaded()) {
1491 return;
1492 }
1493
1494 if (minimap && minimap_enabled) {
1495 // If minimap exists and is enabled, hide it
1496 HideMinimap();
1497 minimap_enabled = false;
1498 } else if (minimap) {
1499 // If minimap exists but is hidden, show it
1500 ShowMinimap();
1501 minimap_enabled = true;
1502 } else {
1503 // Create new minimap
1504 minimap = newd MinimapWindow(root);
1505 minimap->SetSize(wxSize(640, 320));
1506
1507 // Add as dockable pane with configurable size
1508 wxAuiPaneInfo paneInfo;
1509 paneInfo.Caption("Minimap")
1510 .Float()
1511 .FloatingSize(wxSize(640, 320))
1512 .Floatable(true)
1513 .Movable(true)
1514 .Dockable(true) // Allow docking
1515 .Resizable(true) // Allow resizing
1516 .MinSize(300, 200) // Minimum size
1517 .DestroyOnClose(false);
1518
1519 aui_manager->AddPane(minimap, paneInfo);
1520 minimap_enabled = true;
1521 aui_manager->Update();
1522
1523 // Show the minimap immediately without caching
1524 minimap->Show(true);
1525 }
1526}
1527
1528void GUI::HideMinimap() {
1529 if (minimap) {
1530
1531 aui_manager->GetPane(minimap).Show(false);
1532 aui_manager->Update();
1533 }
1534}
1535
1536void GUI::ShowMinimap() {
1537 if (minimap) {
1538 aui_manager->GetPane(minimap).Show(true);
1539 aui_manager->Update();
1540 } else {
1541 // Create minimap if it doesn't exist
1542 CreateMinimap();
1543 }
1544}
1545
1546void GUI::DestroyMinimap() {
1547 if (minimap) {
1548 aui_manager->DetachPane(minimap);
1549 aui_manager->Update();
1550 minimap->Destroy();
1551 minimap = nullptr;
1552 minimap_enabled = false;
1553 }
1554}
1555
1556void GUI::UpdateMinimap(bool immediate) {
1557 if (IsMinimapVisible()) {
1558 if (immediate) {
1559 minimap->Refresh();
1560 } else {
1561 minimap->DelayedUpdate();
1562 }
1563 }
1564}
1565
1566bool GUI::IsMinimapVisible() const {
1567 if (minimap) {
1568 const wxAuiPaneInfo& pi = aui_manager->GetPane(minimap);
1569 if (pi.IsShown()) {
1570 return true;
1571 }
1572 }
1573 return false;
1574}
1575
1576//=============================================================================
1577
1578void GUI::RefreshView() {
1579 EditorTab* editorTab = GetCurrentTab();
1580 if (!editorTab) {
1581 return;
1582 }
1583
1584 if (!dynamic_cast<MapTab*>(editorTab)) {
1585 editorTab->GetWindow()->Refresh();
1586 return;
1587 }
1588
1589 std::vector<EditorTab*> editorTabs;
1590 for (int32_t index = 0; index < tabbook->GetTabCount(); ++index) {
1591 auto* mapTab = dynamic_cast<MapTab*>(tabbook->GetTab(index));
1592 if (mapTab) {
1593 editorTabs.push_back(mapTab);
1594 }
1595 }
1596
1597 for (EditorTab* editorTab : editorTabs) {
1598 editorTab->GetWindow()->Refresh();
1599 }
1600}
1601
1602void GUI::CreateLoadBar(wxString message, bool canCancel /* = false */) {
1603 progressText = message;
1604
1605 progressFrom = 0;
1606 progressTo = 100;
1607 currentProgress = -1;
1608
1609 progressBar = newd wxGenericProgressDialog("Loading", progressText + " (0%)", 100, root, wxPD_APP_MODAL | wxPD_SMOOTH | (canCancel ? wxPD_CAN_ABORT : 0));
1610 progressBar->SetSize(280, -1);
1611 progressBar->Show(true);
1612
1613 for (int idx = 0; idx < tabbook->GetTabCount(); ++idx) {
1614 auto* mt = dynamic_cast<MapTab*>(tabbook->GetTab(idx));
1615 if (mt && mt->GetEditor()->IsLiveServer()) {
1616 mt->GetEditor()->GetLiveServer()->startOperation(progressText);
1617 }
1618 }
1619 progressBar->Update(0);
1620}
1621
1622void GUI::SetLoadScale(int32_t from, int32_t to) {
1623 progressFrom = from;
1624 progressTo = to;
1625}
1626
1627bool GUI::SetLoadDone(int32_t done, const wxString& newMessage) {
1628 if (done == 100) {
1629 DestroyLoadBar();
1630 return true;
1631 } else if (done == currentProgress) {
1632 return true;
1633 }
1634
1635 if (!newMessage.empty()) {
1636 progressText = newMessage;
1637 }
1638
1639 int32_t newProgress = progressFrom + static_cast<int32_t>((done / 100.f) * (progressTo - progressFrom));
1640 newProgress = std::max<int32_t>(0, std::min<int32_t>(100, newProgress));
1641
1642 bool skip = false;
1643 if (progressBar) {
1644 progressBar->Update(
1645 newProgress,
1646 wxString::Format("%s (%d%%)", progressText, newProgress),
1647 &skip
1648 );
1649 currentProgress = newProgress;
1650 }
1651
1652 for (int32_t index = 0; index < tabbook->GetTabCount(); ++index) {
1653 auto* mapTab = dynamic_cast<MapTab*>(tabbook->GetTab(index));
1654 if (mapTab && mapTab->GetEditor()) {
1655 LiveServer* server = mapTab->GetEditor()->GetLiveServer();
1656 if (server) {
1657 server->updateOperation(newProgress);
1658 }
1659 }
1660 }
1661
1662 return skip;
1663}
1664
1665void GUI::DestroyLoadBar() {
1666 if (progressBar) {
1667 progressBar->Show(false);
1668 currentProgress = -1;
1669
1670 // Store pointer temporarily to avoid null issues
1671 wxGenericProgressDialog* tempBar = progressBar;
1672 progressBar = nullptr; // Set to null before destruction to prevent recursion
1673
1674 try {
1675 tempBar->Destroy();
1676 } catch(...) {
1677 // Ignore any exceptions during destroy
1678 }
1679
1680 if (root && root->IsActive()) {
1681 root->Raise();
1682 } else if (root) {
1683 root->RequestUserAttention();
1684 }
1685 }
1686}
1687
1688void GUI::ShowWelcomeDialog(const wxBitmap& icon) {
1689 std::vector<wxString> recent_files = root->GetRecentFiles();
1690 welcomeDialog = newd WelcomeDialog(__W_RME_APPLICATION_NAME__, "Version " + __W_RME_VERSION__, FROM_DIP(root, wxSize(1000, 480)), icon, recent_files);
1691 welcomeDialog->Bind(wxEVT_CLOSE_WINDOW, &GUI::OnWelcomeDialogClosed, this);
1692 welcomeDialog->Bind(WELCOME_DIALOG_ACTION, &GUI::OnWelcomeDialogAction, this);
1693 welcomeDialog->Show();
1694 UpdateMenubar();
1695}
1696
1697void GUI::FinishWelcomeDialog() {
1698 if (welcomeDialog != nullptr) {
1699 welcomeDialog->Hide();
1700 root->Show();
1701 welcomeDialog->Destroy();
1702 welcomeDialog = nullptr;
1703 }
1704}
1705
1706bool GUI::IsWelcomeDialogShown() {
1707 return welcomeDialog != nullptr && welcomeDialog->IsShown();
1708}
1709
1710void GUI::OnWelcomeDialogClosed(wxCloseEvent& event) {
1711 welcomeDialog->Destroy();
1712 root->Close();
1713}
1714
1715void GUI::OnWelcomeDialogAction(wxCommandEvent& event) {
1716 if (event.GetId() == wxID_NEW) {
1717 NewMap();
1718 } else if (event.GetId() == wxID_OPEN) {
1719 LoadMap(FileName(event.GetString()));
1720 }
1721}
1722
1723void GUI::UpdateMenubar() {
1724 root->UpdateMenubar();
1725}
1726
1727void GUI::SetScreenCenterPosition(Position position) {
1728 MapTab* mapTab = GetCurrentMapTab();
1729 if (mapTab) {
1730 // Store old position for comparison
1731 Position oldPosition = mapTab->GetScreenCenterPosition();
1732
1733 // Set the new position
1734 mapTab->SetScreenCenterPosition(position);
1735
1736 // Update minimap if the position changed significantly (e.g., teleport/goto)
1737 // or if the floor changed
1738 if (minimap && IsMinimapVisible() &&
1739 (abs(oldPosition.x - position.x) > 10 ||
1740 abs(oldPosition.y - position.y) > 10 ||
1741 oldPosition.z != position.z)) {
1742 minimap->Refresh();
1743 }
1744 }
1745}
1746
1747void GUI::DoCut() {
1748 if (!IsSelectionMode()) {
1749 return;
1750 }
1751
1752 Editor* editor = GetCurrentEditor();
1753 if (!editor) {
1754 return;
1755 }
1756
1757 editor->copybuffer.cut(*editor, GetCurrentFloor());
1758 RefreshView();
1759 root->UpdateMenubar();
1760}
1761
1762void GUI::DoCopy() {
1763 if (!IsSelectionMode()) {
1764 return;
1765 }
1766
1767 Editor* editor = GetCurrentEditor();
1768 if (!editor) {
1769 return;
1770 }
1771
1772 editor->copybuffer.copy(*editor, GetCurrentFloor());
1773 RefreshView();
1774 root->UpdateMenubar();
1775}
1776
1777void GUI::DoPaste() {
1778 MapTab* mapTab = GetCurrentMapTab();
1779 if (mapTab) {
1780 copybuffer.paste(*mapTab->GetEditor(), mapTab->GetCanvas()->GetCursorPosition());
1781 }
1782}
1783
1784void GUI::PreparePaste() {
1785 Editor* editor = GetCurrentEditor();
1786 if (editor) {
1787 SetSelectionMode();
1788 editor->selection.start();
1789 editor->selection.clear();
1790 editor->selection.finish();
1791 StartPasting();
1792 RefreshView();
1793 }
1794}
1795
1796void GUI::StartPasting() {
1797 if (GetCurrentEditor()) {
1798 pasting = true;
1799 secondary_map = &copybuffer.getBufferMap();
1800 }
1801}
1802
1803void GUI::EndPasting() {
1804 if (pasting) {
1805 pasting = false;
1806 secondary_map = nullptr;
1807 }
1808}
1809
1810bool GUI::CanUndo() {
1811 Editor* editor = GetCurrentEditor();
1812 return (editor && editor->actionQueue->canUndo());
1813}
1814
1815bool GUI::CanRedo() {
1816 Editor* editor = GetCurrentEditor();
1817 return (editor && editor->actionQueue->canRedo());
1818}
1819
1820bool GUI::DoUndo() {
1821 Editor* editor = GetCurrentEditor();
1822 if (editor && editor->actionQueue->canUndo()) {
1823 // Store the current mode before undoing
1824 EditorMode previous_mode = mode;
1825
1826 // Perform the undo operation
1827 editor->actionQueue->undo();
1828
1829 // Switch to selection mode if there's a selection
1830 if (editor->selection.size() > 0) {
1831 SetSelectionMode();
1832 }
1833 // If we were in drawing mode before and there's no selection now, restore drawing mode
1834 else if (previous_mode == DRAWING_MODE && editor->selection.size() == 0) {
1835 SetDrawingMode();
1836 }
1837
1838 SetStatusText("Undo action");
1839 UpdateMinimap();
1840 root->UpdateMenubar();
1841 root->Refresh();
1842 return true;
1843 }
1844 return false;
1845}
1846
1847bool GUI::DoRedo() {
1848 Editor* editor = GetCurrentEditor();
1849 if (editor && editor->actionQueue->canRedo()) {
1850 // Store the current mode before redoing
1851 EditorMode previous_mode = mode;
1852
1853 // Perform the redo operation
1854 editor->actionQueue->redo();
1855
1856 // Switch to selection mode if there's a selection
1857 if (editor->selection.size() > 0) {
1858 SetSelectionMode();
1859 }
1860 // If we were in drawing mode before and there's no selection now, restore drawing mode
1861 else if (previous_mode == DRAWING_MODE && editor->selection.size() == 0) {
1862 SetDrawingMode();
1863 }
1864
1865 SetStatusText("Redo action");
1866 UpdateMinimap();
1867 root->UpdateMenubar();
1868 root->Refresh();
1869 return true;
1870 }
1871 return false;
1872}
1873
1874int GUI::GetCurrentFloor() {
1875 MapTab* tab = GetCurrentMapTab();
1876 ASSERT(tab);
1877 return tab->GetCanvas()->GetFloor();
1878}
1879
1880void GUI::ChangeFloor(int new_floor) {
1881 MapTab* tab = GetCurrentMapTab();
1882 if (tab) {
1883 int old_floor = GetCurrentFloor();
1884 if (new_floor < 0 || new_floor > MAP_MAX_LAYER) {
1885 return;
1886 }
1887
1888 if (old_floor != new_floor) {
1889 tab->GetCanvas()->ChangeFloor(new_floor);
1890 // Only refresh minimap if it's visible - it will use cached blocks for the new floor
1891 if (minimap && IsMinimapVisible()) {
1892 minimap->SetMinimapFloor(new_floor);
1893 }
1894 }
1895 }
1896}
1897
1898void GUI::SetStatusText(wxString text) {
1899 g_gui.root->SetStatusText(text, 0);
1900}
1901
1902void GUI::SetTitle(wxString title) {
1903 if (g_gui.root == nullptr) {
1904 return;
1905 }
1906
1907#ifdef NIGHTLY_BUILD
1908 #ifdef SVN_BUILD
1909 #define TITLE_APPEND (wxString(" (Nightly Build #") << i2ws(SVN_BUILD) << ")")
1910 #else
1911 #define TITLE_APPEND (wxString(" (Nightly Build)"))
1912 #endif
1913#else
1914 #ifdef SVN_BUILD
1915 #define TITLE_APPEND (wxString(" (Build #") << i2ws(SVN_BUILD) << ")")
1916 #else
1917 #define TITLE_APPEND (wxString(""))
1918 #endif
1919#endif
1920#ifdef __EXPERIMENTAL__
1921 if (title != "") {
1922 g_gui.root->SetTitle(title << " - OTAcademy Map Editor BETA" << TITLE_APPEND);
1923 } else {
1924 g_gui.root->SetTitle(wxString("OTAcademy Map Editor BETA") << TITLE_APPEND);
1925 }
1926#elif __SNAPSHOT__
1927 if (title != "") {
1928 g_gui.root->SetTitle(title << " - OTAcademy Map Editor - SNAPSHOT" << TITLE_APPEND);
1929 } else {
1930 g_gui.root->SetTitle(wxString("OTAcademy Map Editor - SNAPSHOT") << TITLE_APPEND);
1931 }
1932#else
1933 if (!title.empty()) {
1934 g_gui.root->SetTitle(title << " - " << __W_RME_APPLICATION_NAME__ << TITLE_APPEND);
1935 } else {
1936 g_gui.root->SetTitle(__W_RME_APPLICATION_NAME__ << TITLE_APPEND);
1937 }
1938#endif
1939}
1940
1941void GUI::UpdateTitle() {
1942 if (tabbook->GetTabCount() > 0) {
1943 SetTitle(tabbook->GetCurrentTab()->GetTitle());
1944 for (int idx = 0; idx < tabbook->GetTabCount(); ++idx) {
1945 if (tabbook->GetTab(idx)) {
1946 tabbook->SetTabLabel(idx, tabbook->GetTab(idx)->GetTitle());
1947
1948 // Update detached view titles if this is a map tab
1949 if (auto* mapTab = dynamic_cast<MapTab*>(tabbook->GetTab(idx))) {
1950 UpdateDetachedViewsTitle(mapTab->GetEditor());
1951 }
1952 }
1953 }
1954 } else {
1955 SetTitle("");
1956 }
1957}
1958
1959void GUI::UpdateMenus() {
1960 wxCommandEvent evt(EVT_UPDATE_MENUS);
1961 g_gui.root->AddPendingEvent(evt);
1962}
1963
1964void GUI::ShowToolbar(ToolBarID id, bool show) {
1965 if (root && root->GetAuiToolBar()) {
1966 root->GetAuiToolBar()->Show(id, show);
1967 }
1968}
1969
1970void GUI::SwitchMode() {
1971 if (mode == DRAWING_MODE) {
1972 SetSelectionMode();
1973 } else {
1974 SetDrawingMode();
1975 }
1976}
1977
1978void GUI::SetSelectionMode() {
1979 if (mode == SELECTION_MODE) {
1980 return;
1981 }
1982
1983 if (current_brush && current_brush->isDoodad()) {
1984 secondary_map = nullptr;
1985 }
1986
1987 tabbook->OnSwitchEditorMode(SELECTION_MODE);
1988 mode = SELECTION_MODE;
1989}
1990
1991void GUI::SetDrawingMode() {
1992 if (mode == DRAWING_MODE) {
1993 return;
1994 }
1995
1996 std::set<MapTab*> al;
1997 for (int idx = 0; idx < tabbook->GetTabCount(); ++idx) {
1998 EditorTab* editorTab = tabbook->GetTab(idx);
1999 if (auto* mapTab = dynamic_cast<MapTab*>(editorTab)) {
2000 if (al.find(mapTab) != al.end()) {
2001 continue;
2002 }
2003
2004 Editor* editor = mapTab->GetEditor();
2005 editor->selection.start();
2006 //editor->selection.clear();
2007 editor->selection.finish();
2008 al.insert(mapTab);
2009 }
2010 }
2011
2012 if (current_brush && current_brush->isDoodad()) {
2013 secondary_map = doodad_buffer_map.get();
2014 } else {
2015 secondary_map = nullptr;
2016 }
2017
2018 tabbook->OnSwitchEditorMode(DRAWING_MODE);
2019 mode = DRAWING_MODE;
2020}
2021
2022void GUI::SetBrushSizeInternal(int nz) {
2023 if (nz != brush_size && current_brush && current_brush->isDoodad() && !current_brush->oneSizeFitsAll()) {
2024 brush_size = nz;
2025 FillDoodadPreviewBuffer();
2026 secondary_map = doodad_buffer_map.get();
2027 } else {
2028 brush_size = nz;
2029 }
2030}
2031
2032void GUI::SetBrushSize(int nz) {
2033 SetBrushSizeInternal(nz);
2034
2035 for (auto& palette : palettes) {
2036 palette->OnUpdateBrushSize(brush_shape, brush_size);
2037 }
2038
2039 root->GetAuiToolBar()->UpdateBrushSize(brush_shape, brush_size);
2040}
2041
2042void GUI::SetBrushVariation(int nz) {
2043 if (nz != brush_variation && current_brush && current_brush->isDoodad()) {
2044 // Monkey!
2045 brush_variation = nz;
2046 FillDoodadPreviewBuffer();
2047 secondary_map = doodad_buffer_map.get();
2048 }
2049}
2050
2051void GUI::SetBrushShape(BrushShape bs) {
2052 if (bs != brush_shape && current_brush && current_brush->isDoodad() && !current_brush->oneSizeFitsAll()) {
2053 // Donkey!
2054 brush_shape = bs;
2055 FillDoodadPreviewBuffer();
2056 secondary_map = doodad_buffer_map.get();
2057 }
2058 brush_shape = bs;
2059
2060 for (auto& palette : palettes) {
2061 palette->OnUpdateBrushSize(brush_shape, brush_size);
2062 }
2063
2064 root->GetAuiToolBar()->UpdateBrushSize(brush_shape, brush_size);
2065}
2066
2067void GUI::SetBrushThickness(bool on, int x, int y) {
2068 use_custom_thickness = on;
2069
2070 if (x != -1 || y != -1) {
2071 custom_thickness_mod = float(max(x, 1)) / float(max(y, 1));
2072 }
2073
2074 if (current_brush && current_brush->isDoodad()) {
2075 FillDoodadPreviewBuffer();
2076 }
2077
2078 RefreshView();
2079}
2080
2081void GUI::SetBrushThickness(int low, int ceil) {
2082 custom_thickness_mod = float(max(low, 1)) / float(max(ceil, 1));
2083
2084 if (use_custom_thickness && current_brush && current_brush->isDoodad()) {
2085 FillDoodadPreviewBuffer();
2086 }
2087
2088 RefreshView();
2089}
2090
2091void GUI::DecreaseBrushSize(bool wrap) {
2092 switch (brush_size) {
2093 case 0: {
2094 if (wrap) {
2095 SetBrushSize(11);
2096 }
2097 break;
2098 }
2099 case 1: {
2100 SetBrushSize(0);
2101 break;
2102 }
2103 case 2:
2104 case 3: {
2105 SetBrushSize(1);
2106 break;
2107 }
2108 case 4:
2109 case 5: {
2110 SetBrushSize(2);
2111 break;
2112 }
2113 case 6:
2114 case 7: {
2115 SetBrushSize(4);
2116 break;
2117 }
2118 case 8:
2119 case 9:
2120 case 10: {
2121 SetBrushSize(6);
2122 break;
2123 }
2124 case 11:
2125 default: {
2126 SetBrushSize(8);
2127 break;
2128 }
2129 }
2130}
2131
2132void GUI::IncreaseBrushSize(bool wrap) {
2133 switch (brush_size) {
2134 case 0: {
2135 SetBrushSize(1);
2136 break;
2137 }
2138 case 1: {
2139 SetBrushSize(2);
2140 break;
2141 }
2142 case 2:
2143 case 3: {
2144 SetBrushSize(4);
2145 break;
2146 }
2147 case 4:
2148 case 5: {
2149 SetBrushSize(6);
2150 break;
2151 }
2152 case 6:
2153 case 7: {
2154 SetBrushSize(8);
2155 break;
2156 }
2157 case 8:
2158 case 9:
2159 case 10: {
2160 SetBrushSize(11);
2161 break;
2162 }
2163 case 11:
2164 default: {
2165 if (wrap) {
2166 SetBrushSize(0);
2167 }
2168 break;
2169 }
2170 }
2171}
2172
2173void GUI::SetDoorLocked(bool on) {
2174 draw_locked_doors = on;
2175 RefreshView();
2176}
2177
2178bool GUI::HasDoorLocked() {
2179 return draw_locked_doors;
2180}
2181
2182Brush* GUI::GetCurrentBrush() const {
2183 return current_brush;
2184}
2185
2186BrushShape GUI::GetBrushShape() const {
2187 if (current_brush == spawn_brush) {
2188 return BRUSHSHAPE_SQUARE;
2189 }
2190
2191 return brush_shape;
2192}
2193
2194int GUI::GetBrushSize() const {
2195 return brush_size;
2196}
2197
2198int GUI::GetBrushVariation() const {
2199 return brush_variation;
2200}
2201
2202int GUI::GetSpawnTime() const {
2203 return creature_spawntime;
2204}
2205
2206void GUI::SelectBrush() {
2207 if (palettes.empty()) {
2208 return;
2209 }
2210
2211 SelectBrushInternal(palettes.front()->GetSelectedBrush());
2212
2213 RefreshView();
2214}
2215
2216bool GUI::SelectBrush(const Brush* whatbrush, PaletteType primary) {
2217 if (palettes.empty()) {
2218 if (!CreatePalette()) {
2219 return false;
2220 }
2221 }
2222
2223 if (!palettes.front()->OnSelectBrush(whatbrush, primary)) {
2224 return false;
2225 }
2226
2227 SelectBrushInternal(const_cast<Brush*>(whatbrush));
2228 root->GetAuiToolBar()->UpdateBrushButtons();
2229 return true;
2230}
2231
2232void GUI::SelectBrushInternal(Brush* brush) {
2233 // Store previous brush before changing current brush
2234 previous_brush = current_brush;
2235 current_brush = brush;
2236 if (!current_brush) {
2237 return;
2238 }
2239
2240 brush_variation = min(brush_variation, brush->getMaxVariation());
2241 FillDoodadPreviewBuffer();
2242 if (brush->isDoodad()) {
2243 secondary_map = doodad_buffer_map.get();
2244 }
2245
2246 SetDrawingMode();
2247 RefreshView();
2248}
2249
2250void GUI::SelectPreviousBrush() {
2251 if (previous_brush) {
2252 SelectBrush(previous_brush);
2253 }
2254}
2255
2256void GUI::FillDoodadPreviewBuffer() {
2257 if (!current_brush || !current_brush->isDoodad()) {
2258 return;
2259 }
2260
2261 doodad_buffer_map.get()->clear();
2262
2263 DoodadBrush* brush = current_brush->asDoodad();
2264 if (brush->isEmpty(GetBrushVariation())) {
2265 return;
2266 }
2267
2268 int object_count = 0;
2269 int area;
2270 if (GetBrushShape() == BRUSHSHAPE_SQUARE) {
2271 area = 2 * GetBrushSize();
2272 area = area * area + 1;
2273 } else {
2274 if (GetBrushSize() == 1) {
2275 // There is a huge deviation here with the other formula.
2276 area = 5;
2277 } else {
2278 area = int(0.5 + GetBrushSize() * GetBrushSize() * PI);
2279 }
2280 }
2281 const int object_range = (use_custom_thickness ? int(area * custom_thickness_mod) : brush->getThickness() * area / max(1, brush->getThicknessCeiling()));
2282 const int final_object_count = max(1, object_range + random(object_range));
2283
2284 // Starting position for the center of the preview
2285 Position center_pos(0x8000, 0x8000, 0x8);
2286
2287 if (brush_size > 0 && !brush->oneSizeFitsAll()) {
2288 while (object_count < final_object_count) {
2289 int retries = 0;
2290 bool exit = false;
2291
2292 // Try to place objects 5 times
2293 while (retries < 5 && !exit) {
2294
2295 int pos_retries = 0;
2296 int xpos = 0, ypos = 0;
2297 bool found_pos = false;
2298 if (GetBrushShape() == BRUSHSHAPE_CIRCLE) {
2299 while (pos_retries < 5 && !found_pos) {
2300 xpos = random(-brush_size, brush_size);
2301 ypos = random(-brush_size, brush_size);
2302 float distance = sqrt(float(xpos * xpos) + float(ypos * ypos));
2303 if (distance < g_gui.GetBrushSize() + 0.005) {
2304 found_pos = true;
2305 } else {
2306 ++pos_retries;
2307 }
2308 }
2309 } else {
2310 found_pos = true;
2311 xpos = random(-brush_size, brush_size);
2312 ypos = random(-brush_size, brush_size);
2313 }
2314
2315 if (!found_pos) {
2316 ++retries;
2317 continue;
2318 }
2319
2320 // Decide whether the zone should have a composite or several single objects.
2321 bool fail = false;
2322 if (random(brush->getTotalChance(GetBrushVariation())) <= brush->getCompositeChance(GetBrushVariation())) {
2323 // Composite
2324 const CompositeTileList& composites = brush->getComposite(GetBrushVariation());
2325
2326 // Figure out if the placement is valid
2327 for (const auto& composite : composites) {
2328 // Include the z offset in the position calculation
2329 Position pos = center_pos + Position(composite.first.x + xpos, composite.first.y + ypos, composite.first.z);
2330 if (Tile* tile = doodad_buffer_map.get()->getTile(pos)) {
2331 if (!tile->empty()) {
2332 fail = true;
2333 break;
2334 }
2335 }
2336 }
2337 if (fail) {
2338 ++retries;
2339 break;
2340 }
2341
2342 // Transfer items to the stack
2343 for (const auto& composite : composites) {
2344 // Include the z offset in the position calculation
2345 Position pos = center_pos + Position(composite.first.x + xpos, composite.first.y + ypos, composite.first.z);
2346 const ItemVector& items = composite.second;
2347 Tile* tile = doodad_buffer_map.get()->getTile(pos);
2348
2349 if (!tile) {
2350 tile = doodad_buffer_map.get()->allocator(doodad_buffer_map.get()->createTileL(pos));
2351 }
2352
2353 for (auto item : items) {
2354 tile->addItem(item->deepCopy());
2355 }
2356 doodad_buffer_map.get()->setTile(tile->getPosition(), tile);
2357 }
2358 exit = true;
2359 } else if (brush->hasSingleObjects(GetBrushVariation())) {
2360 Position pos = center_pos + Position(xpos, ypos, 0);
2361 Tile* tile = doodad_buffer_map.get()->getTile(pos);
2362 if (tile) {
2363 if (!tile->empty()) {
2364 fail = true;
2365 break;
2366 }
2367 } else {
2368 tile = doodad_buffer_map.get()->allocator(doodad_buffer_map.get()->createTileL(pos));
2369 }
2370 int variation = GetBrushVariation();
2371 brush->draw(doodad_buffer_map.get(), tile, &variation);
2372 // std::cout << "\tpos: " << tile->getPosition() << std::endl;
2373 doodad_buffer_map.get()->setTile(tile->getPosition(), tile);
2374 exit = true;
2375 }
2376 if (fail) {
2377 ++retries;
2378 break;
2379 }
2380 }
2381 ++object_count;
2382 }
2383 } else {
2384 if (brush->hasCompositeObjects(GetBrushVariation()) && random(brush->getTotalChance(GetBrushVariation())) <= brush->getCompositeChance(GetBrushVariation())) {
2385 // Composite
2386 const CompositeTileList& composites = brush->getComposite(GetBrushVariation());
2387
2388 // All placement is valid...
2389
2390 // Transfer items to the buffer
2391 for (const auto& composite : composites) {
2392 // Include the z offset in the position calculation
2393 Position pos = center_pos + composite.first;
2394 const ItemVector& items = composite.second;
2395 Tile* tile = doodad_buffer_map.get()->allocator(doodad_buffer_map.get()->createTileL(pos));
2396 // std::cout << pos << " = " << center_pos << " + " << buffer_tile->getPosition() << std::endl;
2397
2398 for (auto item : items) {
2399 tile->addItem(item->deepCopy());
2400 }
2401 doodad_buffer_map.get()->setTile(tile->getPosition(), tile);
2402 }
2403 } else if (brush->hasSingleObjects(GetBrushVariation())) {
2404 Tile* tile = doodad_buffer_map.get()->allocator(doodad_buffer_map.get()->createTileL(center_pos));
2405 int variation = GetBrushVariation();
2406 brush->draw(doodad_buffer_map.get(), tile, &variation);
2407 doodad_buffer_map.get()->setTile(center_pos, tile);
2408 }
2409 }
2410}
2411
2412long GUI::PopupDialog(wxWindow* parent, wxString title, wxString text, long style, wxString confisavename, uint32_t configsavevalue) {
2413 if (text.empty()) {
2414 return wxID_ANY;
2415 }
2416
2417 wxMessageDialog dlg(parent, text, title, style);
2418 return dlg.ShowModal();
2419}
2420
2421long GUI::PopupDialog(wxString title, wxString text, long style, wxString configsavename, uint32_t configsavevalue) {
2422 return g_gui.PopupDialog(g_gui.root, title, text, style, configsavename, configsavevalue);
2423}
2424
2425void GUI::ListDialog(wxWindow* parent, wxString title, const wxArrayString& param_items) {
2426 if (param_items.empty()) {
2427 return;
2428 }
2429
2430 wxArrayString list_items(param_items);
2431
2432 // Create the window
2433 wxDialog* dlg = newd wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxRESIZE_BORDER | wxCAPTION | wxCLOSE_BOX);
2434
2435 wxSizer* sizer = newd wxBoxSizer(wxVERTICAL);
2436 wxListBox* item_list = newd wxListBox(dlg, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, nullptr, wxLB_SINGLE);
2437 item_list->SetMinSize(wxSize(500, 300));
2438
2439 for (size_t i = 0; i != list_items.GetCount();) {
2440 wxString str = list_items[i];
2441 size_t pos = str.find("\n");
2442 if (pos != wxString::npos) {
2443 // Split string!
2444 item_list->Append(str.substr(0, pos));
2445 list_items[i] = str.substr(pos + 1);
2446 continue;
2447 }
2448 item_list->Append(list_items[i]);
2449 ++i;
2450 }
2451 sizer->Add(item_list, 1, wxEXPAND);
2452
2453 wxSizer* stdsizer = newd wxBoxSizer(wxHORIZONTAL);
2454 stdsizer->Add(newd wxButton(dlg, wxID_OK, "OK"), wxSizerFlags(1).Center());
2455 sizer->Add(stdsizer, wxSizerFlags(0).Center());
2456
2457 dlg->SetSizerAndFit(sizer);
2458
2459 // Show the window
2460 dlg->ShowModal();
2461 delete dlg;
2462}
2463
2464void GUI::ShowTextBox(wxWindow* parent, wxString title, wxString content) {
2465 wxDialog* dlg = newd wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxRESIZE_BORDER | wxCAPTION | wxCLOSE_BOX);
2466 wxSizer* topsizer = newd wxBoxSizer(wxVERTICAL);
2467 wxTextCtrl* text_field = newd wxTextCtrl(dlg, wxID_ANY, content, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY);
2468 text_field->SetMinSize(wxSize(400, 550));
2469 topsizer->Add(text_field, wxSizerFlags(5).Expand());
2470
2471 wxSizer* choicesizer = newd wxBoxSizer(wxHORIZONTAL);
2472 choicesizer->Add(newd wxButton(dlg, wxID_CANCEL, "OK"), wxSizerFlags(1).Center());
2473 topsizer->Add(choicesizer, wxSizerFlags(0).Center());
2474 dlg->SetSizerAndFit(topsizer);
2475
2476 dlg->ShowModal();
2477}
2478
2479void GUI::SetHotkey(int index, Hotkey& hotkey) {
2480 ASSERT(index >= 0 && index <= 9);
2481 hotkeys[index] = hotkey;
2482 SetStatusText("Set hotkey " + i2ws(index) + ".");
2483}
2484
2485const Hotkey& GUI::GetHotkey(int index) const {
2486 ASSERT(index >= 0 && index <= 9);
2487 return hotkeys[index];
2488}
2489
2490void GUI::SaveHotkeys() const {
2491 std::ostringstream os;
2492 for (const auto& hotkey : hotkeys) {
2493 os << hotkey << '\n';
2494 }
2495 g_settings.setString(Config::NUMERICAL_HOTKEYS, os.str());
2496}
2497
2498void GUI::LoadHotkeys() {
2499 std::istringstream is;
2500 is.str(g_settings.getString(Config::NUMERICAL_HOTKEYS));
2501
2502 std::string line;
2503 int index = 0;
2504 while (getline(is, line)) {
2505 std::istringstream line_is;
2506 line_is.str(line);
2507 line_is >> hotkeys[index];
2508
2509 ++index;
2510 }
2511}
2512
2513Hotkey::Hotkey() :
2514 type(NONE) {
2515 ////
2516}
2517
2518Hotkey::Hotkey(Position _pos) :
2519 type(POSITION), pos(_pos) {
2520 ////
2521}
2522
2523Hotkey::Hotkey(Brush* brush) :
2524 type(BRUSH), brushname(brush->getName()) {
2525 ////
2526}
2527
2528Hotkey::Hotkey(std::string _name) :
2529 type(BRUSH), brushname(_name) {
2530 ////
2531}
2532
2533Hotkey::~Hotkey() {
2534 ////
2535}
2536
2537std::ostream& operator<<(std::ostream& os, const Hotkey& hotkey) {
2538 switch (hotkey.type) {
2539 case Hotkey::POSITION: {
2540 os << "pos:{" << hotkey.pos << "}";
2541 } break;
2542 case Hotkey::BRUSH: {
2543 if (hotkey.brushname.find('{') != std::string::npos || hotkey.brushname.find('}') != std::string::npos) {
2544 break;
2545 }
2546 os << "brush:{" << hotkey.brushname << "}";
2547 } break;
2548 default: {
2549 os << "none:{}";
2550 } break;
2551 }
2552 return os;
2553}
2554
2555std::istream& operator>>(std::istream& is, Hotkey& hotkey) {
2556 std::string type;
2557 getline(is, type, ':');
2558 if (type == "none") {
2559 is.ignore(2); // ignore "{}"
2560 } else if (type == "pos") {
2561 is.ignore(1); // ignore "{"
2562 Position pos;
2563 is >> pos;
2564 hotkey = Hotkey(pos);
2565 is.ignore(1); // ignore "}"
2566 } else if (type == "brush") {
2567 is.ignore(1); // ignore "{"
2568 std::string brushname;
2569 getline(is, brushname, '}');
2570 hotkey = Hotkey(brushname);
2571 } else {
2572 // Do nothing...
2573 }
2574
2575 return is;
2576}
2577
2578void SetWindowToolTip(wxWindow* a, const wxString& tip) {
2579 a->SetToolTip(tip);
2580}
2581
2582void SetWindowToolTip(wxWindow* a, wxWindow* b, const wxString& tip) {
2583 a->SetToolTip(tip);
2584 b->SetToolTip(tip);
2585}
2586
2587int GUI::GetHotkeyEventId(const std::string& action) {
2588 // Convert action string to menu event ID
2589 // This needs to be implemented based on your menu system
2590 // Return -1 if no matching action is found
2591 return -1; // Temporary return until implementation
2592}
2593
2594void GUI::CheckAutoSave() {
2595 uint32_t now = time(nullptr);
2596
2597 // Only check once per second
2598 if (now - last_autosave_check < 1) {
2599 return;
2600 }
2601 last_autosave_check = now;
2602
2603 if (!g_settings.getBoolean(Config::AUTO_SAVE_ENABLED)) {
2604 //OutputDebugStringA("Autosave disabled\n");
2605 return;
2606 }
2607
2608 if (!IsEditorOpen()) {
2609 OutputDebugStringA("No editor open - skipping autosave check\n");
2610 return;
2611 }
2612
2613 uint32_t interval = g_settings.getInteger(Config::AUTO_SAVE_INTERVAL); // Already in seconds
2614 uint32_t time_passed = now - last_autosave;
2615
2616 char debug_buffer[256];
2617 sprintf(debug_buffer, "Autosave check - Time passed: %u seconds, Interval: %u seconds, Next save in: %u seconds\n",
2618 time_passed, interval, interval - (time_passed % interval));
2619 OutputDebugStringA(debug_buffer);
2620
2621 if (now - last_autosave >= interval) {
2622 Editor* editor = GetCurrentEditor();
2623 if (editor) {
2624 OutputDebugStringA("Performing autosave...\n");
2625
2626 // Create autosave directory in RME data folder
2627 wxString data_dir = GetDataDirectory();
2628 wxString autosave_dir = data_dir + "maps/autosave/";
2629
2630 if (!wxDirExists(data_dir + "maps")) {
2631 wxMkdir(data_dir + "maps", wxS_DIR_DEFAULT);
2632 }
2633 if (!wxDirExists(autosave_dir)) {
2634 wxMkdir(autosave_dir, wxS_DIR_DEFAULT);
2635 }
2636
2637 // Create autosave filename based on current map name
2638 FileName current = wxstr(editor->map.getName());
2639 wxString name = current.GetName();
2640 if (name.empty()) {
2641 name = "untitled";
2642 } else {
2643 // Remove any existing autosave timestamps from the name
2644 size_t pos;
2645 while ((pos = name.find("_autosave_")) != wxString::npos) {
2646 // Find the end of the timestamp (next underscore or end of string)
2647 size_t end = name.find("_", pos + 10); // Skip past "_autosave_"
2648 if (end == wxString::npos) {
2649 // If no underscore found, remove everything after _autosave_
2650 name = name.substr(0, pos);
2651 } else {
2652 // Remove the _autosave_ and timestamp portion
2653 name = name.substr(0, pos);
2654 }
2655 }
2656
2657 /*
2658 } else {
2659 // Strip any existing autosave timestamps from the name
2660 wxString pattern = "_autosave_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}";
2661 wxRegEx regex(pattern);
2662 while (regex.Matches(name)) {
2663 size_t start, len;
2664 regex.GetMatch(&start, &len);
2665 name = name.Mid(0, start) + name.Mid(start + len);
2666 }
2667 }
2668
2669 */
2670 }
2671
2672 wxString ext = current.GetExt();
2673 if (ext.empty()) {
2674 ext = "otbm";
2675 }
2676
2677 wxString autosave_name = autosave_dir + name + "_autosave_" +
2678 wxDateTime::Now().Format("%Y-%m-%d_%H-%M-%S") + "." + ext;
2679
2680 OutputDebugStringA("Saving to: ");
2681 OutputDebugStringA(autosave_name.c_str());
2682 OutputDebugStringA("\n");
2683
2684 // Use existing SaveCurrentMap with our autosave filename
2685 SaveCurrentMap(autosave_name, false);
2686 last_autosave = now;
2687 OutputDebugStringA("Autosave complete\n");
2688 }
2689 }
2690}
2691
2692void GUI::ApplyDarkMode() {
2693 if (root) {
2694 g_darkMode.ApplyTheme(root);
2695
2696 // Apply to all floating windows and panes
2697 if (aui_manager) {
2698 wxAuiPaneInfoArray& panes = aui_manager->GetAllPanes();
2699 for (size_t i = 0; i < panes.GetCount(); ++i) {
2700 if (panes[i].window) {
2701 g_darkMode.ApplyTheme(panes[i].window);
2702 }
2703 }
2704 aui_manager->Update();
2705 }
2706
2707 // Apply to the menu bar
2708 if (MainMenuBar* menuBar = dynamic_cast<MainMenuBar*>(root->GetMenuBar())) {
2709 g_darkMode.ApplyThemeToMainMenuBar(menuBar);
2710 }
2711
2712 // Apply to minimap if it exists
2713 if (minimap) {
2714 g_darkMode.ApplyTheme(minimap);
2715 }
2716
2717 // Apply to search window if it exists
2718 if (search_result_window) {
2719 g_darkMode.ApplyTheme(search_result_window);
2720 }
2721
2722 // Apply to all palette windows
2723 for (PaletteWindow* palette : palettes) {
2724 if (palette) {
2725 g_darkMode.ApplyTheme(palette);
2726 }
2727 }
2728 }
2729}
2730
2731// Detached views management
2732void GUI::RegisterDetachedView(Editor* editor, wxFrame* frame) {
2733 // Add the frame to the list of detached views for this editor
2734 detached_views[editor].push_back(frame);
2735
2736 // Store a pointer to our detached_views map in the frame to allow proper cleanup
2737 // We'll use a custom event handler to check if the editor is still valid
2738 frame->Bind(wxEVT_IDLE, [this, editor, frame](wxIdleEvent& event) {
2739 // Check if the editor is still in our map (it may have been deleted)
2740 if (detached_views.find(editor) == detached_views.end()) {
2741 // Editor was deleted, close this detached view
2742 frame->Close(true);
2743 }
2744 event.Skip();
2745 });
2746}
2747
2748void GUI::RegisterDockableView(Editor* editor, MapWindow* window) {
2749 // Skip registration if editor or window is null
2750 if (!editor || !window)
2751 return;
2752
2753 // Add the dockable window to the list for this editor
2754 dockable_views[editor].push_back(window);
2755
2756 // Add a handler to check for editor validity periodically
2757 window->Bind(wxEVT_IDLE, [this, editor, window](wxIdleEvent& event) {
2758 // Check if the editor is still valid (might have been deleted)
2759 bool editorExists = false;
2760 for (int i = 0; i < tabbook->GetTabCount(); ++i) {
2761 auto* mapTab = dynamic_cast<MapTab*>(tabbook->GetTab(i));
2762 if (mapTab && mapTab->GetEditor() == editor) {
2763 editorExists = true;
2764 break;
2765 }
2766 }
2767
2768 // If editor no longer exists, close this window
2769 if (!editorExists && dockable_views.find(editor) != dockable_views.end()) {
2770 if (aui_manager->GetPane(window).IsOk()) {
2771 aui_manager->DetachPane(window);
2772 window->Destroy();
2773 }
2774 // The destroy event will trigger UnregisterDockableView
2775 }
2776
2777 event.Skip();
2778 });
2779}
2780
2781void GUI::UnregisterDetachedView(Editor* editor, wxFrame* frame) {
2782 // Remove the frame from the list of detached views for this editor
2783 if (detached_views.find(editor) != detached_views.end()) {
2784 detached_views[editor].remove(frame);
2785
2786 // If the list is now empty, remove the editor from the map
2787 if (detached_views[editor].empty()) {
2788 detached_views.erase(editor);
2789 }
2790 }
2791}
2792
2793void GUI::UnregisterDockableView(Editor* editor, MapWindow* window) {
2794 // Remove the window from the list of dockable views for this editor
2795 if (dockable_views.find(editor) != dockable_views.end()) {
2796 dockable_views[editor].remove(window);
2797
2798 // If the list is now empty, remove the editor from the map
2799 if (dockable_views[editor].empty()) {
2800 dockable_views.erase(editor);
2801 }
2802 }
2803}
2804
2805bool GUI::HasDetachedViews(Editor* editor) const {
2806 // Check if the editor has any detached views or dockable panels
2807 auto detached_it = detached_views.find(editor);
2808 auto dockable_it = dockable_views.find(editor);
2809
2810 return (detached_it != detached_views.end() && !detached_it->second.empty()) ||
2811 (dockable_it != dockable_views.end() && !dockable_it->second.empty());
2812}
2813
2814bool GUI::CloseDetachedViews(Editor* editor) {
2815 bool had_views = false;
2816
2817 // Close all detached frame views for the given editor
2818 auto frame_it = detached_views.find(editor);
2819 if (frame_it != detached_views.end()) {
2820 // Make a copy of the list since closing frames will modify the original
2821 std::list<wxFrame*> frames_to_close = frame_it->second;
2822
2823 for (wxFrame* frame : frames_to_close) {
2824 // Force close the frame immediately instead of destroy
2825 // This ensures synchronous closing rather than asynchronous destruction
2826 frame->Close(true);
2827 }
2828
2829 // Clear the list
2830 detached_views.erase(editor);
2831 had_views = true;
2832 }
2833
2834 // Close all dockable panel views for the given editor
2835 auto dock_it = dockable_views.find(editor);
2836 if (dock_it != dockable_views.end()) {
2837 // Make a copy of the list since closing windows will modify the original
2838 std::list<MapWindow*> windows_to_close = dock_it->second;
2839
2840 for (MapWindow* window : windows_to_close) {
2841 // For dockable panels, we need to remove them from the AUI manager
2842 if (aui_manager->GetPane(window).IsOk()) {
2843 aui_manager->DetachPane(window);
2844 window->Destroy();
2845 }
2846 }
2847
2848 // Clear the list
2849 dockable_views.erase(editor);
2850 had_views = true;
2851
2852 // Update the AUI manager to reflect the changes
2853 aui_manager->Update();
2854 }
2855
2856 // Process any pending events to ensure everything is closed
2857 wxTheApp->ProcessPendingEvents();
2858
2859 return had_views;
2860}
2861
2862void GUI::UpdateDetachedViewsTitle(Editor* editor) {
2863 // Update titles of all detached views for this editor
2864 auto it = detached_views.find(editor);
2865 if (it != detached_views.end()) {
2866 for (wxFrame* frame : it->second) {
2867 wxString title = frame->GetTitle();
2868 if (title.Contains("Always-on-top View:")) {
2869 frame->SetTitle(wxString::Format("Always-on-top View: %s", wxstr(editor->map.getName())));
2870 } else {
2871 frame->SetTitle(wxString::Format("Detached View: %s", wxstr(editor->map.getName())));
2872 }
2873 }
2874 }
2875}
2876
2877void GUI::StoreSearchState(uint16_t itemId, bool onSelection) {
2878 has_last_search = true;
2879 last_search_itemid = itemId;
2880 last_search_on_selection = onSelection;
2881
2882 if (search_result_window) {
2883 last_ignored_ids_text = search_result_window->GetIgnoredItemsText();
2884 last_ignored_ids_enabled = search_result_window->IsIgnoreListEnabled();
2885 }
2886
2887 OutputDebugStringA(wxString::Format("GUI::StoreSearchState - Stored search for item ID %d, ignore list %s\n",
2888 last_search_itemid, last_ignored_ids_enabled ? "enabled" : "disabled").c_str());
2889}
2890
2891void GUI::RestoreSearchState(SearchResultWindow* window) {
2892 if (!window || !has_last_search)
2893 return;
2894
2895 window->SetIgnoredIds(last_ignored_ids_text, last_ignored_ids_enabled);
2896
2897 OutputDebugStringA(wxString::Format("GUI::RestoreSearchState - Restored search for item ID %d\n", last_search_itemid).c_str());
2898}
2899
2900uint16_t GUI::GetCurrentActionID() const {
2901 PaletteWindow* palette = GetPalette();
2902 if (palette) {
2903 return palette->GetActionID();
2904 }
2905 return 0;
2906}
2907
2908bool GUI::IsCurrentActionIDEnabled() const {
2909 PaletteWindow* palette = GetPalette();
2910 if (palette) {
2911 return palette->IsActionIDEnabled();
2912 }
2913 return false;
2914}
2915
Top