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