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