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