6752: Ensure `ListDirectory/Info` entries are unique, support selecting storage location in demo browser and editor file browser r=def- a=Robyt3

Description in commits below. Screenshots/video:

- Storage location selection for demos:
![screenshot_2023-06-18_17-25-23](https://github.com/ddnet/ddnet/assets/23437060/bcc94977-50ba-494b-894d-7bd8fea7f91e)

- Storage location selection for maps:
![screenshot_2023-06-18_17-25-33](https://github.com/ddnet/ddnet/assets/23437060/2b8b7822-b4b9-409f-86eb-e44f5c072541)

- Link to "themes" folder:
![screenshot_2023-06-18_17-26-02](https://github.com/ddnet/ddnet/assets/23437060/eba661a6-0bee-4977-ab95-35e400b5b291)

- Video showing navigation between storages (`temp` is the save directory and `temp2`, `temp3` and `data` are other storages)

https://github.com/ddnet/ddnet/assets/23437060/5736d212-3848-44c2-aa81-7f2da9b98008

Closes #5496.

## Checklist

- [X] Tested the change ingame
- [X] Provided screenshots if it is a visual change
- [ ] Tested in combination with possibly related configuration options
- [ ] Written a unit test (especially base/) or added coverage to integration test
- [ ] Considered possible null pointers and out of bounds array indexing
- [ ] Changed no physics that affect existing maps
- [ ] Tested the change with [ASan+UBSan or valgrind's memcheck](https://github.com/ddnet/ddnet/#using-addresssanitizer--undefinedbehavioursanitizer-or-valgrinds-memcheck) (optional)


Co-authored-by: Robert Müller <robytemueller@gmail.com>
This commit is contained in:
bors[bot] 2023-07-03 19:05:42 +00:00 committed by GitHub
commit 06fe73619e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 361 additions and 138 deletions

View file

@ -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 <base/math.h>
#include <base/system.h>
#include <engine/client/updater.h>
#include <engine/shared/linereader.h>
#include <engine/storage.h>
#include <unordered_set>
#ifdef CONF_PLATFORM_HAIKU
#include <cstdlib>
#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<std::string> m_Seen;
};
static int ListDirectoryInfoUniqueCallback(const CFsFileInfo *pInfo, int IsDir, int Type, void *pUser)
{
SListDirectoryInfoUniqueCallbackData *pData = static_cast<SListDirectoryInfoUniqueCallbackData *>(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<std::string> m_Seen;
};
static int ListDirectoryUniqueCallback(const char *pName, int IsDir, int Type, void *pUser)
{
SListDirectoryUniqueCallbackData *pData = static_cast<SListDirectoryUniqueCallbackData *>(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)
{

View file

@ -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;

View file

@ -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);
}

View file

@ -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<IO_MAX_PATH_LENGTH> m_DemoRenameInput;
CLineInputBuffered<IO_MAX_PATH_LENGTH> m_DemoSliceInput;
CLineInputBuffered<IO_MAX_PATH_LENGTH> 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};

View file

@ -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
}

View file

@ -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<CEditor *>(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<CEditor *>(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<IGameClient>();
vec2 Center = pGameClient->m_Camera.m_Center;

View file

@ -977,6 +977,8 @@ public:
CLineInputBuffered<IO_MAX_PATH_LENGTH> m_FileDialogFilterInput;
char *m_pFileDialogPath;
int m_FileDialogFileType;
bool m_FileDialogMultipleStorages = false;
bool m_FileDialogShowingRoot = false;
int m_FilesSelectedIndex;
CLineInputBuffered<IO_MAX_PATH_LENGTH> 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;

View file

@ -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")