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