Loading...
Searching...
No Matches
FileDialog.cpp
Go to the documentation of this file.
1//
2// Created by denzel on 11/04/2025.
3//
4
5#include "hellfire/utilities/FileDialog.h"
6
7#include <imgui.h>
8#ifdef _WIN32
9#include <windows.h>
10#include <commdlg.h>
11#include <shobjidl.h>
12#include <shlobj.h>
13#endif
14
15namespace hellfire::Utility {
16 std::string FileDialog::win32_open_file(const std::vector<FileFilter> &filters) {
17 std::string filepath;
18
19 OPENFILENAMEA ofn;
20 CHAR szFile[260] = {};
21 ZeroMemory(&ofn, sizeof(ofn));
22 ofn.lStructSize = sizeof(ofn);
23 ofn.hwndOwner = GetActiveWindow();
24 ofn.lpstrFile = szFile;
25 ofn.nMaxFile = sizeof(szFile);
26
27 // Convert filters to Windows format
28 std::string filterStr;
29 if (filters.empty()) {
30 // Default to all files if no filters provided
31 filterStr = "All Files\0*.*\0";
32 } else {
33 for (const auto &filter: filters) {
34 filterStr += filter.name + '\0' + filter.extensions + '\0';
35 }
36 }
37
38 // Create a non-const copy that will persist for the duration of the call
39 std::vector filterBuf(filterStr.begin(), filterStr.end());
40 filterBuf.push_back('\0'); // Add final null terminator
41
42 ofn.lpstrFilter = filterBuf.data();
43 ofn.nFilterIndex = 1;
44 ofn.lpstrFileTitle = NULL;
45 ofn.nMaxFileTitle = 0;
46 ofn.lpstrInitialDir = NULL;
47 ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR;
48
49 if (GetOpenFileNameA(&ofn) == TRUE) {
50 filepath = ofn.lpstrFile;
51 }
52
53 return filepath;
54 }
55
56 std::string FileDialog::win32_save_file(const std::string &default_filename,
57 const std::vector<FileFilter> &filters, std::string &save_name_to) {
58 std::string filepath;
59
60 OPENFILENAMEA ofn;
61 CHAR szFile[260] = {0};
62
63 // Pre-populate with default filename if provided
64 if (!default_filename.empty()) {
65 strncpy(szFile, default_filename.c_str(), sizeof(szFile) - 1);
66 }
67
68 ZeroMemory(&ofn, sizeof(ofn));
69 ofn.lStructSize = sizeof(ofn);
70 ofn.hwndOwner = GetActiveWindow();
71 ofn.lpstrFile = szFile;
72 ofn.nMaxFile = sizeof(szFile);
73
74 // Convert filters to Windows format
75 std::string filterStr;
76 std::string defaultExt;
77
78 if (filters.empty()) {
79 // Default to all files if no filters provided
80 filterStr = "All Files\0*.*\0";
81 } else {
82 for (const auto &filter: filters) {
83 filterStr += filter.name + '\0' + filter.extensions + '\0';
84
85 // Set default extension from first filter if not already set
86 if (defaultExt.empty() && !filter.extensions.empty()) {
87 // Extract extension from the first extension in the list
88 size_t pos = filter.extensions.find("*.");
89 if (pos != std::string::npos) {
90 size_t endPos = filter.extensions.find(';', pos);
91 if (endPos == std::string::npos) {
92 endPos = filter.extensions.size();
93 }
94 // Get extension without the "*."
95 defaultExt = filter.extensions.substr(pos + 2, endPos - pos - 2);
96 }
97 }
98 }
99 }
100
101 // Create a non-const copy that will persist for the duration of the call
102 std::vector<char> filterBuf(filterStr.begin(), filterStr.end());
103 filterBuf.push_back('\0'); // Add final null terminator
104
105 ofn.lpstrFilter = filterBuf.data();
106 ofn.nFilterIndex = 1;
107 ofn.lpstrFileTitle = NULL;
108 ofn.nMaxFileTitle = 0;
109 ofn.lpstrInitialDir = NULL;
110 ofn.Flags = OFN_OVERWRITEPROMPT | OFN_NOCHANGEDIR;
111
112 // Set default extension if we found one
113 if (!defaultExt.empty()) {
114 // Windows API requires a non-const char*
115 std::vector<char> extBuf(defaultExt.begin(), defaultExt.end());
116 extBuf.push_back('\0');
117 ofn.lpstrDefExt = extBuf.data();
118 }
119
120 if (GetSaveFileNameA(&ofn) == TRUE) {
121 filepath = ofn.lpstrFile;
122
123 // Extract filename from full path and save to save_name_to
124 size_t lastSlash = filepath.find_last_of("\\/");
125 if (lastSlash != std::string::npos) {
126 save_name_to = filepath.substr(lastSlash + 1);
127 } else {
128 save_name_to = filepath;
129 }
130 }
131
132 return filepath;
133 }
134
135 std::string FileDialog::imgui_open_file(const std::vector<FileFilter> &filters) {
136 static std::string filepath;
137 filepath.clear();
138
139 // Format filter description for display
140 std::string filterDesc = "Supported formats: ";
141 if (filters.empty()) {
142 filterDesc += "All files";
143 } else {
144 for (size_t i = 0; i < filters.size(); ++i) {
145 filterDesc += filters[i].name;
146 if (i < filters.size() - 1) {
147 filterDesc += ", ";
148 }
149 }
150 }
151
152 ImGui::OpenPopup("Open File");
153
154 if (ImGui::BeginPopupModal("Open File", NULL, ImGuiWindowFlags_AlwaysAutoResize)) {
155 static char buf[512] = "assets/";
156 ImGui::Text("%s", filterDesc.c_str());
157 ImGui::InputText("Path to file", buf, 512);
158
159 if (ImGui::Button("OK", ImVec2(120, 0))) {
160 filepath = buf;
161 ImGui::CloseCurrentPopup();
162 }
163 ImGui::SameLine();
164 if (ImGui::Button("Cancel", ImVec2(120, 0))) {
165 ImGui::CloseCurrentPopup();
166 }
167 ImGui::EndPopup();
168 }
169
170 return filepath;
171 }
172
173 std::string FileDialog::win32_select_folder(const std::string& title) {
174 std::string folder_path;
175
176 // Initialize COM (required for IFileDialog)
177 HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
178 if (FAILED(hr)) {
179 return folder_path;
180 }
181
182 IFileDialog* pfd = nullptr;
183 hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pfd));
184
185 if (SUCCEEDED(hr)) {
186 // Get current options and add folder picker flag
187 DWORD dwOptions;
188 hr = pfd->GetOptions(&dwOptions);
189 if (SUCCEEDED(hr)) {
190 // FOS_PICKFOLDERS makes it a folder picker instead of file picker
191 hr = pfd->SetOptions(dwOptions | FOS_PICKFOLDERS);
192 }
193
194 // Set the dialog title if provided
195 if (SUCCEEDED(hr) && !title.empty()) {
196 std::wstring wideTitle(title.begin(), title.end());
197 pfd->SetTitle(wideTitle.c_str());
198 }
199
200 // Show the dialog
201 if (SUCCEEDED(hr)) {
202 hr = pfd->Show(GetActiveWindow());
203 }
204
205 // Get the result
206 if (SUCCEEDED(hr)) {
207 IShellItem* psi = nullptr;
208 hr = pfd->GetResult(&psi);
209 if (SUCCEEDED(hr)) {
210 PWSTR pszPath = nullptr;
211 hr = psi->GetDisplayName(SIGDN_FILESYSPATH, &pszPath);
212 if (SUCCEEDED(hr) && pszPath) {
213 // Convert wide string to narrow string
214 const int size = WideCharToMultiByte(CP_UTF8, 0, pszPath, -1, nullptr, 0, nullptr, nullptr);
215 if (size > 0) {
216 folder_path.resize(size - 1);
217 WideCharToMultiByte(CP_UTF8, 0, pszPath, -1, &folder_path[0], size, nullptr, nullptr);
218 }
219 CoTaskMemFree(pszPath);
220 }
221 psi->Release();
222 }
223 }
224 pfd->Release();
225 }
226
227 CoUninitialize();
228 return folder_path;
229
230 }
231
232 std::string FileDialog::imgui_save_file(const std::string &default_filename,
233 const std::vector<FileFilter> &filters) {
234 static std::string filepath;
235 filepath.clear();
236
237 // Format filter description for display
238 std::string filterDesc = "Save as: ";
239 if (filters.empty()) {
240 filterDesc += "All files";
241 } else {
242 for (size_t i = 0; i < filters.size(); ++i) {
243 filterDesc += filters[i].name;
244 if (i < filters.size() - 1) {
245 filterDesc += ", ";
246 }
247 }
248 }
249
250 ImGui::OpenPopup("Save File");
251
252 if (ImGui::BeginPopupModal("Save File", NULL, ImGuiWindowFlags_AlwaysAutoResize)) {
253 static char buf[512] = "";
254
255 // Initialize with default filename if empty
256 if (buf[0] == '\0' && !default_filename.empty()) {
257 strncpy(buf, default_filename.c_str(), sizeof(buf) - 1);
258 }
259
260 ImGui::Text("%s", filterDesc.c_str());
261 ImGui::InputText("Filename", buf, 512);
262
263 if (ImGui::Button("Save", ImVec2(120, 0))) {
264 filepath = buf;
265
266 // Extract default extension from first filter if we need to add one
267 if (!filters.empty() && filepath.find('.') == std::string::npos) {
268 const auto &firstFilter = filters[0];
269 size_t pos = firstFilter.extensions.find("*.");
270 if (pos != std::string::npos) {
271 size_t endPos = firstFilter.extensions.find(';', pos);
272 if (endPos == std::string::npos) {
273 endPos = firstFilter.extensions.size();
274 }
275 std::string ext = firstFilter.extensions.substr(pos + 1, endPos - pos - 1);
276 filepath += ext;
277 }
278 }
279
280 ImGui::CloseCurrentPopup();
281 }
282
283 ImGui::SameLine();
284
285 if (ImGui::Button("Cancel", ImVec2(120, 0))) {
286 ImGui::CloseCurrentPopup();
287 }
288
289 ImGui::EndPopup();
290 }
291
292 return filepath;
293 }
294
295
296 std::string FileDialog::open_file(const std::vector<FileFilter> &filters) {
297#ifdef _WIN32
298 return win32_open_file(filters);
299#else
300 return imgui_open_file(filters);
301#endif
302 }
303
304 std::string FileDialog::select_folder(const std::string& title) {
305#ifdef _WIN32
306 return win32_select_folder(title);
307#else
308 return imgui_select_folder(title); // TODO: Filepicker for other platforms using imgui
309#endif
310 }
311
312 std::string FileDialog::save_file(std::string &save_name_to, const std::string &default_filename,
313 const std::vector<FileFilter> &filters) {
314#ifdef _WIN32
315 return win32_save_file(default_filename, filters, save_name_to);
316#else
317 return imgui_save_file(default_filename, filters);
318#endif
319 }
320}
static std::string imgui_open_file(const std::vector< FileFilter > &filters)
static std::string select_folder(const std::string &title)
static std::string imgui_save_file(const std::string &default_filename, const std::vector< FileFilter > &filters)
static std::string win32_select_folder(const std::string &title)