| 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 | |
| 12 | using namespace FontIcons; |
| 13 | |
| 14 | static constexpr const char *FILETYPE_EXTENSIONS[] = { |
| 15 | ".map" , |
| 16 | ".png" , |
| 17 | ".opus" }; |
| 18 | |
| 19 | void 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 | |
| 79 | void 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 = 400.0f; |
| 384 | constexpr float = 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 = 400.0f; |
| 441 | constexpr float = 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 = 400.0f; |
| 456 | constexpr float = 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 | |
| 465 | bool CFileBrowser::IsValidSaveFilename() const |
| 466 | { |
| 467 | return m_pCurrentPath == m_aCurrentFolder || |
| 468 | (m_pCurrentPath == m_aCurrentLink && str_startswith(str: m_aCurrentLink, prefix: "themes" )); |
| 469 | } |
| 470 | |
| 471 | void CFileBrowser::OnEditorClose() |
| 472 | { |
| 473 | if(m_PreviewSound >= 0 && Sound()->IsPlaying(SampleId: m_PreviewSound)) |
| 474 | { |
| 475 | Sound()->Pause(SampleId: m_PreviewSound); |
| 476 | } |
| 477 | } |
| 478 | |
| 479 | void 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 | |
| 489 | bool CFileBrowser::CanPreviewFile() const |
| 490 | { |
| 491 | return m_FileType == CFileBrowser::EFileType::IMAGE || |
| 492 | m_FileType == CFileBrowser::EFileType::SOUND; |
| 493 | } |
| 494 | |
| 495 | void 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 | |
| 531 | void 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 | |
| 584 | const 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 | |
| 610 | void 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 | |
| 624 | void 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 | |
| 643 | void 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 | |
| 664 | void 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 | |
| 701 | void 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 | |
| 773 | int 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 | |
| 815 | std::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 | |
| 836 | bool 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 | |
| 841 | bool 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 | |
| 846 | bool 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 | |
| 851 | bool 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 | |
| 856 | CUi::EPopupMenuFunctionResult CFileBrowser::CPopupNewFolder::(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 | |
| 910 | CUi::EPopupMenuFunctionResult CFileBrowser::CPopupConfirmDelete::(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 | |
| 967 | CUi::EPopupMenuFunctionResult CFileBrowser::CPopupConfirmOverwrite::(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 | |