diff --git a/src/engine/shared/storage.cpp b/src/engine/shared/storage.cpp index 4b01a3d1d..db09e6eb2 100644 --- a/src/engine/shared/storage.cpp +++ b/src/engine/shared/storage.cpp @@ -1,11 +1,14 @@ /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */ -#include "linereader.h" #include #include + #include +#include #include +#include + #ifdef CONF_PLATFORM_HAIKU #include #endif @@ -309,14 +312,38 @@ public: m_aBinarydir[0] = '\0'; } + int NumPaths() const override + { + return m_NumPaths; + } + + struct SListDirectoryInfoUniqueCallbackData + { + FS_LISTDIR_CALLBACK_FILEINFO m_pfnDelegate; + void *m_pDelegateUser; + std::unordered_set m_Seen; + }; + + static int ListDirectoryInfoUniqueCallback(const CFsFileInfo *pInfo, int IsDir, int Type, void *pUser) + { + SListDirectoryInfoUniqueCallbackData *pData = static_cast(pUser); + auto [_, InsertionTookPlace] = pData->m_Seen.emplace(pInfo->m_pName); + if(InsertionTookPlace) + return pData->m_pfnDelegate(pInfo, IsDir, Type, pData->m_pDelegateUser); + return 0; + } + void ListDirectoryInfo(int Type, const char *pPath, FS_LISTDIR_CALLBACK_FILEINFO pfnCallback, void *pUser) override { char aBuffer[IO_MAX_PATH_LENGTH]; if(Type == TYPE_ALL) { + SListDirectoryInfoUniqueCallbackData Data; + Data.m_pfnDelegate = pfnCallback; + Data.m_pDelegateUser = pUser; // list all available directories for(int i = TYPE_SAVE; i < m_NumPaths; ++i) - fs_listdir_fileinfo(GetPath(i, pPath, aBuffer, sizeof(aBuffer)), pfnCallback, i, pUser); + fs_listdir_fileinfo(GetPath(i, pPath, aBuffer, sizeof(aBuffer)), ListDirectoryInfoUniqueCallback, i, &Data); } else if(Type >= TYPE_SAVE && Type < m_NumPaths) { @@ -329,14 +356,33 @@ public: } } + struct SListDirectoryUniqueCallbackData + { + FS_LISTDIR_CALLBACK m_pfnDelegate; + void *m_pDelegateUser; + std::unordered_set m_Seen; + }; + + static int ListDirectoryUniqueCallback(const char *pName, int IsDir, int Type, void *pUser) + { + SListDirectoryUniqueCallbackData *pData = static_cast(pUser); + auto [_, InsertionTookPlace] = pData->m_Seen.emplace(pName); + if(InsertionTookPlace) + return pData->m_pfnDelegate(pName, IsDir, Type, pData->m_pDelegateUser); + return 0; + } + void ListDirectory(int Type, const char *pPath, FS_LISTDIR_CALLBACK pfnCallback, void *pUser) override { char aBuffer[IO_MAX_PATH_LENGTH]; if(Type == TYPE_ALL) { + SListDirectoryUniqueCallbackData Data; + Data.m_pfnDelegate = pfnCallback; + Data.m_pDelegateUser = pUser; // list all available directories for(int i = TYPE_SAVE; i < m_NumPaths; ++i) - fs_listdir(GetPath(i, pPath, aBuffer, sizeof(aBuffer)), pfnCallback, i, pUser); + fs_listdir(GetPath(i, pPath, aBuffer, sizeof(aBuffer)), ListDirectoryUniqueCallback, i, &Data); } else if(Type >= TYPE_SAVE && Type < m_NumPaths) { diff --git a/src/engine/storage.h b/src/engine/storage.h index 19d4a18e0..ddabded15 100644 --- a/src/engine/storage.h +++ b/src/engine/storage.h @@ -42,6 +42,8 @@ public: STORAGETYPE_CLIENT, }; + virtual int NumPaths() const = 0; + virtual void ListDirectory(int Type, const char *pPath, FS_LISTDIR_CALLBACK pfnCallback, void *pUser) = 0; virtual void ListDirectoryInfo(int Type, const char *pPath, FS_LISTDIR_CALLBACK_FILEINFO pfnCallback, void *pUser) = 0; virtual IOHANDLE OpenFile(const char *pFilename, int Flags, int Type, char *pBuffer = nullptr, int BufferSize = 0) = 0; diff --git a/src/game/client/components/menus.cpp b/src/game/client/components/menus.cpp index c3fae894a..27852d98d 100644 --- a/src/game/client/components/menus.cpp +++ b/src/game/client/components/menus.cpp @@ -70,6 +70,7 @@ CMenus::CMenus() m_ShowStart = true; str_copy(m_aCurrentDemoFolder, "demos"); + m_DemolistStorageType = IStorage::TYPE_ALL; m_DemoPlayerState = DEMOPLAYER_NONE; m_Dummy = false; @@ -1530,9 +1531,9 @@ int CMenus::Render() } else if(Storage()->RenameFile(aBufOld, aBufNew, m_vDemos[m_DemolistSelectedIndex].m_StorageType)) { - str_copy(g_Config.m_UiDemoSelected, m_DemoRenameInput.GetString()); - if(str_endswith(g_Config.m_UiDemoSelected, ".demo")) - g_Config.m_UiDemoSelected[str_length(g_Config.m_UiDemoSelected) - str_length(".demo")] = '\0'; + str_copy(m_aCurrentDemoSelectionName, m_DemoRenameInput.GetString()); + if(str_endswith(m_aCurrentDemoSelectionName, ".demo")) + m_aCurrentDemoSelectionName[str_length(m_aCurrentDemoSelectionName) - str_length(".demo")] = '\0'; DemolistPopulate(); DemolistOnUpdate(false); } diff --git a/src/game/client/components/menus.h b/src/game/client/components/menus.h index b63cedb21..3a8764006 100644 --- a/src/game/client/components/menus.h +++ b/src/game/client/components/menus.h @@ -259,6 +259,7 @@ protected: char m_aFilename[IO_MAX_PATH_LENGTH]; char m_aName[IO_MAX_PATH_LENGTH]; bool m_IsDir; + bool m_IsLink; int m_StorageType; time_t m_Date; @@ -318,6 +319,7 @@ protected: }; char m_aCurrentDemoFolder[IO_MAX_PATH_LENGTH]; + char m_aCurrentDemoSelectionName[IO_MAX_PATH_LENGTH]; CLineInputBuffered m_DemoRenameInput; CLineInputBuffered m_DemoSliceInput; CLineInputBuffered m_DemoRenderInput; @@ -325,6 +327,7 @@ protected: bool m_DemolistSelectedIsDir; bool m_DemolistSelectedReveal = false; int m_DemolistStorageType; + bool m_DemolistMultipleStorages = false; int m_Speed = 4; std::chrono::nanoseconds m_DemoPopulateStartTime{0}; diff --git a/src/game/client/components/menus_demo.cpp b/src/game/client/components/menus_demo.cpp index 73b078692..bbe0a36ba 100644 --- a/src/game/client/components/menus_demo.cpp +++ b/src/game/client/components/menus_demo.cpp @@ -724,9 +724,9 @@ void CMenus::RenderDemoPlayerSliceSavePopup(CUIRect MainView) { char aPath[IO_MAX_PATH_LENGTH]; str_format(aPath, sizeof(aPath), "%s/%s", m_aCurrentDemoFolder, m_DemoSliceInput.GetString()); - str_copy(g_Config.m_UiDemoSelected, m_DemoSliceInput.GetString()); - if(str_endswith(g_Config.m_UiDemoSelected, ".demo")) - g_Config.m_UiDemoSelected[str_length(g_Config.m_UiDemoSelected) - str_length(".demo")] = '\0'; + str_copy(m_aCurrentDemoSelectionName, m_DemoSliceInput.GetString()); + if(str_endswith(m_aCurrentDemoSelectionName, ".demo")) + m_aCurrentDemoSelectionName[str_length(m_aCurrentDemoSelectionName) - str_length(".demo")] = '\0'; m_DemoPlayerState = DEMOPLAYER_NONE; Client()->DemoSlice(aPath, CMenus::DemoFilterChat, &s_RemoveChat); DemolistPopulate(); @@ -741,7 +741,9 @@ void CMenus::RenderDemoPlayerSliceSavePopup(CUIRect MainView) int CMenus::DemolistFetchCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser) { CMenus *pSelf = (CMenus *)pUser; - if(str_comp(pInfo->m_pName, ".") == 0 || (str_comp(pInfo->m_pName, "..") == 0 && str_comp(pSelf->m_aCurrentDemoFolder, "demos") == 0) || (!IsDir && !str_endswith(pInfo->m_pName, ".demo"))) + if(str_comp(pInfo->m_pName, ".") == 0 || + (str_comp(pInfo->m_pName, "..") == 0 && (pSelf->m_aCurrentDemoFolder[0] == '\0' || (!pSelf->m_DemolistMultipleStorages && str_comp(pSelf->m_aCurrentDemoFolder, "demos") == 0))) || + (!IsDir && !str_endswith(pInfo->m_pName, ".demo"))) { return 0; } @@ -762,6 +764,7 @@ int CMenus::DemolistFetchCallback(const CFsFileInfo *pInfo, int IsDir, int Stora Item.m_Date = pInfo->m_TimeModified; } Item.m_IsDir = IsDir != 0; + Item.m_IsLink = false; Item.m_StorageType = StorageType; pSelf->m_vDemos.push_back(Item); @@ -776,21 +779,66 @@ int CMenus::DemolistFetchCallback(const CFsFileInfo *pInfo, int IsDir, int Stora void CMenus::DemolistPopulate() { m_vDemos.clear(); - if(!str_comp(m_aCurrentDemoFolder, "demos")) - m_DemolistStorageType = IStorage::TYPE_ALL; - m_DemoPopulateStartTime = time_get_nanoseconds(); - Storage()->ListDirectoryInfo(m_DemolistStorageType, m_aCurrentDemoFolder, DemolistFetchCallback, this); - if(g_Config.m_BrDemoFetchInfo) - FetchAllHeaders(); + int NumStoragesWithDemos = 0; + for(int StorageType = IStorage::TYPE_SAVE; StorageType < Storage()->NumPaths(); ++StorageType) + { + if(Storage()->FolderExists("demos", StorageType)) + { + NumStoragesWithDemos++; + } + } + m_DemolistMultipleStorages = NumStoragesWithDemos > 1; - std::stable_sort(m_vDemos.begin(), m_vDemos.end()); + if(m_aCurrentDemoFolder[0] == '\0') + { + { + CDemoItem Item; + str_copy(Item.m_aFilename, "demos"); + str_copy(Item.m_aName, Localize("All combined")); + Item.m_InfosLoaded = false; + Item.m_Valid = false; + Item.m_Date = 0; + Item.m_IsDir = true; + Item.m_IsLink = true; + Item.m_StorageType = IStorage::TYPE_ALL; + m_vDemos.push_back(Item); + } + + for(int StorageType = IStorage::TYPE_SAVE; StorageType < Storage()->NumPaths(); ++StorageType) + { + if(Storage()->FolderExists("demos", StorageType)) + { + CDemoItem Item; + str_copy(Item.m_aFilename, "demos"); + Storage()->GetCompletePath(StorageType, "demos", Item.m_aName, sizeof(Item.m_aName)); + str_append(Item.m_aName, "/", sizeof(Item.m_aName)); + Item.m_InfosLoaded = false; + Item.m_Valid = false; + Item.m_Date = 0; + Item.m_IsDir = true; + Item.m_IsLink = true; + Item.m_StorageType = StorageType; + m_vDemos.push_back(Item); + } + } + } + else + { + m_DemoPopulateStartTime = time_get_nanoseconds(); + Storage()->ListDirectoryInfo(m_DemolistStorageType, m_aCurrentDemoFolder, DemolistFetchCallback, this); + + if(g_Config.m_BrDemoFetchInfo) + FetchAllHeaders(); + + std::stable_sort(m_vDemos.begin(), m_vDemos.end()); + } } void CMenus::DemolistOnUpdate(bool Reset) { if(Reset) - g_Config.m_UiDemoSelected[0] = '\0'; + m_aCurrentDemoSelectionName[0] = '\0'; else { bool Found = false; @@ -800,7 +848,7 @@ void CMenus::DemolistOnUpdate(bool Reset) { SelectedIndex++; - if(str_comp(g_Config.m_UiDemoSelected, Item.m_aName) == 0) + if(str_comp(m_aCurrentDemoSelectionName, Item.m_aName) == 0) { Found = true; break; @@ -813,7 +861,6 @@ void CMenus::DemolistOnUpdate(bool Reset) m_DemolistSelectedIndex = Reset ? !m_vDemos.empty() ? 0 : -1 : m_DemolistSelectedIndex >= (int)m_vDemos.size() ? m_vDemos.size() - 1 : m_DemolistSelectedIndex; - m_DemolistSelectedIsDir = m_DemolistSelectedIndex < 0 ? false : m_vDemos[m_DemolistSelectedIndex].m_IsDir; m_DemolistSelectedReveal = true; } @@ -854,7 +901,9 @@ void CMenus::RenderDemoList(CUIRect MainView) CDemoItem &Item = m_vDemos[m_DemolistSelectedIndex]; if(str_comp(Item.m_aFilename, "..") == 0) str_copy(aFooterLabel, Localize("Parent Folder")); - else if(m_DemolistSelectedIsDir) + else if(m_vDemos[m_DemolistSelectedIndex].m_IsLink) + str_copy(aFooterLabel, Localize("Folder Link")); + else if(m_vDemos[m_DemolistSelectedIndex].m_IsDir) str_copy(aFooterLabel, Localize("Folder")); else if(!FetchHeader(Item)) str_copy(aFooterLabel, Localize("Invalid Demo")); @@ -1090,7 +1139,7 @@ void CMenus::RenderDemoList(CUIRect MainView) FileIcon.x += 2.0f; const char *pIconType; - if(str_comp(Item.m_aFilename, "..") == 0) + if(Item.m_IsLink || str_comp(Item.m_aFilename, "..") == 0) pIconType = FONT_ICON_FOLDER_TREE; else if(Item.m_IsDir) pIconType = FONT_ICON_FOLDER; @@ -1103,7 +1152,9 @@ void CMenus::RenderDemoList(CUIRect MainView) TextRender()->SetCurFont(TextRender()->GetFont(TEXT_FONT_ICON_FONT)); TextRender()->TextColor(IconColor); + TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING); UI()->DoLabel(&FileIcon, pIconType, 12.0f, TEXTALIGN_ML); + TextRender()->SetRenderFlags(0); TextRender()->TextColor(TextRender()->DefaultTextColor()); TextRender()->SetCurFont(nullptr); @@ -1155,7 +1206,7 @@ void CMenus::RenderDemoList(CUIRect MainView) { m_DemolistSelectedIndex = NewSelected; if(m_DemolistSelectedIndex >= 0) - str_copy(g_Config.m_UiDemoSelected, m_vDemos[m_DemolistSelectedIndex].m_aName); + str_copy(m_aCurrentDemoSelectionName, m_vDemos[m_DemolistSelectedIndex].m_aName); DemolistOnUpdate(false); } @@ -1174,22 +1225,41 @@ void CMenus::RenderDemoList(CUIRect MainView) } static CButtonContainer s_PlayButton; - if(DoButton_Menu(&s_PlayButton, m_DemolistSelectedIsDir ? Localize("Open") : Localize("Play", "Demo browser"), 0, &PlayRect) || s_ListBox.WasItemActivated() || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER) || (Input()->KeyPress(KEY_P) && m_pClient->m_GameConsole.IsClosed())) + if(DoButton_Menu(&s_PlayButton, (m_DemolistSelectedIndex >= 0 && m_vDemos[m_DemolistSelectedIndex].m_IsDir) ? Localize("Open") : Localize("Play", "Demo browser"), 0, &PlayRect) || s_ListBox.WasItemActivated() || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER) || (Input()->KeyPress(KEY_P) && m_pClient->m_GameConsole.IsClosed())) { if(m_DemolistSelectedIndex >= 0) { - if(m_DemolistSelectedIsDir) // folder + if(m_vDemos[m_DemolistSelectedIndex].m_IsDir) // folder { - if(str_comp(m_vDemos[m_DemolistSelectedIndex].m_aFilename, "..") == 0) // parent folder - fs_parent_dir(m_aCurrentDemoFolder); + const bool ParentFolder = str_comp(m_vDemos[m_DemolistSelectedIndex].m_aFilename, "..") == 0; + if(ParentFolder) // parent folder + { + str_copy(m_aCurrentDemoSelectionName, fs_filename(m_aCurrentDemoFolder)); + str_append(m_aCurrentDemoSelectionName, "/"); + if(fs_parent_dir(m_aCurrentDemoFolder)) + { + m_aCurrentDemoFolder[0] = '\0'; + if(m_DemolistStorageType == IStorage::TYPE_ALL) + { + m_aCurrentDemoSelectionName[0] = '\0'; // will select first list item + } + else + { + Storage()->GetCompletePath(m_DemolistStorageType, "demos", m_aCurrentDemoSelectionName, sizeof(m_aCurrentDemoSelectionName)); + str_append(m_aCurrentDemoSelectionName, "/"); + } + } + } else // sub folder { - str_append(m_aCurrentDemoFolder, "/"); + if(m_aCurrentDemoFolder[0] != '\0') + str_append(m_aCurrentDemoFolder, "/"); + else + m_DemolistStorageType = m_vDemos[m_DemolistSelectedIndex].m_StorageType; str_append(m_aCurrentDemoFolder, m_vDemos[m_DemolistSelectedIndex].m_aFilename); - m_DemolistStorageType = m_vDemos[m_DemolistSelectedIndex].m_StorageType; } DemolistPopulate(); - DemolistOnUpdate(true); + DemolistOnUpdate(!ParentFolder); } else // file { @@ -1211,8 +1281,7 @@ void CMenus::RenderDemoList(CUIRect MainView) if(DoButton_Menu(&s_DirectoryButtonID, Localize("Demos directory"), 0, &DirectoryButton)) { char aBuf[IO_MAX_PATH_LENGTH]; - Storage()->GetCompletePath(IStorage::TYPE_SAVE, "demos", aBuf, sizeof(aBuf)); - Storage()->CreateFolder("demos", IStorage::TYPE_SAVE); + Storage()->GetCompletePath(m_DemolistSelectedIndex >= 0 ? m_vDemos[m_DemolistSelectedIndex].m_StorageType : IStorage::TYPE_SAVE, m_aCurrentDemoFolder[0] == '\0' ? "demos" : m_aCurrentDemoFolder, aBuf, sizeof(aBuf)); if(!open_file(aBuf)) { dbg_msg("menus", "couldn't open file '%s'", aBuf); @@ -1220,43 +1289,34 @@ void CMenus::RenderDemoList(CUIRect MainView) } GameClient()->m_Tooltips.DoToolTip(&s_DirectoryButtonID, &DirectoryButton, Localize("Open the directory that contains the demo files")); - if(!m_DemolistSelectedIsDir) + if(m_DemolistSelectedIndex >= 0 && !m_vDemos[m_DemolistSelectedIndex].m_IsDir) { static CButtonContainer s_DeleteButton; if(DoButton_Menu(&s_DeleteButton, Localize("Delete"), 0, &DeleteRect) || UI()->ConsumeHotkey(CUI::HOTKEY_DELETE) || (Input()->KeyPress(KEY_D) && m_pClient->m_GameConsole.IsClosed())) { - if(m_DemolistSelectedIndex >= 0) - { - char aBuf[128 + IO_MAX_PATH_LENGTH]; - str_format(aBuf, sizeof(aBuf), Localize("Are you sure that you want to delete the demo '%s'?"), m_vDemos[m_DemolistSelectedIndex].m_aFilename); - PopupConfirm(Localize("Delete demo"), aBuf, Localize("Yes"), Localize("No"), &CMenus::PopupConfirmDeleteDemo); - return; - } + char aBuf[128 + IO_MAX_PATH_LENGTH]; + str_format(aBuf, sizeof(aBuf), Localize("Are you sure that you want to delete the demo '%s'?"), m_vDemos[m_DemolistSelectedIndex].m_aFilename); + PopupConfirm(Localize("Delete demo"), aBuf, Localize("Yes"), Localize("No"), &CMenus::PopupConfirmDeleteDemo); + return; } static CButtonContainer s_RenameButton; if(DoButton_Menu(&s_RenameButton, Localize("Rename"), 0, &RenameRect)) { - if(m_DemolistSelectedIndex >= 0) - { - m_Popup = POPUP_RENAME_DEMO; - m_DemoRenameInput.Set(m_vDemos[m_DemolistSelectedIndex].m_aFilename); - UI()->SetActiveItem(&m_DemoRenameInput); - return; - } + m_Popup = POPUP_RENAME_DEMO; + m_DemoRenameInput.Set(m_vDemos[m_DemolistSelectedIndex].m_aFilename); + UI()->SetActiveItem(&m_DemoRenameInput); + return; } #if defined(CONF_VIDEORECORDER) static CButtonContainer s_RenderButton; if(DoButton_Menu(&s_RenderButton, Localize("Render"), 0, &RenderRect) || (Input()->KeyPress(KEY_R) && m_pClient->m_GameConsole.IsClosed())) { - if(m_DemolistSelectedIndex >= 0) - { - m_Popup = POPUP_RENDER_DEMO; - m_DemoRenderInput.Set(m_vDemos[m_DemolistSelectedIndex].m_aFilename); - UI()->SetActiveItem(&m_DemoRenderInput); - return; - } + m_Popup = POPUP_RENDER_DEMO; + m_DemoRenderInput.Set(m_vDemos[m_DemolistSelectedIndex].m_aFilename); + UI()->SetActiveItem(&m_DemoRenderInput); + return; } #endif } diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index 24d291111..157cb18a6 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -823,7 +823,7 @@ bool CEditor::CallbackOpenMap(const char *pFileName, int StorageType, void *pUse CEditor *pEditor = (CEditor *)pUser; if(pEditor->Load(pFileName, StorageType)) { - pEditor->m_ValidSaveFilename = StorageType == IStorage::TYPE_SAVE && pEditor->m_pFileDialogPath == pEditor->m_aFileDialogCurrentFolder; + pEditor->m_ValidSaveFilename = StorageType == IStorage::TYPE_SAVE && (pEditor->m_pFileDialogPath == pEditor->m_aFileDialogCurrentFolder || (pEditor->m_pFileDialogPath == pEditor->m_aFileDialogCurrentLink && str_comp(pEditor->m_aFileDialogCurrentLink, "themes") == 0)); pEditor->m_Dialog = DIALOG_NONE; return true; } @@ -852,6 +852,8 @@ bool CEditor::CallbackAppendMap(const char *pFileName, int StorageType, void *pU bool CEditor::CallbackSaveMap(const char *pFileName, int StorageType, void *pUser) { + dbg_assert(StorageType == IStorage::TYPE_SAVE, "Saving only allowed for IStorage::TYPE_SAVE"); + CEditor *pEditor = static_cast(pUser); char aBuf[IO_MAX_PATH_LENGTH]; // add map extension @@ -865,7 +867,7 @@ bool CEditor::CallbackSaveMap(const char *pFileName, int StorageType, void *pUse if(pEditor->Save(pFileName)) { str_copy(pEditor->m_aFileName, pFileName); - pEditor->m_ValidSaveFilename = StorageType == IStorage::TYPE_SAVE && pEditor->m_pFileDialogPath == pEditor->m_aFileDialogCurrentFolder; + pEditor->m_ValidSaveFilename = true; pEditor->m_Map.m_Modified = false; } else @@ -888,6 +890,8 @@ bool CEditor::CallbackSaveMap(const char *pFileName, int StorageType, void *pUse bool CEditor::CallbackSaveCopyMap(const char *pFileName, int StorageType, void *pUser) { + dbg_assert(StorageType == IStorage::TYPE_SAVE, "Saving only allowed for IStorage::TYPE_SAVE"); + CEditor *pEditor = static_cast(pUser); char aBuf[IO_MAX_PATH_LENGTH]; // add map extension @@ -4542,7 +4546,7 @@ static int EditorListdirCallback(const CFsFileInfo *pInfo, int IsDir, int Storag { CEditor *pEditor = (CEditor *)pUser; if((pInfo->m_pName[0] == '.' && (pInfo->m_pName[1] == 0 || - (pInfo->m_pName[1] == '.' && pInfo->m_pName[2] == 0 && (!str_comp(pEditor->m_pFileDialogPath, "maps") || !str_comp(pEditor->m_pFileDialogPath, "mapres"))))) || + (pInfo->m_pName[1] == '.' && pInfo->m_pName[2] == 0 && (pEditor->m_FileDialogShowingRoot || (!pEditor->m_FileDialogMultipleStorages && (!str_comp(pEditor->m_pFileDialogPath, "maps") || !str_comp(pEditor->m_pFileDialogPath, "mapres"))))))) || (!IsDir && ((pEditor->m_FileDialogFileType == CEditor::FILETYPE_MAP && !str_endswith(pInfo->m_pName, ".map")) || (pEditor->m_FileDialogFileType == CEditor::FILETYPE_IMG && !str_endswith(pInfo->m_pName, ".png")) || (pEditor->m_FileDialogFileType == CEditor::FILETYPE_SOUND && !str_endswith(pInfo->m_pName, ".opus"))))) @@ -4614,54 +4618,57 @@ void CEditor::RenderFileDialog() if(m_FileDialogFileType == CEditor::FILETYPE_IMG || m_FileDialogFileType == CEditor::FILETYPE_SOUND) View.VSplitMid(&View, &Preview); - // title - CUIRect ButtonTimeModified, ButtonFileName; - Title.VSplitRight(10.0f, &Title, nullptr); - Title.VSplitRight(90.0f, &Title, &ButtonTimeModified); - Title.VSplitRight(10.0f, &Title, nullptr); - Title.VSplitRight(90.0f, &Title, &ButtonFileName); - Title.VSplitRight(10.0f, &Title, nullptr); - - const char *aSortIndicator[3] = {"▼", "", "▲"}; - - static int s_ButtonTimeModified = 0; - char aBufLabelButtonTimeModified[64]; - str_format(aBufLabelButtonTimeModified, sizeof(aBufLabelButtonTimeModified), "Time modified %s", aSortIndicator[m_SortByTimeModified + 1]); - if(DoButton_Editor(&s_ButtonTimeModified, aBufLabelButtonTimeModified, 0, &ButtonTimeModified, 0, "Sort by time modified")) + // title bar + if(!m_FileDialogShowingRoot) { - if(m_SortByTimeModified == 1) + CUIRect ButtonTimeModified, ButtonFileName; + Title.VSplitRight(10.0f, &Title, nullptr); + Title.VSplitRight(90.0f, &Title, &ButtonTimeModified); + Title.VSplitRight(10.0f, &Title, nullptr); + Title.VSplitRight(90.0f, &Title, &ButtonFileName); + Title.VSplitRight(10.0f, &Title, nullptr); + + const char *aSortIndicator[3] = {"▼", "", "▲"}; + + static int s_ButtonTimeModified = 0; + char aBufLabelButtonTimeModified[64]; + str_format(aBufLabelButtonTimeModified, sizeof(aBufLabelButtonTimeModified), "Time modified %s", aSortIndicator[m_SortByTimeModified + 1]); + if(DoButton_Editor(&s_ButtonTimeModified, aBufLabelButtonTimeModified, 0, &ButtonTimeModified, 0, "Sort by time modified")) { - m_SortByTimeModified = -1; - } - else if(m_SortByTimeModified == -1) - { - m_SortByTimeModified = 0; - } - else - { - m_SortByTimeModified = 1; + if(m_SortByTimeModified == 1) + { + m_SortByTimeModified = -1; + } + else if(m_SortByTimeModified == -1) + { + m_SortByTimeModified = 0; + } + else + { + m_SortByTimeModified = 1; + } + + RefreshFilteredFileList(); } - RefreshFilteredFileList(); - } - - static int s_ButtonFileName = 0; - char aBufLabelButtonFilename[64]; - str_format(aBufLabelButtonFilename, sizeof(aBufLabelButtonFilename), "Filename %s", aSortIndicator[m_SortByFilename + 1]); - if(DoButton_Editor(&s_ButtonFileName, aBufLabelButtonFilename, 0, &ButtonFileName, 0, "Sort by file name")) - { - if(m_SortByFilename == 1) + static int s_ButtonFileName = 0; + char aBufLabelButtonFilename[64]; + str_format(aBufLabelButtonFilename, sizeof(aBufLabelButtonFilename), "Filename %s", aSortIndicator[m_SortByFilename + 1]); + if(DoButton_Editor(&s_ButtonFileName, aBufLabelButtonFilename, 0, &ButtonFileName, 0, "Sort by file name")) { - m_SortByFilename = -1; - m_SortByTimeModified = 0; - } - else - { - m_SortByFilename = 1; - m_SortByTimeModified = 0; - } + if(m_SortByFilename == 1) + { + m_SortByFilename = -1; + m_SortByTimeModified = 0; + } + else + { + m_SortByFilename = 1; + m_SortByTimeModified = 0; + } - RefreshFilteredFileList(); + RefreshFilteredFileList(); + } } Title.Draw(ColorRGBA(1, 1, 1, 0.25f), IGraphics::CORNER_ALL, 4.0f); @@ -4669,13 +4676,13 @@ void CEditor::RenderFileDialog() UI()->DoLabel(&Title, m_pFileDialogTitle, 12.0f, TEXTALIGN_ML); // pathbox - char aPath[IO_MAX_PATH_LENGTH], aBuf[128 + IO_MAX_PATH_LENGTH]; - if(m_FilesSelectedIndex != -1) + if(m_FilesSelectedIndex >= 0 && m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType >= IStorage::TYPE_SAVE) + { + char aPath[IO_MAX_PATH_LENGTH], aBuf[128 + IO_MAX_PATH_LENGTH]; Storage()->GetCompletePath(m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType, m_pFileDialogPath, aPath, sizeof(aPath)); - else - aPath[0] = 0; - str_format(aBuf, sizeof(aBuf), "Current path: %s", aPath); - UI()->DoLabel(&PathBox, aBuf, 10.0f, TEXTALIGN_ML); + str_format(aBuf, sizeof(aBuf), "Current path: %s", aPath); + UI()->DoLabel(&PathBox, aBuf, 10.0f, TEXTALIGN_ML); + } const auto &&UpdateFileNameInput = [this]() { if(m_FilesSelectedIndex >= 0 && !m_vpFilteredFileList[m_FilesSelectedIndex]->m_IsDir) @@ -4898,14 +4905,16 @@ void CEditor::RenderFileDialog() } else { - if(str_comp(m_vpFilteredFileList[i]->m_aFilename, "..") == 0) + if(m_vpFilteredFileList[i]->m_IsLink || str_comp(m_vpFilteredFileList[i]->m_aFilename, "..") == 0) pIconType = FONT_ICON_FOLDER_TREE; else pIconType = FONT_ICON_FOLDER; } TextRender()->SetCurFont(TextRender()->GetFont(TEXT_FONT_ICON_FONT)); + TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING); UI()->DoLabel(&FileIcon, pIconType, 12.0f, TEXTALIGN_ML); + TextRender()->SetRenderFlags(0); TextRender()->SetCurFont(nullptr); SLabelProperties Props; @@ -4945,16 +4954,39 @@ void CEditor::RenderFileDialog() CUIRect Button; ButtonBar.VSplitRight(50.0f, &ButtonBar, &Button); - bool IsDir = m_FilesSelectedIndex >= 0 && m_vpFilteredFileList[m_FilesSelectedIndex]->m_IsDir; + const bool IsDir = m_FilesSelectedIndex >= 0 && m_vpFilteredFileList[m_FilesSelectedIndex]->m_IsDir; if(DoButton_Editor(&s_OkButton, IsDir ? "Open" : m_pFileDialogButtonText, 0, &Button, 0, nullptr) || s_ListBox.WasItemActivated()) { if(IsDir) // folder { m_FileDialogFilterInput.Clear(); - if(str_comp(m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename, "..") == 0) // parent folder + const bool ParentFolder = str_comp(m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename, "..") == 0; + if(ParentFolder) // parent folder { + str_copy(m_aFilesSelectedName, fs_filename(m_pFileDialogPath)); + str_append(m_aFilesSelectedName, "/"); if(fs_parent_dir(m_pFileDialogPath)) - m_pFileDialogPath = m_aFileDialogCurrentFolder; // leave the link + { + if(str_comp(m_pFileDialogPath, m_aFileDialogCurrentFolder) == 0) + { + m_FileDialogShowingRoot = true; + if(m_FileDialogStorageType == IStorage::TYPE_ALL) + { + m_aFilesSelectedName[0] = '\0'; // will select first list item + } + else + { + Storage()->GetCompletePath(m_FileDialogStorageType, m_pFileDialogPath, m_aFilesSelectedName, sizeof(m_aFilesSelectedName)); + str_append(m_aFilesSelectedName, "/"); + } + } + else + { + m_pFileDialogPath = m_aFileDialogCurrentFolder; // leave the link + str_copy(m_aFilesSelectedName, m_aFileDialogCurrentLink); + str_append(m_aFilesSelectedName, "/"); + } + } } else // sub folder { @@ -4969,28 +5001,26 @@ void CEditor::RenderFileDialog() str_copy(aTemp, m_pFileDialogPath); str_format(m_pFileDialogPath, IO_MAX_PATH_LENGTH, "%s/%s", aTemp, m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename); } + if(m_FileDialogShowingRoot) + m_FileDialogStorageType = m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType; + m_FileDialogShowingRoot = false; } - FilelistPopulate(!str_comp(m_pFileDialogPath, "maps") || !str_comp(m_pFileDialogPath, "mapres") ? m_FileDialogStorageType : - m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType); + FilelistPopulate(m_FileDialogStorageType, ParentFolder); UpdateFileNameInput(); } else // file { + const int StorageType = m_FilesSelectedIndex >= 0 ? m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType : m_FileDialogStorageType; str_format(m_aFileSaveName, sizeof(m_aFileSaveName), "%s/%s", m_pFileDialogPath, m_FileDialogFileNameInput.GetString()); if(!str_endswith(m_aFileSaveName, FILETYPE_EXTENSIONS[m_FileDialogFileType])) str_append(m_aFileSaveName, FILETYPE_EXTENSIONS[m_FileDialogFileType]); - if(!str_comp(m_pFileDialogButtonText, "Save")) + if(!str_comp(m_pFileDialogButtonText, "Save") && Storage()->FileExists(m_aFileSaveName, StorageType)) { - if(Storage()->FileExists(m_aFileSaveName, IStorage::TYPE_SAVE)) - { - m_PopupEventType = m_pfnFileDialogFunc == &CallbackSaveCopyMap ? POPEVENT_SAVE_COPY : POPEVENT_SAVE; - m_PopupEventActivated = true; - } - else if(m_pfnFileDialogFunc) - m_pfnFileDialogFunc(m_aFileSaveName, m_FilesSelectedIndex >= 0 ? m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType : m_FileDialogStorageType, m_pFileDialogUser); + m_PopupEventType = m_pfnFileDialogFunc == &CallbackSaveCopyMap ? POPEVENT_SAVE_COPY : POPEVENT_SAVE; + m_PopupEventActivated = true; } else if(m_pfnFileDialogFunc) - m_pfnFileDialogFunc(m_aFileSaveName, m_FilesSelectedIndex >= 0 ? m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType : m_FileDialogStorageType, m_pFileDialogUser); + m_pfnFileDialogFunc(m_aFileSaveName, StorageType, m_pFileDialogUser); } } @@ -5008,9 +5038,11 @@ void CEditor::RenderFileDialog() ButtonBar.VSplitRight(90.0f, &ButtonBar, &Button); if(DoButton_Editor(&s_ShowDirectoryButton, "Show directory", 0, &Button, 0, "Open the current directory in the file browser")) { - if(!open_file(aPath)) + char aOpenPath[IO_MAX_PATH_LENGTH]; + Storage()->GetCompletePath(m_FilesSelectedIndex >= 0 ? m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType : IStorage::TYPE_SAVE, m_pFileDialogPath, aOpenPath, sizeof(aOpenPath)); + if(!open_file(aOpenPath)) { - ShowFileDialogError("Failed to open the directory '%s'.", aPath); + ShowFileDialogError("Failed to open the directory '%s'.", aOpenPath); } } @@ -5051,7 +5083,7 @@ void CEditor::RenderFileDialog() else s_ConfirmDeletePopupContext.Reset(); - if(m_FileDialogStorageType == IStorage::TYPE_SAVE) + if(!m_FileDialogShowingRoot && m_FileDialogStorageType == IStorage::TYPE_SAVE) { ButtonBar.VSplitLeft(70.0f, &Button, &ButtonBar); if(DoButton_Editor(&s_NewFolderButton, "New folder", 0, &Button, 0, nullptr)) @@ -5076,7 +5108,8 @@ void CEditor::RefreshFilteredFileList() m_vpFilteredFileList.push_back(&Item); } } - SortFilteredFileList(); + if(!m_FileDialogShowingRoot) + SortFilteredFileList(); if(!m_vpFilteredFileList.empty()) { if(m_aFilesSelectedName[0]) @@ -5104,18 +5137,66 @@ void CEditor::FilelistPopulate(int StorageType, bool KeepSelection) { m_FileDialogLastPopulatedStorageType = StorageType; m_vCompleteFileList.clear(); - if(m_FileDialogStorageType != IStorage::TYPE_SAVE && !str_comp(m_pFileDialogPath, "maps")) + if(m_FileDialogShowingRoot) { - CFilelistItem Item; - str_copy(Item.m_aFilename, "downloadedmaps"); - str_copy(Item.m_aName, "downloadedmaps/"); - Item.m_IsDir = true; - Item.m_IsLink = true; - Item.m_StorageType = IStorage::TYPE_SAVE; - Item.m_TimeModified = 0; - m_vCompleteFileList.push_back(Item); + { + CFilelistItem Item; + str_copy(Item.m_aFilename, m_pFileDialogPath); + str_copy(Item.m_aName, "All combined"); + Item.m_IsDir = true; + Item.m_IsLink = true; + Item.m_StorageType = IStorage::TYPE_ALL; + Item.m_TimeModified = 0; + m_vCompleteFileList.push_back(Item); + } + + for(int CheckStorageType = IStorage::TYPE_SAVE; CheckStorageType < Storage()->NumPaths(); ++CheckStorageType) + { + if(Storage()->FolderExists(m_pFileDialogPath, CheckStorageType)) + { + CFilelistItem Item; + str_copy(Item.m_aFilename, m_pFileDialogPath); + Storage()->GetCompletePath(CheckStorageType, m_pFileDialogPath, Item.m_aName, sizeof(Item.m_aName)); + str_append(Item.m_aName, "/", sizeof(Item.m_aName)); + Item.m_IsDir = true; + Item.m_IsLink = true; + Item.m_StorageType = CheckStorageType; + Item.m_TimeModified = 0; + m_vCompleteFileList.push_back(Item); + } + } + } + else + { + // Add links for downloadedmaps and themes + if(!str_comp(m_pFileDialogPath, "maps")) + { + if(str_comp(m_pFileDialogButtonText, "Save") != 0 && Storage()->FolderExists("downloadedmaps", StorageType)) + { + CFilelistItem Item; + str_copy(Item.m_aFilename, "downloadedmaps"); + str_copy(Item.m_aName, "downloadedmaps/"); + Item.m_IsDir = true; + Item.m_IsLink = true; + Item.m_StorageType = StorageType; + Item.m_TimeModified = 0; + m_vCompleteFileList.push_back(Item); + } + + if(Storage()->FolderExists("themes", StorageType)) + { + CFilelistItem Item; + str_copy(Item.m_aFilename, "themes"); + str_copy(Item.m_aName, "themes/"); + Item.m_IsDir = true; + Item.m_IsLink = true; + Item.m_StorageType = StorageType; + Item.m_TimeModified = 0; + m_vCompleteFileList.push_back(Item); + } + } + Storage()->ListDirectoryInfo(StorageType, m_pFileDialogPath, EditorListdirCallback, this); } - Storage()->ListDirectoryInfo(StorageType, m_pFileDialogPath, EditorListdirCallback, this); RefreshFilteredFileList(); if(!KeepSelection) { @@ -5132,8 +5213,25 @@ void CEditor::InvokeFileDialog(int StorageType, int FileType, const char *pTitle const char *pBasePath, const char *pDefaultName, bool (*pfnFunc)(const char *pFileName, int StorageType, void *pUser), void *pUser) { - UI()->ClosePopupMenus(); m_FileDialogStorageType = StorageType; + if(m_FileDialogStorageType == IStorage::TYPE_ALL) + { + int NumStoragedWithFolder = 0; + for(int CheckStorageType = IStorage::TYPE_SAVE; CheckStorageType < Storage()->NumPaths(); ++CheckStorageType) + { + if(Storage()->FolderExists(m_pFileDialogPath, CheckStorageType)) + { + NumStoragedWithFolder++; + } + } + m_FileDialogMultipleStorages = NumStoragedWithFolder > 1; + } + else + { + m_FileDialogMultipleStorages = false; + } + + UI()->ClosePopupMenus(); m_pFileDialogTitle = pTitle; m_pFileDialogButtonText = pButtonText; m_pfnFileDialogFunc = pfnFunc; @@ -5146,6 +5244,7 @@ void CEditor::InvokeFileDialog(int StorageType, int FileType, const char *pTitle m_FileDialogFileType = FileType; m_FilePreviewState = PREVIEW_UNLOADED; m_FileDialogOpening = true; + m_FileDialogShowingRoot = false; if(pDefaultName) m_FileDialogFileNameInput.Set(pDefaultName); @@ -7151,8 +7250,15 @@ void CEditor::OnRender() void CEditor::LoadCurrentMap() { - Load(m_pClient->GetCurrentMapPath(), IStorage::TYPE_ALL); - m_ValidSaveFilename = true; + if(Load(m_pClient->GetCurrentMapPath(), IStorage::TYPE_SAVE)) + { + m_ValidSaveFilename = true; + } + else + { + Load(m_pClient->GetCurrentMapPath(), IStorage::TYPE_ALL); + m_ValidSaveFilename = false; + } CGameClient *pGameClient = (CGameClient *)Kernel()->RequestInterface(); vec2 Center = pGameClient->m_Camera.m_Center; diff --git a/src/game/editor/editor.h b/src/game/editor/editor.h index 06e21ba80..09b19aeb5 100644 --- a/src/game/editor/editor.h +++ b/src/game/editor/editor.h @@ -977,6 +977,8 @@ public: CLineInputBuffered m_FileDialogFilterInput; char *m_pFileDialogPath; int m_FileDialogFileType; + bool m_FileDialogMultipleStorages = false; + bool m_FileDialogShowingRoot = false; int m_FilesSelectedIndex; CLineInputBuffered m_FileDialogNewFolderNameInput; @@ -1004,6 +1006,8 @@ public: return true; if(str_comp(pRhs->m_aFilename, "..") == 0) return false; + if(pLhs->m_IsLink != pRhs->m_IsLink) + return pLhs->m_IsLink; if(pLhs->m_IsDir != pRhs->m_IsDir) return pLhs->m_IsDir; return str_comp_filenames(pLhs->m_aName, pRhs->m_aName) < 0; @@ -1015,6 +1019,8 @@ public: return true; if(str_comp(pRhs->m_aFilename, "..") == 0) return false; + if(pLhs->m_IsLink != pRhs->m_IsLink) + return pLhs->m_IsLink; if(pLhs->m_IsDir != pRhs->m_IsDir) return pLhs->m_IsDir; return str_comp_filenames(pLhs->m_aName, pRhs->m_aName) > 0; diff --git a/src/game/variables.h b/src/game/variables.h index f67d3ff6d..d7e01b4a7 100644 --- a/src/game/variables.h +++ b/src/game/variables.h @@ -140,7 +140,6 @@ MACRO_CONFIG_COL(UiColor, ui_color, 0xE4A046AF, CFGFLAG_CLIENT | CFGFLAG_SAVE | MACRO_CONFIG_INT(UiColorizePing, ui_colorize_ping, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Highlight ping") MACRO_CONFIG_INT(UiColorizeGametype, ui_colorize_gametype, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Highlight gametype") -MACRO_CONFIG_STR(UiDemoSelected, ui_demo_selected, 256, "", CFGFLAG_CLIENT | CFGFLAG_SAVE, "Selected demo file") MACRO_CONFIG_INT(UiCloseWindowAfterChangingSetting, ui_close_window_after_changing_setting, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Close window after changing setting") MACRO_CONFIG_INT(UiUnreadNews, ui_unread_news, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Whether there is unread news")