1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3
4#include "file_browser.h"
5
6#include <engine/font_icons.h>
7#include <engine/keys.h>
8#include <engine/sound.h>
9#include <engine/storage.h>
10
11#include <game/editor/editor.h>
12
13static constexpr const char *FILETYPE_EXTENSIONS[] = {
14 ".map",
15 ".png",
16 ".opus"};
17
18void CFileBrowser::ShowFileDialog(
19 int StorageType, EFileType FileType,
20 const char *pTitle, const char *pButtonText,
21 const char *pInitialPath, const char *pInitialFilename,
22 FFileDialogOpenCallback pfnOpenCallback, void *pOpenCallbackUser)
23{
24 m_StorageType = StorageType;
25 m_FileType = FileType;
26 if(m_StorageType == IStorage::TYPE_ALL)
27 {
28 int NumStoragesWithFolder = 0;
29 for(int CheckStorageType = IStorage::TYPE_SAVE; CheckStorageType < Storage()->NumPaths(); ++CheckStorageType)
30 {
31 if(Storage()->FolderExists(pFilename: pInitialPath, Type: CheckStorageType))
32 {
33 NumStoragesWithFolder++;
34 }
35 }
36 m_MultipleStorages = NumStoragesWithFolder > 1;
37 }
38 else
39 {
40 m_MultipleStorages = false;
41 }
42 m_SaveAction = m_StorageType == IStorage::TYPE_SAVE;
43
44 Ui()->ClosePopupMenus();
45 str_copy(dst&: m_aTitle, src: pTitle);
46 str_copy(dst&: m_aButtonText, src: pButtonText);
47 m_pfnOpenCallback = pfnOpenCallback;
48 m_pOpenCallbackUser = pOpenCallbackUser;
49 m_ShowingRoot = false;
50 str_copy(dst&: m_aInitialFolder, src: pInitialPath);
51 str_copy(dst&: m_aCurrentFolder, src: pInitialPath);
52 m_aCurrentLink[0] = '\0';
53 m_pCurrentPath = m_aCurrentFolder;
54 m_FilenameInput.Set(pInitialFilename);
55 m_FilterInput.Clear();
56 dbg_assert(m_PreviewState == EPreviewState::UNLOADED, "Preview was not unloaded before showing file dialog");
57 m_ListBox.Reset();
58
59 FilelistPopulate(StorageType: m_StorageType, KeepSelection: false);
60
61 if(m_SaveAction)
62 {
63 Ui()->SetActiveItem(&m_FilenameInput);
64 if(!m_FilenameInput.IsEmpty())
65 {
66 UpdateSelectedIndex(pDisplayName: m_FilenameInput.GetString());
67 }
68 }
69 else
70 {
71 Ui()->SetActiveItem(&m_FilterInput);
72 UpdateFilenameInput();
73 }
74
75 Editor()->m_Dialog = DIALOG_FILE;
76}
77
78void CFileBrowser::OnRender(CUIRect _)
79{
80 if(Editor()->m_Dialog != DIALOG_FILE)
81 {
82 return;
83 }
84
85 Ui()->MapScreen();
86 CUIRect View = *Ui()->Screen();
87 CUIRect Preview = {.x: 0.0f, .y: 0.0f, .w: 0.0f, .h: 0.0f};
88 const float OriginalWidth = View.w;
89 const float OriginalHeight = View.h;
90
91 // Prevent UI elements below the file browser from being activated.
92 Ui()->SetHotItem(this);
93
94 View.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f), Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
95 View.VMargin(Cut: 150.0f, pOtherRect: &View);
96 View.HMargin(Cut: 50.0f, pOtherRect: &View);
97 View.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.75f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
98 View.Margin(Cut: 10.0f, pOtherRect: &View);
99
100 CUIRect Title, FileBox, FileBoxLabel, ButtonBar, PathBox;
101 View.HSplitTop(Cut: 20.0f, pTop: &Title, pBottom: &View);
102 View.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &View);
103 View.HSplitBottom(Cut: 20.0f, pTop: &View, pBottom: &ButtonBar);
104 View.HSplitBottom(Cut: 5.0f, pTop: &View, pBottom: nullptr);
105 View.HSplitBottom(Cut: 15.0f, pTop: &View, pBottom: &PathBox);
106 View.HSplitBottom(Cut: 5.0f, pTop: &View, pBottom: nullptr);
107 View.HSplitBottom(Cut: 15.0f, pTop: &View, pBottom: &FileBox);
108 FileBox.VSplitLeft(Cut: 55.0f, pLeft: &FileBoxLabel, pRight: &FileBox);
109 View.HSplitBottom(Cut: 5.0f, pTop: &View, pBottom: nullptr);
110 if(CanPreviewFile())
111 {
112 View.VSplitMid(pLeft: &View, pRight: &Preview);
113 }
114
115 // Title bar sort buttons
116 if(!m_ShowingRoot)
117 {
118 CUIRect ButtonTimeModified, ButtonFilename;
119 Title.VSplitRight(Cut: m_ListBox.ScrollbarWidthMax(), pLeft: &Title, pRight: nullptr);
120 Title.VSplitRight(Cut: 90.0f, pLeft: &Title, pRight: &ButtonTimeModified);
121 Title.VSplitRight(Cut: 5.0f, pLeft: &Title, pRight: nullptr);
122 Title.VSplitRight(Cut: 90.0f, pLeft: &Title, pRight: &ButtonFilename);
123 Title.VSplitRight(Cut: 5.0f, pLeft: &Title, pRight: nullptr);
124
125 static constexpr const char *SORT_INDICATORS[] = {"", "▲", "▼"};
126
127 char aLabelButtonSortTimeModified[64];
128 str_format(buffer: aLabelButtonSortTimeModified, buffer_size: sizeof(aLabelButtonSortTimeModified), format: "Time modified %s", SORT_INDICATORS[(int)m_SortByTimeModified]);
129 if(Editor()->DoButton_Editor(pId: &m_ButtonSortTimeModifiedId, pText: aLabelButtonSortTimeModified, Checked: 0, pRect: &ButtonTimeModified, Flags: BUTTONFLAG_LEFT, pToolTip: "Sort by time modified."))
130 {
131 if(m_SortByTimeModified == ESortDirection::ASCENDING)
132 {
133 m_SortByTimeModified = ESortDirection::DESCENDING;
134 }
135 else if(m_SortByTimeModified == ESortDirection::DESCENDING)
136 {
137 m_SortByTimeModified = ESortDirection::NEUTRAL;
138 }
139 else
140 {
141 m_SortByTimeModified = ESortDirection::ASCENDING;
142 }
143
144 RefreshFilteredFileList();
145 }
146
147 char aLabelButtonSortFilename[64];
148 str_format(buffer: aLabelButtonSortFilename, buffer_size: sizeof(aLabelButtonSortFilename), format: "Filename %s", SORT_INDICATORS[(int)m_SortByFilename]);
149 if(Editor()->DoButton_Editor(pId: &m_ButtonSortFilenameId, pText: aLabelButtonSortFilename, Checked: 0, pRect: &ButtonFilename, Flags: BUTTONFLAG_LEFT, pToolTip: "Sort by filename."))
150 {
151 if(m_SortByFilename == ESortDirection::DESCENDING)
152 {
153 m_SortByFilename = ESortDirection::ASCENDING;
154 m_SortByTimeModified = ESortDirection::NEUTRAL;
155 }
156 else
157 {
158 m_SortByFilename = ESortDirection::DESCENDING;
159 m_SortByTimeModified = ESortDirection::NEUTRAL;
160 }
161
162 RefreshFilteredFileList();
163 }
164 }
165
166 // Title
167 Title.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
168 Title.VMargin(Cut: 10.0f, pOtherRect: &Title);
169 Ui()->DoLabel(pRect: &Title, pText: m_aTitle, Size: 12.0f, Align: TEXTALIGN_ML);
170
171 // Current path
172 if(m_SelectedFileIndex >= 0 && m_vpFilteredFileList[m_SelectedFileIndex]->m_StorageType >= IStorage::TYPE_SAVE)
173 {
174 char aPath[IO_MAX_PATH_LENGTH];
175 Storage()->GetCompletePath(Type: m_vpFilteredFileList[m_SelectedFileIndex]->m_StorageType, pDir: m_pCurrentPath, pBuffer: aPath, BufferSize: sizeof(aPath));
176 char aPathLabel[128 + IO_MAX_PATH_LENGTH];
177 str_format(buffer: aPathLabel, buffer_size: sizeof(aPathLabel), format: "Current path: %s", aPath);
178 Ui()->DoLabel(pRect: &PathBox, pText: aPathLabel, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: {.m_MaxWidth = PathBox.w, .m_EllipsisAtEnd = true});
179 }
180
181 m_ListBox.SetActive(!Ui()->IsPopupOpen());
182
183 // Filename/filter input
184 if(m_SaveAction)
185 {
186 // Filename input when saving
187 Ui()->DoLabel(pRect: &FileBoxLabel, pText: "Filename:", Size: 10.0f, Align: TEXTALIGN_ML);
188 if(Ui()->DoEditBox(pLineInput: &m_FilenameInput, pRect: &FileBox, FontSize: 10.0f))
189 {
190 // Remove '/' and '\'
191 for(int i = 0; m_FilenameInput.GetString()[i]; ++i)
192 {
193 if(m_FilenameInput.GetString()[i] == '/' || m_FilenameInput.GetString()[i] == '\\')
194 {
195 m_FilenameInput.SetRange(pString: m_FilenameInput.GetString() + i + 1, Begin: i, End: m_FilenameInput.GetLength());
196 --i;
197 }
198 }
199 UpdateSelectedIndex(pDisplayName: m_FilenameInput.GetString());
200 }
201 }
202 else
203 {
204 // Filter input when loading
205 Ui()->DoLabel(pRect: &FileBoxLabel, pText: "Search:", Size: 10.0f, Align: TEXTALIGN_ML);
206 if(Input()->KeyPress(Key: KEY_F) && Input()->ModifierIsPressed())
207 {
208 Ui()->SetActiveItem(&m_FilterInput);
209 m_FilterInput.SelectAll();
210 }
211 if(Ui()->DoClearableEditBox(pLineInput: &m_FilterInput, pRect: &FileBox, FontSize: 10.0f))
212 {
213 RefreshFilteredFileList();
214 if(m_vpFilteredFileList.empty())
215 {
216 m_SelectedFileIndex = -1;
217 }
218 else if(m_SelectedFileIndex == -1 ||
219 (!m_FilterInput.IsEmpty() && !str_find_nocase(haystack: m_vpFilteredFileList[m_SelectedFileIndex]->m_aDisplayName, needle: m_FilterInput.GetString())))
220 {
221 m_SelectedFileIndex = -1;
222 for(size_t i = 0; i < m_vpFilteredFileList.size(); i++)
223 {
224 if(str_find_nocase(haystack: m_vpFilteredFileList[i]->m_aDisplayName, needle: m_FilterInput.GetString()))
225 {
226 m_SelectedFileIndex = i;
227 break;
228 }
229 }
230 if(m_SelectedFileIndex == -1)
231 {
232 m_SelectedFileIndex = 0;
233 }
234 }
235 str_copy(dst&: m_aSelectedFileDisplayName, src: m_SelectedFileIndex >= 0 ? m_vpFilteredFileList[m_SelectedFileIndex]->m_aDisplayName : "");
236 UpdateFilenameInput();
237 m_ListBox.ScrollToSelected();
238 m_PreviewState = EPreviewState::UNLOADED;
239 }
240 }
241
242 // File preview
243 if(m_SelectedFileIndex >= 0 && CanPreviewFile())
244 {
245 UpdateFilePreview();
246 Preview.Margin(Cut: 10.0f, pOtherRect: &Preview);
247 RenderFilePreview(Preview);
248 }
249
250 // File list
251 m_ListBox.DoStart(RowHeight: 15.0f, NumItems: m_vpFilteredFileList.size(), ItemsPerRow: 1, RowsPerScroll: 5, SelectedIndex: m_SelectedFileIndex, pRect: &View, Background: false, BackgroundCorners: IGraphics::CORNER_ALL, ForceShowScrollbar: true);
252
253 for(size_t i = 0; i < m_vpFilteredFileList.size(); i++)
254 {
255 const CListboxItem Item = m_ListBox.DoNextItem(pId: m_vpFilteredFileList[i], Selected: m_SelectedFileIndex >= 0 && (size_t)m_SelectedFileIndex == i);
256 if(!Item.m_Visible)
257 {
258 continue;
259 }
260
261 CUIRect Button, FileIcon, TimeModified;
262 Item.m_Rect.VMargin(Cut: 2.0f, pOtherRect: &Button);
263 Button.VSplitLeft(Cut: Button.h, pLeft: &FileIcon, pRight: &Button);
264 Button.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Button);
265 Button.VSplitRight(Cut: 100.0f, pLeft: &Button, pRight: &TimeModified);
266 Button.VSplitRight(Cut: 5.0f, pLeft: &Button, pRight: nullptr);
267
268 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
269 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
270 Ui()->DoLabel(pRect: &FileIcon, pText: DetermineFileFontIcon(pItem: m_vpFilteredFileList[i]), Size: 12.0f, Align: TEXTALIGN_ML);
271 TextRender()->SetRenderFlags(0);
272 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
273
274 Ui()->DoLabel(pRect: &Button, pText: m_vpFilteredFileList[i]->m_aDisplayName, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: {.m_MaxWidth = Button.w, .m_EllipsisAtEnd = true});
275
276 if(!m_vpFilteredFileList[i]->m_IsLink && str_comp(a: m_vpFilteredFileList[i]->m_aFilename, b: "..") != 0)
277 {
278 char aLabelTimeModified[64];
279 str_timestamp_ex(time: m_vpFilteredFileList[i]->m_TimeModified, buffer: aLabelTimeModified, buffer_size: sizeof(aLabelTimeModified), format: "%d.%m.%Y %H:%M");
280 Ui()->DoLabel(pRect: &TimeModified, pText: aLabelTimeModified, Size: 10.0f, Align: TEXTALIGN_MR);
281 }
282 }
283
284 const int NewSelection = m_ListBox.DoEnd();
285 if(NewSelection != m_SelectedFileIndex)
286 {
287 m_SelectedFileIndex = NewSelection;
288 str_copy(dst&: m_aSelectedFileDisplayName, src: m_SelectedFileIndex >= 0 ? m_vpFilteredFileList[m_SelectedFileIndex]->m_aDisplayName : "");
289 const bool WasChanged = m_FilenameInput.WasChanged();
290 UpdateFilenameInput();
291 if(!WasChanged) // ensure that changed flag is not set if it wasn't previously set, as this would reset the selection after DoEditBox is called
292 {
293 m_FilenameInput.WasChanged(); // this clears the changed flag
294 }
295 m_PreviewState = EPreviewState::UNLOADED;
296 }
297
298 // Buttons
299 const float ButtonSpacing = ButtonBar.w > 600.0f ? 40.0f : 10.0f;
300
301 CUIRect Button;
302 ButtonBar.VSplitRight(Cut: 50.0f, pLeft: &ButtonBar, pRight: &Button);
303 const bool IsDir = m_SelectedFileIndex >= 0 && m_vpFilteredFileList[m_SelectedFileIndex]->m_IsDir;
304 const char *pOpenTooltip = IsDir ? "Open the selected folder." : (m_SaveAction ? "Save file with the specified name." : "Open the selected file.");
305 if(Editor()->DoButton_Editor(pId: &m_ButtonOkId, pText: IsDir ? "Open" : m_aButtonText, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: pOpenTooltip) ||
306 m_ListBox.WasItemActivated() ||
307 (m_ListBox.Active() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER)))
308 {
309 if(IsDir)
310 {
311 m_FilterInput.Clear();
312 Ui()->SetActiveItem(&m_FilterInput);
313 const bool ParentFolder = str_comp(a: m_vpFilteredFileList[m_SelectedFileIndex]->m_aFilename, b: "..") == 0;
314 if(ParentFolder)
315 {
316 str_copy(dst&: m_aSelectedFileDisplayName, src: fs_filename(path: m_pCurrentPath));
317 str_append(dst&: m_aSelectedFileDisplayName, src: "/");
318 if(fs_parent_dir(path: m_pCurrentPath))
319 {
320 if(str_comp(a: m_pCurrentPath, b: m_aCurrentFolder) == 0)
321 {
322 m_ShowingRoot = true;
323 if(m_StorageType == IStorage::TYPE_ALL)
324 {
325 m_aSelectedFileDisplayName[0] = '\0'; // will select first list item
326 }
327 else
328 {
329 Storage()->GetCompletePath(Type: m_StorageType, pDir: m_pCurrentPath, pBuffer: m_aSelectedFileDisplayName, BufferSize: sizeof(m_aSelectedFileDisplayName));
330 str_append(dst&: m_aSelectedFileDisplayName, src: "/");
331 }
332 }
333 else
334 {
335 m_pCurrentPath = m_aCurrentFolder; // leave the link
336 str_copy(dst&: m_aSelectedFileDisplayName, src: m_aCurrentLink);
337 str_append(dst&: m_aSelectedFileDisplayName, src: "/");
338 }
339 }
340 }
341 else // sub folder
342 {
343 if(m_vpFilteredFileList[m_SelectedFileIndex]->m_IsLink)
344 {
345 m_pCurrentPath = m_aCurrentLink; // follow the link
346 str_copy(dst&: m_aCurrentLink, src: m_vpFilteredFileList[m_SelectedFileIndex]->m_aFilename);
347 }
348 else
349 {
350 str_append(dst: m_pCurrentPath, src: "/", dst_size: IO_MAX_PATH_LENGTH);
351 str_append(dst: m_pCurrentPath, src: m_vpFilteredFileList[m_SelectedFileIndex]->m_aFilename, dst_size: IO_MAX_PATH_LENGTH);
352 }
353 if(m_ShowingRoot)
354 {
355 m_StorageType = m_vpFilteredFileList[m_SelectedFileIndex]->m_StorageType;
356 }
357 m_ShowingRoot = false;
358 }
359 FilelistPopulate(StorageType: m_StorageType, KeepSelection: ParentFolder);
360 UpdateFilenameInput();
361 }
362 else // file
363 {
364 const int StorageType = m_SelectedFileIndex >= 0 ? m_vpFilteredFileList[m_SelectedFileIndex]->m_StorageType : m_StorageType;
365 char aSaveFilePath[IO_MAX_PATH_LENGTH];
366 str_format(buffer: aSaveFilePath, buffer_size: sizeof(aSaveFilePath), format: "%s/%s", m_pCurrentPath, m_FilenameInput.GetString());
367 if(!str_endswith(str: aSaveFilePath, suffix: FILETYPE_EXTENSIONS[(int)m_FileType]))
368 {
369 str_append(dst&: aSaveFilePath, src: FILETYPE_EXTENSIONS[(int)m_FileType]);
370 }
371
372 char aFilename[IO_MAX_PATH_LENGTH];
373 fs_split_file_extension(filename: fs_filename(path: aSaveFilePath), name: aFilename, name_size: sizeof(aFilename));
374 if(m_SaveAction && !str_valid_filename(str: aFilename))
375 {
376 Editor()->ShowFileDialogError(pFormat: "This name cannot be used for files and folders.");
377 }
378 else if(m_SaveAction && Storage()->FileExists(pFilename: aSaveFilePath, Type: StorageType))
379 {
380 m_PopupConfirmOverwrite.m_pFileBrowser = this;
381 str_copy(dst&: m_PopupConfirmOverwrite.m_aOverwritePath, src: aSaveFilePath);
382 constexpr float PopupWidth = 400.0f;
383 constexpr float PopupHeight = 150.0f;
384 Ui()->DoPopupMenu(pId: &m_PopupConfirmOverwrite,
385 X: OriginalWidth / 2.0f - PopupWidth / 2.0f, Y: OriginalHeight / 2.0f - PopupHeight / 2.0f, Width: PopupWidth, Height: PopupHeight,
386 pContext: &m_PopupConfirmOverwrite, pfnFunc: CPopupConfirmOverwrite::Render);
387 }
388 else if(m_pfnOpenCallback && (m_SaveAction || m_SelectedFileIndex >= 0))
389 {
390 m_pfnOpenCallback(aSaveFilePath, StorageType, m_pOpenCallbackUser);
391 }
392 }
393 }
394
395 ButtonBar.VSplitRight(Cut: ButtonSpacing, pLeft: &ButtonBar, pRight: nullptr);
396 ButtonBar.VSplitRight(Cut: 50.0f, pLeft: &ButtonBar, pRight: &Button);
397 if(Editor()->DoButton_Editor(pId: &m_ButtonCancelId, pText: "Cancel", Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Close this dialog.") ||
398 (m_ListBox.Active() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE)))
399 {
400 Editor()->OnDialogClose();
401 }
402
403 ButtonBar.VSplitRight(Cut: ButtonSpacing, pLeft: &ButtonBar, pRight: nullptr);
404 ButtonBar.VSplitRight(Cut: 50.0f, pLeft: &ButtonBar, pRight: &Button);
405 if(Editor()->DoButton_Editor(pId: &m_ButtonRefreshId, pText: "Refresh", Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Refresh the list of files.") ||
406 (m_ListBox.Active() && (Input()->KeyIsPressed(Key: KEY_F5) || (Input()->ModifierIsPressed() && Input()->KeyIsPressed(Key: KEY_R)))))
407 {
408 FilelistPopulate(StorageType: m_StorageType, KeepSelection: true);
409 }
410
411 if(m_SelectedFileIndex >= 0 && m_vpFilteredFileList[m_SelectedFileIndex]->m_StorageType != IStorage::TYPE_ALL)
412 {
413 ButtonBar.VSplitRight(Cut: ButtonSpacing, pLeft: &ButtonBar, pRight: nullptr);
414 ButtonBar.VSplitRight(Cut: 90.0f, pLeft: &ButtonBar, pRight: &Button);
415 if(Editor()->DoButton_Editor(pId: &m_ButtonShowDirectoryId, pText: "Show directory", Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Open the current directory in the file browser."))
416 {
417 char aOpenPath[IO_MAX_PATH_LENGTH];
418 Storage()->GetCompletePath(Type: m_vpFilteredFileList[m_SelectedFileIndex]->m_StorageType, pDir: m_pCurrentPath, pBuffer: aOpenPath, BufferSize: sizeof(aOpenPath));
419 if(!Client()->ViewFile(pFilename: aOpenPath))
420 {
421 Editor()->ShowFileDialogError(pFormat: "Failed to open the directory '%s'.", aOpenPath);
422 }
423 }
424 }
425
426 ButtonBar.VSplitRight(Cut: ButtonSpacing, pLeft: &ButtonBar, pRight: nullptr);
427 ButtonBar.VSplitRight(Cut: 50.0f, pLeft: &ButtonBar, pRight: &Button);
428 if(m_SelectedFileIndex >= 0 &&
429 m_vpFilteredFileList[m_SelectedFileIndex]->m_StorageType == IStorage::TYPE_SAVE &&
430 !m_vpFilteredFileList[m_SelectedFileIndex]->m_IsLink &&
431 str_comp(a: m_vpFilteredFileList[m_SelectedFileIndex]->m_aFilename, b: "..") != 0)
432 {
433 if(Editor()->DoButton_Editor(pId: &m_ButtonDeleteId, pText: "Delete", Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: IsDir ? "Delete the selected folder." : "Delete the selected file.") ||
434 (m_ListBox.Active() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_DELETE)))
435 {
436 m_PopupConfirmDelete.m_pFileBrowser = this;
437 m_PopupConfirmDelete.m_IsDirectory = IsDir;
438 str_format(buffer: m_PopupConfirmDelete.m_aDeletePath, buffer_size: sizeof(m_PopupConfirmDelete.m_aDeletePath), format: "%s/%s", m_pCurrentPath, m_vpFilteredFileList[m_SelectedFileIndex]->m_aFilename);
439 constexpr float PopupWidth = 400.0f;
440 constexpr float PopupHeight = 150.0f;
441 Ui()->DoPopupMenu(pId: &m_PopupConfirmDelete,
442 X: OriginalWidth / 2.0f - PopupWidth / 2.0f, Y: OriginalHeight / 2.0f - PopupHeight / 2.0f, Width: PopupWidth, Height: PopupHeight,
443 pContext: &m_PopupConfirmDelete, pfnFunc: CPopupConfirmDelete::Render);
444 }
445 }
446
447 if(!m_ShowingRoot && m_StorageType == IStorage::TYPE_SAVE)
448 {
449 ButtonBar.VSplitLeft(Cut: 70.0f, pLeft: &Button, pRight: &ButtonBar);
450 if(Editor()->DoButton_Editor(pId: &m_ButtonNewFolderId, pText: "New folder", Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Create a new folder."))
451 {
452 m_PopupNewFolder.m_pFileBrowser = this;
453 m_PopupNewFolder.m_NewFolderNameInput.Clear();
454 constexpr float PopupWidth = 400.0f;
455 constexpr float PopupHeight = 110.0f;
456 Ui()->DoPopupMenu(pId: &m_PopupNewFolder,
457 X: OriginalWidth / 2.0f - PopupWidth / 2.0f, Y: OriginalHeight / 2.0f - PopupHeight / 2.0f, Width: PopupWidth, Height: PopupHeight,
458 pContext: &m_PopupNewFolder, pfnFunc: CPopupNewFolder::Render);
459 Ui()->SetActiveItem(&m_PopupNewFolder.m_NewFolderNameInput);
460 }
461 }
462}
463
464bool CFileBrowser::IsValidSaveFilename() const
465{
466 return m_pCurrentPath == m_aCurrentFolder ||
467 (m_pCurrentPath == m_aCurrentLink && str_startswith(str: m_aCurrentLink, prefix: "themes"));
468}
469
470void CFileBrowser::OnEditorClose()
471{
472 if(m_PreviewSound >= 0 && Sound()->IsPlaying(SampleId: m_PreviewSound))
473 {
474 Sound()->Pause(SampleId: m_PreviewSound);
475 }
476}
477
478void CFileBrowser::OnDialogClose()
479{
480 m_PreviewState = EPreviewState::UNLOADED;
481 Graphics()->UnloadTexture(pIndex: &m_PreviewImage);
482 m_PreviewImageWidth = -1;
483 m_PreviewImageHeight = -1;
484 Sound()->UnloadSample(SampleId: m_PreviewSound);
485 m_PreviewSound = -1;
486}
487
488bool CFileBrowser::CanPreviewFile() const
489{
490 return m_FileType == CFileBrowser::EFileType::IMAGE ||
491 m_FileType == CFileBrowser::EFileType::SOUND;
492}
493
494void CFileBrowser::UpdateFilePreview()
495{
496 if(m_PreviewState != EPreviewState::UNLOADED ||
497 !str_endswith(str: m_vpFilteredFileList[m_SelectedFileIndex]->m_aFilename, suffix: FILETYPE_EXTENSIONS[(int)m_FileType]))
498 {
499 return;
500 }
501
502 if(m_FileType == CFileBrowser::EFileType::IMAGE)
503 {
504 char aImagePath[IO_MAX_PATH_LENGTH];
505 str_format(buffer: aImagePath, buffer_size: sizeof(aImagePath), format: "%s/%s", m_pCurrentPath, m_vpFilteredFileList[m_SelectedFileIndex]->m_aFilename);
506 CImageInfo PreviewImageInfo;
507 if(Graphics()->LoadPng(Image&: PreviewImageInfo, pFilename: aImagePath, StorageType: m_vpFilteredFileList[m_SelectedFileIndex]->m_StorageType))
508 {
509 Graphics()->UnloadTexture(pIndex: &m_PreviewImage);
510 m_PreviewImageWidth = PreviewImageInfo.m_Width;
511 m_PreviewImageHeight = PreviewImageInfo.m_Height;
512 m_PreviewImage = Graphics()->LoadTextureRawMove(Image&: PreviewImageInfo, Flags: 0, pTexName: aImagePath);
513 m_PreviewState = EPreviewState::LOADED;
514 }
515 else
516 {
517 m_PreviewState = EPreviewState::ERROR;
518 }
519 }
520 else if(m_FileType == CFileBrowser::EFileType::SOUND)
521 {
522 char aSoundPath[IO_MAX_PATH_LENGTH];
523 str_format(buffer: aSoundPath, buffer_size: sizeof(aSoundPath), format: "%s/%s", m_pCurrentPath, m_vpFilteredFileList[m_SelectedFileIndex]->m_aFilename);
524 Sound()->UnloadSample(SampleId: m_PreviewSound);
525 m_PreviewSound = Sound()->LoadOpus(pFilename: aSoundPath, StorageType: m_vpFilteredFileList[m_SelectedFileIndex]->m_StorageType);
526 m_PreviewState = m_PreviewSound == -1 ? EPreviewState::ERROR : EPreviewState::LOADED;
527 }
528}
529
530void CFileBrowser::RenderFilePreview(CUIRect Preview)
531{
532 if(m_FileType == CFileBrowser::EFileType::IMAGE)
533 {
534 if(m_PreviewState == EPreviewState::LOADED)
535 {
536 CUIRect PreviewLabel, PreviewImage;
537 Preview.HSplitTop(Cut: 20.0f, pTop: &PreviewLabel, pBottom: &PreviewImage);
538
539 char aSizeLabel[64];
540 str_format(buffer: aSizeLabel, buffer_size: sizeof(aSizeLabel), format: "Size: %d × %d", m_PreviewImageWidth, m_PreviewImageHeight);
541 Ui()->DoLabel(pRect: &PreviewLabel, pText: aSizeLabel, Size: 12.0f, Align: TEXTALIGN_ML);
542
543 int Width = m_PreviewImageWidth;
544 int Height = m_PreviewImageHeight;
545 if(m_PreviewImageWidth > PreviewImage.w)
546 {
547 Height = m_PreviewImageHeight * PreviewImage.w / m_PreviewImageWidth;
548 Width = PreviewImage.w;
549 }
550 if(Height > PreviewImage.h)
551 {
552 Width = Width * PreviewImage.h / Height;
553 Height = PreviewImage.h;
554 }
555
556 Graphics()->TextureSet(Texture: m_PreviewImage);
557 Graphics()->BlendNormal();
558 Graphics()->QuadsBegin();
559 IGraphics::CQuadItem QuadItem(PreviewImage.x, PreviewImage.y, Width, Height);
560 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
561 Graphics()->QuadsEnd();
562 }
563 else if(m_PreviewState == EPreviewState::ERROR)
564 {
565 Ui()->DoLabel(pRect: &Preview, pText: "Failed to load the image (check the local console for details).", Size: 12.0f, Align: TEXTALIGN_TL, LabelProps: {.m_MaxWidth = Preview.w});
566 }
567 }
568 else if(m_FileType == CFileBrowser::EFileType::SOUND)
569 {
570 if(m_PreviewState == EPreviewState::LOADED)
571 {
572 Preview.HSplitTop(Cut: 20.0f, pTop: &Preview, pBottom: nullptr);
573 Preview.VSplitLeft(Cut: Preview.h / 4.0f, pLeft: nullptr, pRight: &Preview);
574 Editor()->DoAudioPreview(View: Preview, pPlayPauseButtonId: &m_ButtonPlayPauseId, pStopButtonId: &m_ButtonStopId, pSeekBarId: &m_SeekBarId, SampleId: m_PreviewSound);
575 }
576 else if(m_PreviewState == EPreviewState::ERROR)
577 {
578 Ui()->DoLabel(pRect: &Preview, pText: "Failed to load the sound (check the local console for details). Make sure you enabled sounds in the settings.", Size: 12.0f, Align: TEXTALIGN_TL, LabelProps: {.m_MaxWidth = Preview.w});
579 }
580 }
581}
582
583const char *CFileBrowser::DetermineFileFontIcon(const CFilelistItem *pItem) const
584{
585 if(!pItem->m_IsDir)
586 {
587 switch(m_FileType)
588 {
589 case EFileType::MAP:
590 return FontIcon::MAP;
591 case EFileType::IMAGE:
592 return FontIcon::IMAGE;
593 case EFileType::SOUND:
594 return FontIcon::MUSIC;
595 default:
596 dbg_assert_failed("m_FileType invalid: %d", (int)m_FileType);
597 }
598 }
599 else if(pItem->m_IsLink || str_comp(a: pItem->m_aFilename, b: "..") == 0)
600 {
601 return FontIcon::FOLDER_TREE;
602 }
603 else
604 {
605 return FontIcon::FOLDER;
606 }
607}
608
609void CFileBrowser::UpdateFilenameInput()
610{
611 if(m_SelectedFileIndex >= 0 && !m_vpFilteredFileList[m_SelectedFileIndex]->m_IsDir)
612 {
613 char aNameWithoutExt[IO_MAX_PATH_LENGTH];
614 fs_split_file_extension(filename: m_vpFilteredFileList[m_SelectedFileIndex]->m_aFilename, name: aNameWithoutExt, name_size: sizeof(aNameWithoutExt));
615 m_FilenameInput.Set(aNameWithoutExt);
616 }
617 else
618 {
619 m_FilenameInput.Clear();
620 }
621}
622
623void CFileBrowser::UpdateSelectedIndex(const char *pDisplayName)
624{
625 m_SelectedFileIndex = -1;
626 m_aSelectedFileDisplayName[0] = '\0';
627 for(size_t i = 0; i < m_vpFilteredFileList.size(); i++)
628 {
629 if(str_comp_nocase(a: m_vpFilteredFileList[i]->m_aDisplayName, b: pDisplayName) == 0)
630 {
631 m_SelectedFileIndex = i;
632 str_copy(dst&: m_aSelectedFileDisplayName, src: m_vpFilteredFileList[i]->m_aDisplayName);
633 break;
634 }
635 }
636 if(m_SelectedFileIndex >= 0)
637 {
638 m_ListBox.ScrollToSelected();
639 }
640}
641
642void CFileBrowser::SortFilteredFileList()
643{
644 if(m_SortByFilename == ESortDirection::ASCENDING)
645 {
646 std::sort(first: m_vpFilteredFileList.begin(), last: m_vpFilteredFileList.end(), comp: CFileBrowser::CompareFilenameAscending);
647 }
648 else
649 {
650 std::sort(first: m_vpFilteredFileList.begin(), last: m_vpFilteredFileList.end(), comp: CFileBrowser::CompareFilenameDescending);
651 }
652
653 if(m_SortByTimeModified == ESortDirection::ASCENDING)
654 {
655 std::stable_sort(first: m_vpFilteredFileList.begin(), last: m_vpFilteredFileList.end(), comp: CFileBrowser::CompareTimeModifiedAscending);
656 }
657 else if(m_SortByTimeModified == ESortDirection::DESCENDING)
658 {
659 std::stable_sort(first: m_vpFilteredFileList.begin(), last: m_vpFilteredFileList.end(), comp: CFileBrowser::CompareTimeModifiedDescending);
660 }
661}
662
663void CFileBrowser::RefreshFilteredFileList()
664{
665 m_vpFilteredFileList.clear();
666 for(const CFilelistItem &Item : m_vCompleteFileList)
667 {
668 if(m_FilterInput.IsEmpty() || str_find_nocase(haystack: Item.m_aDisplayName, needle: m_FilterInput.GetString()))
669 {
670 m_vpFilteredFileList.push_back(x: &Item);
671 }
672 }
673 if(!m_ShowingRoot)
674 {
675 SortFilteredFileList();
676 }
677 if(!m_vpFilteredFileList.empty())
678 {
679 if(m_aSelectedFileDisplayName[0] != '\0')
680 {
681 for(size_t i = 0; i < m_vpFilteredFileList.size(); i++)
682 {
683 if(str_comp(a: m_vpFilteredFileList[i]->m_aDisplayName, b: m_aSelectedFileDisplayName) == 0)
684 {
685 m_SelectedFileIndex = i;
686 break;
687 }
688 }
689 }
690 m_SelectedFileIndex = std::clamp<int>(val: m_SelectedFileIndex, lo: 0, hi: m_vpFilteredFileList.size() - 1);
691 str_copy(dst&: m_aSelectedFileDisplayName, src: m_vpFilteredFileList[m_SelectedFileIndex]->m_aDisplayName);
692 }
693 else
694 {
695 m_SelectedFileIndex = -1;
696 m_aSelectedFileDisplayName[0] = '\0';
697 }
698}
699
700void CFileBrowser::FilelistPopulate(int StorageType, bool KeepSelection)
701{
702 m_vCompleteFileList.clear();
703 if(m_ShowingRoot)
704 {
705 {
706 CFilelistItem Item;
707 str_copy(dst&: Item.m_aFilename, src: m_pCurrentPath);
708 str_copy(dst&: Item.m_aDisplayName, src: "All combined");
709 Item.m_IsDir = true;
710 Item.m_IsLink = true;
711 Item.m_StorageType = IStorage::TYPE_ALL;
712 Item.m_TimeModified = 0;
713 m_vCompleteFileList.push_back(x: Item);
714 }
715
716 for(int CheckStorageType = IStorage::TYPE_SAVE; CheckStorageType < Storage()->NumPaths(); ++CheckStorageType)
717 {
718 if(Storage()->FolderExists(pFilename: m_pCurrentPath, Type: CheckStorageType))
719 {
720 CFilelistItem Item;
721 str_copy(dst&: Item.m_aFilename, src: m_pCurrentPath);
722 Storage()->GetCompletePath(Type: CheckStorageType, pDir: m_pCurrentPath, pBuffer: Item.m_aDisplayName, BufferSize: sizeof(Item.m_aDisplayName));
723 str_append(dst: Item.m_aDisplayName, src: "/", dst_size: sizeof(Item.m_aDisplayName));
724 Item.m_IsDir = true;
725 Item.m_IsLink = true;
726 Item.m_StorageType = CheckStorageType;
727 Item.m_TimeModified = 0;
728 m_vCompleteFileList.push_back(x: Item);
729 }
730 }
731 }
732 else
733 {
734 // Add links for downloadedmaps and themes
735 if(!str_comp(a: m_pCurrentPath, b: "maps"))
736 {
737 if(!m_SaveAction && Storage()->FolderExists(pFilename: "downloadedmaps", Type: StorageType))
738 {
739 CFilelistItem Item;
740 str_copy(dst&: Item.m_aFilename, src: "downloadedmaps");
741 str_copy(dst&: Item.m_aDisplayName, src: "downloadedmaps/");
742 Item.m_IsDir = true;
743 Item.m_IsLink = true;
744 Item.m_StorageType = StorageType;
745 Item.m_TimeModified = 0;
746 m_vCompleteFileList.push_back(x: Item);
747 }
748
749 if(Storage()->FolderExists(pFilename: "themes", Type: StorageType))
750 {
751 CFilelistItem Item;
752 str_copy(dst&: Item.m_aFilename, src: "themes");
753 str_copy(dst&: Item.m_aDisplayName, src: "themes/");
754 Item.m_IsDir = true;
755 Item.m_IsLink = true;
756 Item.m_StorageType = StorageType;
757 Item.m_TimeModified = 0;
758 m_vCompleteFileList.push_back(x: Item);
759 }
760 }
761 Storage()->ListDirectoryInfo(Type: StorageType, pPath: m_pCurrentPath, pfnCallback: DirectoryListingCallback, pUser: this);
762 }
763 RefreshFilteredFileList();
764 if(!KeepSelection)
765 {
766 m_SelectedFileIndex = m_vpFilteredFileList.empty() ? -1 : 0;
767 str_copy(dst&: m_aSelectedFileDisplayName, src: m_SelectedFileIndex >= 0 ? m_vpFilteredFileList[m_SelectedFileIndex]->m_aDisplayName : "");
768 }
769 m_PreviewState = EPreviewState::UNLOADED;
770}
771
772int CFileBrowser::DirectoryListingCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser)
773{
774 CFileBrowser *pFileBrowser = static_cast<CFileBrowser *>(pUser);
775 if(IsDir)
776 {
777 if(str_comp(a: pInfo->m_pName, b: ".") == 0)
778 {
779 return 0;
780 }
781 if(str_comp(a: pInfo->m_pName, b: "..") == 0 &&
782 !pFileBrowser->m_MultipleStorages &&
783 str_comp(a: pFileBrowser->m_aInitialFolder, b: pFileBrowser->m_pCurrentPath) == 0)
784 {
785 return 0;
786 }
787 }
788 else
789 {
790 if(!str_endswith(str: pInfo->m_pName, suffix: FILETYPE_EXTENSIONS[(int)pFileBrowser->m_FileType]))
791 {
792 return 0;
793 }
794 }
795
796 CFilelistItem Item;
797 str_copy(dst&: Item.m_aFilename, src: pInfo->m_pName);
798 if(IsDir)
799 {
800 str_format(buffer: Item.m_aDisplayName, buffer_size: sizeof(Item.m_aDisplayName), format: "%s/", pInfo->m_pName);
801 }
802 else
803 {
804 fs_split_file_extension(filename: pInfo->m_pName, name: Item.m_aDisplayName, name_size: sizeof(Item.m_aDisplayName));
805 }
806 Item.m_IsDir = IsDir != 0;
807 Item.m_IsLink = false;
808 Item.m_StorageType = StorageType;
809 Item.m_TimeModified = pInfo->m_TimeModified;
810 pFileBrowser->m_vCompleteFileList.push_back(x: Item);
811 return 0;
812}
813
814std::optional<bool> CFileBrowser::CompareCommon(const CFilelistItem *pLhs, const CFilelistItem *pRhs)
815{
816 if(str_comp(a: pLhs->m_aFilename, b: "..") == 0)
817 {
818 return true;
819 }
820 if(str_comp(a: pRhs->m_aFilename, b: "..") == 0)
821 {
822 return false;
823 }
824 if(pLhs->m_IsLink != pRhs->m_IsLink)
825 {
826 return pLhs->m_IsLink;
827 }
828 if(pLhs->m_IsDir != pRhs->m_IsDir)
829 {
830 return pLhs->m_IsDir;
831 }
832 return std::nullopt;
833}
834
835bool CFileBrowser::CompareFilenameAscending(const CFilelistItem *pLhs, const CFilelistItem *pRhs)
836{
837 return CompareCommon(pLhs, pRhs).value_or(u: str_comp_filenames(a: pLhs->m_aDisplayName, b: pRhs->m_aDisplayName) < 0);
838}
839
840bool CFileBrowser::CompareFilenameDescending(const CFilelistItem *pLhs, const CFilelistItem *pRhs)
841{
842 return CompareCommon(pLhs, pRhs).value_or(u: str_comp_filenames(a: pLhs->m_aDisplayName, b: pRhs->m_aDisplayName) > 0);
843}
844
845bool CFileBrowser::CompareTimeModifiedAscending(const CFilelistItem *pLhs, const CFilelistItem *pRhs)
846{
847 return CompareCommon(pLhs, pRhs).value_or(u: pLhs->m_TimeModified < pRhs->m_TimeModified);
848}
849
850bool CFileBrowser::CompareTimeModifiedDescending(const CFilelistItem *pLhs, const CFilelistItem *pRhs)
851{
852 return CompareCommon(pLhs, pRhs).value_or(u: pLhs->m_TimeModified > pRhs->m_TimeModified);
853}
854
855CUi::EPopupMenuFunctionResult CFileBrowser::CPopupNewFolder::Render(void *pContext, CUIRect View, bool Active)
856{
857 CPopupNewFolder *pNewFolderContext = static_cast<CPopupNewFolder *>(pContext);
858 CFileBrowser *pFileBrowser = pNewFolderContext->m_pFileBrowser;
859 CEditor *pEditor = pFileBrowser->Editor();
860
861 CUIRect Label, ButtonBar, FolderName, ButtonCancel, ButtonCreate;
862
863 View.Margin(Cut: 10.0f, pOtherRect: &View);
864 View.HSplitBottom(Cut: 20.0f, pTop: &View, pBottom: &ButtonBar);
865 ButtonBar.VSplitLeft(Cut: 110.0f, pLeft: &ButtonCancel, pRight: &ButtonBar);
866 ButtonBar.VSplitRight(Cut: 110.0f, pLeft: &ButtonBar, pRight: &ButtonCreate);
867
868 View.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &View);
869 pFileBrowser->Ui()->DoLabel(pRect: &Label, pText: "Create new folder", Size: 20.0f, Align: TEXTALIGN_MC);
870 View.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &View);
871
872 View.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &View);
873 pFileBrowser->Ui()->DoLabel(pRect: &Label, pText: "Name:", Size: 10.0f, Align: TEXTALIGN_ML);
874 Label.VSplitLeft(Cut: 50.0f, pLeft: nullptr, pRight: &FolderName);
875 FolderName.HMargin(Cut: 2.0f, pOtherRect: &FolderName);
876 pEditor->DoEditBox(pLineInput: &pNewFolderContext->m_NewFolderNameInput, pRect: &FolderName, FontSize: 12.0f);
877
878 if(pEditor->DoButton_Editor(pId: &pNewFolderContext->m_ButtonCancelId, pText: "Cancel", Checked: 0, pRect: &ButtonCancel, Flags: BUTTONFLAG_LEFT, pToolTip: nullptr))
879 {
880 return CUi::POPUP_CLOSE_CURRENT;
881 }
882
883 if(pEditor->DoButton_Editor(pId: &pNewFolderContext->m_ButtonCreateId, pText: "Create", Checked: 0, pRect: &ButtonCreate, Flags: BUTTONFLAG_LEFT, pToolTip: nullptr) ||
884 (Active && pFileBrowser->Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER)))
885 {
886 char aFolderPath[IO_MAX_PATH_LENGTH];
887 str_format(buffer: aFolderPath, buffer_size: sizeof(aFolderPath), format: "%s/%s", pFileBrowser->m_pCurrentPath, pNewFolderContext->m_NewFolderNameInput.GetString());
888 if(!str_valid_filename(str: pNewFolderContext->m_NewFolderNameInput.GetString()))
889 {
890 pEditor->ShowFileDialogError(pFormat: "This name cannot be used for files and folders.");
891 }
892 else if(!pFileBrowser->Storage()->CreateFolder(pFoldername: aFolderPath, Type: pFileBrowser->m_StorageType))
893 {
894 pEditor->ShowFileDialogError(pFormat: "Failed to create the folder '%s'.", aFolderPath);
895 }
896 else
897 {
898 char aFolderDisplayName[IO_MAX_PATH_LENGTH];
899 str_format(buffer: aFolderDisplayName, buffer_size: sizeof(aFolderDisplayName), format: "%s/", pNewFolderContext->m_NewFolderNameInput.GetString());
900 pFileBrowser->FilelistPopulate(StorageType: pFileBrowser->m_StorageType, KeepSelection: false);
901 pFileBrowser->UpdateSelectedIndex(pDisplayName: aFolderDisplayName);
902 return CUi::POPUP_CLOSE_CURRENT;
903 }
904 }
905
906 return CUi::POPUP_KEEP_OPEN;
907}
908
909CUi::EPopupMenuFunctionResult CFileBrowser::CPopupConfirmDelete::Render(void *pContext, CUIRect View, bool Active)
910{
911 CPopupConfirmDelete *pConfirmDeleteContext = static_cast<CPopupConfirmDelete *>(pContext);
912 CFileBrowser *pFileBrowser = pConfirmDeleteContext->m_pFileBrowser;
913 CEditor *pEditor = pFileBrowser->Editor();
914
915 CUIRect Label, ButtonBar, ButtonCancel, ButtonDelete;
916 View.Margin(Cut: 10.0f, pOtherRect: &View);
917 View.HSplitBottom(Cut: 20.0f, pTop: &View, pBottom: &ButtonBar);
918 View.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &View);
919 ButtonBar.VSplitLeft(Cut: 110.0f, pLeft: &ButtonCancel, pRight: &ButtonBar);
920 ButtonBar.VSplitRight(Cut: 110.0f, pLeft: &ButtonBar, pRight: &ButtonDelete);
921
922 pFileBrowser->Ui()->DoLabel(pRect: &Label, pText: "Confirm delete", Size: 20.0f, Align: TEXTALIGN_MC);
923
924 char aMessage[IO_MAX_PATH_LENGTH + 128];
925 str_format(buffer: aMessage, buffer_size: sizeof(aMessage), format: "Are you sure that you want to delete the %s '%s'?",
926 pConfirmDeleteContext->m_IsDirectory ? "folder" : "file", pConfirmDeleteContext->m_aDeletePath);
927 pFileBrowser->Ui()->DoLabel(pRect: &View, pText: aMessage, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: {.m_MaxWidth = View.w});
928
929 if(pEditor->DoButton_Editor(pId: &pConfirmDeleteContext->m_ButtonCancelId, pText: "Cancel", Checked: 0, pRect: &ButtonCancel, Flags: BUTTONFLAG_LEFT, pToolTip: nullptr))
930 {
931 return CUi::POPUP_CLOSE_CURRENT;
932 }
933
934 if(pEditor->DoButton_Editor(pId: &pConfirmDeleteContext->m_ButtonDeleteId, pText: "Delete", Checked: EditorButtonChecked::DANGEROUS_ACTION, pRect: &ButtonDelete, Flags: BUTTONFLAG_LEFT, pToolTip: nullptr) ||
935 (Active && pFileBrowser->Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER)))
936 {
937 if(pConfirmDeleteContext->m_IsDirectory)
938 {
939 if(pFileBrowser->Storage()->RemoveFolder(pFilename: pConfirmDeleteContext->m_aDeletePath, Type: IStorage::TYPE_SAVE))
940 {
941 pFileBrowser->FilelistPopulate(StorageType: IStorage::TYPE_SAVE, KeepSelection: true);
942 }
943 else
944 {
945 pEditor->ShowFileDialogError(pFormat: "Failed to delete folder '%s'. Make sure it's empty first. Check the local console for details.", pConfirmDeleteContext->m_aDeletePath);
946 }
947 }
948 else
949 {
950 if(pFileBrowser->Storage()->RemoveFile(pFilename: pConfirmDeleteContext->m_aDeletePath, Type: IStorage::TYPE_SAVE))
951 {
952 pFileBrowser->FilelistPopulate(StorageType: IStorage::TYPE_SAVE, KeepSelection: true);
953 }
954 else
955 {
956 pEditor->ShowFileDialogError(pFormat: "Failed to delete file '%s'. Check the local console for details.", pConfirmDeleteContext->m_aDeletePath);
957 }
958 }
959 pFileBrowser->UpdateFilenameInput();
960 return CUi::POPUP_CLOSE_CURRENT;
961 }
962
963 return CUi::POPUP_KEEP_OPEN;
964}
965
966CUi::EPopupMenuFunctionResult CFileBrowser::CPopupConfirmOverwrite::Render(void *pContext, CUIRect View, bool Active)
967{
968 CPopupConfirmOverwrite *pConfirmOverwriteContext = static_cast<CPopupConfirmOverwrite *>(pContext);
969 CFileBrowser *pFileBrowser = pConfirmOverwriteContext->m_pFileBrowser;
970 CEditor *pEditor = pFileBrowser->Editor();
971
972 CUIRect Label, ButtonBar, ButtonCancel, ButtonOverride;
973 View.Margin(Cut: 10.0f, pOtherRect: &View);
974 View.HSplitBottom(Cut: 20.0f, pTop: &View, pBottom: &ButtonBar);
975 View.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &View);
976 ButtonBar.VSplitLeft(Cut: 110.0f, pLeft: &ButtonCancel, pRight: &ButtonBar);
977 ButtonBar.VSplitRight(Cut: 110.0f, pLeft: &ButtonBar, pRight: &ButtonOverride);
978
979 pFileBrowser->Ui()->DoLabel(pRect: &Label, pText: "Confirm overwrite", Size: 20.0f, Align: TEXTALIGN_MC);
980
981 char aMessage[IO_MAX_PATH_LENGTH + 128];
982 str_format(buffer: aMessage, buffer_size: sizeof(aMessage), format: "The file '%s' already exists.\n\nAre you sure that you want to overwrite it?",
983 pConfirmOverwriteContext->m_aOverwritePath);
984 pFileBrowser->Ui()->DoLabel(pRect: &View, pText: aMessage, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: {.m_MaxWidth = View.w});
985
986 if(pEditor->DoButton_Editor(pId: &pConfirmOverwriteContext->m_ButtonCancelId, pText: "Cancel", Checked: 0, pRect: &ButtonCancel, Flags: BUTTONFLAG_LEFT, pToolTip: nullptr))
987 {
988 return CUi::POPUP_CLOSE_CURRENT;
989 }
990
991 if(pEditor->DoButton_Editor(pId: &pConfirmOverwriteContext->m_ButtonOverwriteId, pText: "Overwrite", Checked: EditorButtonChecked::DANGEROUS_ACTION, pRect: &ButtonOverride, Flags: BUTTONFLAG_LEFT, pToolTip: nullptr) ||
992 (Active && pFileBrowser->Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER)))
993 {
994 pFileBrowser->m_pfnOpenCallback(pConfirmOverwriteContext->m_aOverwritePath, IStorage::TYPE_SAVE, pFileBrowser->m_pOpenCallbackUser);
995 return CUi::POPUP_CLOSE_CURRENT;
996 }
997
998 return CUi::POPUP_KEEP_OPEN;
999}
1000