Ember
Loading...
Searching...
No Matches
FileExplorerTab.cpp
Go to the documentation of this file.
3#include "Utils/Logger.h"
5#include <algorithm>
6#include <functional>
7#include <limits>
8#include <vector>
9#include <wx/artprov.h>
10#include <wx/bmpbuttn.h>
11#include <wx/button.h>
12#include <wx/dnd.h>
13#include <wx/popupwin.h>
14#include <wx/sizer.h>
15#include <wx/statbmp.h>
16#include <wx/statline.h>
17#include <wx/stattext.h>
18#include <wx/stdpaths.h>
19#include <wx/tglbtn.h>
20
21// ============================================================================
22// ICON CUSTOMIZATION GUIDE
23// ============================================================================
24// To replace built-in icons with custom ones:
25// 1. Place your custom PNG icons in: resources/icons/
26// 2. Modify LoadGridIcon() and LoadTreeIcon() methods below
27// 3. Example icon names: folder.png, file.png, xml.png, png.png, json.png
28// 4. Recommended sizes: Grid icons 96x96, Tree icons 16x16
29// ============================================================================
30
31// Custom tree item data to store file paths
32class FileTreeItemData : public wxTreeItemData {
33 public:
34 FileTreeItemData(const wxString &path, bool isDir) : m_path(path), m_isDirectory(isDir) {}
35
36 wxString GetPath() const { return m_path; }
37 bool IsDirectory() const { return m_isDirectory; }
38
39 private:
40 wxString m_path;
42};
43
44// ============================================================================
45// Navigation History Popup Window
46// ============================================================================
47
48class HistoryPopup : public wxPopupTransientWindow {
49 public:
50 HistoryPopup(wxWindow *parent, const std::vector<wxString> &backStack, const std::vector<wxString> &forwardStack,
51 const wxString &currentPath, std::function<void(const wxString &)> onNavigate)
52 : wxPopupTransientWindow(parent, wxBORDER_SIMPLE), m_onNavigate(onNavigate),
53 m_hoveredIndex(std::numeric_limits<size_t>::max()) {
54 SetBackgroundColour(wxColour(45, 45, 45));
55
56 wxBoxSizer *mainSizer = new wxBoxSizer(wxVERTICAL);
57
58 // Build items list (back stack reversed + current + forward stack)
59 // Back stack (most recent first)
60 for (int i = backStack.size() - 1; i >= 0; i--) {
61 if (m_items.size() >= 15)
62 break;
63 m_items.push_back({backStack[i], ItemType::BACK});
64 }
65
66 // Current location
67 m_items.push_back({currentPath, ItemType::CURRENT});
68 size_t currentIndex = m_items.size() - 1;
69
70 // Forward stack (in order)
71 for (size_t i = 0; i < forwardStack.size() && m_items.size() < 15; i++) {
72 m_items.push_back({forwardStack[forwardStack.size() - 1 - i], ItemType::FORWARD});
73 }
74
75 // Create scrolled window for items
76 wxScrolledWindow *scrollWin = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
77 scrollWin->SetScrollRate(0, 10);
78 scrollWin->SetBackgroundColour(wxColour(45, 45, 45));
79
80 wxBoxSizer *itemsSizer = new wxBoxSizer(wxVERTICAL);
81
82 // Add header
83 wxStaticText *header = new wxStaticText(scrollWin, wxID_ANY, "Navigation History");
84 header->SetForegroundColour(wxColour(220, 220, 220));
85 wxFont headerFont = header->GetFont();
86 headerFont.SetWeight(wxFONTWEIGHT_BOLD);
87 headerFont.SetPointSize(headerFont.GetPointSize() + 1);
88 header->SetFont(headerFont);
89 itemsSizer->Add(header, 0, wxALL | wxALIGN_CENTER, 8);
90
91 // Add separator
92 wxStaticLine *separator = new wxStaticLine(scrollWin, wxID_ANY);
93 itemsSizer->Add(separator, 0, wxEXPAND | wxLEFT | wxRIGHT, 5);
94
95 // Add items
96 for (size_t i = 0; i < m_items.size(); i++) {
97 wxPanel *itemPanel = CreateHistoryItem(scrollWin, m_items[i], i, i == currentIndex);
98 itemsSizer->Add(itemPanel, 0, wxEXPAND);
99 }
100
101 scrollWin->SetSizer(itemsSizer);
102 scrollWin->SetMinSize(wxSize(400, 200));
103 mainSizer->Add(scrollWin, 1, wxEXPAND);
104
105 SetSizer(mainSizer);
106
107 // Force layout before calculating size
108 Layout();
109
110 // Calculate size based on content (larger size for better visibility)
111 wxSize bestSize = scrollWin->GetBestSize();
112 int width = wxMax(400, wxMin(500, bestSize.GetWidth() + 40));
113 int height = wxMax(250, wxMin(500, bestSize.GetHeight() + 40));
114
115 SetClientSize(wxSize(width, height));
116 }
117
118 private:
119 enum class ItemType { BACK, CURRENT, FORWARD };
120
121 struct HistoryItem {
122 wxString path;
124 };
125
126 wxPanel *CreateHistoryItem(wxWindow *parent, const HistoryItem &item, size_t index, bool isCurrent) {
127 wxPanel *panel = new wxPanel(parent, wxID_ANY);
128 panel->SetBackgroundColour(isCurrent ? wxColour(70, 100, 140) : wxColour(45, 45, 45));
129 panel->SetMinSize(wxSize(380, 40)); // Larger size for better visibility
130
131 wxBoxSizer *sizer = new wxBoxSizer(wxHORIZONTAL);
132
133 // Add icon based on type
134 wxString icon;
135 if (item.type == ItemType::BACK)
136 icon = " ← ";
137 else if (item.type == ItemType::FORWARD)
138 icon = " → ";
139 else
140 icon = " • ";
141
142 wxStaticText *iconText = new wxStaticText(panel, wxID_ANY, icon);
143 iconText->SetForegroundColour(wxColour(150, 150, 150));
144 wxFont iconFont = iconText->GetFont();
145 iconFont.SetPointSize(iconFont.GetPointSize() + 2);
146 iconText->SetFont(iconFont);
147 sizer->Add(iconText, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, 8);
148
149 // Extract folder name from path
150 wxFileName fn(item.path);
151 wxString folderName = fn.GetDirs().IsEmpty() ? item.path : fn.GetDirs().Last();
152
153 wxStaticText *text = new wxStaticText(panel, wxID_ANY, folderName);
154 text->SetForegroundColour(isCurrent ? wxColour(255, 255, 255) : wxColour(200, 200, 200));
155 wxFont textFont = text->GetFont();
156 textFont.SetPointSize(textFont.GetPointSize() + 1);
157 if (isCurrent) {
158 textFont.SetWeight(wxFONTWEIGHT_BOLD);
159 }
160 text->SetFont(textFont);
161 sizer->Add(text, 1, wxALIGN_CENTER_VERTICAL | wxALL, 10);
162
163 panel->SetSizer(sizer);
164 panel->SetToolTip(item.path);
165
166 // Bind hover and click events
167 panel->Bind(wxEVT_ENTER_WINDOW, [this, panel, index, isCurrent](wxMouseEvent &) {
168 if (!isCurrent) {
169 panel->SetBackgroundColour(wxColour(60, 80, 110));
170 panel->Refresh();
171 m_hoveredIndex = index;
172 }
173 });
174
175 panel->Bind(wxEVT_LEAVE_WINDOW, [this, panel, isCurrent](wxMouseEvent &) {
176 if (!isCurrent) {
177 panel->SetBackgroundColour(wxColour(45, 45, 45));
178 panel->Refresh();
179 m_hoveredIndex = std::numeric_limits<size_t>::max();
180 }
181 });
182
183 panel->Bind(wxEVT_LEFT_DOWN, [this, index](wxMouseEvent &) {
184 if (m_onNavigate && index < m_items.size()) {
185 m_onNavigate(m_items[index].path);
186 Dismiss();
187 }
188 });
189
190 // Also bind to child controls for proper event handling
191 iconText->Bind(wxEVT_LEFT_DOWN, [this, index](wxMouseEvent &) {
192 if (m_onNavigate && index < m_items.size()) {
193 m_onNavigate(m_items[index].path);
194 Dismiss();
195 }
196 });
197
198 text->Bind(wxEVT_LEFT_DOWN, [this, index](wxMouseEvent &) {
199 if (m_onNavigate && index < m_items.size()) {
200 m_onNavigate(m_items[index].path);
201 Dismiss();
202 }
203 });
204
205 return panel;
206 }
207
208 std::function<void(const wxString &)> m_onNavigate;
209 std::vector<HistoryItem> m_items;
211};
212
214 : m_panel(nullptr), m_treeSearchCtrl(nullptr), m_gridSearchCtrl(nullptr), m_treeCtrl(nullptr), m_splitter(nullptr),
215 m_leftPanel(nullptr), m_rightPanel(nullptr), m_gridCtrl(nullptr), m_gridSizer(nullptr), m_historyPanel(nullptr),
217 m_functionalPanel(nullptr), m_breadcrumbSizer(nullptr), m_contentSizer(nullptr), m_treeToggleBtn(nullptr),
218 m_gridToggleBtn(nullptr), m_xmlFilterBtn(nullptr), m_pngFilterBtn(nullptr), m_jsonFilterBtn(nullptr),
219 m_gridBackButton(nullptr), m_gridForwardButton(nullptr), m_historyButton(nullptr), m_activeFilter(""),
220 m_isInitialized(false), m_syncInProgress(false), m_treeVisible(true), m_gridVisible(true),
225 m_treeSortFilesBy(static_cast<int>(EmberForge::AppPreferences::SortBy::Name)),
226 m_treeItemSize(24) // Default to Medium
227 ,
229 m_gridSortFilesBy(static_cast<int>(EmberForge::AppPreferences::SortBy::Name)),
230 m_gridIconSize(80) // Default to Medium
231{
232 // Load settings from preferences
233 LoadSettings();
234 // Create the main panel
235 m_panel = new wxPanel(parent, wxID_ANY);
236 m_panel->SetBackgroundColour(wxColour(45, 45, 45));
237
238 // Bind keyboard events for navigation shortcuts
239 m_panel->Bind(wxEVT_CHAR_HOOK, &FileExplorerTab::OnNavigationKeyDown, this);
240
241 // Bind click event to detect clicks outside history panel
242 m_panel->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &event) {
244 // Check if click is outside history panel
245 wxPoint clickPos = event.GetPosition();
246 wxPoint panelPos = m_historyPanel->GetPosition();
247 wxSize panelSize = m_historyPanel->GetSize();
248 wxRect historyRect(panelPos, panelSize);
249
250 if (!historyRect.Contains(clickPos)) {
252 }
253 }
254 event.Skip(); // Allow the event to propagate
255 });
256
257 // Determine start path based on preferences
259 const auto &bottomSettings = prefs.GetBottomPanelSettings();
260
261 switch (bottomSettings.startPathMode) {
264 LOG_INFO("FileExplorer", "Using executable directory as start path");
265 break;
266
268 if (!bottomSettings.customStartPath.empty() && wxDir::Exists(wxString(bottomSettings.customStartPath))) {
269 m_rootPath = wxString(bottomSettings.customStartPath);
270 LOG_INFO("FileExplorer", "Using custom start path: " + m_rootPath.ToStdString());
271 } else {
273 LOG_WARNING("FileExplorer", "Custom path not found, using executable directory");
274 }
275 break;
276
277 default:
279 break;
280 }
281
282 m_currentGridPath = m_rootPath; // Start at root
283
284 LOG_INFO("FileExplorer", "File explorer tab created with root: " + m_rootPath.ToStdString());
285}
286
288 // Clean up breadcrumb button client data
289 if (m_breadcrumbSizer) {
290 for (size_t i = 0; i < m_breadcrumbSizer->GetItemCount(); i++) {
291 wxSizerItem *item = m_breadcrumbSizer->GetItem(i);
292 if (item && item->IsWindow()) {
293 wxButton *btn = dynamic_cast<wxButton *>(item->GetWindow());
294 if (btn && btn->GetClientData()) {
295 wxString *pathPtr = static_cast<wxString *>(btn->GetClientData());
296 delete pathPtr;
297 btn->SetClientData(nullptr);
298 }
299 }
300 }
301 }
302
303 if (m_imageList) {
304 delete m_imageList;
305 }
306 LOG_INFO("FileExplorer", "File explorer tab destroyed");
307}
308
310 // Return the resources directory path using centralized ResourcePath
311 wxString resourcesDir = EmberForge::ResourcePath::GetResourcesDir();
312 // Remove trailing separator if present (for consistency with original behavior)
313 if (resourcesDir.EndsWith(wxFileName::GetPathSeparator())) {
314 resourcesDir.RemoveLast();
315 }
316 return resourcesDir;
317}
318
321 const auto &bottomSettings = prefs.GetBottomPanelSettings();
322
323 m_showBreadcrumb = bottomSettings.showBreadcrumb;
324 m_showHistory = bottomSettings.showHistory;
325
326 // Load tree view settings
327 m_treeShowHiddenFiles = bottomSettings.treeView.showHiddenFiles;
328 m_treeShowFileExtensions = bottomSettings.treeView.showFileExtensions;
329 m_treeSortFilesBy = static_cast<int>(bottomSettings.treeView.sortFilesBy);
330 m_treeItemSize = static_cast<int>(bottomSettings.treeView.itemSize);
331
332 // Load grid view settings
333 m_gridShowHiddenFiles = bottomSettings.gridView.showHiddenFiles;
334 m_gridShowFileExtensions = bottomSettings.gridView.showFileExtensions;
335 m_gridSortFilesBy = static_cast<int>(bottomSettings.gridView.sortFilesBy);
336 m_gridIconSize = static_cast<int>(bottomSettings.gridView.iconSize);
337
338 LOG_INFO("FileExplorer",
339 "Settings loaded - showBreadcrumb: " + std::to_string(m_showBreadcrumb) +
340 ", tree(hidden:" + std::to_string(m_treeShowHiddenFiles) +
341 " ext:" + std::to_string(m_treeShowFileExtensions) + " sort:" + std::to_string(m_treeSortFilesBy) +
342 ")" + ", grid(hidden:" + std::to_string(m_gridShowHiddenFiles) + " ext:" +
343 std::to_string(m_gridShowFileExtensions) + " sort:" + std::to_string(m_gridSortFilesBy) + ")");
344}
345
346wxString FileExplorerTab::GetDisplayName(const wxString &filename, bool isDirectory, bool isTreeView) const {
347 // Directories always show full name
348 if (isDirectory) {
349 return filename;
350 }
351
352 // Files: show with or without extension based on view-specific preference
353 bool showExtensions = isTreeView ? m_treeShowFileExtensions : m_gridShowFileExtensions;
354 if (showExtensions) {
355 return filename;
356 }
357
358 // Remove extension
359 wxFileName fn(filename);
360 return fn.GetName(); // Returns filename without extension
361}
362
364 if (m_isInitialized) {
365 return;
366 }
367
368 CreateLayout();
369 PopulateTree();
372
373 // Initialize toggle button states to match actual visibility
375
376 // Sync toolbar search bar width with initial splitter position
378
379 // Apply breadcrumb and history visibility from settings
382
383 m_isInitialized = true;
384
385 LOG_INFO("FileExplorer", "File explorer tab initialized");
386}
387
389 wxBoxSizer *mainSizer = new wxBoxSizer(wxVERTICAL);
390
391 // Create favorites bar at the top
392 CreateFavoritesBar(mainSizer);
393
394 // Create the split layout (tree + grid)
395 CreateSplitLayout(mainSizer);
396
397 m_panel->SetSizer(mainSizer);
398 m_panel->Layout();
399}
400
401void FileExplorerTab::CreateFavoritesBar(wxBoxSizer *parentSizer) {
402 // Create favorites panel
403 wxPanel *favPanel = new wxPanel(m_panel, wxID_ANY);
404 favPanel->SetBackgroundColour(wxColour(35, 35, 35));
405
406 wxBoxSizer *favSizer = new wxBoxSizer(wxHORIZONTAL);
407
408 // Add tree search control - width will be synced with splitter
409 m_treeSearchCtrl = new wxSearchCtrl(favPanel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(200, -1));
410 m_treeSearchCtrl->SetDescriptiveText("Search tree...");
411 m_treeSearchCtrl->ShowCancelButton(true);
412 m_treeSearchCtrl->SetBackgroundColour(wxColour(60, 60, 60));
413 m_treeSearchCtrl->SetForegroundColour(wxColour(200, 200, 200));
415 m_treeSearchCtrl->Bind(wxEVT_SEARCHCTRL_CANCEL_BTN, &FileExplorerTab::OnTreeSearchCancel, this);
416 favSizer->Add(m_treeSearchCtrl, 0, wxALIGN_CENTER_VERTICAL | wxALL, 2);
417
418 // Add vertical separator - this will visually align with the splitter
419 wxStaticLine *separator = new wxStaticLine(favPanel, wxID_ANY, wxDefaultPosition, wxSize(2, 25), wxLI_VERTICAL);
420 separator->SetBackgroundColour(wxColour(100, 100, 100));
421 favSizer->Add(separator, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, 5);
422
423 // Add label
424 wxStaticText *favLabel = new wxStaticText(favPanel, wxID_ANY, "Favorites:");
425 favLabel->SetForegroundColour(wxColour(200, 200, 200));
426 favSizer->Add(favLabel, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5);
427
428 // Create toggle buttons for file type filters
429 m_xmlFilterBtn = new wxToggleButton(favPanel, wxID_ANY, "All XML");
430 m_xmlFilterBtn->SetBackgroundColour(wxColour(55, 55, 55));
431 m_xmlFilterBtn->SetForegroundColour(wxColour(200, 200, 200));
432 m_xmlFilterBtn->Bind(wxEVT_TOGGLEBUTTON, &FileExplorerTab::OnFavoriteFilter, this);
433 favSizer->Add(m_xmlFilterBtn, 0, wxALL, 2);
434
435 m_pngFilterBtn = new wxToggleButton(favPanel, wxID_ANY, "All PNG");
436 m_pngFilterBtn->SetBackgroundColour(wxColour(55, 55, 55));
437 m_pngFilterBtn->SetForegroundColour(wxColour(200, 200, 200));
438 m_pngFilterBtn->Bind(wxEVT_TOGGLEBUTTON, &FileExplorerTab::OnFavoriteFilter, this);
439 favSizer->Add(m_pngFilterBtn, 0, wxALL, 2);
440
441 m_jsonFilterBtn = new wxToggleButton(favPanel, wxID_ANY, "All JSON");
442 m_jsonFilterBtn->SetBackgroundColour(wxColour(55, 55, 55));
443 m_jsonFilterBtn->SetForegroundColour(wxColour(200, 200, 200));
444 m_jsonFilterBtn->Bind(wxEVT_TOGGLEBUTTON, &FileExplorerTab::OnFavoriteFilter, this);
445 favSizer->Add(m_jsonFilterBtn, 0, wxALL, 2);
446
447 favSizer->AddStretchSpacer(1);
448
449 // Add vertical separator before view toggles
450 wxStaticLine *viewSeparator = new wxStaticLine(favPanel, wxID_ANY, wxDefaultPosition, wxSize(2, 25), wxLI_VERTICAL);
451 viewSeparator->SetBackgroundColour(wxColour(100, 100, 100));
452 favSizer->Add(viewSeparator, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, 5);
453
454 // Add label for view toggles
455 wxStaticText *viewLabel = new wxStaticText(favPanel, wxID_ANY, "View:");
456 viewLabel->SetForegroundColour(wxColour(200, 200, 200));
457 favSizer->Add(viewLabel, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5);
458
459 // Create toggle buttons for tree/grid visibility
460 m_treeToggleBtn = new wxToggleButton(favPanel, wxID_ANY, "Tree");
461 m_treeToggleBtn->SetValue(true); // Tree visible by default
462 m_treeToggleBtn->SetBackgroundColour(wxColour(55, 55, 55));
463 m_treeToggleBtn->SetForegroundColour(wxColour(200, 200, 200));
464 m_treeToggleBtn->Bind(wxEVT_TOGGLEBUTTON, &FileExplorerTab::OnTreeToggle, this);
465 favSizer->Add(m_treeToggleBtn, 0, wxALL, 2);
466
467 m_gridToggleBtn = new wxToggleButton(favPanel, wxID_ANY, "Grid");
468 m_gridToggleBtn->SetValue(true); // Grid visible by default
469 m_gridToggleBtn->SetBackgroundColour(wxColour(55, 55, 55));
470 m_gridToggleBtn->SetForegroundColour(wxColour(200, 200, 200));
471 m_gridToggleBtn->Bind(wxEVT_TOGGLEBUTTON, &FileExplorerTab::OnGridToggle, this);
472 favSizer->Add(m_gridToggleBtn, 0, wxALL, 2);
473
474 favPanel->SetSizer(favSizer);
475 parentSizer->Add(favPanel, 0, wxEXPAND | wxALL, 2);
476}
477
478void FileExplorerTab::CreateSplitLayout(wxBoxSizer *parentSizer) {
479 // Create splitter window
480 m_splitter = new wxSplitterWindow(m_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSP_3D | wxSP_LIVE_UPDATE);
481 m_splitter->SetMinimumPaneSize(150);
482 m_splitter->SetSashGravity(0.15); // 15% for tree, 85% for grid
483
484 // Bind splitter events to sync toolbar with splitter position
485 m_splitter->Bind(wxEVT_SPLITTER_SASH_POS_CHANGED, &FileExplorerTab::OnSplitterSashPosChanged, this);
486 m_splitter->Bind(wxEVT_SPLITTER_SASH_POS_CHANGING, &FileExplorerTab::OnSplitterSashPosChanging, this);
487
488 // Create left panel for tree
489 m_leftPanel = new wxPanel(m_splitter, wxID_ANY);
490 m_leftPanel->SetBackgroundColour(wxColour(45, 45, 45));
491 wxBoxSizer *leftSizer = new wxBoxSizer(wxVERTICAL);
492
493 // Create image list for tree icons (size based on preference)
494 m_imageList = new wxImageList(m_treeItemSize, m_treeItemSize);
495 wxIcon folderIcon = LoadTreeIcon("folder");
496 wxIcon fileIcon = LoadTreeIcon("file");
497 if (folderIcon.IsOk()) {
498 m_folderIcon = m_imageList->Add(folderIcon);
499 }
500 if (fileIcon.IsOk()) {
501 m_fileIcon = m_imageList->Add(fileIcon);
502 }
503
504 // Create tree control
505 m_treeCtrl =
506 new wxTreeCtrl(m_leftPanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTR_DEFAULT_STYLE | wxTR_HIDE_ROOT);
507 m_treeCtrl->SetBackgroundColour(wxColour(45, 45, 45));
508 m_treeCtrl->SetForegroundColour(wxColour(200, 200, 200));
509 m_treeCtrl->AssignImageList(m_imageList);
510 m_imageList = nullptr; // Tree now owns it
511
512 // Set tree item spacing based on size preference
513 // Spacing = itemSize + padding (4 pixels for small, 6 for medium, 8 for large)
514 int spacing = m_treeItemSize + (m_treeItemSize == 16 ? 4 : (m_treeItemSize == 24 ? 6 : 8));
515 m_treeCtrl->SetSpacing(spacing);
516
517 // Bind tree events
518 m_treeCtrl->Bind(wxEVT_TREE_SEL_CHANGED, &FileExplorerTab::OnSelectionChanged, this);
519 m_treeCtrl->Bind(wxEVT_TREE_ITEM_ACTIVATED, &FileExplorerTab::OnItemActivated, this);
520 m_treeCtrl->Bind(wxEVT_TREE_ITEM_EXPANDING, &FileExplorerTab::OnItemExpanding, this);
521 m_treeCtrl->Bind(wxEVT_TREE_BEGIN_DRAG, &FileExplorerTab::OnBeginDrag, this);
522 m_treeCtrl->Bind(wxEVT_TREE_ITEM_MENU, &FileExplorerTab::OnTreeContextMenu, this);
523
524 // Close history panel when clicking on tree
525 m_treeCtrl->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &event) {
526 if (m_isHistoryVisible) {
528 }
529 event.Skip();
530 });
531
532 leftSizer->Add(m_treeCtrl, 1, wxEXPAND | wxALL, 2);
533 m_leftPanel->SetSizer(leftSizer);
534
535 // Create right panel for grid
536 m_rightPanel = new wxPanel(m_splitter, wxID_ANY);
537 m_rightPanel->SetBackgroundColour(wxColour(50, 50, 50));
538 wxBoxSizer *rightSizer = new wxBoxSizer(wxVERTICAL);
539
540 // Create breadcrumb and grid
541 CreateBreadcrumb(m_rightPanel, rightSizer);
542
543 // Store the content sizer for toggling between grid and history
544 m_contentSizer = rightSizer;
545
546 CreateIconGrid(m_rightPanel, rightSizer);
547 CreateHistoryPanel(m_rightPanel, rightSizer);
548
549 m_rightPanel->SetSizer(rightSizer);
550
551 // Split the window with initial position at 200 pixels (15% for tree, 85% for grid)
552 m_splitter->SplitVertically(m_leftPanel, m_rightPanel, 200);
553
554 parentSizer->Add(m_splitter, 1, wxEXPAND | wxALL, 0);
555}
556
557void FileExplorerTab::CreateBreadcrumb(wxPanel *rightPanel, wxBoxSizer *rightSizer) {
558 // Create container panel that holds navigation, breadcrumb, and functional panels
559 m_breadcrumbContainerPanel = new wxPanel(rightPanel, wxID_ANY);
560 m_breadcrumbContainerPanel->SetBackgroundColour(wxColour(40, 40, 40));
561 m_breadcrumbContainerPanel->SetMinSize(wxSize(-1, 50)); // Taller container for bigger buttons
562 wxBoxSizer *containerSizer = new wxBoxSizer(wxHORIZONTAL);
563
564 // ===== LEFT SIDE: Navigation Panel (Back/Forward buttons + Grid Search) =====
565 m_navigationPanel = new wxPanel(m_breadcrumbContainerPanel, wxID_ANY);
566 m_navigationPanel->SetBackgroundColour(wxColour(40, 40, 40));
567 wxBoxSizer *navSizer = new wxBoxSizer(wxHORIZONTAL);
568
569 // Load back button bitmaps
570 wxString leftArrowPath = m_rootPath + wxFileName::GetPathSeparator() + "buttons" + wxFileName::GetPathSeparator() +
571 "left_arrow" + wxFileName::GetPathSeparator();
572 wxBitmap gridBackNormal(leftArrowPath + "LeftArrow_Normal_20.png", wxBITMAP_TYPE_PNG);
573 wxBitmap gridBackHovered(leftArrowPath + "LeftArrow_Hovered_20.png", wxBITMAP_TYPE_PNG);
574 wxBitmap gridBackPressed(leftArrowPath + "LeftArrow_Pressed_20.png", wxBITMAP_TYPE_PNG);
575 wxBitmap gridBackDisabled(leftArrowPath + "LeftArrow_Disabled_20.png", wxBITMAP_TYPE_PNG);
576
578 new wxBitmapButton(m_navigationPanel, wxID_ANY, gridBackNormal, wxDefaultPosition, wxSize(36, 36));
579 m_gridBackButton->SetBitmapHover(gridBackHovered);
580 m_gridBackButton->SetBitmapPressed(gridBackPressed);
581 m_gridBackButton->SetBitmapDisabled(gridBackDisabled);
582 m_gridBackButton->SetBackgroundColour(wxColour(40, 40, 40));
583 m_gridBackButton->SetToolTip("Go Back (Alt+Left)");
584 m_gridBackButton->Enable(false); // Disabled initially (no history)
585 m_gridBackButton->Bind(wxEVT_BUTTON, &FileExplorerTab::OnBackButton, this);
586 navSizer->Add(m_gridBackButton, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4);
587
588 // Load forward button bitmaps
589 wxString rightArrowPath = m_rootPath + wxFileName::GetPathSeparator() + "buttons" + wxFileName::GetPathSeparator() +
590 "right_arrow" + wxFileName::GetPathSeparator();
591 wxBitmap gridForwardNormal(rightArrowPath + "RightArrow_Normal_20.png", wxBITMAP_TYPE_PNG);
592 wxBitmap gridForwardHovered(rightArrowPath + "RightArrow_Hovered_20.png", wxBITMAP_TYPE_PNG);
593 wxBitmap gridForwardPressed(rightArrowPath + "RightArrow_Pressed_20.png", wxBITMAP_TYPE_PNG);
594 wxBitmap gridForwardDisabled(rightArrowPath + "RightArrow_Disabled_20.png", wxBITMAP_TYPE_PNG);
595
597 new wxBitmapButton(m_navigationPanel, wxID_ANY, gridForwardNormal, wxDefaultPosition, wxSize(36, 36));
598 m_gridForwardButton->SetBitmapHover(gridForwardHovered);
599 m_gridForwardButton->SetBitmapPressed(gridForwardPressed);
600 m_gridForwardButton->SetBitmapDisabled(gridForwardDisabled);
601 m_gridForwardButton->SetBackgroundColour(wxColour(40, 40, 40));
602 m_gridForwardButton->SetToolTip("Go Forward (Alt+Right)");
603 m_gridForwardButton->Enable(false); // Disabled initially (no forward history)
604 m_gridForwardButton->Bind(wxEVT_BUTTON, &FileExplorerTab::OnForwardButton, this);
605 navSizer->Add(m_gridForwardButton, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 10);
606
607 // Add grid search control
608 m_gridSearchCtrl = new wxSearchCtrl(m_navigationPanel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(200, -1));
609 m_gridSearchCtrl->SetDescriptiveText("Search grid...");
610 m_gridSearchCtrl->ShowCancelButton(true);
611 m_gridSearchCtrl->SetBackgroundColour(wxColour(60, 60, 60));
612 m_gridSearchCtrl->SetForegroundColour(wxColour(200, 200, 200));
614 m_gridSearchCtrl->Bind(wxEVT_SEARCHCTRL_CANCEL_BTN, &FileExplorerTab::OnGridSearchCancel, this);
615 navSizer->Add(m_gridSearchCtrl, 0, wxALIGN_CENTER_VERTICAL | wxALL, 2);
616
617 m_navigationPanel->SetSizer(navSizer);
618 containerSizer->Add(m_navigationPanel, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxTOP | wxBOTTOM, 5);
619
620 // ===== CENTER: Breadcrumb Panel (Path buttons) =====
621 m_breadcrumbPanel = new wxPanel(m_breadcrumbContainerPanel, wxID_ANY);
622 m_breadcrumbPanel->SetBackgroundColour(wxColour(40, 40, 40));
623
624 m_breadcrumbSizer = new wxBoxSizer(wxHORIZONTAL);
625 // Breadcrumb path items will be added here dynamically by UpdateBreadcrumb()
626
628 containerSizer->Add(m_breadcrumbPanel, 1, wxEXPAND | wxTOP | wxBOTTOM, 5);
629
630 // ===== RIGHT SIDE: Functional Panel (History and other functional buttons) =====
631 m_functionalPanel = new wxPanel(m_breadcrumbContainerPanel, wxID_ANY);
632 m_functionalPanel->SetBackgroundColour(wxColour(40, 40, 40));
633 wxBoxSizer *functionalSizer = new wxBoxSizer(wxHORIZONTAL);
634
635 // Load history button bitmaps
636 wxString historyPath = m_rootPath + wxFileName::GetPathSeparator() + "buttons" + wxFileName::GetPathSeparator() +
637 "history" + wxFileName::GetPathSeparator();
638 wxBitmap historyNormal(historyPath + "History_Normal_20.png", wxBITMAP_TYPE_PNG);
639 wxBitmap historyHovered(historyPath + "History_Hovered_20.png", wxBITMAP_TYPE_PNG);
640 wxBitmap historyPressed(historyPath + "History_Pressed_20.png", wxBITMAP_TYPE_PNG);
641 wxBitmap historyDisabled(historyPath + "History_Disabled_20.png", wxBITMAP_TYPE_PNG);
642
643 m_historyButton = new wxBitmapButton(m_functionalPanel, wxID_ANY, historyNormal, wxDefaultPosition, wxSize(36, 36));
644 m_historyButton->SetBitmapHover(historyHovered);
645 m_historyButton->SetBitmapPressed(historyPressed);
646 m_historyButton->SetBitmapDisabled(historyDisabled);
647 m_historyButton->SetBackgroundColour(wxColour(40, 40, 40));
648 m_historyButton->SetToolTip("Show Navigation History");
649 m_historyButton->Bind(wxEVT_BUTTON, &FileExplorerTab::OnHistoryButton, this);
650 functionalSizer->Add(m_historyButton, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, 10);
651
652 m_functionalPanel->SetSizer(functionalSizer);
653 containerSizer->Add(m_functionalPanel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT | wxTOP | wxBOTTOM, 5);
654
655 // Set container sizer and add to parent
656 m_breadcrumbContainerPanel->SetSizer(containerSizer);
657 rightSizer->Add(m_breadcrumbContainerPanel, 0, wxEXPAND | wxALL, 2);
658}
659
660void FileExplorerTab::CreateIconGrid(wxPanel *rightPanel, wxBoxSizer *rightSizer) {
661 // Create scrolled window for the icon grid
662 m_gridCtrl = new wxScrolledWindow(rightPanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL | wxHSCROLL);
663 m_gridCtrl->SetBackgroundColour(wxColour(50, 50, 50));
664 m_gridCtrl->SetScrollRate(10, 10);
665
666 // Create wrap sizer for grid layout
667 // wxEXTEND_LAST_ON_EACH_LINE: 0x0000 means items won't stretch to fill
668 m_gridSizer = new wxWrapSizer(wxHORIZONTAL, 0);
669 m_gridCtrl->SetSizer(m_gridSizer);
670
671 // Bind right-click on empty space in grid
672 m_gridCtrl->Bind(wxEVT_RIGHT_DOWN, &FileExplorerTab::OnGridBackgroundContextMenu, this);
673
674 // Close history panel when clicking on grid
675 m_gridCtrl->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &event) {
676 if (m_isHistoryVisible) {
678 }
679 event.Skip();
680 });
681
682 // Add to sizer - will be visible by default
683 rightSizer->Add(m_gridCtrl, 1, wxEXPAND | wxALL, 2);
684}
685
686void FileExplorerTab::CreateHistoryPanel(wxPanel *rightPanel, wxBoxSizer *rightSizer) {
687 // Create scrolled window for the history panel
688 m_historyPanel = new wxScrolledWindow(rightPanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
689 m_historyPanel->SetBackgroundColour(wxColour(45, 45, 45));
690 m_historyPanel->SetScrollRate(0, 10);
691
692 // Add to sizer but hide initially
693 rightSizer->Add(m_historyPanel, 1, wxEXPAND | wxALL, 2);
694 m_historyPanel->Hide();
695}
696
697wxBitmap FileExplorerTab::LoadGridIcon(const wxString &iconType, const wxSize &size) {
698 // Check cache first
699 wxString cacheKey = iconType + wxString::Format("_%dx%d", size.GetWidth(), size.GetHeight());
700 auto it = m_gridIcons.find(cacheKey);
701 if (it != m_gridIcons.end()) {
702 return it->second;
703 }
704
705 // Try to load custom icons for specific file types
706 if (iconType == "xml" || iconType == "json" || iconType == "txt" || iconType == "png" || iconType == "markdown") {
707 wxString iconPath = m_rootPath + wxFileName::GetPathSeparator() + "icons" + wxFileName::GetPathSeparator() +
708 iconType + "_file" + wxFileName::GetPathSeparator() + iconType + "_file_128px.png";
709
710 if (wxFileExists(iconPath)) {
711 wxBitmap customIcon;
712 customIcon.LoadFile(iconPath, wxBITMAP_TYPE_PNG);
713 if (customIcon.IsOk()) {
714 wxImage img = customIcon.ConvertToImage();
715 img.Rescale(size.GetWidth(), size.GetHeight(), wxIMAGE_QUALITY_HIGH);
716 wxBitmap scaled(img);
717 m_gridIcons[cacheKey] = scaled;
718 LOG_INFO("FileExplorer",
719 "Loaded custom " + iconType.Upper().ToStdString() + " icon: " + iconPath.ToStdString());
720 return scaled;
721 }
722 } else {
723 LOG_WARNING("FileExplorer",
724 "Custom " + iconType.Upper().ToStdString() + " icon not found: " + iconPath.ToStdString());
725 }
726 }
727
728 // Fall back to wxArtProvider
729 wxArtID artId = wxART_NORMAL_FILE;
730 if (iconType == "folder") {
731 artId = wxART_FOLDER;
732 } else if (iconType == "file" || iconType == "xml" || iconType == "json" || iconType == "txt" ||
733 iconType == "png" || iconType == "markdown") {
734 artId = wxART_NORMAL_FILE;
735 }
736
737 wxBitmap bitmap = wxArtProvider::GetBitmap(artId, wxART_OTHER, size);
738 m_gridIcons[cacheKey] = bitmap;
739 return bitmap;
740}
741
742wxIcon FileExplorerTab::LoadTreeIcon(const wxString &iconType) {
743 // Try to load custom icon (commented out for now)
744 // wxString iconPath = m_rootPath + "/icons/" + iconType + "_small.png";
745 // if (wxFileExists(iconPath)) {
746 // wxIcon icon;
747 // icon.LoadFile(iconPath, wxBITMAP_TYPE_PNG, m_treeItemSize, m_treeItemSize);
748 // if (icon.IsOk()) return icon;
749 // }
750
751 // Fall back to wxArtProvider (use dynamic size based on preference)
752 wxArtID artId = wxART_NORMAL_FILE;
753 if (iconType == "folder") {
754 artId = wxART_FOLDER;
755 } else if (iconType == "file") {
756 artId = wxART_NORMAL_FILE;
757 }
758
759 return wxArtProvider::GetIcon(artId, wxART_OTHER, wxSize(m_treeItemSize, m_treeItemSize));
760}
761
763 if (!m_treeCtrl) {
764 LOG_ERROR("FileExplorer", "Tree control is null, cannot populate");
765 return;
766 }
767
768 LOG_INFO("FileExplorer", wxString::Format("Populating tree with root: %s", m_rootPath).ToStdString());
769
770 m_treeCtrl->DeleteAllItems();
771
772 // Create root with the actual path name
773 wxFileName fn(m_rootPath);
774 wxString rootName = fn.GetFullName().IsEmpty() ? m_rootPath : fn.GetFullName();
775 wxTreeItemId root = m_treeCtrl->AddRoot(rootName);
776
777 // Add the resources directory contents
779
780 // Don't expand root when using wxTR_HIDE_ROOT flag - it causes assertion
781 // The root children are already visible since root is hidden
782
783 LOG_INFO("FileExplorer", "Tree population completed");
784}
785
786void FileExplorerTab::AddDirectoryToTree(const wxString &path, wxTreeItemId parentItem) {
787 LOG_INFO("FileExplorer", wxString::Format("Adding directory to tree: %s", path).ToStdString());
788
789 if (!wxDir::Exists(path)) {
790 LOG_ERROR("FileExplorer", wxString::Format("Directory does not exist: %s", path).ToStdString());
791 return;
792 }
793
794 wxDir dir(path);
795 if (!dir.IsOpened()) {
796 LOG_ERROR("FileExplorer", wxString::Format("Cannot open directory: %s", path).ToStdString());
797 return;
798 }
799
800 // Collect directories and files for sorting
801 struct FileInfo {
802 wxString name;
803 wxString fullPath;
804 bool isDirectory;
805 wxDateTime modTime;
806 wxULongLong size;
807 };
808 std::vector<FileInfo> directories;
809 std::vector<FileInfo> files;
810
811 // First, collect all subdirectories
812 wxString filename;
813 bool cont = dir.GetFirst(&filename, wxEmptyString, wxDIR_DIRS);
814 while (cont) {
815 // Skip hidden directories based on preference
816 bool isHidden = filename.StartsWith(".");
817 if (m_treeShowHiddenFiles || !isHidden) {
818 wxString fullPath = path + wxFileName::GetPathSeparator() + filename;
819
820 // When filter is active, only show directories that contain matching files
821 if (DirectoryContainsMatchingFiles(fullPath)) {
822 FileInfo info;
823 info.name = filename;
824 info.fullPath = fullPath;
825 info.isDirectory = true;
826
827 // Get file info for sorting
828 wxFileName fn(fullPath);
829 fn.GetTimes(nullptr, &info.modTime, nullptr);
830 info.size = 0; // Directories don't have a simple size
831
832 directories.push_back(info);
833 }
834 }
835 cont = dir.GetNext(&filename);
836 }
837
838 // Then, collect all files (with filter applied)
839 cont = dir.GetFirst(&filename, wxEmptyString, wxDIR_FILES);
840 while (cont) {
841 // Skip hidden files based on preference, and apply favorites filter
842 bool isHidden = filename.StartsWith(".");
843 if ((m_treeShowHiddenFiles || !isHidden) && PassesFilter(filename)) {
844 wxString fullPath = path + wxFileName::GetPathSeparator() + filename;
845
846 FileInfo info;
847 info.name = filename;
848 info.fullPath = fullPath;
849 info.isDirectory = false;
850
851 // Get file info for sorting
852 wxFileName fn(fullPath);
853 fn.GetTimes(nullptr, &info.modTime, nullptr);
854 info.size = fn.GetSize();
855
856 files.push_back(info);
857 }
858 cont = dir.GetNext(&filename);
859 }
860
861 // Sort based on preference
862 auto sortFunc = [this](const FileInfo &a, const FileInfo &b) -> bool {
864 switch (sortBy) {
866 return a.name.CmpNoCase(b.name) < 0;
868 return a.modTime > b.modTime; // Most recent first
870 return a.size > b.size; // Largest first
872 wxString extA = wxFileName(a.name).GetExt().Lower();
873 wxString extB = wxFileName(b.name).GetExt().Lower();
874 if (extA != extB)
875 return extA < extB;
876 return a.name.CmpNoCase(b.name) < 0;
877 }
878 }
879 return a.name.CmpNoCase(b.name) < 0; // Default fallback
880 };
881
882 std::sort(directories.begin(), directories.end(), sortFunc);
883 std::sort(files.begin(), files.end(), sortFunc);
884
885 // Add sorted directories to tree
886 for (const auto &dirInfo : directories) {
887 wxString displayName = GetDisplayName(dirInfo.name, true, true);
888 wxTreeItemId item = m_treeCtrl->AppendItem(parentItem, displayName, m_folderIcon, m_folderIcon,
889 new FileTreeItemData(dirInfo.fullPath, true));
890
891 // Recursively populate subdirectory immediately (no lazy loading)
892 AddDirectoryToTree(dirInfo.fullPath, item);
893 }
894
895 // Add sorted files to tree
896 for (const auto &fileInfo : files) {
897 wxString displayName = GetDisplayName(fileInfo.name, false, true);
898 m_treeCtrl->AppendItem(parentItem, displayName, m_fileIcon, m_fileIcon,
899 new FileTreeItemData(fileInfo.fullPath, false));
900 }
901}
902
903void FileExplorerTab::OnItemExpanding(wxTreeEvent &event) {
904 wxTreeItemId item = event.GetItem();
905 if (!item.IsOk()) {
906 return;
907 }
908
909 // Check if we've already populated this item
910 wxTreeItemIdValue cookie;
911 wxTreeItemId child = m_treeCtrl->GetFirstChild(item, cookie);
912 if (child.IsOk()) {
913 wxString text = m_treeCtrl->GetItemText(child);
914 if (text == "Loading...") {
915 // Remove the dummy item
916 m_treeCtrl->Delete(child);
917
918 // Get the path for this item
919 FileTreeItemData *data = dynamic_cast<FileTreeItemData *>(m_treeCtrl->GetItemData(item));
920 if (data && data->IsDirectory()) {
921 AddDirectoryToTree(data->GetPath(), item);
922 }
923 }
924 }
925}
926
927wxWindow *FileExplorerTab::GetWidget() { return m_panel; }
928
929wxString FileExplorerTab::GetTitle() const { return "File Explorer"; }
930
931wxString FileExplorerTab::GetTabType() const { return "FileExplorer"; }
932
933wxBitmap FileExplorerTab::GetIcon() const { return wxNullBitmap; }
934
936 // Reload settings in case they changed
937 LoadSettings();
938
939 // Update breadcrumb and history visibility based on settings
942
943 // Update tree image list if size changed
945
946 // Refresh both tree and grid
947 PopulateTree();
948 if (!m_currentGridPath.IsEmpty()) {
950 }
951}
952
953void FileExplorerTab::OnActivated() { LOG_INFO("FileExplorer", "File explorer tab activated"); }
954
955void FileExplorerTab::OnDeactivated() { LOG_INFO("FileExplorer", "File explorer tab deactivated"); }
956
957void FileExplorerTab::OnClosed() { LOG_INFO("FileExplorer", "File explorer tab closed"); }
958
959bool FileExplorerTab::IsValid() const { return m_isInitialized && m_panel != nullptr && m_treeCtrl != nullptr; }
960
961bool FileExplorerTab::CanClose() const { return true; }
962
963bool FileExplorerTab::CanMove() const { return true; }
964
966 if (m_treeCtrl) {
967 wxTreeItemId sel = m_treeCtrl->GetSelection();
968 if (sel.IsOk()) {
969 FileTreeItemData *data = dynamic_cast<FileTreeItemData *>(m_treeCtrl->GetItemData(sel));
970 if (data) {
971 return data->GetPath();
972 }
973 }
974 }
975 return wxEmptyString;
976}
977
978void FileExplorerTab::SetRootDirectory(const wxString &path) {
979 m_rootPath = path;
980 if (m_isInitialized) {
981 PopulateTree();
982 }
983}
984
985void FileExplorerTab::OnSelectionChanged(wxTreeEvent &event) {
986 // No automatic syncing - user must double-click to navigate
987 // Just log the selection for debugging
988 wxTreeItemId item = event.GetItem();
989 if (item.IsOk()) {
990 FileTreeItemData *data = dynamic_cast<FileTreeItemData *>(m_treeCtrl->GetItemData(item));
991 if (data) {
992 LOG_INFO("FileExplorer", "Tree selection changed: " + data->GetPath().ToStdString());
993 }
994 }
995
996 event.Skip();
997}
998
999void FileExplorerTab::OnItemActivated(wxTreeEvent &event) {
1000 wxTreeItemId item = event.GetItem();
1001 if (!item.IsOk()) {
1002 event.Skip();
1003 return;
1004 }
1005
1006 FileTreeItemData *data = dynamic_cast<FileTreeItemData *>(m_treeCtrl->GetItemData(item));
1007 if (!data) {
1008 event.Skip();
1009 return;
1010 }
1011
1012 // Only sync and navigate when double-clicking folders
1013 if (data->IsDirectory()) {
1014 wxString path = data->GetPath();
1015 LOG_INFO("FileExplorer", "Tree folder activated (double-click): " + path.ToStdString());
1016 NavigateToPath(path);
1017 } else {
1018 // For files, just log (future: open file in editor)
1019 LOG_INFO("FileExplorer", "Tree file activated: " + data->GetPath().ToStdString());
1020 }
1021
1022 event.Skip();
1023}
1024
1025void FileExplorerTab::OnBeginDrag(wxTreeEvent &event) {
1026 wxTreeItemId item = event.GetItem();
1027 if (!item.IsOk()) {
1028 return;
1029 }
1030
1031 // Get the item data
1032 FileTreeItemData *data = dynamic_cast<FileTreeItemData *>(m_treeCtrl->GetItemData(item));
1033 if (!data) {
1034 return;
1035 }
1036
1037 // Only allow dragging files, not directories
1038 if (data->IsDirectory()) {
1039 LOG_INFO("FileExplorer", "Cannot drag directories");
1040 return;
1041 }
1042
1043 wxString filePath = data->GetPath();
1044
1045 // Only allow dragging XML files
1046 if (!filePath.Lower().EndsWith(".xml")) {
1047 LOG_INFO("FileExplorer", "Only XML files can be dragged");
1048 return;
1049 }
1050
1051 LOG_INFO("FileExplorer", "Starting drag for file: " + filePath.ToStdString());
1052
1053 // Create a file data object with the file path
1054 wxFileDataObject fileData;
1055 fileData.AddFile(filePath);
1056
1057 // Create a drop source and begin the drag operation
1058 wxDropSource dragSource(fileData, m_treeCtrl);
1059 wxDragResult result = dragSource.DoDragDrop(wxDrag_DefaultMove);
1060
1061 // Log the result
1062 switch (result) {
1063 case wxDragCopy:
1064 LOG_INFO("FileExplorer", "File copied");
1065 break;
1066 case wxDragMove:
1067 LOG_INFO("FileExplorer", "File moved");
1068 break;
1069 case wxDragCancel:
1070 LOG_INFO("FileExplorer", "Drag cancelled");
1071 break;
1072 default:
1073 LOG_INFO("FileExplorer", "Drag completed with unknown result");
1074 break;
1075 }
1076}
1077
1078void FileExplorerTab::OnTreeSearchTextChanged(wxCommandEvent &event) {
1079 wxString searchText = m_treeSearchCtrl->GetValue();
1080 FilterTree(searchText);
1081}
1082
1083void FileExplorerTab::OnTreeSearchCancel(wxCommandEvent &event) {
1084 m_treeSearchCtrl->SetValue(wxEmptyString);
1085 FilterTree(wxEmptyString);
1086}
1087
1088void FileExplorerTab::OnGridSearchTextChanged(wxCommandEvent &event) {
1089 wxString searchText = m_gridSearchCtrl->GetValue();
1090 FilterGrid(searchText);
1091}
1092
1093void FileExplorerTab::OnGridSearchCancel(wxCommandEvent &event) {
1094 m_gridSearchCtrl->SetValue(wxEmptyString);
1095 FilterGrid(wxEmptyString);
1096}
1097
1098void FileExplorerTab::FilterTree(const wxString &searchText) {
1099 if (!m_treeCtrl) {
1100 return;
1101 }
1102
1103 // Simply repopulate the tree - it will rebuild from scratch
1104 PopulateTree();
1105
1106 if (searchText.IsEmpty()) {
1107 return;
1108 }
1109
1110 wxTreeItemId root = m_treeCtrl->GetRootItem();
1111 if (!root.IsOk()) {
1112 return;
1113 }
1114
1115 // Expand and highlight only matching items
1116 bool foundAny = false;
1117 ExpandAllMatching(root, searchText.Lower(), foundAny);
1118
1119 if (foundAny) {
1120 LOG_INFO("FileExplorer", "Search found matches for: " + searchText.ToStdString());
1121 } else {
1122 LOG_INFO("FileExplorer", "No matches found for: " + searchText.ToStdString());
1123 }
1124}
1125
1126void FileExplorerTab::ExpandAllMatching(wxTreeItemId item, const wxString &searchText, bool &foundAny) {
1127 if (!item.IsOk()) {
1128 return;
1129 }
1130
1131 // Check if current item matches
1132 bool thisMatches = ItemMatchesSearch(item, searchText);
1133
1134 // Process children first (so we can safely delete non-matching ones)
1135 wxTreeItemIdValue cookie;
1136 wxTreeItemId child = m_treeCtrl->GetFirstChild(item, cookie);
1137 bool anyChildMatches = false;
1138
1139 std::vector<wxTreeItemId> childrenToDelete;
1140
1141 while (child.IsOk()) {
1142 wxTreeItemId nextChild = m_treeCtrl->GetNextChild(item, cookie);
1143
1144 bool childOrDescendantMatches = false;
1145 ExpandAllMatching(child, searchText, childOrDescendantMatches);
1146
1147 if (childOrDescendantMatches) {
1148 anyChildMatches = true;
1149 } else {
1150 // Mark for deletion
1151 childrenToDelete.push_back(child);
1152 }
1153
1154 child = nextChild;
1155 }
1156
1157 // Delete non-matching children
1158 for (const auto &childToDelete : childrenToDelete) {
1159 m_treeCtrl->Delete(childToDelete);
1160 }
1161
1162 // Determine if this item should be kept
1163 bool shouldKeep = thisMatches || anyChildMatches;
1164
1165 if (shouldKeep) {
1166 // Expand this item to reveal matching children
1167 if (anyChildMatches || thisMatches) {
1168 // Don't expand root item when using wxTR_HIDE_ROOT
1169 if (item != m_treeCtrl->GetRootItem()) {
1170 m_treeCtrl->Expand(item);
1171 }
1172 }
1173
1174 // Highlight directly matching items
1175 m_treeCtrl->SetItemBold(item, thisMatches);
1176 foundAny = true;
1177 }
1178}
1179
1180bool FileExplorerTab::ItemMatchesSearch(wxTreeItemId item, const wxString &searchText) const {
1181 if (!item.IsOk() || searchText.IsEmpty()) {
1182 return false;
1183 }
1184
1185 // Get item text and convert to lowercase for case-insensitive search
1186 wxString itemText = m_treeCtrl->GetItemText(item).Lower();
1187
1188 // Check if the item text contains the search text
1189 return itemText.Contains(searchText);
1190}
1191
1192bool FileExplorerTab::DirectoryContainsMatchingFiles(const wxString &dirPath) const {
1193 // If no filter is active, all directories are valid
1194 if (m_activeFilter.IsEmpty()) {
1195 return true;
1196 }
1197
1198 if (!wxDir::Exists(dirPath)) {
1199 return false;
1200 }
1201
1202 wxDir dir(dirPath);
1203 if (!dir.IsOpened()) {
1204 return false;
1205 }
1206
1207 // Check files in this directory
1208 wxString filename;
1209 bool cont = dir.GetFirst(&filename, wxEmptyString, wxDIR_FILES);
1210 while (cont) {
1211 if (!filename.StartsWith(".") && PassesFilter(filename)) {
1212 return true; // Found a matching file
1213 }
1214 cont = dir.GetNext(&filename);
1215 }
1216
1217 // Recursively check subdirectories
1218 cont = dir.GetFirst(&filename, wxEmptyString, wxDIR_DIRS);
1219 while (cont) {
1220 if (!filename.StartsWith(".")) {
1221 wxString fullPath = dirPath + wxFileName::GetPathSeparator() + filename;
1222 if (DirectoryContainsMatchingFiles(fullPath)) {
1223 return true; // Found a matching file in subdirectory
1224 }
1225 }
1226 cont = dir.GetNext(&filename);
1227 }
1228
1229 return false; // No matching files found
1230}
1231
1232void FileExplorerTab::FilterGrid(const wxString &searchText) {
1233 if (!m_gridCtrl) {
1234 LOG_WARNING("FileExplorer", "Grid control not initialized");
1235 return;
1236 }
1237
1238 if (m_gridItems.empty()) {
1239 LOG_WARNING("FileExplorer", "No grid items to filter");
1240 return;
1241 }
1242
1243 wxString lowerSearchText = searchText.Lower();
1244 bool hasFilter = !searchText.IsEmpty();
1245
1246 LOG_INFO("FileExplorer", "Filtering " + std::to_string(m_gridItems.size()) + " grid items with search: '" +
1247 searchText.ToStdString() + "'");
1248
1249 int matchCount = 0;
1250 // Show/hide grid items based on search text
1251 for (auto &item : m_gridItems) {
1252 if (item.widget) {
1253 if (hasFilter) {
1254 // Check if item name contains the search text (case-insensitive)
1255 wxString lowerItemName = item.name.Lower();
1256 bool matches = lowerItemName.Contains(lowerSearchText);
1257
1258 if (matches) {
1259 LOG_INFO("FileExplorer",
1260 " MATCH: '" + item.name.ToStdString() + "' contains '" + searchText.ToStdString() + "'");
1261 matchCount++;
1262 }
1263
1264 item.widget->Show(matches);
1265 } else {
1266 // No filter - show all items
1267 item.widget->Show(true);
1268 }
1269 }
1270 }
1271
1272 // Update layout to reflect visibility changes
1273 m_gridSizer->Layout();
1274 m_gridCtrl->FitInside();
1275 m_gridCtrl->Refresh();
1276
1277 if (hasFilter) {
1278 LOG_INFO("FileExplorer", "Grid filter complete: " + std::to_string(matchCount) + " matches found for '" +
1279 searchText.ToStdString() + "'");
1280 } else {
1281 LOG_INFO("FileExplorer", "Grid filter cleared - showing all items");
1282 }
1283}
1284
1285// ============================================================================
1286// Grid Population Methods
1287// ============================================================================
1288
1289void FileExplorerTab::PopulateGrid(const wxString &path) {
1290 if (!m_gridCtrl || !m_gridSizer) {
1291 LOG_ERROR("FileExplorer", "Grid control not initialized");
1292 return;
1293 }
1294
1295 ClearGrid();
1296 m_currentGridPath = path;
1297
1298 LOG_INFO("FileExplorer", "Populating grid with path: " + path.ToStdString());
1299
1300 if (!wxDir::Exists(path)) {
1301 LOG_ERROR("FileExplorer", "Path does not exist: " + path.ToStdString());
1302 return;
1303 }
1304
1305 wxDir dir(path);
1306 if (!dir.IsOpened()) {
1307 LOG_ERROR("FileExplorer", "Cannot open directory: " + path.ToStdString());
1308 return;
1309 }
1310
1311 // Collect directories and files for sorting
1312 struct FileInfo {
1313 wxString name;
1314 wxString fullPath;
1315 bool isDirectory;
1316 wxDateTime modTime;
1317 wxULongLong size;
1318 };
1319 std::vector<FileInfo> directories;
1320 std::vector<FileInfo> files;
1321
1322 // First collect directories
1323 wxString filename;
1324 bool cont = dir.GetFirst(&filename, wxEmptyString, wxDIR_DIRS);
1325 while (cont) {
1326 bool isHidden = filename.StartsWith(".");
1327 if (m_gridShowHiddenFiles || !isHidden) {
1328 wxString fullPath = path + wxFileName::GetPathSeparator() + filename;
1329
1330 FileInfo info;
1331 info.name = filename;
1332 info.fullPath = fullPath;
1333 info.isDirectory = true;
1334
1335 wxFileName fn(fullPath);
1336 fn.GetTimes(nullptr, &info.modTime, nullptr);
1337 info.size = 0;
1338
1339 directories.push_back(info);
1340 }
1341 cont = dir.GetNext(&filename);
1342 }
1343
1344 // Then collect files
1345 cont = dir.GetFirst(&filename, wxEmptyString, wxDIR_FILES);
1346 while (cont) {
1347 bool isHidden = filename.StartsWith(".");
1348 if (m_gridShowHiddenFiles || !isHidden) {
1349 wxString fullPath = path + wxFileName::GetPathSeparator() + filename;
1350
1351 FileInfo info;
1352 info.name = filename;
1353 info.fullPath = fullPath;
1354 info.isDirectory = false;
1355
1356 wxFileName fn(fullPath);
1357 fn.GetTimes(nullptr, &info.modTime, nullptr);
1358 info.size = fn.GetSize();
1359
1360 files.push_back(info);
1361 }
1362 cont = dir.GetNext(&filename);
1363 }
1364
1365 // Sort based on preference
1366 auto sortFunc = [this](const FileInfo &a, const FileInfo &b) -> bool {
1368 switch (sortBy) {
1370 return a.name.CmpNoCase(b.name) < 0;
1372 return a.modTime > b.modTime; // Most recent first
1374 return a.size > b.size; // Largest first
1376 wxString extA = wxFileName(a.name).GetExt().Lower();
1377 wxString extB = wxFileName(b.name).GetExt().Lower();
1378 if (extA != extB)
1379 return extA < extB;
1380 return a.name.CmpNoCase(b.name) < 0;
1381 }
1382 }
1383 return a.name.CmpNoCase(b.name) < 0; // Default fallback
1384 };
1385
1386 std::sort(directories.begin(), directories.end(), sortFunc);
1387 std::sort(files.begin(), files.end(), sortFunc);
1388
1389 // Add sorted directories to grid
1390 for (const auto &dirInfo : directories) {
1391 wxString displayName = GetDisplayName(dirInfo.name, true, false);
1392 AddGridItem(displayName, dirInfo.fullPath, true);
1393 }
1394
1395 // Add sorted files to grid
1396 for (const auto &fileInfo : files) {
1397 wxString displayName = GetDisplayName(fileInfo.name, false, false);
1398 AddGridItem(displayName, fileInfo.fullPath, false);
1399 }
1400
1401 // Update layout properly
1402 m_gridSizer->Layout();
1403
1404 // Set the virtual size to exactly fit the content (no extra space to distribute)
1405 wxSize minSize = m_gridSizer->GetMinSize();
1406 m_gridCtrl->SetVirtualSize(minSize);
1407
1408 m_gridCtrl->Layout();
1409 m_gridCtrl->FitInside();
1410 m_gridCtrl->Refresh();
1411}
1412
1414 if (!m_gridSizer)
1415 return;
1416
1417 // Clear selection
1418 m_selectedGridItem = nullptr;
1419
1420 // Delete all grid item widgets and their client data
1421 for (auto &item : m_gridItems) {
1422 if (item.widget) {
1423 // First, delete the client data to avoid memory leak
1424 FileTreeItemData *data = static_cast<FileTreeItemData *>(item.widget->GetClientData());
1425 if (data) {
1426 delete data;
1427 item.widget->SetClientData(nullptr);
1428 }
1429 // Then destroy the widget
1430 item.widget->Destroy();
1431 }
1432 }
1433 m_gridItems.clear();
1434
1435 m_gridSizer->Clear();
1436}
1437
1438void FileExplorerTab::AddGridItem(const wxString &name, const wxString &fullPath, bool isDirectory) {
1439 if (!m_gridCtrl || !m_gridSizer)
1440 return;
1441
1442 // Calculate panel size and text size based on icon size
1443 int panelWidth = m_gridIconSize + 40; // Icon + padding
1444 // Adjust height based on icon size to accommodate larger text
1445 int textPadding = (m_gridIconSize == 48) ? 40 : ((m_gridIconSize == 80) ? 50 : 60);
1446 int panelHeight = m_gridIconSize + textPadding; // Icon + label + padding
1447
1448 // Create a panel for this item
1449 wxPanel *itemPanel = new wxPanel(m_gridCtrl, wxID_ANY, wxDefaultPosition, wxSize(panelWidth, panelHeight));
1450 itemPanel->SetBackgroundColour(wxColour(50, 50, 50));
1451 itemPanel->SetToolTip(name); // Show full name on hover
1452
1453 wxBoxSizer *itemSizer = new wxBoxSizer(wxVERTICAL);
1454
1455 // Determine icon type (use fullPath to get extension, even if name has extension stripped)
1456 wxString iconType = isDirectory ? "folder" : "file";
1457 if (!isDirectory) {
1458 wxString ext = wxFileName(fullPath).GetExt().Lower();
1459 if (ext == "xml")
1460 iconType = "xml";
1461 else if (ext == "json")
1462 iconType = "json";
1463 else if (ext == "txt")
1464 iconType = "txt";
1465 else if (ext == "png" || ext == "jpg" || ext == "jpeg")
1466 iconType = "png";
1467 else if (ext == "md" || ext == "markdown")
1468 iconType = "markdown";
1469 }
1470
1471 // Load and add icon (use dynamic icon size from preferences)
1472 wxBitmap icon = LoadGridIcon(iconType, wxSize(m_gridIconSize, m_gridIconSize));
1473 wxStaticBitmap *iconCtrl = new wxStaticBitmap(itemPanel, wxID_ANY, icon);
1474 iconCtrl->SetToolTip(name); // Tooltip on icon too
1475 itemSizer->Add(iconCtrl, 0, wxALIGN_CENTER | wxALL, 5);
1476
1477 // Add label with smart ellipsization and font size based on icon size
1478 int labelWidth = panelWidth - 10; // Panel width minus padding
1479 wxStaticText *label = new wxStaticText(itemPanel, wxID_ANY, name, wxDefaultPosition, wxSize(labelWidth, -1),
1480 wxST_ELLIPSIZE_END | wxALIGN_CENTER_HORIZONTAL);
1481 label->SetForegroundColour(wxColour(200, 200, 200));
1482
1483 // Scale font size based on icon size
1484 wxFont labelFont = label->GetFont();
1485 if (m_gridIconSize == 48) {
1486 // Small icons: smaller font
1487 labelFont = labelFont.Smaller();
1488 } else if (m_gridIconSize == 80) {
1489 // Medium icons: normal font (no change)
1490 // labelFont stays as is
1491 } else if (m_gridIconSize == 128) {
1492 // Large icons: larger font
1493 labelFont = labelFont.Larger();
1494 }
1495 label->SetFont(labelFont);
1496 label->SetToolTip(name); // Tooltip on label too
1497 itemSizer->Add(label, 0, wxALIGN_CENTER | wxALL, 2);
1498
1499 itemPanel->SetSizer(itemSizer);
1500
1501 // Bind events
1502 itemPanel->Bind(wxEVT_LEFT_DOWN, &FileExplorerTab::OnGridItemMouseDown, this);
1503 itemPanel->Bind(wxEVT_LEFT_UP, &FileExplorerTab::OnGridItemMouseUp, this);
1504 itemPanel->Bind(wxEVT_MOTION, &FileExplorerTab::OnGridItemMouseMove, this);
1505 itemPanel->Bind(wxEVT_LEFT_DCLICK, &FileExplorerTab::OnGridItemDoubleClick, this);
1506 itemPanel->Bind(wxEVT_RIGHT_DOWN, &FileExplorerTab::OnGridContextMenu, this);
1507
1508 iconCtrl->Bind(wxEVT_LEFT_DOWN, &FileExplorerTab::OnGridItemMouseDown, this);
1509 iconCtrl->Bind(wxEVT_LEFT_UP, &FileExplorerTab::OnGridItemMouseUp, this);
1510 iconCtrl->Bind(wxEVT_MOTION, &FileExplorerTab::OnGridItemMouseMove, this);
1511 iconCtrl->Bind(wxEVT_LEFT_DCLICK, &FileExplorerTab::OnGridItemDoubleClick, this);
1512 iconCtrl->Bind(wxEVT_RIGHT_DOWN, &FileExplorerTab::OnGridContextMenu, this);
1513
1514 label->Bind(wxEVT_LEFT_DOWN, &FileExplorerTab::OnGridItemMouseDown, this);
1515 label->Bind(wxEVT_LEFT_UP, &FileExplorerTab::OnGridItemMouseUp, this);
1516 label->Bind(wxEVT_MOTION, &FileExplorerTab::OnGridItemMouseMove, this);
1517 label->Bind(wxEVT_LEFT_DCLICK, &FileExplorerTab::OnGridItemDoubleClick, this);
1518 label->Bind(wxEVT_RIGHT_DOWN, &FileExplorerTab::OnGridContextMenu, this);
1519
1520 // Store client data for event handling
1521 itemPanel->SetClientData(new FileTreeItemData(fullPath, isDirectory));
1522
1523 // Add with fixed positioning - no stretch, just natural spacing
1524 m_gridSizer->Add(itemPanel, 0, wxALL, 5);
1525
1526 // Track the item
1527 GridItem gridItem;
1528 gridItem.name = name;
1529 gridItem.fullPath = fullPath;
1530 gridItem.isDirectory = isDirectory;
1531 gridItem.widget = itemPanel;
1532 m_gridItems.push_back(gridItem);
1533}
1534
1535// ============================================================================
1536// Event Handlers - Grid
1537// ============================================================================
1538
1540 wxWindow *window = dynamic_cast<wxWindow *>(event.GetEventObject());
1541 if (!window) {
1542 event.Skip();
1543 return;
1544 }
1545
1546 // Get the top-level panel
1547 wxWindow *panelWindow = window;
1548 while (panelWindow && panelWindow->GetParent() != m_gridCtrl) {
1549 panelWindow = panelWindow->GetParent();
1550 }
1551
1552 // Make sure we found a valid panel with client data
1553 if (panelWindow && panelWindow->GetClientData()) {
1554 FileTreeItemData *data = static_cast<FileTreeItemData *>(panelWindow->GetClientData());
1555 if (data != nullptr) {
1556 if (data->IsDirectory()) {
1557 // Navigate into folder - use CallAfter to avoid destroying the widget
1558 // while we're still processing its event
1559 wxString path = data->GetPath();
1560 LOG_INFO("FileExplorer", "Navigating to: " + path.ToStdString());
1561 m_panel->CallAfter([this, path]() { NavigateToPath(path); });
1562 return; // Don't call event.Skip() since we're handling this
1563 } else {
1564 // File activated - could trigger open/load
1565 LOG_INFO("FileExplorer", "File activated: " + data->GetPath().ToStdString());
1566 }
1567 }
1568 }
1569
1570 event.Skip();
1571}
1572
1573void FileExplorerTab::OnGridItemMouseDown(wxMouseEvent &event) {
1574 wxWindow *window = dynamic_cast<wxWindow *>(event.GetEventObject());
1575 if (!window) {
1576 event.Skip();
1577 return;
1578 }
1579
1580 // Get the top-level panel (might be clicking on icon or label)
1581 wxWindow *panelWindow = window;
1582 while (panelWindow && panelWindow->GetParent() != m_gridCtrl) {
1583 panelWindow = panelWindow->GetParent();
1584 }
1585
1586 // Make sure we found a valid panel with client data
1587 if (panelWindow && panelWindow->GetClientData()) {
1588 FileTreeItemData *data = static_cast<FileTreeItemData *>(panelWindow->GetClientData());
1589 if (data != nullptr) {
1590 LOG_INFO("FileExplorer", "Grid item clicked: " + data->GetPath().ToStdString());
1591
1592 // Clear previous selection
1593 if (m_selectedGridItem && m_selectedGridItem != panelWindow) {
1594 m_selectedGridItem->SetBackgroundColour(wxColour(50, 50, 50));
1595 m_selectedGridItem->Refresh();
1596 }
1597
1598 // Highlight new selection
1599 panelWindow->SetBackgroundColour(EmberForge::AppPreferencesManager::GetAccentColor());
1600 panelWindow->Refresh();
1601 m_selectedGridItem = panelWindow;
1602
1603 // Prepare for potential drag operation
1604 m_dragStartPos = event.GetPosition();
1605 m_dragWindow = panelWindow;
1606 }
1607 }
1608
1609 event.Skip();
1610}
1611
1612void FileExplorerTab::OnGridItemMouseMove(wxMouseEvent &event) {
1613 if (!event.LeftIsDown() || !m_dragWindow) {
1614 event.Skip();
1615 return;
1616 }
1617
1618 // Check if we've moved enough to start a drag
1619 wxPoint currentPos = event.GetPosition();
1620 int dx = abs(currentPos.x - m_dragStartPos.x);
1621 int dy = abs(currentPos.y - m_dragStartPos.y);
1622
1623 if (!m_isDragging && (dx > 3 || dy > 3)) {
1624 // Start the drag operation
1625 FileTreeItemData *data = static_cast<FileTreeItemData *>(m_dragWindow->GetClientData());
1626 if (!data) {
1627 event.Skip();
1628 return;
1629 }
1630
1631 // Only allow dragging files, not directories
1632 if (data->IsDirectory()) {
1633 LOG_INFO("FileExplorer", "Cannot drag directories");
1634 m_dragWindow = nullptr;
1635 event.Skip();
1636 return;
1637 }
1638
1639 wxString filePath = data->GetPath();
1640
1641 // Only allow dragging XML files
1642 if (!filePath.Lower().EndsWith(".xml")) {
1643 LOG_INFO("FileExplorer", "Only XML files can be dragged");
1644 m_dragWindow = nullptr;
1645 event.Skip();
1646 return;
1647 }
1648
1649 m_isDragging = true;
1650 LOG_INFO("FileExplorer", "Starting drag for file: " + filePath.ToStdString());
1651
1652 // Create a file data object with the file path
1653 wxFileDataObject fileData;
1654 fileData.AddFile(filePath);
1655
1656 // Create a drop source and begin the drag operation
1657 wxDropSource dragSource(fileData, m_gridCtrl);
1658 wxDragResult result = dragSource.DoDragDrop(wxDrag_DefaultMove);
1659
1660 // Log the result
1661 switch (result) {
1662 case wxDragCopy:
1663 LOG_INFO("FileExplorer", "File copied");
1664 break;
1665 case wxDragMove:
1666 LOG_INFO("FileExplorer", "File moved");
1667 break;
1668 case wxDragCancel:
1669 LOG_INFO("FileExplorer", "Drag cancelled");
1670 break;
1671 default:
1672 LOG_INFO("FileExplorer", "Drag completed with unknown result");
1673 break;
1674 }
1675
1676 // Reset drag state
1677 m_isDragging = false;
1678 m_dragWindow = nullptr;
1679 }
1680
1681 event.Skip();
1682}
1683
1684void FileExplorerTab::OnGridItemMouseUp(wxMouseEvent &event) {
1685 // Reset drag state
1686 m_isDragging = false;
1687 m_dragWindow = nullptr;
1688
1689 event.Skip();
1690}
1691
1692// ============================================================================
1693// Event Handlers - Favorites/Filter
1694// ============================================================================
1695
1696void FileExplorerTab::OnFavoriteFilter(wxCommandEvent &event) {
1697 wxObject *source = event.GetEventObject();
1698
1699 // Determine which button was clicked
1700 wxString newFilter = "";
1701 if (source == m_xmlFilterBtn) {
1702 if (m_xmlFilterBtn->GetValue()) {
1703 newFilter = ".xml";
1704 m_pngFilterBtn->SetValue(false);
1705 m_jsonFilterBtn->SetValue(false);
1706 }
1707 } else if (source == m_pngFilterBtn) {
1708 if (m_pngFilterBtn->GetValue()) {
1709 newFilter = ".png";
1710 m_xmlFilterBtn->SetValue(false);
1711 m_jsonFilterBtn->SetValue(false);
1712 }
1713 } else if (source == m_jsonFilterBtn) {
1714 if (m_jsonFilterBtn->GetValue()) {
1715 newFilter = ".json";
1716 m_xmlFilterBtn->SetValue(false);
1717 m_pngFilterBtn->SetValue(false);
1718 }
1719 }
1720
1721 // Update filter and refresh tree
1722 m_activeFilter = newFilter;
1723 LOG_INFO("FileExplorer",
1724 "Filter changed to: " + (m_activeFilter.IsEmpty() ? "none" : m_activeFilter.ToStdString()));
1725 PopulateTree();
1726
1727 // Highlight active button
1728 if (m_xmlFilterBtn && m_xmlFilterBtn->GetValue()) {
1730 } else if (m_xmlFilterBtn) {
1731 m_xmlFilterBtn->SetBackgroundColour(wxColour(55, 55, 55));
1732 }
1733
1734 if (m_pngFilterBtn && m_pngFilterBtn->GetValue()) {
1736 } else if (m_pngFilterBtn) {
1737 m_pngFilterBtn->SetBackgroundColour(wxColour(55, 55, 55));
1738 }
1739
1740 if (m_jsonFilterBtn && m_jsonFilterBtn->GetValue()) {
1742 } else if (m_jsonFilterBtn) {
1743 m_jsonFilterBtn->SetBackgroundColour(wxColour(55, 55, 55));
1744 }
1745}
1746
1747// ============================================================================
1748// Event Handlers - View Toggle
1749// ============================================================================
1750
1751void FileExplorerTab::OnTreeToggle(wxCommandEvent &event) {
1752 m_treeVisible = m_treeToggleBtn->GetValue();
1753
1754 // Ensure at least one view is visible - if trying to hide tree and grid is already hidden, enable grid
1755 if (!m_treeVisible && !m_gridVisible) {
1756 m_gridVisible = true;
1757 m_gridToggleBtn->SetValue(true);
1758 }
1759
1762
1763 LOG_INFO("FileExplorer", "Tree visibility: " + std::string(m_treeVisible ? "on" : "off"));
1764}
1765
1766void FileExplorerTab::OnGridToggle(wxCommandEvent &event) {
1767 m_gridVisible = m_gridToggleBtn->GetValue();
1768
1769 // Ensure at least one view is visible - if trying to hide grid and tree is already hidden, enable tree
1770 if (!m_gridVisible && !m_treeVisible) {
1771 m_treeVisible = true;
1772 m_treeToggleBtn->SetValue(true);
1773 }
1774
1777
1778 LOG_INFO("FileExplorer", "Grid visibility: " + std::string(m_gridVisible ? "on" : "off"));
1779}
1780
1783 return;
1784
1785 // Sync button values with actual visibility state
1786 m_treeToggleBtn->SetValue(m_treeVisible);
1787 m_gridToggleBtn->SetValue(m_gridVisible);
1788
1789 // Update tree button appearance
1790 if (m_treeVisible) {
1792 } else {
1793 m_treeToggleBtn->SetBackgroundColour(wxColour(55, 55, 55)); // Inactive - gray
1794 }
1795 m_treeToggleBtn->Refresh();
1796
1797 // Update grid button appearance
1798 if (m_gridVisible) {
1800 } else {
1801 m_gridToggleBtn->SetBackgroundColour(wxColour(55, 55, 55)); // Inactive - gray
1802 }
1803 m_gridToggleBtn->Refresh();
1804}
1805
1806void FileExplorerTab::OnSplitterSashPosChanged(wxSplitterEvent &event) {
1808 event.Skip();
1809}
1810
1813 event.Skip();
1814}
1815
1818 return;
1819
1820 // Get the splitter sash position (width of left panel)
1821 int sashPos = m_splitter->GetSashPosition();
1822
1823 // Account for margins/padding (the search control has 2px margin on each side)
1824 // Also account for the splitter itself starting after toolbar panel margin
1825 int searchWidth = sashPos - 8; // Adjust for margins
1826
1827 // Clamp to reasonable minimum width
1828 if (searchWidth < 100)
1829 searchWidth = 100;
1830 if (searchWidth > 500)
1831 searchWidth = 500;
1832
1833 // Only update if width actually changed
1834 wxSize currentSize = m_treeSearchCtrl->GetSize();
1835 if (currentSize.GetWidth() != searchWidth) {
1836 m_treeSearchCtrl->SetMinSize(wxSize(searchWidth, -1));
1837 m_treeSearchCtrl->SetSize(wxSize(searchWidth, currentSize.GetHeight()));
1838
1839 // Force layout update of parent
1840 wxWindow *parent = m_treeSearchCtrl->GetParent();
1841 if (parent) {
1842 parent->Layout();
1843 parent->Refresh();
1844 }
1845 }
1846}
1847
1849 if (!m_breadcrumbPanel)
1850 return;
1851
1852 // Show/hide the breadcrumb path only
1853 // Keep navigation buttons (back/forward) and grid search visible
1854 if (m_showBreadcrumb) {
1855 m_breadcrumbPanel->Show();
1856 } else {
1857 m_breadcrumbPanel->Hide();
1858 }
1859
1860 // Force layout update
1863 }
1864 if (m_rightPanel) {
1865 m_rightPanel->Layout();
1866 }
1867 m_panel->Layout();
1868}
1869
1871 if (!m_functionalPanel)
1872 return;
1873
1874 // Show/hide the history button (in functional panel)
1875 if (m_showHistory) {
1876 m_functionalPanel->Show();
1877 } else {
1878 m_functionalPanel->Hide();
1879 }
1880
1881 // Force layout update
1884 }
1885 if (m_rightPanel) {
1886 m_rightPanel->Layout();
1887 }
1888 m_panel->Layout();
1889}
1890
1892 if (!m_treeCtrl)
1893 return;
1894
1895 // Get the current image list (if any) to check if size changed
1896 wxImageList *oldImageList = m_treeCtrl->GetImageList();
1897 int oldSize = oldImageList ? oldImageList->GetSize().GetWidth() : -1;
1898
1899 // Only rebuild if size changed
1900 if (oldSize == m_treeItemSize) {
1901 return;
1902 }
1903
1904 LOG_INFO("FileExplorer",
1905 wxString::Format("Updating tree image list size from %d to %d", oldSize, m_treeItemSize).ToStdString());
1906
1907 // Create new image list with updated size
1908 wxImageList *newImageList = new wxImageList(m_treeItemSize, m_treeItemSize);
1909
1910 // Reload icons at new size
1911 wxIcon folderIcon = LoadTreeIcon("folder");
1912 wxIcon fileIcon = LoadTreeIcon("file");
1913
1914 if (folderIcon.IsOk()) {
1915 m_folderIcon = newImageList->Add(folderIcon);
1916 }
1917 if (fileIcon.IsOk()) {
1918 m_fileIcon = newImageList->Add(fileIcon);
1919 }
1920
1921 // Assign new image list (tree takes ownership)
1922 m_treeCtrl->AssignImageList(newImageList);
1923
1924 // Update tree spacing based on new size
1925 int spacing = m_treeItemSize + (m_treeItemSize == 16 ? 4 : (m_treeItemSize == 24 ? 6 : 8));
1926 m_treeCtrl->SetSpacing(spacing);
1927
1928 // Force tree to refresh with new image list
1929 m_treeCtrl->Refresh();
1930}
1931
1933 if (!m_splitter || !m_leftPanel || !m_rightPanel)
1934 return;
1935
1936 // First, unsplit if currently split
1937 if (m_splitter->IsSplit()) {
1938 // Determine which panel to keep based on what will be visible
1940 // Both visible - keep split, nothing to do here
1941 } else if (m_treeVisible) {
1942 m_splitter->Unsplit(m_rightPanel);
1943 } else if (m_gridVisible) {
1944 m_splitter->Unsplit(m_leftPanel);
1945 }
1946 }
1947
1948 // Now handle the visibility
1950 // Show both panels in split view
1951 m_leftPanel->Show();
1952 m_rightPanel->Show();
1953 if (!m_splitter->IsSplit()) {
1954 m_splitter->SplitVertically(m_leftPanel, m_rightPanel, 200);
1955 }
1956 } else if (m_treeVisible && !m_gridVisible) {
1957 // Show only tree
1958 m_rightPanel->Hide();
1959 m_leftPanel->Show();
1960 // Replace the splitter content with just the left panel
1961 if (!m_splitter->IsSplit()) {
1962 m_splitter->Initialize(m_leftPanel);
1963 }
1964 } else if (!m_treeVisible && m_gridVisible) {
1965 // Show only grid
1966 m_leftPanel->Hide();
1967 m_rightPanel->Show();
1968 // Replace the splitter content with just the right panel
1969 if (!m_splitter->IsSplit()) {
1970 m_splitter->Initialize(m_rightPanel);
1971 }
1972 }
1973
1974 m_splitter->Layout();
1975 m_panel->Layout();
1976 m_panel->Refresh();
1977}
1978
1979// ============================================================================
1980// Event Handlers - Breadcrumb
1981// ============================================================================
1982
1983void FileExplorerTab::OnBreadcrumbClick(wxCommandEvent &event) {
1984 // Close history panel if it's open
1985 if (m_isHistoryVisible) {
1987 }
1988
1989 wxButton *btn = dynamic_cast<wxButton *>(event.GetEventObject());
1990 if (btn) {
1991 // Safely get the path from client data
1992 wxString *pathPtr = static_cast<wxString *>(btn->GetClientData());
1993 if (pathPtr && wxDirExists(*pathPtr)) {
1994 wxString pathCopy = *pathPtr; // Make a copy before navigating
1995 NavigateToPath(pathCopy);
1996 }
1997 }
1998}
1999
2000// ============================================================================
2001// Helper Methods
2002// ============================================================================
2003
2004void FileExplorerTab::NavigateToPath(const wxString &path, bool recordHistory) {
2005 if (!wxDir::Exists(path)) {
2006 LOG_ERROR("FileExplorer", "Cannot navigate to non-existent path: " + path.ToStdString());
2007 return;
2008 }
2009
2010 // Store previous path for activity log
2011 wxString previousPath = m_currentGridPath;
2012
2013 // Add current path to navigation history if requested (and we're not already navigating via history)
2014 if (recordHistory && !m_isNavigatingHistory && !previousPath.IsEmpty() && previousPath != path) {
2015 AddToNavigationHistory(previousPath);
2016 }
2017
2018 m_currentGridPath = path;
2019 PopulateGrid(path);
2021 // No automatic tree syncing - tree and grid are independent
2022
2023 // Log navigation activity with from -> to format
2024 if (recordHistory) {
2025 if (!previousPath.IsEmpty() && previousPath != path) {
2026 LogActivity(ActivityType::NAVIGATE, path, previousPath + " -> " + path);
2027 } else {
2029 }
2030 }
2031
2032 // Update button states after navigation
2033 if (recordHistory) {
2035 }
2036}
2037
2038wxTreeItemId FileExplorerTab::FindTreeItemByPath(const wxString &path, wxTreeItemId parent) {
2039 if (!m_treeCtrl || !parent.IsOk()) {
2040 // If parent is not provided, start from root
2041 if (!m_treeCtrl)
2042 return wxTreeItemId();
2043 parent = m_treeCtrl->GetRootItem();
2044 if (!parent.IsOk())
2045 return wxTreeItemId();
2046 }
2047
2048 // Check if this item matches the path
2049 FileTreeItemData *data = dynamic_cast<FileTreeItemData *>(m_treeCtrl->GetItemData(parent));
2050 if (data && data->GetPath() == path) {
2051 return parent;
2052 }
2053
2054 // Recursively search children
2055 wxTreeItemIdValue cookie;
2056 wxTreeItemId child = m_treeCtrl->GetFirstChild(parent, cookie);
2057
2058 while (child.IsOk()) {
2059 // First check if the child itself matches
2060 FileTreeItemData *childData = dynamic_cast<FileTreeItemData *>(m_treeCtrl->GetItemData(child));
2061 if (childData && childData->GetPath() == path) {
2062 return child;
2063 }
2064
2065 // Then recursively search the child's subtree (only if path starts with child's path)
2066 if (childData && path.StartsWith(childData->GetPath())) {
2067 wxTreeItemId found = FindTreeItemByPath(path, child);
2068 if (found.IsOk()) {
2069 return found;
2070 }
2071 }
2072
2073 child = m_treeCtrl->GetNextChild(parent, cookie);
2074 }
2075
2076 return wxTreeItemId(); // Not found
2077}
2078
2081 return;
2082
2083 // Delete client data from existing breadcrumb buttons before clearing
2084 if (m_breadcrumbSizer) {
2085 for (size_t i = 0; i < m_breadcrumbSizer->GetItemCount(); i++) {
2086 wxSizerItem *item = m_breadcrumbSizer->GetItem(i);
2087 if (item && item->IsWindow()) {
2088 wxButton *btn = dynamic_cast<wxButton *>(item->GetWindow());
2089 if (btn && btn->GetClientData()) {
2090 wxString *pathPtr = static_cast<wxString *>(btn->GetClientData());
2091 if (pathPtr) {
2092 delete pathPtr;
2093 btn->SetClientData(nullptr);
2094 }
2095 }
2096 }
2097 }
2098
2099 // Clear all breadcrumb items (navigation buttons are in separate panel, so safe to clear)
2100 m_breadcrumbSizer->Clear(true);
2101 }
2102
2103 // Get relative path from root to current grid path
2104 wxString relativePath = m_currentGridPath;
2105 if (relativePath.StartsWith(m_rootPath)) {
2106 relativePath = relativePath.Mid(m_rootPath.Length());
2107 if (relativePath.StartsWith(wxFileName::GetPathSeparator())) {
2108 relativePath = relativePath.Mid(1);
2109 }
2110 }
2111
2112 // Add root button
2113 wxButton *rootBtn =
2114 new wxButton(m_breadcrumbPanel, wxID_ANY, "Resources", wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT);
2115 rootBtn->SetBackgroundColour(wxColour(60, 60, 60));
2116 rootBtn->SetForegroundColour(wxColour(200, 200, 200));
2117 rootBtn->SetMinSize(wxSize(-1, 32)); // Taller button for better visibility
2118 wxFont rootFont = rootBtn->GetFont();
2119 rootFont.SetPointSize(rootFont.GetPointSize() + 1);
2120 rootBtn->SetFont(rootFont);
2121 rootBtn->SetClientData(new wxString(m_rootPath));
2122 rootBtn->Bind(wxEVT_BUTTON, &FileExplorerTab::OnBreadcrumbClick, this);
2123 m_breadcrumbSizer->Add(rootBtn, 0, wxALIGN_CENTER_VERTICAL | wxALL, 2);
2124
2125 // If we're not at root, add subdirectories
2126 if (!relativePath.IsEmpty()) {
2127 wxArrayString pathParts = wxSplit(relativePath, wxFileName::GetPathSeparator());
2128 wxString currentPath = m_rootPath;
2129
2130 for (size_t i = 0; i < pathParts.GetCount(); i++) {
2131 if (pathParts[i].IsEmpty())
2132 continue;
2133
2134 // Add separator
2135 wxStaticText *separator = new wxStaticText(m_breadcrumbPanel, wxID_ANY, " > ");
2136 separator->SetForegroundColour(wxColour(150, 150, 150));
2137 wxFont sepFont = separator->GetFont();
2138 sepFont.SetPointSize(sepFont.GetPointSize() + 1);
2139 separator->SetFont(sepFont);
2140 m_breadcrumbSizer->Add(separator, 0, wxALIGN_CENTER_VERTICAL | wxALL, 2);
2141
2142 // Add directory button
2143 currentPath += wxFileName::GetPathSeparator() + pathParts[i];
2144 wxButton *dirBtn = new wxButton(m_breadcrumbPanel, wxID_ANY, pathParts[i], wxDefaultPosition, wxDefaultSize,
2145 wxBU_EXACTFIT);
2146 dirBtn->SetBackgroundColour(wxColour(60, 60, 60));
2147 dirBtn->SetForegroundColour(wxColour(200, 200, 200));
2148 dirBtn->SetMinSize(wxSize(-1, 32)); // Taller button for better visibility
2149 wxFont dirFont = dirBtn->GetFont();
2150 dirFont.SetPointSize(dirFont.GetPointSize() + 1);
2151 dirBtn->SetFont(dirFont);
2152 dirBtn->SetClientData(new wxString(currentPath));
2153 dirBtn->Bind(wxEVT_BUTTON, &FileExplorerTab::OnBreadcrumbClick, this);
2154 m_breadcrumbSizer->Add(dirBtn, 0, wxALIGN_CENTER_VERTICAL | wxALL, 2);
2155 }
2156 }
2157
2158 m_breadcrumbPanel->Layout();
2159 m_breadcrumbPanel->Refresh();
2160}
2161
2162bool FileExplorerTab::PassesFilter(const wxString &filename) const {
2163 if (m_activeFilter.IsEmpty()) {
2164 return true; // No filter, show all
2165 }
2166
2167 wxString ext = wxFileName(filename).GetExt().Lower();
2168
2169 if (m_activeFilter == ".xml") {
2170 return ext == "xml";
2171 } else if (m_activeFilter == ".png") {
2172 return ext == "png" || ext == "jpg" || ext == "jpeg";
2173 } else if (m_activeFilter == ".json") {
2174 return ext == "json";
2175 }
2176
2177 return false;
2178}
2179
2180bool FileExplorerTab::DirectoryContainsFilteredFiles(const wxString &path) const {
2181 // If no filter is active, all directories are valid
2182 if (m_activeFilter.IsEmpty()) {
2183 return true;
2184 }
2185
2186 if (!wxDir::Exists(path)) {
2187 return false;
2188 }
2189
2190 wxDir dir(path);
2191 if (!dir.IsOpened()) {
2192 return false;
2193 }
2194
2195 // Check files in current directory
2196 wxString filename;
2197 bool cont = dir.GetFirst(&filename, wxEmptyString, wxDIR_FILES);
2198 while (cont) {
2199 if (!filename.StartsWith(".") && PassesFilter(filename)) {
2200 return true; // Found a matching file
2201 }
2202 cont = dir.GetNext(&filename);
2203 }
2204
2205 // Recursively check subdirectories
2206 cont = dir.GetFirst(&filename, wxEmptyString, wxDIR_DIRS);
2207 while (cont) {
2208 if (!filename.StartsWith(".")) {
2209 wxString fullPath = path + wxFileName::GetPathSeparator() + filename;
2210 if (DirectoryContainsFilteredFiles(fullPath)) {
2211 return true; // Found matching files in subdirectory
2212 }
2213 }
2214 cont = dir.GetNext(&filename);
2215 }
2216
2217 return false; // No matching files found in this directory or its subdirectories
2218}
2219
2220// ============================================================================
2221// Context Menu Event Handlers
2222// ============================================================================
2223
2224void FileExplorerTab::OnTreeContextMenu(wxTreeEvent &event) {
2225 wxTreeItemId item = event.GetItem();
2226 if (!item.IsOk()) {
2227 return;
2228 }
2229
2230 // Select the item that was right-clicked
2231 m_treeCtrl->SelectItem(item);
2232
2233 // Get the item data
2234 FileTreeItemData *data = dynamic_cast<FileTreeItemData *>(m_treeCtrl->GetItemData(item));
2235 if (!data) {
2236 return;
2237 }
2238
2239 // Only show context menu for directories
2240 if (!data->IsDirectory()) {
2241 LOG_INFO("FileExplorer", "Context menu only available for folders");
2242 return;
2243 }
2244
2245 wxString folderPath = data->GetPath();
2246 LOG_INFO("FileExplorer", "Tree context menu for: " + folderPath.ToStdString());
2247
2248 // Store the path for use in menu handlers
2249 m_contextMenuPath = folderPath;
2250
2251 // Create the context menu
2252 wxMenu contextMenu;
2253 contextMenu.Append(ID_CONTEXT_OPEN, "Open");
2254 contextMenu.AppendSeparator();
2255 contextMenu.Append(ID_CONTEXT_RENAME, "Rename");
2256 contextMenu.Append(ID_CONTEXT_DELETE, "Delete");
2257 contextMenu.AppendSeparator();
2258 contextMenu.Append(ID_CONTEXT_COPY, "Copy");
2259 contextMenu.Append(ID_CONTEXT_CUT, "Cut");
2260 contextMenu.Append(ID_CONTEXT_PASTE, "Paste");
2261 contextMenu.AppendSeparator();
2262 contextMenu.Append(ID_CONTEXT_REFRESH, "Refresh");
2263 contextMenu.AppendSeparator();
2264 contextMenu.Append(ID_CONTEXT_PROPERTIES, "Properties");
2265
2266 // Enable/disable paste based on clipboard state
2267 contextMenu.Enable(ID_CONTEXT_PASTE, !m_clipboardPath.IsEmpty());
2268
2269 // Bind menu events
2270 contextMenu.Bind(wxEVT_MENU, &FileExplorerTab::OnContextMenuOpen, this, ID_CONTEXT_OPEN);
2271 contextMenu.Bind(wxEVT_MENU, &FileExplorerTab::OnContextMenuRename, this, ID_CONTEXT_RENAME);
2272 contextMenu.Bind(wxEVT_MENU, &FileExplorerTab::OnContextMenuDelete, this, ID_CONTEXT_DELETE);
2273 contextMenu.Bind(wxEVT_MENU, &FileExplorerTab::OnContextMenuCopy, this, ID_CONTEXT_COPY);
2274 contextMenu.Bind(wxEVT_MENU, &FileExplorerTab::OnContextMenuCut, this, ID_CONTEXT_CUT);
2275 contextMenu.Bind(wxEVT_MENU, &FileExplorerTab::OnContextMenuPaste, this, ID_CONTEXT_PASTE);
2276 contextMenu.Bind(wxEVT_MENU, &FileExplorerTab::OnContextMenuRefresh, this, ID_CONTEXT_REFRESH);
2277 contextMenu.Bind(wxEVT_MENU, &FileExplorerTab::OnContextMenuProperties, this, ID_CONTEXT_PROPERTIES);
2278
2279 // Show the menu on the tree control at the mouse position
2280 m_treeCtrl->PopupMenu(&contextMenu);
2281}
2282
2284 // This handler is for clicks on the grid background (empty space)
2285 // Check if we actually clicked on empty space or on a grid item
2286 wxWindow *window = dynamic_cast<wxWindow *>(event.GetEventObject());
2287
2288 // If the click was on the grid control itself (not a child panel), show background menu
2289 if (window == m_gridCtrl) {
2290 LOG_INFO("FileExplorer", "Grid background context menu");
2291
2292 // Create context menu for empty space
2293 wxMenu contextMenu;
2294 contextMenu.Append(ID_CONTEXT_NEW_FOLDER, "New Folder");
2295 contextMenu.Append(ID_CONTEXT_PASTE, "Paste");
2296 contextMenu.AppendSeparator();
2297 contextMenu.Append(ID_CONTEXT_REFRESH, "Refresh");
2298
2299 // Enable/disable paste based on clipboard state
2300 contextMenu.Enable(ID_CONTEXT_PASTE, !m_clipboardPath.IsEmpty());
2301
2302 // Set context path to current grid directory
2304
2305 // Bind menu events
2306 contextMenu.Bind(wxEVT_MENU, &FileExplorerTab::OnContextMenuNewFolder, this, ID_CONTEXT_NEW_FOLDER);
2307 contextMenu.Bind(wxEVT_MENU, &FileExplorerTab::OnContextMenuPaste, this, ID_CONTEXT_PASTE);
2308 contextMenu.Bind(wxEVT_MENU, &FileExplorerTab::OnContextMenuRefresh, this, ID_CONTEXT_REFRESH);
2309
2310 // Show the menu
2311 m_gridCtrl->PopupMenu(&contextMenu);
2312 } else {
2313 // Click was on a child element, don't handle it here
2314 event.Skip();
2315 }
2316}
2317
2318void FileExplorerTab::OnGridContextMenu(wxMouseEvent &event) {
2319 wxWindow *window = dynamic_cast<wxWindow *>(event.GetEventObject());
2320 if (!window) {
2321 event.Skip();
2322 return;
2323 }
2324
2325 // Get the top-level panel
2326 wxWindow *panelWindow = window;
2327 while (panelWindow && panelWindow->GetParent() != m_gridCtrl) {
2328 panelWindow = panelWindow->GetParent();
2329 }
2330
2331 // Make sure we found a valid panel with client data
2332 if (panelWindow && panelWindow->GetClientData()) {
2333 FileTreeItemData *data = static_cast<FileTreeItemData *>(panelWindow->GetClientData());
2334 if (data != nullptr) {
2335 wxString itemPath = data->GetPath();
2336 bool isDirectory = data->IsDirectory();
2337
2338 LOG_INFO("FileExplorer",
2339 wxString::Format("Grid context menu for %s: %s", isDirectory ? "folder" : "file", itemPath)
2340 .ToStdString());
2341
2342 // Highlight the selected item visually
2343 if (m_selectedGridItem && m_selectedGridItem != panelWindow) {
2344 m_selectedGridItem->SetBackgroundColour(wxColour(50, 50, 50));
2345 m_selectedGridItem->Refresh();
2346 }
2347 m_selectedGridItem = panelWindow;
2348 panelWindow->SetBackgroundColour(wxColour(70, 90, 120));
2349 panelWindow->Refresh();
2350
2351 // Store the path for use in menu handlers
2352 m_contextMenuPath = itemPath;
2353
2354 // Create the context menu
2355 wxMenu contextMenu;
2356
2357 if (isDirectory) {
2358 // Folder context menu
2359 contextMenu.Append(ID_CONTEXT_OPEN, "Open");
2360 contextMenu.AppendSeparator();
2361 contextMenu.Append(ID_CONTEXT_RENAME, "Rename");
2362 contextMenu.Append(ID_CONTEXT_DELETE, "Delete");
2363 contextMenu.AppendSeparator();
2364 contextMenu.Append(ID_CONTEXT_COPY, "Copy");
2365 contextMenu.Append(ID_CONTEXT_CUT, "Cut");
2366 contextMenu.Append(ID_CONTEXT_PASTE, "Paste");
2367 contextMenu.AppendSeparator();
2368 contextMenu.Append(ID_CONTEXT_REFRESH, "Refresh");
2369 contextMenu.AppendSeparator();
2370 contextMenu.Append(ID_CONTEXT_PROPERTIES, "Properties");
2371
2372 // Enable/disable paste based on clipboard state
2373 contextMenu.Enable(ID_CONTEXT_PASTE, !m_clipboardPath.IsEmpty());
2374 } else {
2375 // File context menu
2376 contextMenu.Append(ID_CONTEXT_OPEN, "Open");
2377 contextMenu.AppendSeparator();
2378 contextMenu.Append(ID_CONTEXT_RENAME, "Rename");
2379 contextMenu.Append(ID_CONTEXT_DELETE, "Delete");
2380 contextMenu.AppendSeparator();
2381 contextMenu.Append(ID_CONTEXT_COPY, "Copy");
2382 contextMenu.Append(ID_CONTEXT_CUT, "Cut");
2383 contextMenu.AppendSeparator();
2384 contextMenu.Append(ID_CONTEXT_PROPERTIES, "Properties");
2385 }
2386
2387 // Bind menu events
2388 contextMenu.Bind(wxEVT_MENU, &FileExplorerTab::OnContextMenuOpen, this, ID_CONTEXT_OPEN);
2389 contextMenu.Bind(wxEVT_MENU, &FileExplorerTab::OnContextMenuRename, this, ID_CONTEXT_RENAME);
2390 contextMenu.Bind(wxEVT_MENU, &FileExplorerTab::OnContextMenuDelete, this, ID_CONTEXT_DELETE);
2391 contextMenu.Bind(wxEVT_MENU, &FileExplorerTab::OnContextMenuCopy, this, ID_CONTEXT_COPY);
2392 contextMenu.Bind(wxEVT_MENU, &FileExplorerTab::OnContextMenuCut, this, ID_CONTEXT_CUT);
2393 contextMenu.Bind(wxEVT_MENU, &FileExplorerTab::OnContextMenuPaste, this, ID_CONTEXT_PASTE);
2394 contextMenu.Bind(wxEVT_MENU, &FileExplorerTab::OnContextMenuRefresh, this, ID_CONTEXT_REFRESH);
2395 contextMenu.Bind(wxEVT_MENU, &FileExplorerTab::OnContextMenuProperties, this, ID_CONTEXT_PROPERTIES);
2396
2397 // Show the menu on the grid control at the mouse position
2398 m_gridCtrl->PopupMenu(&contextMenu);
2399
2400 // Don't skip the event - we handled it by showing the context menu
2401 return;
2402 }
2403 }
2404
2405 // Only skip if we didn't handle the event (no valid panel/data found)
2406 event.Skip();
2407}
2408
2409void FileExplorerTab::OnContextMenuOpen(wxCommandEvent &event) {
2410 if (m_contextMenuPath.IsEmpty()) {
2411 return;
2412 }
2413
2414 // Store path before deferring (m_contextMenuPath might change)
2415 wxString pathToOpen = m_contextMenuPath;
2416
2417 if (wxDir::Exists(pathToOpen)) {
2418 // It's a folder - navigate into it
2419 LOG_INFO("FileExplorer", "Opening folder: " + pathToOpen.ToStdString());
2420
2421 // Defer navigation to after menu event is fully processed
2422 // This prevents segfault from destroying widgets while menu is still active
2423 m_panel->CallAfter([this, pathToOpen]() { NavigateToPath(pathToOpen); });
2424 } else if (wxFileExists(pathToOpen)) {
2425 // It's a file - open it
2426 LOG_INFO("FileExplorer", "Opening file: " + pathToOpen.ToStdString());
2427
2428 // Defer to after menu event processing
2429 m_panel->CallAfter([this, pathToOpen]() { OpenFile(pathToOpen); });
2430 }
2431}
2432
2433void FileExplorerTab::OnContextMenuNewFolder(wxCommandEvent &event) {
2434 if (m_contextMenuPath.IsEmpty()) {
2435 return;
2436 }
2437
2438 LOG_INFO("FileExplorer", "Create new folder in: " + m_contextMenuPath.ToStdString());
2439
2440 // Show input dialog for folder name
2441 wxTextEntryDialog dialog(m_panel, "Enter folder name:", "New Folder", "New Folder");
2442
2443 if (dialog.ShowModal() == wxID_OK) {
2444 wxString folderName = dialog.GetValue().Trim();
2445
2446 if (folderName.IsEmpty()) {
2447 wxMessageBox("Folder name cannot be empty", "Error", wxOK | wxICON_ERROR, m_panel);
2448 return;
2449 }
2450
2451 // Create the folder
2452 if (CreateNewFolder(m_contextMenuPath, folderName)) {
2453 // Refresh the views
2454 PopulateTree();
2456 LOG_INFO("FileExplorer", "Folder created successfully: " + folderName.ToStdString());
2457 }
2458 }
2459}
2460
2461void FileExplorerTab::OnContextMenuRename(wxCommandEvent &event) {
2462 if (m_contextMenuPath.IsEmpty()) {
2463 return;
2464 }
2465
2466 wxFileName fn(m_contextMenuPath);
2467 wxString oldName = fn.GetFullName();
2468 bool isDirectory = wxDir::Exists(m_contextMenuPath);
2469
2470 LOG_INFO("FileExplorer",
2471 wxString::Format("Rename %s: %s", isDirectory ? "folder" : "file", m_contextMenuPath).ToStdString());
2472
2473 // Show input dialog for new name
2474 wxTextEntryDialog dialog(m_panel, "Enter new name:", isDirectory ? "Rename Folder" : "Rename File", oldName);
2475
2476 if (dialog.ShowModal() == wxID_OK) {
2477 wxString newName = dialog.GetValue().Trim();
2478
2479 if (newName.IsEmpty()) {
2480 wxMessageBox(wxString::Format("%s name cannot be empty", isDirectory ? "Folder" : "File"), "Error",
2481 wxOK | wxICON_ERROR, m_panel);
2482 return;
2483 }
2484
2485 if (newName == oldName) {
2486 return; // No change
2487 }
2488
2489 bool success = false;
2490 if (isDirectory) {
2491 success = RenameFolder(m_contextMenuPath, newName);
2492 if (success) {
2493 // Update current grid path if we renamed the current directory
2494 wxString newPath = fn.GetPath() + wxFileName::GetPathSeparator() + newName;
2496 m_currentGridPath = newPath;
2497 }
2498 }
2499 } else {
2500 success = RenameFile(m_contextMenuPath, newName);
2501 }
2502
2503 if (success) {
2504 // Refresh the views
2505 PopulateTree();
2508 LOG_INFO("FileExplorer",
2509 wxString::Format("%s renamed successfully to: %s", isDirectory ? "Folder" : "File", newName)
2510 .ToStdString());
2511 }
2512 }
2513}
2514
2515void FileExplorerTab::OnContextMenuDelete(wxCommandEvent &event) {
2516 if (m_contextMenuPath.IsEmpty()) {
2517 return;
2518 }
2519
2520 wxFileName fn(m_contextMenuPath);
2521 wxString itemName = fn.GetFullName();
2522 bool isDirectory = wxDir::Exists(m_contextMenuPath);
2523
2524 LOG_INFO(
2525 "FileExplorer",
2526 wxString::Format("Delete %s request: %s", isDirectory ? "folder" : "file", m_contextMenuPath).ToStdString());
2527
2528 // Show confirmation dialog
2529 wxString message;
2530 if (isDirectory) {
2531 message = wxString::Format(
2532 "Are you sure you want to delete the folder '%s' and all its contents?\n\nThis action cannot be undone.",
2533 itemName);
2534 } else {
2535 message = wxString::Format("Are you sure you want to delete the file '%s'?\n\nThis action cannot be undone.",
2536 itemName);
2537 }
2538 wxMessageDialog confirmDialog(m_panel, message, "Confirm Delete", wxYES_NO | wxNO_DEFAULT | wxICON_WARNING);
2539
2540 if (confirmDialog.ShowModal() == wxID_YES) {
2541 bool success = false;
2542 if (isDirectory) {
2544 if (success) {
2545 // If we deleted the current directory, navigate to parent
2547 m_currentGridPath.StartsWith(m_contextMenuPath + wxFileName::GetPathSeparator())) {
2548 wxString parentPath = fn.GetPath();
2549 if (!parentPath.IsEmpty()) {
2550 m_currentGridPath = parentPath;
2551 } else {
2553 }
2554 }
2555 }
2556 } else {
2557 success = DeleteFile(m_contextMenuPath);
2558 }
2559
2560 if (success) {
2561 // Refresh the views
2562 PopulateTree();
2565 LOG_INFO("FileExplorer",
2566 wxString::Format("%s deleted successfully: %s", isDirectory ? "Folder" : "File", itemName)
2567 .ToStdString());
2568 }
2569 }
2570}
2571
2572void FileExplorerTab::OnContextMenuCopy(wxCommandEvent &event) {
2573 if (m_contextMenuPath.IsEmpty()) {
2574 return;
2575 }
2576
2577 bool isDirectory = wxDir::Exists(m_contextMenuPath);
2578 LOG_INFO("FileExplorer",
2579 wxString::Format("Copy %s: %s", isDirectory ? "folder" : "file", m_contextMenuPath).ToStdString());
2580
2581 // Store in clipboard
2583 m_clipboardIsCut = false;
2584
2585 wxFileName fn(m_contextMenuPath);
2586 wxString itemName = fn.GetFullName();
2587 LOG_INFO("FileExplorer",
2588 wxString::Format("%s '%s' copied to clipboard", isDirectory ? "Folder" : "File", itemName).ToStdString());
2590}
2591
2592void FileExplorerTab::OnContextMenuCut(wxCommandEvent &event) {
2593 if (m_contextMenuPath.IsEmpty()) {
2594 return;
2595 }
2596
2597 bool isDirectory = wxDir::Exists(m_contextMenuPath);
2598 LOG_INFO("FileExplorer",
2599 wxString::Format("Cut %s: %s", isDirectory ? "folder" : "file", m_contextMenuPath).ToStdString());
2600
2601 // Store in clipboard
2603 m_clipboardIsCut = true;
2604
2605 wxFileName fn(m_contextMenuPath);
2606 wxString itemName = fn.GetFullName();
2607 LOG_INFO("FileExplorer",
2608 wxString::Format("%s '%s' cut to clipboard", isDirectory ? "Folder" : "File", itemName).ToStdString());
2610}
2611
2612void FileExplorerTab::OnContextMenuPaste(wxCommandEvent &event) {
2613 if (m_clipboardPath.IsEmpty() || m_contextMenuPath.IsEmpty()) {
2614 return;
2615 }
2616
2617 // Paste destination must be a folder
2618 if (!wxDir::Exists(m_contextMenuPath)) {
2619 wxMessageBox("Can only paste into folders", "Error", wxOK | wxICON_ERROR, m_panel);
2620 return;
2621 }
2622
2623 bool sourceIsDir = wxDir::Exists(m_clipboardPath);
2624 wxFileName sourceFn(m_clipboardPath);
2625 wxString sourceName = sourceFn.GetFullName();
2626 wxString destPath = m_contextMenuPath + wxFileName::GetPathSeparator() + sourceName;
2627
2628 LOG_INFO("FileExplorer", wxString::Format("Paste: %s %s %s to %s", m_clipboardIsCut ? "moving" : "copying",
2629 sourceIsDir ? "folder" : "file", m_clipboardPath, destPath)
2630 .ToStdString());
2631
2632 // Check if source still exists
2633 bool sourceExists = sourceIsDir ? wxDir::Exists(m_clipboardPath) : wxFileExists(m_clipboardPath);
2634 if (!sourceExists) {
2635 wxMessageBox(wxString::Format("Source %s no longer exists", sourceIsDir ? "folder" : "file"), "Error",
2636 wxOK | wxICON_ERROR, m_panel);
2637 m_clipboardPath.Clear();
2638 return;
2639 }
2640
2641 // Prevent pasting a folder into itself or its subdirectories
2642 if (sourceIsDir && m_contextMenuPath.StartsWith(m_clipboardPath)) {
2643 wxMessageBox("Cannot paste a folder into itself or its subdirectories", "Error", wxOK | wxICON_ERROR, m_panel);
2644 LOG_ERROR("FileExplorer", "Attempted to paste folder into itself");
2645 return;
2646 }
2647
2648 // Check if destination already exists
2649 bool destExists = sourceIsDir ? wxDir::Exists(destPath) : wxFileExists(destPath);
2650 if (destExists) {
2651 wxMessageBox(
2652 wxString::Format("A %s with this name already exists in the destination", sourceIsDir ? "folder" : "file"),
2653 "Error", wxOK | wxICON_ERROR, m_panel);
2654 return;
2655 }
2656
2657 // Perform the operation
2658 bool success = false;
2659 if (m_clipboardIsCut) {
2660 // Move operation
2661 if (sourceIsDir) {
2662 success = MoveFolder(m_clipboardPath, destPath);
2663 } else {
2664 success = MoveFile(m_clipboardPath, destPath);
2665 }
2666 if (success) {
2667 wxString sourcePath = m_clipboardPath; // Save before clearing
2668 m_clipboardPath.Clear(); // Clear clipboard after cut+paste
2669 LOG_INFO("FileExplorer",
2670 wxString::Format("%s moved successfully", sourceIsDir ? "Folder" : "File").ToStdString());
2671 LogActivity(ActivityType::PASTE, destPath, sourcePath + " -> " + destPath + " (moved)");
2672 }
2673 } else {
2674 // Copy operation
2675 if (sourceIsDir) {
2676 success = CopyFolder(m_clipboardPath, destPath);
2677 } else {
2678 success = CopyFile(m_clipboardPath, destPath);
2679 }
2680 if (success) {
2681 LOG_INFO("FileExplorer",
2682 wxString::Format("%s copied successfully", sourceIsDir ? "Folder" : "File").ToStdString());
2683 LogActivity(ActivityType::PASTE, destPath, m_clipboardPath + " -> " + destPath + " (copied)");
2684 // Keep clipboard for multiple paste operations
2685 }
2686 }
2687
2688 if (success) {
2689 // Refresh views
2690 PopulateTree();
2693 }
2694}
2695
2696void FileExplorerTab::OnContextMenuRefresh(wxCommandEvent &event) {
2697 LOG_INFO("FileExplorer", "Refreshing file explorer");
2698
2699 // Refresh both tree and grid
2700 PopulateTree();
2703
2704 wxMessageBox("File explorer refreshed", "Refresh", wxOK | wxICON_INFORMATION, m_panel);
2705}
2706
2707void FileExplorerTab::OnContextMenuProperties(wxCommandEvent &event) {
2708 if (m_contextMenuPath.IsEmpty()) {
2709 return;
2710 }
2711
2712 bool isDirectory = wxDir::Exists(m_contextMenuPath);
2713 LOG_INFO("FileExplorer",
2714 wxString::Format("Show properties for %s: %s", isDirectory ? "folder" : "file", m_contextMenuPath)
2715 .ToStdString());
2716
2717 if (isDirectory) {
2719 } else {
2721 }
2722}
2723
2724// ============================================================================
2725// Context Menu Helper Methods
2726// ============================================================================
2727
2728bool FileExplorerTab::CreateNewFolder(const wxString &parentPath, const wxString &folderName) {
2729 wxString newFolderPath = parentPath + wxFileName::GetPathSeparator() + folderName;
2730
2731 // Check if folder already exists
2732 if (wxDir::Exists(newFolderPath)) {
2733 wxMessageBox("A folder with this name already exists", "Error", wxOK | wxICON_ERROR, m_panel);
2734 return false;
2735 }
2736
2737 // Create the directory
2738 if (wxFileName::Mkdir(newFolderPath, wxS_DIR_DEFAULT, 0)) {
2739 LOG_INFO("FileExplorer", "Created folder: " + newFolderPath.ToStdString());
2740 LogActivity(ActivityType::CREATE_FOLDER, newFolderPath, folderName);
2741 return true;
2742 } else {
2743 wxMessageBox("Failed to create folder. Check permissions.", "Error", wxOK | wxICON_ERROR, m_panel);
2744 LOG_ERROR("FileExplorer", "Failed to create folder: " + newFolderPath.ToStdString());
2745 return false;
2746 }
2747}
2748
2749bool FileExplorerTab::RenameFolder(const wxString &oldPath, const wxString &newName) {
2750 wxFileName fn(oldPath);
2751 wxString newPath = fn.GetPath() + wxFileName::GetPathSeparator() + newName;
2752
2753 // Check if target already exists
2754 if (wxDir::Exists(newPath)) {
2755 wxMessageBox("A folder with this name already exists", "Error", wxOK | wxICON_ERROR, m_panel);
2756 return false;
2757 }
2758
2759 // Rename the directory
2760 if (wxRenameFile(oldPath, newPath, false)) {
2761 LOG_INFO("FileExplorer",
2762 "Renamed folder from '" + oldPath.ToStdString() + "' to '" + newPath.ToStdString() + "'");
2763 LogActivity(ActivityType::RENAME_FOLDER, newPath, oldPath + " -> " + newPath);
2764 return true;
2765 } else {
2766 wxMessageBox("Failed to rename folder. Check permissions.", "Error", wxOK | wxICON_ERROR, m_panel);
2767 LOG_ERROR("FileExplorer", "Failed to rename folder: " + oldPath.ToStdString());
2768 return false;
2769 }
2770}
2771
2772bool FileExplorerTab::DeleteFolder(const wxString &folderPath) {
2773 // Use wxFileName::Rmdir to recursively delete
2774 if (wxFileName::Rmdir(folderPath, wxPATH_RMDIR_RECURSIVE)) {
2775 LOG_INFO("FileExplorer", "Deleted folder: " + folderPath.ToStdString());
2776 LogActivity(ActivityType::DELETE_FOLDER, folderPath, wxFileName(folderPath).GetFullName());
2777 return true;
2778 } else {
2779 wxMessageBox("Failed to delete folder. Check permissions or if folder is in use.", "Error", wxOK | wxICON_ERROR,
2780 m_panel);
2781 LOG_ERROR("FileExplorer", "Failed to delete folder: " + folderPath.ToStdString());
2782 return false;
2783 }
2784}
2785
2786void FileExplorerTab::ShowFolderProperties(const wxString &folderPath) {
2787 if (!wxDir::Exists(folderPath)) {
2788 wxMessageBox("Folder does not exist", "Error", wxOK | wxICON_ERROR, m_panel);
2789 return;
2790 }
2791
2792 wxFileName fn(folderPath);
2793 wxString folderName = fn.GetFullName().IsEmpty() ? folderPath : fn.GetFullName();
2794
2795 // Get folder information
2796 wxDateTime modTime;
2797 if (fn.GetTimes(nullptr, &modTime, nullptr)) {
2798 // Successfully got modification time
2799 }
2800
2801 int fileCount = CountFilesInDirectory(folderPath, false);
2802 int totalCount = CountFilesInDirectory(folderPath, true);
2803 wxString sizeStr = GetDirectorySize(folderPath);
2804
2805 // Build properties message
2806 wxString properties;
2807 properties += "Folder Properties\n";
2808 properties += "================\n\n";
2809 properties += "Name: " + folderName + "\n";
2810 properties += "Location: " + fn.GetPath() + "\n";
2811 properties += "Full Path: " + folderPath + "\n\n";
2812 properties += "Files (direct): " + wxString::Format("%d", fileCount) + "\n";
2813 properties += "Total Items (recursive): " + wxString::Format("%d", totalCount) + "\n";
2814 properties += "Size: " + sizeStr + "\n\n";
2815
2816 if (modTime.IsValid()) {
2817 properties += "Modified: " + modTime.Format("%Y-%m-%d %H:%M:%S") + "\n";
2818 }
2819
2820 // Show in a message box (could be a custom dialog for better UX)
2821 wxMessageBox(properties, "Folder Properties", wxOK | wxICON_INFORMATION, m_panel);
2822}
2823
2824wxString FileExplorerTab::GetDirectorySize(const wxString &path) const {
2825 if (!wxDir::Exists(path)) {
2826 return "0 bytes";
2827 }
2828
2829 wxDir dir(path);
2830 if (!dir.IsOpened()) {
2831 return "Unknown";
2832 }
2833
2834 unsigned long long totalSize = 0;
2835 wxString filename;
2836
2837 // Count files in this directory
2838 bool cont = dir.GetFirst(&filename, wxEmptyString, wxDIR_FILES);
2839 while (cont) {
2840 wxString fullPath = path + wxFileName::GetPathSeparator() + filename;
2841 wxFileName fn(fullPath);
2842 totalSize += fn.GetSize().ToULong();
2843 cont = dir.GetNext(&filename);
2844 }
2845
2846 // Recursively count subdirectories
2847 cont = dir.GetFirst(&filename, wxEmptyString, wxDIR_DIRS);
2848 while (cont) {
2849 if (!filename.StartsWith(".")) {
2850 // wxString subPath = path + wxFileName::GetPathSeparator() + filename;
2851 // wxString subSize = GetDirectorySize(subPath);
2852 // Parse the size (this is simplified, would need better parsing)
2853 // For now, we'll skip adding subdirectory sizes to keep it simple
2854 }
2855 cont = dir.GetNext(&filename);
2856 }
2857
2858 // Format size nicely
2859 if (totalSize < 1024) {
2860 return wxString::Format("%llu bytes", totalSize);
2861 } else if (totalSize < 1024 * 1024) {
2862 return wxString::Format("%.2f KB", totalSize / 1024.0);
2863 } else if (totalSize < 1024 * 1024 * 1024) {
2864 return wxString::Format("%.2f MB", totalSize / (1024.0 * 1024.0));
2865 } else {
2866 return wxString::Format("%.2f GB", totalSize / (1024.0 * 1024.0 * 1024.0));
2867 }
2868}
2869
2870int FileExplorerTab::CountFilesInDirectory(const wxString &path, bool recursive) const {
2871 if (!wxDir::Exists(path)) {
2872 return 0;
2873 }
2874
2875 wxDir dir(path);
2876 if (!dir.IsOpened()) {
2877 return 0;
2878 }
2879
2880 int count = 0;
2881 wxString filename;
2882
2883 // Count files
2884 bool cont = dir.GetFirst(&filename, wxEmptyString, wxDIR_FILES);
2885 while (cont) {
2886 count++;
2887 cont = dir.GetNext(&filename);
2888 }
2889
2890 // Count directories
2891 cont = dir.GetFirst(&filename, wxEmptyString, wxDIR_DIRS);
2892 while (cont) {
2893 if (!filename.StartsWith(".")) {
2894 count++; // Count the directory itself
2895
2896 if (recursive) {
2897 wxString subPath = path + wxFileName::GetPathSeparator() + filename;
2898 count += CountFilesInDirectory(subPath, true);
2899 }
2900 }
2901 cont = dir.GetNext(&filename);
2902 }
2903
2904 return count;
2905}
2906
2907bool FileExplorerTab::CopyFolder(const wxString &sourcePath, const wxString &destPath) {
2908 if (!wxDir::Exists(sourcePath)) {
2909 wxMessageBox("Source folder does not exist", "Error", wxOK | wxICON_ERROR, m_panel);
2910 LOG_ERROR("FileExplorer", "Source folder does not exist: " + sourcePath.ToStdString());
2911 return false;
2912 }
2913
2914 // Create destination directory
2915 if (!wxFileName::Mkdir(destPath, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL)) {
2916 wxMessageBox("Failed to create destination folder", "Error", wxOK | wxICON_ERROR, m_panel);
2917 LOG_ERROR("FileExplorer", "Failed to create destination: " + destPath.ToStdString());
2918 return false;
2919 }
2920
2921 wxDir dir(sourcePath);
2922 if (!dir.IsOpened()) {
2923 wxMessageBox("Cannot open source folder", "Error", wxOK | wxICON_ERROR, m_panel);
2924 LOG_ERROR("FileExplorer", "Cannot open source folder: " + sourcePath.ToStdString());
2925 return false;
2926 }
2927
2928 wxString filename;
2929
2930 // Copy all files
2931 bool cont = dir.GetFirst(&filename, wxEmptyString, wxDIR_FILES);
2932 while (cont) {
2933 wxString sourceFile = sourcePath + wxFileName::GetPathSeparator() + filename;
2934 wxString destFile = destPath + wxFileName::GetPathSeparator() + filename;
2935
2936 if (!wxCopyFile(sourceFile, destFile, false)) {
2937 wxMessageBox("Failed to copy file: " + filename, "Error", wxOK | wxICON_ERROR, m_panel);
2938 LOG_ERROR("FileExplorer", "Failed to copy file: " + sourceFile.ToStdString());
2939 return false;
2940 }
2941
2942 cont = dir.GetNext(&filename);
2943 }
2944
2945 // Recursively copy subdirectories
2946 cont = dir.GetFirst(&filename, wxEmptyString, wxDIR_DIRS);
2947 while (cont) {
2948 if (!filename.StartsWith(".")) {
2949 wxString sourceSubDir = sourcePath + wxFileName::GetPathSeparator() + filename;
2950 wxString destSubDir = destPath + wxFileName::GetPathSeparator() + filename;
2951
2952 if (!CopyFolder(sourceSubDir, destSubDir)) {
2953 return false;
2954 }
2955 }
2956 cont = dir.GetNext(&filename);
2957 }
2958
2959 LOG_INFO("FileExplorer",
2960 "Successfully copied folder: " + sourcePath.ToStdString() + " to " + destPath.ToStdString());
2961 return true;
2962}
2963
2964bool FileExplorerTab::MoveFolder(const wxString &sourcePath, const wxString &destPath) {
2965 // Try simple rename first (works if on same filesystem)
2966 if (wxRenameFile(sourcePath, destPath, false)) {
2967 LOG_INFO("FileExplorer",
2968 "Folder moved using rename: " + sourcePath.ToStdString() + " to " + destPath.ToStdString());
2969 return true;
2970 }
2971
2972 // If rename fails, fall back to copy + delete
2973 LOG_INFO("FileExplorer", "Rename failed, using copy+delete for move operation");
2974
2975 if (!CopyFolder(sourcePath, destPath)) {
2976 return false;
2977 }
2978
2979 // Delete source after successful copy
2980 if (!DeleteFolder(sourcePath)) {
2981 wxMessageBox("Folder was copied but source could not be deleted", "Warning", wxOK | wxICON_WARNING, m_panel);
2982 LOG_WARNING("FileExplorer", "Move succeeded but source deletion failed: " + sourcePath.ToStdString());
2983 return true; // Still consider it success since copy worked
2984 }
2985
2986 LOG_INFO("FileExplorer",
2987 "Successfully moved folder: " + sourcePath.ToStdString() + " to " + destPath.ToStdString());
2988 return true;
2989}
2990
2991// ============================================================================
2992// File Operations
2993// ============================================================================
2994
2995bool FileExplorerTab::RenameFile(const wxString &oldPath, const wxString &newName) {
2996 wxFileName fn(oldPath);
2997 wxString newPath = fn.GetPath() + wxFileName::GetPathSeparator() + newName;
2998
2999 // Check if target already exists
3000 if (wxFileExists(newPath)) {
3001 wxMessageBox("A file with this name already exists", "Error", wxOK | wxICON_ERROR, m_panel);
3002 return false;
3003 }
3004
3005 // Rename the file
3006 if (wxRenameFile(oldPath, newPath, false)) {
3007 LOG_INFO("FileExplorer",
3008 "Renamed file from '" + oldPath.ToStdString() + "' to '" + newPath.ToStdString() + "'");
3009 LogActivity(ActivityType::RENAME_FILE, newPath, oldPath + " -> " + newPath);
3010 return true;
3011 } else {
3012 wxMessageBox("Failed to rename file. Check permissions.", "Error", wxOK | wxICON_ERROR, m_panel);
3013 LOG_ERROR("FileExplorer", "Failed to rename file: " + oldPath.ToStdString());
3014 return false;
3015 }
3016}
3017
3018bool FileExplorerTab::DeleteFile(const wxString &filePath) {
3019 if (wxRemoveFile(filePath)) {
3020 LOG_INFO("FileExplorer", "Deleted file: " + filePath.ToStdString());
3021 LogActivity(ActivityType::DELETE_FILE, filePath, wxFileName(filePath).GetFullName());
3022 return true;
3023 } else {
3024 wxMessageBox("Failed to delete file. Check permissions or if file is in use.", "Error", wxOK | wxICON_ERROR,
3025 m_panel);
3026 LOG_ERROR("FileExplorer", "Failed to delete file: " + filePath.ToStdString());
3027 return false;
3028 }
3029}
3030
3031bool FileExplorerTab::CopyFile(const wxString &sourcePath, const wxString &destPath) {
3032 if (!wxFileExists(sourcePath)) {
3033 wxMessageBox("Source file does not exist", "Error", wxOK | wxICON_ERROR, m_panel);
3034 LOG_ERROR("FileExplorer", "Source file does not exist: " + sourcePath.ToStdString());
3035 return false;
3036 }
3037
3038 if (wxCopyFile(sourcePath, destPath, false)) {
3039 LOG_INFO("FileExplorer",
3040 "Successfully copied file: " + sourcePath.ToStdString() + " to " + destPath.ToStdString());
3041 return true;
3042 } else {
3043 wxMessageBox("Failed to copy file", "Error", wxOK | wxICON_ERROR, m_panel);
3044 LOG_ERROR("FileExplorer", "Failed to copy file: " + sourcePath.ToStdString());
3045 return false;
3046 }
3047}
3048
3049bool FileExplorerTab::MoveFile(const wxString &sourcePath, const wxString &destPath) {
3050 // Try simple rename first (works if on same filesystem)
3051 if (wxRenameFile(sourcePath, destPath, false)) {
3052 LOG_INFO("FileExplorer",
3053 "File moved using rename: " + sourcePath.ToStdString() + " to " + destPath.ToStdString());
3054 return true;
3055 }
3056
3057 // If rename fails, fall back to copy + delete
3058 LOG_INFO("FileExplorer", "Rename failed, using copy+delete for file move operation");
3059
3060 if (!CopyFile(sourcePath, destPath)) {
3061 return false;
3062 }
3063
3064 // Delete source after successful copy
3065 if (!DeleteFile(sourcePath)) {
3066 wxMessageBox("File was copied but source could not be deleted", "Warning", wxOK | wxICON_WARNING, m_panel);
3067 LOG_WARNING("FileExplorer", "File move succeeded but source deletion failed: " + sourcePath.ToStdString());
3068 return true; // Still consider it success since copy worked
3069 }
3070
3071 LOG_INFO("FileExplorer", "Successfully moved file: " + sourcePath.ToStdString() + " to " + destPath.ToStdString());
3072 return true;
3073}
3074
3075void FileExplorerTab::ShowFileProperties(const wxString &filePath) {
3076 if (!wxFileExists(filePath)) {
3077 wxMessageBox("File does not exist", "Error", wxOK | wxICON_ERROR, m_panel);
3078 return;
3079 }
3080
3081 wxFileName fn(filePath);
3082 wxString fileName = fn.GetFullName();
3083 wxString ext = fn.GetExt().Lower();
3084
3085 // Get file information
3086 wxDateTime modTime, createTime;
3087 fn.GetTimes(nullptr, &modTime, &createTime);
3088
3089 wxULongLong fileSize = fn.GetSize();
3090
3091 // Build properties message
3092 wxString properties;
3093 properties += "File Properties\n";
3094 properties += "===============\n\n";
3095 properties += "Name: " + fileName + "\n";
3096 properties += "Type: " + (ext.IsEmpty() ? "File" : ext.Upper() + " File") + "\n";
3097 properties += "Location: " + fn.GetPath() + "\n";
3098 properties += "Full Path: " + filePath + "\n\n";
3099
3100 // Format file size
3101 if (fileSize < 1024) {
3102 properties += wxString::Format("Size: %llu bytes\n", fileSize.GetValue());
3103 } else if (fileSize < 1024 * 1024) {
3104 properties +=
3105 wxString::Format("Size: %.2f KB (%llu bytes)\n", fileSize.ToDouble() / 1024.0, fileSize.GetValue());
3106 } else if (fileSize < 1024 * 1024 * 1024) {
3107 properties += wxString::Format("Size: %.2f MB (%llu bytes)\n", fileSize.ToDouble() / (1024.0 * 1024.0),
3108 fileSize.GetValue());
3109 } else {
3110 properties += wxString::Format("Size: %.2f GB (%llu bytes)\n", fileSize.ToDouble() / (1024.0 * 1024.0 * 1024.0),
3111 fileSize.GetValue());
3112 }
3113
3114 properties += "\n";
3115
3116 if (createTime.IsValid()) {
3117 properties += "Created: " + createTime.Format("%Y-%m-%d %H:%M:%S") + "\n";
3118 }
3119 if (modTime.IsValid()) {
3120 properties += "Modified: " + modTime.Format("%Y-%m-%d %H:%M:%S") + "\n";
3121 }
3122
3123 // Show in a message box (could be a custom dialog for better UX)
3124 wxMessageBox(properties, "File Properties", wxOK | wxICON_INFORMATION, m_panel);
3125}
3126
3127void FileExplorerTab::OpenFile(const wxString &filePath) {
3128 if (!wxFileExists(filePath)) {
3129 wxMessageBox("File does not exist", "Error", wxOK | wxICON_ERROR, m_panel);
3130 return;
3131 }
3132
3133 wxString ext = wxFileName(filePath).GetExt().Lower();
3134
3135 LOG_INFO("FileExplorer", "Opening file: " + filePath.ToStdString() + " (type: " + ext.ToStdString() + ")");
3136
3137 // Log the activity
3138 LogActivity(ActivityType::OPEN_FILE, filePath, ext.Upper());
3139
3140 // For now, just show a message that file opening is not yet implemented
3141 // TODO: Integrate with tab system to open files in appropriate editors
3142 wxString message = "File opening not yet implemented.\n\n";
3143 message += "File: " + wxFileName(filePath).GetFullName() + "\n";
3144 message += "Type: " + (ext.IsEmpty() ? "Unknown" : ext.Upper()) + "\n";
3145 message += "Path: " + filePath;
3146
3147 wxMessageBox(message, "Open File", wxOK | wxICON_INFORMATION, m_panel);
3148
3149 // Future implementation:
3150 // if (ext == "xml") {
3151 // // Open in XML editor tab
3152 // } else if (ext == "png" || ext == "jpg" || ext == "jpeg") {
3153 // // Open in image viewer tab
3154 // } else if (ext == "json") {
3155 // // Open in JSON editor tab
3156 // } else {
3157 // // Open with system default or text editor
3158 // }
3159}
3160
3161// ============================================================================
3162// Navigation History Implementation
3163// ============================================================================
3164
3166 // Add current path to back stack
3167 m_backStack.push_back(path);
3168
3169 // Clear forward stack when navigating to a new location
3170 // (you can't go forward after branching to a new path)
3171 m_forwardStack.clear();
3172
3173 // Optional: Limit history size to prevent excessive memory usage
3174 const size_t MAX_HISTORY_SIZE = 50;
3175 if (m_backStack.size() > MAX_HISTORY_SIZE) {
3176 m_backStack.erase(m_backStack.begin());
3177 }
3178
3179 LOG_INFO("FileExplorer", "Added to navigation history: " + path.ToStdString() +
3180 " (back stack size: " + std::to_string(m_backStack.size()) + ")");
3181}
3182
3184 // Check if buttons exist (might not be created yet during initialization)
3185 bool backEnabled = !m_backStack.empty();
3186 bool forwardEnabled = !m_forwardStack.empty();
3187
3188 // Update grid/breadcrumb area buttons
3189 if (m_gridBackButton) {
3190 m_gridBackButton->Enable(backEnabled);
3191 if (backEnabled) {
3192 wxString backPath = m_backStack.back();
3193 m_gridBackButton->SetToolTip("Go Back to: " + backPath + " (Alt+Left)");
3194 } else {
3195 m_gridBackButton->SetToolTip("Go Back (Alt+Left)");
3196 }
3197 }
3198
3199 if (m_gridForwardButton) {
3200 m_gridForwardButton->Enable(forwardEnabled);
3201 if (forwardEnabled) {
3202 wxString forwardPath = m_forwardStack.back();
3203 m_gridForwardButton->SetToolTip("Go Forward to: " + forwardPath + " (Alt+Right)");
3204 } else {
3205 m_gridForwardButton->SetToolTip("Go Forward (Alt+Right)");
3206 }
3207 }
3208}
3209
3210void FileExplorerTab::OnBackButton(wxCommandEvent &event) {
3211 if (m_backStack.empty()) {
3212 return;
3213 }
3214
3215 // Close history panel if it's open
3216 if (m_isHistoryVisible) {
3218 }
3219
3220 // Get the previous path from back stack
3221 wxString previousPath = m_backStack.back();
3222 m_backStack.pop_back();
3223
3224 // Add current path to forward stack
3226
3227 LOG_INFO("FileExplorer", "Navigating back to: " + previousPath.ToStdString());
3228
3229 // Navigate without recording to history
3230 m_isNavigatingHistory = true;
3231 NavigateToPath(previousPath, false);
3232 m_isNavigatingHistory = false;
3233
3234 // Update button states
3236}
3237
3238void FileExplorerTab::OnForwardButton(wxCommandEvent &event) {
3239 if (m_forwardStack.empty()) {
3240 return;
3241 }
3242
3243 // Close history panel if it's open
3244 if (m_isHistoryVisible) {
3246 }
3247
3248 // Get the next path from forward stack
3249 wxString nextPath = m_forwardStack.back();
3250 m_forwardStack.pop_back();
3251
3252 // Add current path to back stack
3253 m_backStack.push_back(m_currentGridPath);
3254
3255 LOG_INFO("FileExplorer", "Navigating forward to: " + nextPath.ToStdString());
3256
3257 // Navigate without recording to history
3258 m_isNavigatingHistory = true;
3259 NavigateToPath(nextPath, false);
3260 m_isNavigatingHistory = false;
3261
3262 // Update button states
3264}
3265
3266void FileExplorerTab::OnHistoryButton(wxCommandEvent &event) {
3267 // Toggle between history view and grid view
3269}
3270
3272 // Check for Alt+Left (Back) or Alt+Right (Forward)
3273 if (event.AltDown()) {
3274 if (event.GetKeyCode() == WXK_LEFT) {
3275 // Alt+Left: Go Back
3276 if (!m_backStack.empty()) {
3277 wxCommandEvent dummy;
3278 OnBackButton(dummy);
3279 }
3280 return;
3281 } else if (event.GetKeyCode() == WXK_RIGHT) {
3282 // Alt+Right: Go Forward
3283 if (!m_forwardStack.empty()) {
3284 wxCommandEvent dummy;
3285 OnForwardButton(dummy);
3286 }
3287 return;
3288 }
3289 }
3290
3291 // Let other key events propagate
3292 event.Skip();
3293}
3294
3297 return;
3298
3300
3301 if (m_isHistoryVisible) {
3302 // Show history panel, hide grid
3303 LOG_INFO("FileExplorer", "Showing navigation history panel");
3304 m_gridCtrl->Hide();
3306 m_historyPanel->Show();
3307
3308 // Update button appearance to show it's active
3309 if (m_historyButton) {
3310 m_historyButton->SetBackgroundColour(wxColour(60, 80, 110)); // Highlight color
3311 }
3312 } else {
3313 // Show grid, hide history panel
3314 LOG_INFO("FileExplorer", "Hiding navigation history panel");
3315 m_historyPanel->Hide();
3316 m_gridCtrl->Show();
3317
3318 // Reset button appearance
3319 if (m_historyButton) {
3320 m_historyButton->SetBackgroundColour(wxColour(40, 40, 40)); // Normal color
3321 }
3322 }
3323
3324 // Force layout update
3325 m_contentSizer->Layout();
3326 if (m_panel) {
3327 m_panel->Layout();
3328 }
3329}
3330
3332 if (!m_historyPanel)
3333 return;
3334
3335 // Helper function to format path with last 2 folders
3336 auto FormatShortPath = [](const wxString &fullPath) -> wxString {
3337 wxFileName pathFn(fullPath);
3338 wxArrayString dirs = pathFn.GetDirs();
3339 wxString filename = pathFn.GetFullName();
3340
3341 wxString result = "..";
3342
3343 if (!filename.IsEmpty()) {
3344 // It's a file - show last 2 parent directories + filename
3345 int startIdx = std::max(0, (int)dirs.GetCount() - 2);
3346 for (size_t i = startIdx; i < dirs.GetCount(); i++) {
3347 result += "/" + dirs[i];
3348 }
3349 result += "/" + filename;
3350 } else if (dirs.GetCount() > 0) {
3351 // It's a directory - show last 3 directories (including the target folder itself)
3352 int startIdx = std::max(0, (int)dirs.GetCount() - 3);
3353 for (size_t i = startIdx; i < dirs.GetCount(); i++) {
3354 result += "/" + dirs[i];
3355 }
3356 } else {
3357 // Fallback to full path if we can't parse it
3358 result = fullPath;
3359 }
3360
3361 return result;
3362 };
3363
3364 // Clear existing content properly
3365 // DestroyChildren will delete all child windows
3366 m_historyPanel->DestroyChildren();
3367
3368 // If there's an existing sizer, wxWidgets will delete it when we set a new one
3369 // So we don't need to manually delete it
3370
3371 // Main vertical sizer
3372 wxBoxSizer *mainSizer = new wxBoxSizer(wxVERTICAL);
3373
3374 // Activity section header (fixed, doesn't scroll)
3375 wxPanel *activityHeaderPanel = new wxPanel(m_historyPanel, wxID_ANY);
3376 activityHeaderPanel->SetBackgroundColour(wxColour(50, 50, 50));
3377 wxBoxSizer *activityHeaderSizer = new wxBoxSizer(wxHORIZONTAL);
3378
3379 wxStaticText *activityHeader = new wxStaticText(activityHeaderPanel, wxID_ANY, "Activity Log");
3380 activityHeader->SetForegroundColour(wxColour(200, 200, 200));
3381 wxFont activityHeaderFont = activityHeader->GetFont();
3382 activityHeaderFont.SetWeight(wxFONTWEIGHT_BOLD);
3383 activityHeader->SetFont(activityHeaderFont);
3384 activityHeaderSizer->Add(activityHeader, 0, wxALL, 8);
3385
3386 activityHeaderPanel->SetSizer(activityHeaderSizer);
3387 mainSizer->Add(activityHeaderPanel, 0, wxEXPAND);
3388
3389 // Activity scrollable content
3390 wxScrolledWindow *activityScrollWindow = new wxScrolledWindow(m_historyPanel, wxID_ANY);
3391 activityScrollWindow->SetBackgroundColour(wxColour(45, 45, 45));
3392 activityScrollWindow->SetScrollRate(0, 10);
3393
3394 wxBoxSizer *activityContentSizer = new wxBoxSizer(wxVERTICAL);
3395
3396 if (!m_activityHistory.empty()) {
3397
3398 // Add recent activities (limit to 20)
3399 size_t activityCount = wxMin(m_activityHistory.size(), 20u);
3400 for (size_t i = 0; i < activityCount; i++) {
3401 const auto &activity = m_activityHistory[i];
3402
3403 wxPanel *activityPanel = new wxPanel(activityScrollWindow, wxID_ANY);
3404 activityPanel->SetBackgroundColour(wxColour(45, 45, 45));
3405 wxBoxSizer *activitySizer = new wxBoxSizer(wxHORIZONTAL);
3406
3407 // Action label based on activity type
3408 wxString actionLabel;
3409 wxColour labelColor;
3410 switch (activity.type) {
3412 actionLabel = "[NAVIGATE]";
3413 labelColor = wxColour(100, 150, 200);
3414 break;
3416 actionLabel = "[OPEN]";
3417 labelColor = wxColour(150, 200, 150);
3418 break;
3420 actionLabel = "[CREATE]";
3421 labelColor = wxColour(100, 200, 100);
3422 break;
3424 actionLabel = "[DELETE FILE]";
3425 labelColor = wxColour(200, 100, 100);
3426 break;
3428 actionLabel = "[DELETE FOLDER]";
3429 labelColor = wxColour(200, 100, 100);
3430 break;
3432 actionLabel = "[RENAME FILE]";
3433 labelColor = wxColour(200, 180, 100);
3434 break;
3436 actionLabel = "[RENAME FOLDER]";
3437 labelColor = wxColour(200, 180, 100);
3438 break;
3439 case ActivityType::COPY:
3440 actionLabel = "[COPY]";
3441 labelColor = wxColour(150, 150, 200);
3442 break;
3443 case ActivityType::CUT:
3444 actionLabel = "[CUT]";
3445 labelColor = wxColour(200, 150, 100);
3446 break;
3448 actionLabel = "[PASTE]";
3449 labelColor = wxColour(150, 200, 200);
3450 break;
3451 }
3452
3453 wxStaticText *actionText = new wxStaticText(activityPanel, wxID_ANY, actionLabel);
3454 actionText->SetForegroundColour(labelColor);
3455 wxFont actionFont = actionText->GetFont();
3456 actionFont.SetWeight(wxFONTWEIGHT_BOLD);
3457 actionText->SetFont(actionFont);
3458 activitySizer->Add(actionText, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, 10);
3459
3460 // Activity details
3461 wxString displayText;
3462
3463 // For operations with details (rename, navigate, etc), format both paths
3464 if (!activity.details.IsEmpty() &&
3465 (activity.type == ActivityType::RENAME_FILE || activity.type == ActivityType::RENAME_FOLDER ||
3466 activity.type == ActivityType::NAVIGATE || activity.type == ActivityType::PASTE)) {
3467 // Parse "before -> after" format (using ASCII arrow with 3 spaces)
3468 int arrowPos = activity.details.Find(" -> ");
3469 if (arrowPos != wxNOT_FOUND) {
3470 wxString beforePath = activity.details.Mid(0, arrowPos);
3471 wxString afterPart = activity.details.Mid(arrowPos + 8); // Skip " -> "
3472
3473 // For paste operations, extract the path before "(moved)" or "(copied)"
3474 wxString afterPath = afterPart;
3475 wxString suffix = "";
3476 int suffixPos = afterPart.Find(" (");
3477 if (suffixPos != wxNOT_FOUND) {
3478 afterPath = afterPart.Mid(0, suffixPos);
3479 suffix = afterPart.Mid(suffixPos); // Keep " (moved)" or " (copied)"
3480 }
3481
3482 displayText = FormatShortPath(beforePath) + " -> " + FormatShortPath(afterPath) + suffix;
3483 } else {
3484 displayText = activity.details;
3485 }
3486 } else {
3487 // Show short path for all other operations
3488 displayText = FormatShortPath(activity.path);
3489 }
3490
3491 // Check if we need to color the arrow green
3492 int displayArrowPos = displayText.Find(" -> ");
3493 if (displayArrowPos != wxNOT_FOUND) {
3494 // Split into three parts: before, arrow, after
3495 wxString beforeText = displayText.Mid(0, displayArrowPos);
3496 wxString afterText = displayText.Mid(displayArrowPos + 8); // Skip " -> "
3497
3498 // Create a horizontal sizer for the text parts
3499 wxBoxSizer *textSizer = new wxBoxSizer(wxHORIZONTAL);
3500
3501 // Before text
3502 wxStaticText *textBefore = new wxStaticText(activityPanel, wxID_ANY, beforeText);
3503 textBefore->SetForegroundColour(wxColour(200, 200, 200));
3504 textSizer->Add(textBefore, 0, wxALIGN_CENTER_VERTICAL);
3505
3506 // Arrow (green)
3507 wxStaticText *arrowText = new wxStaticText(activityPanel, wxID_ANY, " -> ");
3508 arrowText->SetForegroundColour(wxColour(100, 255, 100)); // Bright green
3509 textSizer->Add(arrowText, 0, wxALIGN_CENTER_VERTICAL);
3510
3511 // After text
3512 wxStaticText *textAfter = new wxStaticText(activityPanel, wxID_ANY, afterText);
3513 textAfter->SetForegroundColour(wxColour(200, 200, 200));
3514 textSizer->Add(textAfter, 0, wxALIGN_CENTER_VERTICAL);
3515
3516 activitySizer->Add(textSizer, 1, wxEXPAND | wxRIGHT, 10);
3517 } else {
3518 // No arrow, use single text widget
3519 wxStaticText *text = new wxStaticText(activityPanel, wxID_ANY, displayText, wxDefaultPosition,
3520 wxDefaultSize, wxST_NO_AUTORESIZE);
3521 text->SetForegroundColour(wxColour(200, 200, 200));
3522 text->Wrap(900); // Wrap text at 900 pixels to utilize full width
3523 activitySizer->Add(text, 1, wxEXPAND | wxRIGHT, 10);
3524 }
3525
3526 // Timestamp
3527 wxString timeStr = activity.timestamp.Format("%H:%M:%S");
3528 wxStaticText *timeText = new wxStaticText(activityPanel, wxID_ANY, timeStr);
3529 timeText->SetForegroundColour(wxColour(120, 120, 120));
3530 wxFont timeFont = timeText->GetFont();
3531 timeFont.SetPointSize(timeFont.GetPointSize() - 1);
3532 timeText->SetFont(timeFont);
3533 activitySizer->Add(timeText, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 10);
3534
3535 activityPanel->SetSizer(activitySizer);
3536 activityPanel->Layout(); // Force layout to calculate wrapped text size
3537 activityPanel->Fit(); // Fit the panel to its content size
3538 // Show full paths in tooltip - details for rename/paste, path otherwise
3539 wxString tooltip = activity.details.IsEmpty() ? activity.path : activity.details;
3540 activityPanel->SetToolTip(tooltip);
3541
3542 // Hover effect for clickable activities
3543 if (activity.type == ActivityType::NAVIGATE || activity.type == ActivityType::OPEN_FILE) {
3544 wxString pathCopy = activity.path;
3545
3546 activityPanel->Bind(wxEVT_ENTER_WINDOW, [](wxMouseEvent &evt) {
3547 wxWindow *win = dynamic_cast<wxWindow *>(evt.GetEventObject());
3548 if (win) {
3549 win->SetBackgroundColour(wxColour(60, 80, 110));
3550 win->Refresh();
3551 }
3552 });
3553
3554 activityPanel->Bind(wxEVT_LEAVE_WINDOW, [](wxMouseEvent &evt) {
3555 wxWindow *win = dynamic_cast<wxWindow *>(evt.GetEventObject());
3556 if (win) {
3557 win->SetBackgroundColour(wxColour(45, 45, 45));
3558 win->Refresh();
3559 }
3560 });
3561
3562 if (activity.type == ActivityType::NAVIGATE && wxDirExists(pathCopy)) {
3563 activityPanel->Bind(wxEVT_LEFT_DOWN, [this, pathCopy](wxMouseEvent &) {
3564 if (m_panel) {
3565 NavigateToPath(pathCopy);
3567 }
3568 });
3569 }
3570 }
3571
3572 activityContentSizer->Add(activityPanel, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 3);
3573 }
3574 } else {
3575 // Show empty message for activities
3576 wxStaticText *emptyText =
3577 new wxStaticText(activityScrollWindow, wxID_ANY, "No activities yet.\nPerform actions to see history.");
3578 emptyText->SetForegroundColour(wxColour(150, 150, 150));
3579 activityContentSizer->Add(emptyText, 0, wxALL | wxALIGN_CENTER, 20);
3580 }
3581
3582 // Set sizer for activity scroll window and fit its virtual size
3583 activityScrollWindow->SetSizer(activityContentSizer);
3584 activityScrollWindow->FitInside();
3585
3586 // Add scroll window directly to main sizer
3587 mainSizer->Add(activityScrollWindow, 1, wxEXPAND);
3588
3589 m_historyPanel->SetSizer(mainSizer);
3590 m_historyPanel->FitInside();
3591 m_historyPanel->Refresh();
3592}
3593
3594void FileExplorerTab::LogActivity(ActivityType type, const wxString &path, const wxString &details) {
3595 ActivityEntry entry;
3596 entry.type = type;
3597 entry.path = path;
3598 entry.details = details;
3599 entry.timestamp = wxDateTime::Now();
3600
3601 // Add to front (most recent first)
3602 m_activityHistory.insert(m_activityHistory.begin(), entry);
3603
3604 // Limit size to prevent unbounded growth
3607 }
3608
3609 // Log the activity
3610 wxString activityName;
3611 switch (type) {
3613 activityName = "Navigate";
3614 break;
3616 activityName = "Open";
3617 break;
3619 activityName = "Create Folder";
3620 break;
3622 activityName = "Delete File";
3623 break;
3625 activityName = "Delete Folder";
3626 break;
3628 activityName = "Rename File";
3629 break;
3631 activityName = "Rename Folder";
3632 break;
3633 case ActivityType::COPY:
3634 activityName = "Copy";
3635 break;
3636 case ActivityType::CUT:
3637 activityName = "Cut";
3638 break;
3640 activityName = "Paste";
3641 break;
3642 }
3643
3644 LOG_INFO("FileExplorer", "Activity: " + activityName.ToStdString() + " - " + path.ToStdString());
3645}
#define LOG_ERROR(category, message)
Definition Logger.h:116
#define LOG_WARNING(category, message)
Definition Logger.h:115
#define LOG_INFO(category, message)
Definition Logger.h:114
Centralized resource path management for EmberForge.
static wxColour GetAccentColor()
Get the accent color as a wxColour for UI elements.
static AppPreferencesManager & GetInstance()
SortBy
File sorting criteria.
BottomPanelSettings & GetBottomPanelSettings()
static wxString GetResourcesDir()
Get the base resources directory.
virtual ~FileExplorerTab()
Destructor.
bool CreateNewFolder(const wxString &parentPath, const wxString &folderName)
wxBitmap GetIcon() const override
Returns the tab icon bitmap; defaults to null.
void OnContextMenuDelete(wxCommandEvent &event)
void OnGridBackgroundContextMenu(wxMouseEvent &event)
void OpenFile(const wxString &filePath)
wxIcon LoadTreeIcon(const wxString &iconType)
void Initialize() override
Called once when the tab is first created.
wxString m_currentGridPath
bool CanMove() const override
Returns true if the tab can be moved/reordered.
void OnContextMenuOpen(wxCommandEvent &event)
void OnForwardButton(wxCommandEvent &event)
void OnSplitterSashPosChanging(wxSplitterEvent &event)
bool CopyFile(const wxString &sourcePath, const wxString &destPath)
wxPanel * m_breadcrumbContainerPanel
void OnGridContextMenu(wxMouseEvent &event)
FileExplorerTab(wxWindow *parent)
Constructor.
void OnActivated() override
Called when the tab becomes active.
wxString m_contextMenuPath
void OnTreeContextMenu(wxTreeEvent &event)
void OnContextMenuCopy(wxCommandEvent &event)
void OnGridSearchTextChanged(wxCommandEvent &event)
void FilterGrid(const wxString &searchText)
void OnContextMenuPaste(wxCommandEvent &event)
void OnContextMenuProperties(wxCommandEvent &event)
void OnGridSearchCancel(wxCommandEvent &event)
std::vector< GridItem > m_gridItems
std::vector< wxString > m_forwardStack
void OnGridItemDoubleClick(wxMouseEvent &event)
static const size_t MAX_ACTIVITY_ENTRIES
wxString GetDisplayName(const wxString &filename, bool isDirectory, bool isTreeView) const
wxScrolledWindow * m_historyPanel
void OnContextMenuCut(wxCommandEvent &event)
wxScrolledWindow * m_gridCtrl
void ShowFileProperties(const wxString &filePath)
void OnTreeToggle(wxCommandEvent &event)
void CreateHistoryPanel(wxPanel *rightPanel, wxBoxSizer *rightSizer)
wxSplitterWindow * m_splitter
void OnHistoryButton(wxCommandEvent &event)
bool MoveFile(const wxString &sourcePath, const wxString &destPath)
void AddDirectoryToTree(const wxString &path, wxTreeItemId parentItem)
bool CopyFolder(const wxString &sourcePath, const wxString &destPath)
wxString GetTabType() const override
Returns the tab type identifier.
void OnBeginDrag(wxTreeEvent &event)
wxWindow * m_dragWindow
void OnContextMenuNewFolder(wxCommandEvent &event)
wxToggleButton * m_jsonFilterBtn
int CountFilesInDirectory(const wxString &path, bool recursive=true) const
void AddGridItem(const wxString &name, const wxString &fullPath, bool isDirectory)
void OnItemExpanding(wxTreeEvent &event)
wxBoxSizer * m_breadcrumbSizer
wxPanel * m_functionalPanel
void OnClosed() override
Called when the tab is closed.
wxWindow * m_selectedGridItem
std::map< wxString, wxBitmap > m_gridIcons
wxWrapSizer * m_gridSizer
bool RenameFolder(const wxString &oldPath, const wxString &newName)
wxWindow * GetWidget() override
Returns the wxWidgets window used as the tab content.
wxString GetTitle() const override
Returns the display title of the tab.
wxToggleButton * m_pngFilterBtn
void OnSelectionChanged(wxTreeEvent &event)
wxBitmap LoadGridIcon(const wxString &iconType, const wxSize &size)
wxTreeCtrl * m_treeCtrl
wxToggleButton * m_gridToggleBtn
void UpdateToggleButtonAppearance()
void OnTreeSearchCancel(wxCommandEvent &event)
void OnSplitterSashPosChanged(wxSplitterEvent &event)
void Refresh() override
Refreshes the tab content.
bool DirectoryContainsMatchingFiles(const wxString &dirPath) const
wxToggleButton * m_xmlFilterBtn
wxString GetSelectedFile() const
Get the current selected file path.
void OnGridToggle(wxCommandEvent &event)
void PopulateGrid(const wxString &path)
void OnBackButton(wxCommandEvent &event)
bool DeleteFolder(const wxString &folderPath)
void AddToNavigationHistory(const wxString &path)
wxTreeItemId FindTreeItemByPath(const wxString &path, wxTreeItemId parent=wxTreeItemId())
wxBitmapButton * m_historyButton
wxString GetDirectorySize(const wxString &path) const
void LogActivity(ActivityType type, const wxString &path, const wxString &details="")
wxImageList * m_imageList
bool DirectoryContainsFilteredFiles(const wxString &path) const
void CreateBreadcrumb(wxPanel *rightPanel, wxBoxSizer *rightSizer)
void OnItemActivated(wxTreeEvent &event)
bool RenameFile(const wxString &oldPath, const wxString &newName)
wxSearchCtrl * m_gridSearchCtrl
void CreateFavoritesBar(wxBoxSizer *parentSizer)
void NavigateToPath(const wxString &path, bool recordHistory=true)
bool DeleteFile(const wxString &filePath)
wxString GetExecutableDirectory() const
void SetRootDirectory(const wxString &path)
Set the root directory to display.
void ShowFolderProperties(const wxString &folderPath)
void OnNavigationKeyDown(wxKeyEvent &event)
wxPanel * m_navigationPanel
void OnContextMenuRefresh(wxCommandEvent &event)
wxPanel * m_breadcrumbPanel
std::vector< wxString > m_backStack
wxSearchCtrl * m_treeSearchCtrl
void FilterTree(const wxString &searchText)
void ExpandAllMatching(wxTreeItemId item, const wxString &searchText, bool &foundAny)
wxToggleButton * m_treeToggleBtn
void OnGridItemMouseMove(wxMouseEvent &event)
void OnGridItemMouseUp(wxMouseEvent &event)
void OnTreeSearchTextChanged(wxCommandEvent &event)
void OnBreadcrumbClick(wxCommandEvent &event)
wxBitmapButton * m_gridBackButton
void CreateSplitLayout(wxBoxSizer *parentSizer)
wxBitmapButton * m_gridForwardButton
void CreateIconGrid(wxPanel *rightPanel, wxBoxSizer *rightSizer)
bool CanClose() const override
Returns true if the tab can be closed.
wxBoxSizer * m_contentSizer
bool MoveFolder(const wxString &sourcePath, const wxString &destPath)
void OnDeactivated() override
Called when the tab becomes inactive.
void OnContextMenuRename(wxCommandEvent &event)
bool ItemMatchesSearch(wxTreeItemId item, const wxString &searchText) const
bool IsValid() const override
Returns true if the tab is in a valid state.
bool PassesFilter(const wxString &filename) const
std::vector< ActivityEntry > m_activityHistory
void OnGridItemMouseDown(wxMouseEvent &event)
void OnFavoriteFilter(wxCommandEvent &event)
bool IsDirectory() const
FileTreeItemData(const wxString &path, bool isDir)
wxString GetPath() const
std::vector< HistoryItem > m_items
std::function< void(const wxString &)> m_onNavigate
HistoryPopup(wxWindow *parent, const std::vector< wxString > &backStack, const std::vector< wxString > &forwardStack, const wxString &currentPath, std::function< void(const wxString &)> onNavigate)
wxPanel * CreateHistoryItem(wxWindow *parent, const HistoryItem &item, size_t index, bool isCurrent)