Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
FilePickerModal.cpp
Go to the documentation of this file.
1/**
2 * @file FilePickerModal.cpp
3 * @brief Implementation of FilePickerModal (Phase 40).
4 * @author Olympe Engine
5 * @date 2026-03-20
6 */
7
8#include "FilePickerModal.h"
9#include "../../third_party/imgui/imgui.h"
10#include "../../system/system_consts.h"
11#include "../../system/system_utils.h"
12
13#ifdef _WIN32
14#include <windows.h>
15#else
16#include <dirent.h>
17#include <sys/stat.h>
18#endif
19
20#include <algorithm>
21#include <cstring>
22
23namespace Olympe {
24
25// ============================================================================
26// Constructor
27// ============================================================================
28
30 : m_fileType(fileType)
31{
32 // Initialize path to default directory for this file type
36}
37
38// ============================================================================
39// Modal Lifecycle
40// ============================================================================
41
42void FilePickerModal::Open(const std::string& currentPath)
43{
44 m_isOpen = true;
45 m_confirmed = false;
46 m_selectedFile = "";
47 m_selectedIndex = -1;
50
51 if (!currentPath.empty())
52 {
53 m_currentPath = currentPath;
54 strncpy_s(m_pathBuffer, sizeof(m_pathBuffer), currentPath.c_str(), _TRUNCATE);
55 }
56 else
57 {
60 }
61
64}
65
67{
68 m_isOpen = false;
69 m_confirmed = false;
70 m_selectedFile = "";
71}
72
74{
75 if (!m_isOpen)
76 return;
77
78 // Center the modal on screen
79 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
80 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
81 ImGui::SetNextWindowSize(ImVec2(900.0f, 600.0f), ImGuiCond_Appearing);
82 ImGui::SetNextWindowSizeConstraints(ImVec2(600.0f, 400.0f), ImVec2(1400.0f, 900.0f));
83
84 bool open = true;
85 std::string title = GetModalTitle();
86 if (ImGui::BeginPopupModal(title.c_str(), &open, ImGuiWindowFlags_AlwaysAutoResize))
87 {
88 // Description
89 ImGui::TextColored(ImVec4(0.8f, 0.95f, 1.0f, 1.0f), "%s", GetDescriptionText().c_str());
90 ImGui::Separator();
91
92 // ====================================================================
93 // Path Navigation
94 // ====================================================================
95
96 ImGui::TextDisabled("Path:");
97 ImGui::SameLine();
98 ImGui::SetNextItemWidth(-100.0f);
99 if (ImGui::InputText("##path", m_pathBuffer, sizeof(m_pathBuffer)))
100 {
103 }
104
105 ImGui::SameLine();
106 if (ImGui::Button("Refresh##refresh", ImVec2(90, 0)))
107 {
109 }
110
111 ImGui::Separator();
112
113 // ====================================================================
114 // Filter Dropdown & Search Filter
115 // ====================================================================
116
117 ImGui::TextDisabled("Filter:");
118 ImGui::SameLine();
119 ImGui::SetNextItemWidth(150.0f);
120
121 // Build filter options based on file type
122 const char* filterOptions[] = { "All (*.*)", "Type 1", "Type 2", "Type 3" };
123 int filterCount = 1;
124
126 {
127 filterOptions[1] = "BehaviorTree (*.bt.json)";
128 filterOptions[2] = "Subgraph (*.ats.json)";
129 filterOptions[3] = "All Files (*.*)";
130 filterCount = 4;
131 }
133 {
134 filterOptions[1] = "Blueprint (*.ats)";
135 filterOptions[2] = "All Files (*.*)";
136 filterCount = 3;
137 }
138
139 if (ImGui::Combo("##filter", &m_selectedFilterIndex, filterOptions, filterCount))
140 {
141 // Update filter based on selection
143 {
145 else if (m_selectedFilterIndex == 1) m_currentFilter = ".bt.json";
146 else if (m_selectedFilterIndex == 2) m_currentFilter = ".ats.json";
147 else m_currentFilter = "*";
148 }
150 {
152 else if (m_selectedFilterIndex == 1) m_currentFilter = ".ats";
153 else m_currentFilter = "*";
154 }
155 else
156 {
157 m_currentFilter = "*";
158 }
160 }
161
162 ImGui::SameLine();
163 ImGui::TextDisabled("Search:");
164 ImGui::SameLine();
165 ImGui::SetNextItemWidth(-1.0f);
166 ImGui::InputText("##search", m_searchBuffer, sizeof(m_searchBuffer));
167
168 ImGui::Separator();
169
170 // ====================================================================
171 // Files and Folders (Split Panel)
172 // ====================================================================
173
174 ImGui::BeginChild("##file_browser", ImVec2(0, 300), true);
175 {
176 // Left column: Folders
177 float folderWidth = 150.0f;
178 ImGui::BeginChild("##folders", ImVec2(folderWidth, -1), true);
179 ImGui::TextDisabled("Folders:");
180
181 // Parent directory ".."
182 if (ImGui::Selectable("..", false))
183 {
184 size_t lastSlash = m_currentPath.find_last_of("/\\");
185 if (lastSlash != std::string::npos)
186 {
190 }
191 }
192
193 // List subdirectories
194 for (const auto& folder : m_folderList)
195 {
196 if (ImGui::Selectable(folder.c_str(), false))
197 {
198 m_currentPath += "/" + folder;
201 }
202 }
203 ImGui::EndChild();
204 }
205
206 // Right column: Files
207 ImGui::SameLine();
208 ImGui::BeginChild("##files", ImVec2(0, -1), true);
209 ImGui::TextDisabled("Available Files:");
210
212
213 ImGui::EndChild();
214 ImGui::EndChild();
215
216 ImGui::Separator();
217
218 // ====================================================================
219 // Selected File Display
220 // ====================================================================
221
222 ImGui::TextDisabled("Selected:");
223 ImGui::SameLine();
224 if (m_selectedIndex >= 0 && m_selectedIndex < static_cast<int>(m_fileList.size()))
225 {
226 ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.5f, 1.0f), "%s", m_fileList[m_selectedIndex].c_str());
227 }
228 else
229 {
230 ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "(none)");
231 }
232
233 ImGui::Separator();
234
235 // ====================================================================
236 // Action Buttons
237 // ====================================================================
238
240
241 ImGui::EndPopup();
242 }
243
244 if (!open)
245 {
246 m_isOpen = false;
247 ImGui::OpenPopup(title.c_str());
248 }
249}
250
251// ============================================================================
252// Helper Methods
253// ============================================================================
254
256{
257 switch (m_fileType)
258 {
260 return "./Gamedata";
262 return "Blueprints";
264 return "./Gamedata/Audio";
266 return "./Gamedata/Tilesets";
267 default:
268 return "./Gamedata";
269 }
270}
271
273{
274 switch (m_fileType)
275 {
277 return ".bt.json";
279 return ".ats";
281 return ".ogg";
283 return ".tsj";
284 default:
285 return "*";
286 }
287}
288
290{
291 switch (m_fileType)
292 {
294 return "Select BehaviorTree File##filepicker_bt";
296 return "Select SubGraph File##filepicker_ats";
298 return "Select Audio File##filepicker_audio";
300 return "Select Tileset File##filepicker_tileset";
301 default:
302 return "Select File##filepicker";
303 }
304}
305
307{
308 switch (m_fileType)
309 {
311 return "Select a BehaviorTree file (.bt.json) to link with this component";
313 return "Select a Blueprint file (.ats) to use as SubGraph";
315 return "Select an Audio file (.ogg)";
317 return "Select a Tileset file (.tsj)";
318 default:
319 return "Select a file";
320 }
321}
322
324{
325 m_fileList.clear();
326 m_folderList.clear();
327 m_selectedIndex = -1;
328
329 std::string pattern = m_currentFilter.empty() ? GetFilePattern() : m_currentFilter;
330
331#ifdef _WIN32
333 std::string searchPath = m_currentPath + "\\*";
335
337 {
338 SYSTEM_LOG << "[FilePickerModal] Directory not found or inaccessible: " << m_currentPath << "\n";
339 return;
340 }
341
342 do
343 {
344 std::string filename = findData.cFileName;
345
346 // Skip "." and ".."
347 if (filename == "." || filename == "..")
348 continue;
349
350 // Separate folders and files
351 if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
352 {
353 m_folderList.push_back(filename);
354 }
355 else
356 {
357 // Check if file matches pattern
358 if (pattern == "*" || filename.find(pattern) != std::string::npos)
359 {
360 m_fileList.push_back(filename);
361 }
362 }
363 } while (FindNextFileA(hFind, &findData) != 0);
364
366
367 // Sort alphabetically
368 std::sort(m_fileList.begin(), m_fileList.end());
369 std::sort(m_folderList.begin(), m_folderList.end());
370
371 SYSTEM_LOG << "[FilePickerModal] Found " << m_fileList.size()
372 << " files matching " << pattern << " and " << m_folderList.size()
373 << " folders in " << m_currentPath << "\n";
374#else
375 DIR* dir = opendir(m_currentPath.c_str());
376 if (!dir)
377 {
378 SYSTEM_LOG << "[FilePickerModal] Directory not found or inaccessible: " << m_currentPath << "\n";
379 return;
380 }
381
382 struct dirent* entry;
383 while ((entry = readdir(dir)) != nullptr)
384 {
385 std::string filename = entry->d_name;
386
387 // Skip "." and ".."
388 if (filename == "." || filename == "..")
389 continue;
390
391 // Check if it's a directory
392 std::string fullPath = m_currentPath + "/" + filename;
393 struct stat st;
394 if (stat(fullPath.c_str(), &st) == 0 && S_ISDIR(st.st_mode))
395 {
396 m_folderList.push_back(filename);
397 }
398 else
399 {
400 // Check if filename matches pattern
401 if (pattern == "*" ||
402 (filename.length() > pattern.length() &&
403 filename.substr(filename.length() - pattern.length()) == pattern))
404 {
405 m_fileList.push_back(filename);
406 }
407 }
408 }
409
410 closedir(dir);
411
412 // Sort alphabetically
413 std::sort(m_fileList.begin(), m_fileList.end());
414 std::sort(m_folderList.begin(), m_folderList.end());
415
416 SYSTEM_LOG << "[FilePickerModal] Found " << m_fileList.size()
417 << " files matching " << pattern << " and " << m_folderList.size()
418 << " folders in " << m_currentPath << "\n";
419#endif
420}
421
423{
424 std::vector<std::string> filteredFiles = GetFilteredFiles();
425
426 ImGui::TextDisabled("Available Files:");
427
428 ImGui::BeginChild("##file_list", ImVec2(0, 250), true);
429
430 for (int i = 0; i < static_cast<int>(filteredFiles.size()); ++i)
431 {
432 const std::string& filename = filteredFiles[i];
433
434 // Find the actual index in the unfiltered list
435 int actualIndex = -1;
436 for (int j = 0; j < static_cast<int>(m_fileList.size()); ++j)
437 {
438 if (m_fileList[j] == filename)
439 {
440 actualIndex = j;
441 break;
442 }
443 }
444
445 bool isSelected = (actualIndex == m_selectedIndex);
446
447 ImGui::PushID(i);
448
449 if (ImGui::Selectable(filename.c_str(), isSelected, ImGuiSelectableFlags_DontClosePopups))
450 {
452 }
453
454 ImGui::PopID();
455 }
456
457 if (filteredFiles.empty())
458 {
459 ImGui::TextDisabled("(no files found)");
460 }
461
462 ImGui::EndChild();
463}
464
466{
468
469 if (!canSelect)
470 ImGui::BeginDisabled(true);
471
472 if (ImGui::Button("Select##select", ImVec2(100, 0)))
473 {
474 if (canSelect)
475 {
476 // Build full path: currentPath / filename
478 m_confirmed = true;
479 m_isOpen = false;
480 ImGui::CloseCurrentPopup();
481 }
482 }
483
484 if (!canSelect)
485 ImGui::EndDisabled();
486
487 ImGui::SameLine();
488
489 if (ImGui::Button("Cancel##cancel", ImVec2(100, 0)))
490 {
491 m_isOpen = false;
492 m_confirmed = false;
493 ImGui::CloseCurrentPopup();
494 }
495}
496
497std::vector<std::string> FilePickerModal::GetFilteredFiles() const
498{
499 std::string searchLower(m_searchBuffer);
500 std::transform(searchLower.begin(), searchLower.end(), searchLower.begin(), ::tolower);
501
502 std::vector<std::string> filtered;
503
504 for (const auto& filename : m_fileList)
505 {
506 std::string filenameLower(filename);
507 std::transform(filenameLower.begin(), filenameLower.end(), filenameLower.begin(), ::tolower);
508
509 if (searchLower.empty() || filenameLower.find(searchLower) != std::string::npos)
510 {
511 filtered.push_back(filename);
512 }
513 }
514
515 return filtered;
516}
517
518} // namespace Olympe
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
Centralized file picker modal for all file selection operations (Phase 40).
std::vector< std::string > GetFilteredFiles() const
Filters and returns files matching the search buffer.
char m_searchBuffer[128]
Search filter text buffer.
void RenderActionButtons()
Renders the action buttons (Select, Cancel).
FilePickerModal(FilePickerType fileType)
Constructs a file picker modal for the given file type.
std::string m_selectedFile
Full path to selected file.
void Close()
Closes the modal without confirming changes.
std::string m_currentFilter
Current file extension filter.
bool m_isOpen
Is modal currently visible.
int m_selectedIndex
Currently highlighted file (-1 = none)
void RenderFileList()
Renders the file list UI component with scrolling and selection.
std::string GetModalTitle() const
Returns the modal title for this file type (e.g., "Select BehaviorTree File").
bool m_confirmed
Did user click Select.
void Open(const std::string &currentPath="")
Opens the modal with optional initial path.
void Render()
Renders the modal UI.
std::vector< std::string > m_fileList
Files found in current directory.
std::string GetFilePattern() const
Returns the file pattern for this file type (e.g., "*.bt.json").
int m_selectedFilterIndex
Current filter type (0=default, 1=.bt.json, etc.)
std::string GetDefaultDirectory() const
Returns the default directory for this file type.
std::vector< std::string > m_folderList
Folders in current directory.
FilePickerType m_fileType
Type of files to browse.
std::string GetDescriptionText() const
Returns the description text for this file type.
std::string m_currentPath
Current directory being browsed.
char m_pathBuffer[512]
Path input text buffer.
void RefreshFileList()
Refreshes file list from current directory.
< Provides AssetID and INVALID_ASSET_ID
FilePickerType
Supported file types for the centralized file picker modal.
@ Tileset
Future: .tsj tileset files.
@ SubGraph
.ats files in Blueprints
@ Audio
Future: .ogg, .wav files.
@ BehaviorTree
.bt.json files in ./Gamedata
#define SYSTEM_LOG