Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
SaveFilePickerModal.cpp
Go to the documentation of this file.
1/**
2 * @file SaveFilePickerModal.cpp
3 * @brief Implementation of SaveFilePickerModal (Phase 40 Enhancement).
4 * @author Olympe Engine
5 * @date 2026-03-21
6 */
7
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#include <fstream>
23
24namespace Olympe {
25
26// ============================================================================
27// Constructor
28// ============================================================================
29
37
38// ============================================================================
39// Modal Lifecycle
40// ============================================================================
41
42void SaveFilePickerModal::Open(const std::string& directory, const std::string& suggestedFilename)
43{
44 m_isOpen = true;
45 m_confirmed = false;
46 m_selectedFile = "";
48
49 if (!directory.empty())
50 {
53 }
54 else
55 {
58 }
59
60 if (!suggestedFilename.empty())
61 {
63 }
64 else
65 {
67 }
68
70}
71
73{
74 m_isOpen = false;
75 m_confirmed = false;
76 m_selectedFile = "";
78}
79
81{
82 if (!m_isOpen)
83 return;
84
85 // Show overwrite confirmation dialog if needed
87 {
89 return;
90 }
91
92 // Center the modal on screen
93 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
94 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
95 ImGui::SetNextWindowSize(ImVec2(900.0f, 600.0f), ImGuiCond_Appearing);
96 ImGui::SetNextWindowSizeConstraints(ImVec2(600.0f, 400.0f), ImVec2(1400.0f, 900.0f));
97
98 bool open = true;
99 std::string title = GetModalTitle();
100 if (ImGui::BeginPopupModal(title.c_str(), &open, ImGuiWindowFlags_AlwaysAutoResize))
101 {
102 // Description
103 ImGui::TextColored(ImVec4(0.8f, 0.95f, 1.0f, 1.0f), "%s", GetDescriptionText().c_str());
104 ImGui::Separator();
105
106 // ====================================================================
107 // Path Navigation
108 // ====================================================================
109
110 ImGui::TextDisabled("Path:");
111 ImGui::SameLine();
112 ImGui::SetNextItemWidth(-100.0f);
113 if (ImGui::InputText("##path", m_pathBuffer, sizeof(m_pathBuffer)))
114 {
117 }
118
119 ImGui::SameLine();
120 if (ImGui::Button("Refresh##refresh", ImVec2(90, 0)))
121 {
123 }
124
125 ImGui::Separator();
126
127 // ====================================================================
128 // Files and Folders (Split Panel)
129 // ====================================================================
130
131 ImGui::BeginChild("##file_browser", ImVec2(0, 250), true);
132 {
133 // Left column: Folders
134 float folderWidth = 150.0f;
135 ImGui::BeginChild("##folders", ImVec2(folderWidth, -1), true);
136 ImGui::TextDisabled("Folders:");
137
138 // Parent directory ".."
139 if (ImGui::Selectable("..", false))
140 {
141 size_t lastSlash = m_currentPath.find_last_of("/\\");
142 if (lastSlash != std::string::npos)
143 {
147 }
148 }
149
150 // List subdirectories
152
153 ImGui::EndChild();
154 }
155
156 // Right column: Files
157 ImGui::SameLine();
158 ImGui::BeginChild("##files", ImVec2(0, -1), true);
159 ImGui::TextDisabled("Available Files:");
160
162
163 ImGui::EndChild();
164 ImGui::EndChild();
165
166 ImGui::Separator();
167
168 // ====================================================================
169 // Filename Input
170 // ====================================================================
171
173
174 ImGui::Separator();
175
176 // ====================================================================
177 // Action Buttons
178 // ====================================================================
179
181
182 ImGui::EndPopup();
183 }
184
185 if (!open)
186 {
187 m_isOpen = false;
188 }
189}
190
191// ============================================================================
192// Helper Methods
193// ============================================================================
194
196{
197 switch (m_fileType)
198 {
200 return "./Gamedata";
202 return "Blueprints";
204 return "./Gamedata/Prefabs";
206 return "./Gamedata/Audio";
207 default:
208 return "./Gamedata";
209 }
210}
211
213{
214 switch (m_fileType)
215 {
217 return ".bt.json";
219 return ".ats";
221 return ".pref.json";
223 return ".ogg";
224 default:
225 return "";
226 }
227}
228
230{
231 switch (m_fileType)
232 {
234 return "Save BehaviorTree As##save_bt";
236 return "Save Blueprint As##save_ats";
238 return "Save Entity Prefab As##save_pref";
240 return "Save Audio As##save_audio";
241 default:
242 return "Save File As##save_file";
243 }
244}
245
247{
248 switch (m_fileType)
249 {
251 return "Save your BehaviorTree with a new name";
253 return "Save your Blueprint with a new name";
255 return "Save your Entity Prefab with a new name";
257 return "Save your audio file with a new name";
258 default:
259 return "Save your file with a new name";
260 }
261}
262
264{
265 m_fileList.clear();
266 m_folderList.clear();
267
268 std::string extension = GetFileExtension();
269
270#ifdef _WIN32
272 std::string searchPath = m_currentPath + "\\*";
274
276 {
277 SYSTEM_LOG << "[SaveFilePickerModal] Directory not found or inaccessible: " << m_currentPath << "\n";
278 return;
279 }
280
281 do
282 {
283 std::string filename = findData.cFileName;
284
285 // Skip "." and ".."
286 if (filename == "." || filename == "..")
287 continue;
288
289 // Separate folders and files
290 if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
291 {
292 m_folderList.push_back(filename);
293 }
294 else
295 {
296 // Only show files with matching extension
297 if (filename.length() >= extension.length() &&
298 filename.substr(filename.length() - extension.length()) == extension)
299 {
300 m_fileList.push_back(filename);
301 }
302 }
303 } while (FindNextFileA(hFind, &findData) != 0);
304
306
307 std::sort(m_fileList.begin(), m_fileList.end());
308 std::sort(m_folderList.begin(), m_folderList.end());
309
310 SYSTEM_LOG << "[SaveFilePickerModal] Found " << m_fileList.size()
311 << " files and " << m_folderList.size()
312 << " folders in " << m_currentPath << "\n";
313#else
314 DIR* dir = opendir(m_currentPath.c_str());
315 if (!dir)
316 {
317 SYSTEM_LOG << "[SaveFilePickerModal] Directory not found or inaccessible: " << m_currentPath << "\n";
318 return;
319 }
320
321 struct dirent* entry;
322 while ((entry = readdir(dir)) != nullptr)
323 {
324 std::string filename = entry->d_name;
325
326 if (filename == "." || filename == "..")
327 continue;
328
329 std::string fullPath = m_currentPath + "/" + filename;
330 struct stat st;
331 if (stat(fullPath.c_str(), &st) == 0 && S_ISDIR(st.st_mode))
332 {
333 m_folderList.push_back(filename);
334 }
335 else
336 {
337 // Only show files with matching extension
338 if (filename.length() >= extension.length() &&
339 filename.substr(filename.length() - extension.length()) == extension)
340 {
341 m_fileList.push_back(filename);
342 }
343 }
344 }
345
346 closedir(dir);
347
348 std::sort(m_fileList.begin(), m_fileList.end());
349 std::sort(m_folderList.begin(), m_folderList.end());
350
351 SYSTEM_LOG << "[SaveFilePickerModal] Found " << m_fileList.size()
352 << " files and " << m_folderList.size()
353 << " folders in " << m_currentPath << "\n";
354#endif
355}
356
358{
359 for (const auto& filename : m_fileList)
360 {
361 if (ImGui::Selectable(filename.c_str(), false, ImGuiSelectableFlags_DontClosePopups))
362 {
363 // Remove extension and populate filename buffer
364 std::string filenameNoExt = filename;
365 std::string ext = GetFileExtension();
366 if (filenameNoExt.length() > ext.length() &&
367 filenameNoExt.substr(filenameNoExt.length() - ext.length()) == ext)
368 {
369 filenameNoExt = filenameNoExt.substr(0, filenameNoExt.length() - ext.length());
370 }
372 }
373 }
374
375 if (m_fileList.empty())
376 {
377 ImGui::TextDisabled("(no files found)");
378 }
379}
380
382{
383 for (const auto& folder : m_folderList)
384 {
385 if (ImGui::Selectable(folder.c_str(), false, ImGuiSelectableFlags_DontClosePopups))
386 {
387 m_currentPath += "/" + folder;
390 }
391 }
392}
393
395{
396 ImGui::TextDisabled("Filename:");
397 ImGui::SameLine();
398 ImGui::SetNextItemWidth(-1.0f);
399 ImGui::InputText("##filename", m_filenameBuffer, sizeof(m_filenameBuffer));
400
401 // Display full path preview
402 ImGui::TextDisabled("Full path: %s/%s%s",
403 m_currentPath.c_str(),
406}
407
409{
410 bool filenameEmpty = (std::strlen(m_filenameBuffer) == 0);
411
412 if (!filenameEmpty)
413 {
414 if (ImGui::Button("Save##save", ImVec2(120, 0)))
415 {
416 std::string fullPath = m_currentPath + "/" +
417 std::string(m_filenameBuffer) +
419
420 if (FileExists(fullPath))
421 {
422 // Show overwrite confirmation
423 m_selectedFile = fullPath;
425 }
426 else
427 {
428 // Save immediately
429 m_selectedFile = fullPath;
430 m_confirmed = true;
431 m_isOpen = false;
432 ImGui::CloseCurrentPopup();
433 SYSTEM_LOG << "[SaveFilePickerModal] File save confirmed: " << fullPath << "\n";
434 }
435 }
436 }
437 else
438 {
439 ImGui::BeginDisabled();
440 ImGui::Button("Save##save", ImVec2(120, 0));
441 ImGui::EndDisabled();
442 }
443
444 ImGui::SameLine();
445
446 if (ImGui::Button("Cancel##cancel", ImVec2(120, 0)))
447 {
448 m_isOpen = false;
449 m_confirmed = false;
451 ImGui::CloseCurrentPopup();
452 }
453}
454
455bool SaveFilePickerModal::FileExists(const std::string& path) const
456{
457 std::ifstream file(path);
458 return file.good();
459}
460
462{
463 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
464 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
465
466 if (ImGui::BeginPopupModal("Overwrite File?", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
467 {
468 ImGui::Text("File already exists. Overwrite?");
469 ImGui::Text("%s", m_selectedFile.c_str());
470 ImGui::Spacing();
471
472 if (ImGui::Button("Yes, Overwrite##yes", ImVec2(120, 0)))
473 {
474 m_confirmed = true;
475 m_isOpen = false;
477 ImGui::CloseCurrentPopup();
478 SYSTEM_LOG << "[SaveFilePickerModal] File overwrite confirmed: " << m_selectedFile << "\n";
479 }
480
481 ImGui::SameLine();
482
483 if (ImGui::Button("No, Cancel##no", ImVec2(120, 0)))
484 {
485 m_selectedFile = "";
487 ImGui::CloseCurrentPopup();
488 }
489
490 ImGui::EndPopup();
491 }
492 else
493 {
494 ImGui::OpenPopup("Overwrite File?");
495 }
496}
497
498} // namespace Olympe
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
Centralized Save As modal for all file save operations (Phase 40 Enhancement).
SaveFilePickerModal(SaveFileType fileType)
Constructs a save file picker modal for the given file type.
void Close()
Closes the modal without confirming changes.
void RenderActionButtons()
Renders action buttons (Save/Cancel).
void RenderFolderList()
Renders folder navigation.
std::string GetModalTitle() const
Returns a description of this file type for the modal title.
std::string GetDefaultDirectory() const
Returns the default directory for this file type.
std::vector< std::string > m_fileList
Files in current directory.
bool m_showOverwriteConfirm
Show overwrite confirmation dialog.
std::string GetDescriptionText() const
Returns user-friendly description text.
std::string m_currentPath
Current directory.
void RefreshFileList()
Refreshes the file and folder lists for the current directory.
void Open(const std::string &directory, const std::string &suggestedFilename="")
Opens the modal with initial directory and filename.
std::vector< std::string > m_folderList
Folders in current directory.
void RenderOverwriteConfirmation()
Renders the overwrite confirmation dialog.
char m_pathBuffer[512]
Path input text buffer.
SaveFileType m_fileType
Type of file to save.
void RenderFileList()
Renders the file list in the modal.
char m_filenameBuffer[256]
Filename input (without extension)
std::string m_selectedFile
Full path with extension.
bool m_confirmed
Did user click Save.
void Render()
Renders the modal UI.
void RenderFilenameInput()
Renders filename input and extension display.
std::string GetFileExtension() const
Returns the file extension for this file type (e.g., ".bt.json").
bool m_isOpen
Is modal currently visible.
bool FileExists(const std::string &path) const
Checks if a file exists at the given path.
< Provides AssetID and INVALID_ASSET_ID
SaveFileType
Supported file types for the centralized save modal.
@ EntityPrefab
.pref.json files (Entity Prefab)
@ Audio
.ogg files (audio assets)
@ Blueprint
.ats files (SubGraph/VisualScript)
@ BehaviorTree
.bt.json files
#define SYSTEM_LOG