From 9e6a15d08f763add2e7d470a583be8f255814cfb Mon Sep 17 00:00:00 2001 From: Joeyrp Date: Mon, 8 Nov 2021 21:02:01 -0500 Subject: [PATCH] File Browser basic functionality working --- CMakeLists.txt | 1 + docs/core.todo | 19 +- docs/project_structure.txt | 2 +- src/gui/fileBrowser.cpp | 273 ++++++++++++++++++++++ src/gui/fileBrowser.h | 81 +++++++ src/internal_libs/assets/types/image.cpp | 15 ++ src/internal_libs/assets/types/image.h | 1 + src/internal_libs/utils/helpers.cpp | 24 ++ src/internal_libs/utils/helpers.h | 7 + src/run_modes/editor/editor.cpp | 48 +++- src/run_modes/editor/editor.h | 5 + src/run_modes/editor/panels/iPanel.h | 3 + src/run_modes/editor/panels/sceneTree.cpp | 14 ++ src/run_modes/editor/panels/sceneTree.h | 22 ++ test_data/dir_icon.png | Bin 0 -> 1124 bytes test_data/dir_icon.xcf | Bin 0 -> 2461 bytes test_data/dir_icon1.png | Bin 0 -> 1425 bytes test_data/dir_icon2.png | Bin 0 -> 1106 bytes test_data/new_dir_icon.png | Bin 0 -> 1201 bytes test_data/new_dir_icon.xcf | Bin 0 -> 2583 bytes test_data/up_arrow_icon.png | Bin 0 -> 698 bytes test_data/up_dir_icon.png | Bin 0 -> 1204 bytes test_data/up_dir_icon.xcf | Bin 0 -> 2634 bytes 23 files changed, 505 insertions(+), 10 deletions(-) create mode 100644 src/gui/fileBrowser.cpp create mode 100644 src/gui/fileBrowser.h create mode 100644 src/run_modes/editor/panels/sceneTree.cpp create mode 100644 src/run_modes/editor/panels/sceneTree.h create mode 100644 test_data/dir_icon.png create mode 100644 test_data/dir_icon.xcf create mode 100644 test_data/dir_icon1.png create mode 100644 test_data/dir_icon2.png create mode 100644 test_data/new_dir_icon.png create mode 100644 test_data/new_dir_icon.xcf create mode 100644 test_data/up_arrow_icon.png create mode 100644 test_data/up_dir_icon.png create mode 100644 test_data/up_dir_icon.xcf diff --git a/CMakeLists.txt b/CMakeLists.txt index 2003e5a..3556932 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ set(LUNARIUM_SRC "src/input/keyboard.cpp" "src/input/inputManager.cpp" "src/gui/gui.cpp" +"src/gui/fileBrowser.cpp" "src/gui/logGui.cpp" "src/gui/luaConsole.cpp" "src/scripting/scriptManager.cpp" diff --git a/docs/core.todo b/docs/core.todo index 495d2ac..4c9c36e 100644 --- a/docs/core.todo +++ b/docs/core.todo @@ -26,12 +26,18 @@ Core: ✔ Add Roboto-Regular.ttf as an internal font @high @done (11/3/2021, 8:35:51 PM) - GUI: - ✔ Dear ImGui class with basic initialization @done (9/10/2021, 1:42:19 PM) - ✔ Debug log window @done (9/10/2021, 4:44:48 PM) - ✔ Add key to show debug log window @done (9/13/2021, 6:47:44 PM) - ☐ Add checkboxes to disable log categories and levels - ✔ Add LUA Console window @done (10/26/2021, 4:43:41 PM) + GUI: + ✔ Dear ImGui class with basic initialization @done (9/10/2021, 1:42:19 PM) + ✔ Debug log window @done (9/10/2021, 4:44:48 PM) + ✔ Add key to show debug log window @done (9/13/2021, 6:47:44 PM) + ☐ Add checkboxes to disable log categories and levels + ✔ Add LUA Console window @done (10/26/2021, 4:43:41 PM) + FileBrowser: + ✔ Allow opening of listed directories @done (11/8/2021, 3:16:26 PM) + ✔ Add indication that an item is directory @done (11/8/2021, 6:19:20 PM) + ✔ Sort items by type (Directories should come first) @done (11/8/2021, 6:26:01 PM) + ☐ Allow the user to type in a filename + ✔ Add a "New Directory" button @done (11/8/2021, 7:15:51 PM) Input: ✔ Port over the Element2D input system and adjust it to use glfw @done (9/8/2021, 8:20:07 PM) @@ -59,6 +65,7 @@ Core: Utils: ✔ Make Logger fully static (no need to ever GetInstance) @done (10/26/2021, 4:43:55 PM) ✔ Need to add a static initialize method @done (10/26/2021, 4:43:57 PM) + ☐ Add a templated return value to the OK variant of OpRes @low Assets: diff --git a/docs/project_structure.txt b/docs/project_structure.txt index 61dee2c..c25bd7c 100644 --- a/docs/project_structure.txt +++ b/docs/project_structure.txt @@ -4,7 +4,7 @@ The root project folder needs to contain a project.xml file that describes the p Project directory structure (this should be auto-generated by the editor when creating a new project): Project root - ├── project.xml + ├── project.lproj (this is actually an xml file but the lproj extension allows it to be identified as a project file without opening it) ├── engine/ │ └── Lunarium_NE.exe (non-editor version of the engine. Used for creating a release build of the project) │ diff --git a/src/gui/fileBrowser.cpp b/src/gui/fileBrowser.cpp new file mode 100644 index 0000000..627a6df --- /dev/null +++ b/src/gui/fileBrowser.cpp @@ -0,0 +1,273 @@ +/****************************************************************************** +* File - fileBrowser.cpp +* Author - Joey Pollack +* Date - 2021/11/04 (y/m/d) +* Mod Date - 2021/11/04 (y/m/d) +* Description - File browser dialog window. Can be used for opening +* and saving files. +******************************************************************************/ + +#include "fileBrowser.h" + +#include +#include +#include +#include +#include + +namespace lunarium +{ + FileBrowser::FileBrowser() + : mIsOpen(false), mSelectionMode(SelectionMode::FILES_ONLY), mWarnOnExisting(false), mpFolderIcon(nullptr), mResult(Result::CANCEL), + mpNewFolderIcon(nullptr), mpUpFolderIcon(nullptr) + { + int x,y,n; + unsigned char* pData = stbi_load("dir_icon.png", &x, &y, &n, 0); + mpFolderIcon = new Image(pData, x, y, ImageFormat::RGBA); + Core::Graphics().RegisterImage(*mpFolderIcon); + mpFolderIcon->FreeRawData(); + + pData = stbi_load("new_dir_icon.png", &x, &y, &n, 0); + mpNewFolderIcon = new Image(pData, x, y, ImageFormat::RGBA); + Core::Graphics().RegisterImage(*mpNewFolderIcon); + mpNewFolderIcon->FreeRawData(); + + pData = stbi_load("up_arrow_icon.png", &x, &y, &n, 0); + mpUpFolderIcon = new Image(pData, x, y, ImageFormat::RGBA); + Core::Graphics().RegisterImage(*mpUpFolderIcon); + mpUpFolderIcon->FreeRawData(); + } + + /// If the given path does not exist this will default to the + /// current working directory + bool FileBrowser::OpenInDirectory(std::filesystem::path path) + { + if (mIsOpen) + return false; + + if (!std::filesystem::exists(path)) + path = std::filesystem::current_path(); + + if (!std::filesystem::is_directory(path)) + return false; + + mCurrentDirectory = path; + + // Get list of items in the current directory + ReloadItems(); + + mIsOpen = true; + return true; + } + + bool FileBrowser::DoFrame() + { + if (!mIsOpen) + return false; + + ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_Appearing); + if (!ImGui::Begin("File Browser", &mIsOpen, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking)) + { + ImGui::End(); + return false; + } + + ImVec2 iconSize(mpNewFolderIcon->GetWidth(), mpNewFolderIcon->GetHeight()); + if (ImGui::ImageButton((ImTextureID) mpNewFolderIcon->GetGLTextureID(), iconSize)) + { + ImGui::OpenPopup("New Folder Name"); + memset(mInputBuffer, 0, mBufferSize); + } + + if (ImGui::BeginPopup("New Folder Name")) + { + if (ImGui::InputText("Folder Name", mInputBuffer, mBufferSize, ImGuiInputTextFlags_EnterReturnsTrue)) + { + std::filesystem::create_directory(mInputBuffer); + ImGui::CloseCurrentPopup(); + ReloadItems(); + } + ImGui::EndPopup(); + } + + ImGui::SameLine(); + iconSize = ImVec2(mpUpFolderIcon->GetWidth(), mpUpFolderIcon->GetHeight()); + if (ImGui::ImageButton((ImTextureID) mpUpFolderIcon->GetGLTextureID(), iconSize)) + { + mCurrentDirectory = mCurrentDirectory.parent_path(); + ReloadItems(); + } + + ImGui::SameLine(); + ImGui::TextUnformatted(mCurrentDirectory.string().c_str()); + + DoListBox(); + + ImGui::Separator(); + if (mpSelectedItem) + { + ImGui::Text("Selected: %s", mpSelectedItem->filename().string().c_str()); + } + else + { + ImGui::Text("Selected: "); + } + + ImGui::Separator(); + + bool res = DoButtons(); + + ImGui::End(); + return res; + } + + void FileBrowser::AddExtensionFilter(std::string ext) + { + mExtensionsFilter.push_back(ext); + } + + void FileBrowser::SetSelectionMode(SelectionMode mode) + { + mSelectionMode = mode; + } + + void FileBrowser::WarnOnExistingFileSelection(bool warn) + { + mWarnOnExisting = warn; + } + + + /// returns nullptr if the dialog has not been opened yet + const std::filesystem::path* FileBrowser::GetSelectedItem() const + { + return mpSelectedItem; + } + + FileBrowser::Result FileBrowser::GetResult() const + { + return mResult; + } + + bool FileBrowser::IsOpen() const + { + return mIsOpen; + } + + void FileBrowser::ReloadItems() + { + mpSelectedItem = nullptr; + mItemsInDir.clear(); + for(auto const& dir_entry: std::filesystem::directory_iterator{mCurrentDirectory}) + { + mItemsInDir.push_back(dir_entry.path()); + } + + // Sort by type and then by name + std::sort(mItemsInDir.begin(), mItemsInDir.end(), [](std::filesystem::path& lhs, std::filesystem::path& rhs) + { + if (std::filesystem::is_directory(lhs) && std::filesystem::is_directory(rhs)) + { + return strcmpi(lhs.filename().string().c_str(), rhs.filename().string().c_str()) < 0; + } + + if (std::filesystem::is_directory(lhs) && !std::filesystem::is_directory(rhs)) + { + return true; + } + + return false; + }); + } + + //////////////////////////////////////////////////////////// + // RENDER HELPER METHODS + //////////////////////////////////////////////////////////// + void FileBrowser::DoListBox() + { + if (ImGui::BeginListBox("", ImVec2(ImGui::GetWindowWidth(), ImGui::GetWindowHeight() * .65f))) + { + for (int i = 0; i < mItemsInDir.size(); i++) + { + if (std::filesystem::is_directory(mItemsInDir[i])) + { + ImVec2 size(mpFolderIcon->GetWidth(), mpFolderIcon->GetHeight()); + ImTextureID id = (ImTextureID) mpFolderIcon->GetGLTextureID(); + ImGui::Image(id, size); + ImGui::SameLine(); + } + if (ImGui::Selectable(mItemsInDir[i].filename().string().c_str())) + { + if (std::filesystem::is_directory(mItemsInDir[i])) + { + mCurrentDirectory /= mItemsInDir[i]; + ReloadItems(); + + if (mSelectionMode == SelectionMode::DIRECTORIES_ONLY || mSelectionMode == SelectionMode::ANY) + { + mpSelectedItem = &mCurrentDirectory; + } + } + else + { + if (mSelectionMode == SelectionMode::FILES_ONLY || mSelectionMode == SelectionMode::ANY) + { + mpSelectedItem = &mItemsInDir[i]; + } + } + } + } + + ImGui::EndListBox(); + } + } + + bool FileBrowser::DoButtons() + { + ImGui::SetCursorPosX(ImGui::GetWindowSize().x - 100.0f); + if (ImGui::Button("OK")) + { + if (std::filesystem::exists(*mpSelectedItem) && mWarnOnExisting) + { + ImGui::OpenPopup("Warn"); + } + else + { + mResult = Result::OK; + mIsOpen = false; + return false; + } + } + + if (ImGui::BeginPopup("Warn")) + { + ImGui::Text("This file already exists. Are you sure you want to overwrite it?"); + if (ImGui::Button("Yes")) + { + mResult = Result::OK; + mIsOpen = false; + ImGui::EndPopup(); + return false; + } + + ImGui::SameLine(); + if (ImGui::Button("No")) + { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } + + ImGui::SameLine(); + + if (ImGui::Button("Cancel")) + { + mResult = Result::CANCEL; + mIsOpen = false; + return false; + } + + return true; + } +} + diff --git a/src/gui/fileBrowser.h b/src/gui/fileBrowser.h new file mode 100644 index 0000000..5f6e7d4 --- /dev/null +++ b/src/gui/fileBrowser.h @@ -0,0 +1,81 @@ +/****************************************************************************** +* File - fileBrowser.h +* Author - Joey Pollack +* Date - 2021/11/04 (y/m/d) +* Mod Date - 2021/11/04 (y/m/d) +* Description - File browser dialog window. Can be used for opening +* and saving files. +******************************************************************************/ + +// Some useful links: +// https://en.cppreference.com/w/cpp/filesystem/is_directory +// https://en.cppreference.com/w/cpp/filesystem/path + +#ifndef FILE_SELECT_H_ +#define FILE_SELECT_H_ + +#include + +#include +#include + +namespace lunarium +{ + class Image; + class FileBrowser + { + public: + enum Result + { + OK, + CANCEL + }; + + enum SelectionMode + { + FILES_ONLY, + DIRECTORIES_ONLY, + ANY, + }; + + public: + + FileBrowser(); + bool OpenInDirectory(std::filesystem::path path); + bool DoFrame(); + bool IsOpen() const; + + void AddExtensionFilter(std::string ext); + void SetSelectionMode(SelectionMode mode); + void WarnOnExistingFileSelection(bool warn); + + // returns nullptr if the dialog has not been opened yet + const std::filesystem::path* GetSelectedItem() const; + Result GetResult() const; + + private: + + bool mIsOpen; + SelectionMode mSelectionMode; + bool mWarnOnExisting; + Result mResult; + std::filesystem::path mCurrentDirectory; + std::vector mItemsInDir; + std::vector mExtensionsFilter; // Show only these extensions. If empty show all files. + std::filesystem::path* mpSelectedItem; + + Image* mpFolderIcon; + Image* mpNewFolderIcon; + Image* mpUpFolderIcon; + static const int mBufferSize = 256; + char mInputBuffer[mBufferSize]; + + private: + void ReloadItems(); + void DoListBox(); + bool DoButtons(); + }; +} + + +#endif // FILE_SELECT_H_ \ No newline at end of file diff --git a/src/internal_libs/assets/types/image.cpp b/src/internal_libs/assets/types/image.cpp index 189520a..f3b44fe 100644 --- a/src/internal_libs/assets/types/image.cpp +++ b/src/internal_libs/assets/types/image.cpp @@ -18,6 +18,21 @@ namespace lunarium { } + Image::Image(unsigned char* data, int width, int height, ImageFormat format) + : Asset(AssetType::ASSET_TYPE_IMAGE), mRawData(data), mRawDataSize(width * height), mWidth(width), mHeight(height), mGLTextureID((unsigned)-1) + { + mFormat = format; + if (mFormat == ImageFormat::RGB) + { + mRawDataSize *= 3; + } + else if (mFormat == ImageFormat::RGBA) + { + mRawDataSize *= 4; + } + + } + Image::~Image() { delete[] mRawData; diff --git a/src/internal_libs/assets/types/image.h b/src/internal_libs/assets/types/image.h index 53ef031..4e69307 100644 --- a/src/internal_libs/assets/types/image.h +++ b/src/internal_libs/assets/types/image.h @@ -19,6 +19,7 @@ namespace lunarium { public: Image(); + Image(unsigned char* data, int width, int height, ImageFormat format); ~Image(); unsigned int GetGLTextureID() const; diff --git a/src/internal_libs/utils/helpers.cpp b/src/internal_libs/utils/helpers.cpp index eff3e16..ff2ca2b 100644 --- a/src/internal_libs/utils/helpers.cpp +++ b/src/internal_libs/utils/helpers.cpp @@ -7,7 +7,9 @@ ******************************************************************************/ #include "helpers.h" +#include "logger.h" #include +#include #ifdef WIN32 #include @@ -38,6 +40,28 @@ namespace lunarium return glsl_version; } + //////////////////////////////////////////////////////////// + // FILE SYSTEM FUNCTIONS + //////////////////////////////////////////////////////////// + std::vector FileSystem::GetFilesInDirectory(std::string path) + { + std::vector files; + for (auto &p : std::filesystem::recursive_directory_iterator(path)) + { + files.push_back(p.path().filename().string()); + } + + return files; + } + + bool FileSystem::MakeDir(std::string path) + { + if (std::filesystem::exists(path)) + return false; + + return std::filesystem::create_directory(path); + } + //////////////////////////////////////////////////////////// // MATH FUNCTIONS //////////////////////////////////////////////////////////// diff --git a/src/internal_libs/utils/helpers.h b/src/internal_libs/utils/helpers.h index ec3785b..19f58dc 100644 --- a/src/internal_libs/utils/helpers.h +++ b/src/internal_libs/utils/helpers.h @@ -23,6 +23,13 @@ namespace lunarium static std::string GetGLSLVersionString(); }; + class FileSystem + { + public: + static std::vector GetFilesInDirectory(std::string path); + static bool MakeDir(std::string path); + }; + class Math { public: diff --git a/src/run_modes/editor/editor.cpp b/src/run_modes/editor/editor.cpp index 065fe49..c3528f0 100644 --- a/src/run_modes/editor/editor.cpp +++ b/src/run_modes/editor/editor.cpp @@ -11,6 +11,7 @@ #include "panels/mainPanel.h" #include #include +#include // Panels #include "panels/about.h" @@ -18,7 +19,7 @@ namespace lunarium { Editor::Editor() - : mLogCat(-1), mpMainPanel(nullptr), mDoNewProject(false), mDoOpenProject(false), + : mLogCat(-1), mpMainPanel(nullptr), mpFileBrowser(nullptr), mpPath(nullptr), mDoNewProject(false), mDoOpenProject(false), mDoSaveProject(false), mDoSaveAs(false) { } @@ -55,6 +56,15 @@ namespace lunarium iter->second->DoFrame(); } } + + + if (mpFileBrowser) + { + if (!mpFileBrowser->DoFrame()) + { + mpPath = mpFileBrowser->GetSelectedItem(); + } + } } uint32_t Editor::GetLogCat() const @@ -76,8 +86,40 @@ namespace lunarium // FILE if (mDoNewProject) { - - mDoNewProject = false; + if (!mpFileBrowser) + { + mpFileBrowser = new FileBrowser; + // mpFileBrowser->WarnOnExistingFileSelection(true); + mpFileBrowser->SetSelectionMode(FileBrowser::SelectionMode::DIRECTORIES_ONLY); + if (!mpFileBrowser->OpenInDirectory("")) + { + delete mpFileBrowser; + mpFileBrowser = nullptr; + Logger::Log(mLogCat, LogLevel::ERROR, "Could not open the File Browser"); + } + } + else + { + if (!mpFileBrowser->IsOpen()) + { + if (mpFileBrowser->GetResult() == FileBrowser::Result::OK) + { + Logger::Log(mLogCat, LogLevel::INFO, "Generating new project at %s", mpPath->string().c_str()); + + // TODO: Generate new project at mpPath + } + else + { + Logger::Log(mLogCat, LogLevel::INFO, "New Project operation cancelled"); + } + + mpPath = nullptr; + delete mpFileBrowser; + mpFileBrowser = nullptr; + mDoNewProject = false; + } + } + } if (mDoOpenProject) diff --git a/src/run_modes/editor/editor.h b/src/run_modes/editor/editor.h index 250218e..80958b8 100644 --- a/src/run_modes/editor/editor.h +++ b/src/run_modes/editor/editor.h @@ -13,10 +13,12 @@ #include #include "panels/iPanel.h" +#include #include namespace lunarium { + class FileBrowser; class MainPanel; class Editor : public iRunMode { @@ -48,6 +50,9 @@ namespace lunarium MainPanel* mpMainPanel; std::map mPanels; + FileBrowser* mpFileBrowser; + const std::filesystem::path* mpPath; + // Menu Bar Events // Don't want to handles these events during rendering bool mDoNewProject; diff --git a/src/run_modes/editor/panels/iPanel.h b/src/run_modes/editor/panels/iPanel.h index 7731c71..d8a1607 100644 --- a/src/run_modes/editor/panels/iPanel.h +++ b/src/run_modes/editor/panels/iPanel.h @@ -15,6 +15,9 @@ namespace lunarium { PT_MAIN, PT_ABOUT, + PT_SCENE_TREE, + PT_CONTENT_BROWSER, + PT_CONSOLE, PT_UNKNOWN, }; diff --git a/src/run_modes/editor/panels/sceneTree.cpp b/src/run_modes/editor/panels/sceneTree.cpp new file mode 100644 index 0000000..38395f0 --- /dev/null +++ b/src/run_modes/editor/panels/sceneTree.cpp @@ -0,0 +1,14 @@ +/****************************************************************************** +* File - sceneTree.cpp +* Author - Joey Pollack +* Date - 2021/11/04 (y/m/d) +* Mod Date - 2021/11/04 (y/m/d) +* Description - The tree view listing all objects in the scene +******************************************************************************/ + +#include "sceneTree.h" + +namespace lunarium +{ + +} \ No newline at end of file diff --git a/src/run_modes/editor/panels/sceneTree.h b/src/run_modes/editor/panels/sceneTree.h new file mode 100644 index 0000000..ee16416 --- /dev/null +++ b/src/run_modes/editor/panels/sceneTree.h @@ -0,0 +1,22 @@ +/****************************************************************************** +* File - sceneTree.h +* Author - Joey Pollack +* Date - 2021/11/04 (y/m/d) +* Mod Date - 2021/11/04 (y/m/d) +* Description - The tree view listing all objects in the scene +******************************************************************************/ + +#ifndef SCENE_TREE_H_ +#define SCENE_TREE_H_ + +#include "iPanel.h" + +namespace lunarium +{ + class SceneTree : public Panel + { + + }; +} + +#endif // SCENE_TREE_H_ \ No newline at end of file diff --git a/test_data/dir_icon.png b/test_data/dir_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d4acfd090a75369070d63c989734560afffc6bf8 GIT binary patch literal 1124 zcmV-q1e^PbP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+U1p7mg6W4hTpY{UILO3g5_Xi&Y2y|^83doiIb|n zOgZz=r%jBo0+OsxVA}rur_(oFoF!5; yoaK#dfR19)Bu0wK&dp{q-ZSbV0u+2XN zDuvYJF-<*J=Q0EMNP6dTD zC#07gO+DM^x!W^zj)-qVdHw^QZ!SLOuD9I#cD!AU37JYXU+PvO`v?<})TsNigyy&n zW8Ygr%W$-USOd)kZ6z)&$ad$*jVpKFcDeG35Tk73kRmG>3|&^2A}bQlNIV*MaV^?N zGpMYfrcld07T*1q-QUU?o*QGPFpM!)d}#EU@+<9bsP|F2%p;lGkt@U{qoxtYAUAr< z0ub&qr?%&JZ}ixOA)LS}2-=?Jj2En7IZP}^Q6IIXJ7>i_3z5RHHQ3uUfE2;EAsAG5 zcLBb1u0SDT*V^N-9nz zmr`nl(+blSt1D*Iy2zqSTx>~;FL^0TT~R)3Ty0IOuX!zNZPK)1`?u7bT5hG)jW?9i z7B}0{=3Cy%Ry%5~YmeP}>fUoNz22!!s?XH;h1}1n@uW70#eJSXs6p+1Gr{9HvB3<) zBr=HmAb^6F!7OBsQ3koeEEK_}Fab5T0jKdG27+;8vCemP4{|@{Mi&1QH~A4c8tC3Z zjt06(?jyHPsI|I(QFI41cHzir(2j#`ja4#!K}zFj!5T-lRHm!s&N^L2P z0oCzsJj`B_Z#C2<{UYw)utskQ=Px((V-nAjzFvaQUg=U8CrOvWI7zw`#!1qpFiug> zr7%vCE`@Q5g5DC&!_XJQI7xa#IC~UyDU6e(t7rB9a@790hC05pg}wus?p@q_FZ@6N z000JJOGiWi{{a60|De66lK=n!32;bRa{vGi!vFvd!vV){sAK>D00(qQO+^Rg3kd-p zE{V_vEdT%jrb$FWR4C8wk}(PbF%(4KE+T2Hw6zgCTRnu0VEGS_`*?#C7HP%8OK54S zdxW(KSjJ`}Uy-n`tpg!L!n{9_6xP~V2;s~J8DrMYxg8l5A`%-v1){pUHv1w6=qX=$ z@1Fn>N%ok@N8Mlr)IdMAYZk!ZEjR-1z*aZd(?J?A^xoHPgCep74y8J~eL1`WDEU!; q!0R(k)Te5raRSVN6L9HJVDkWdup~!ixQnX*0000m8W}jI#B~|dC6Xwlt=PNri@4ZCRjXG^A6}pqjW(m*; z@DKu^m%yXIf&j}|;PBGBFH^{X$ASI8gTUupEP|XMM0^i&T!Yj}-H zO}&y@!PKcG-L_WM#+q!@t7^0`PvMhKgX_s5PT+0T)ab4C_Blv=gcpb1AL8``E!GpUS2JpfG0p=k)+>=DZC^kjgcO{*>|FmIaW-v!FuUP@P& z+=iY~-Bh+z3*&2*ily4$Ul5~zRdqB!hGHLrnrs++Lz3}KDmR;(o$VCntj=jbPnUe_ z9T1r5$WVn_Ix_s$^UO~XY<3dXm6}OoPQD;KX9PA97jF)f(J_; zOg(tOgU@&{9#Woj7%+Yxrw{tKF(ruBk{ouq`xV_G|#69*j z_~d9y5Q+o0j*5bKIKYk$#Xl15L%id72{_)tvfg-vBTma-3Xq<*u;;D2S-ye6kG zDD9dHQ1E8BvkpG&VFOjr=6a?EN^SW?kc^c?a-}4-{3VI>;>prqa=jd2Y&Ap##$sYr zV4)l!K_p@Jxy{&}tBZ?E%gesi7pU z+XcJvKWz~o7SyG%(#1b-k#MVr4OHP*T;C#}h?hanx7qNTT2&SNiKTaB>oG>~n~1j) z(|2xHw03XT)1EjAF+3;!7eb7&_zG^BmjL1g>m`0MqN|4@Dm?JFcWsep9tUm%Rpd3- HL-p(%bi<{o literal 0 HcmV?d00001 diff --git a/test_data/dir_icon1.png b/test_data/dir_icon1.png new file mode 100644 index 0000000000000000000000000000000000000000..0130f5a1f12204f3630648525438a93713ed2fed GIT binary patch literal 1425 zcmV;C1#bF@P) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O<|&vg;@e{MRaG2?+6A4(3rcJDBB9gNfrfajuhl zjwu@?KoZ>oL-oIZ8~uYvYh}n-A6#@!;W5!fozMtseb$Ixx zx;n=v=dEJ6Crm@L<7QC0=k9He3)7J3RnJqVIOip^Th4rQU01@>=utKv zU=$0bXwp@n$Jcc)(qEQ@w||uc3}W$U8DK`hW14dKN z_BD0`p?ySr_x#WitnzCA?67OCgx2c1x?~)Xax@opTgqr*-~uRDG#rY@Yti3ow@$SC=kMJVrmDz zqtGsI)Q(sIgLc3iu)w62)x=`?o?9|_7L2pl{c7DB?8`I&h~V2I7-Y9$*XaNr+sF(F+)0Yjt62;6}uO9P9@8#vIRMthRbp<7fd9RvflU07D668Do(@-UQ@O zvSJNcYn}Br*l3dzXPkA;dDqXV!S*k%cq*a9k}6lEQZ?09 zSA7jN*3^(|O`2)8x#nAFv89c*$?79(d|>W9Ydl%&jF~ac-&up&`})|n(b~=NG<$-~y{5B{r+PEj$?126-ZScEbYGg1 z+}xD@ER4zRg&(>a&{KejtL@^W>0@|(H+2_~epS<1$F7FUds2?ZZp>1k%Suyk?EDsm zZPuiGEYLTTUJ3LdZGjy!syE*ueG#KeHT^BfH;li|qYo4E0j#%B(GNj%#n3gV=pi5f zqNYm&FXR;+6YDqf=p(S`m{`A*N7wU;PWIq$2I&-{-{87G}qwh|iX9u(? z8ZMd<_niL$Njy(6nIKje00006VoOIv0RI600RN!9r;`8x010qNS#tmY4#NNd4#NS* zZ>VGd000McNliru8`sPOA2=2p~NI_d}HtpB|QUp!56jw2;kuEwYPkI!dlQc&qQ^Wmyg`Y?@}4Zy=A|psK1#+MC%u$=3|&u!*X`C5f((t^n|v|F*^h z=-`fAD(n_d0E;JZlbXP7Y6AOz0;vh)sR?lL$1u4B#o{ISNg%ev9f37~RuVa9qdBG- zz2wDcncjAdmnhtT1mPKx){1xc};ESp6&Z(!7raA-|-j0E8H=?t4JPV f5_jswl{2Bi70Pp00000NkvXXu0mjfAxNXK literal 0 HcmV?d00001 diff --git a/test_data/dir_icon2.png b/test_data/dir_icon2.png new file mode 100644 index 0000000000000000000000000000000000000000..92382a847b803a79f138dac0e42b05e8f5cf0ba2 GIT binary patch literal 1106 zcmV-Y1g-mtP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+U1s8mg6W4hTmDmECERf!E!K{b9x88{P`gyaZ)uk z-IesioH1n!E3zc(6S!Re`D3`ha8Tmlf?A3>Mvo)pkhx%z{N=bv@?l-~jlWJF^v?JF z14AW{a=DFLPX&6tKVV_&k4L?;y8Wc>=o)f6lo^>Z*>-6=629nZ-@Ck@E8+Ay`>cJr z@U)#i-v>i6nnEe;xC`{S#u-KCNm6jmtCs{LlC(5oY2xR#bGPYJ@vfzNuAML2LLN)u zeB1`{sYY8*`g!eEh0Yn#b;^%FAo4ZDhuV$N(ir{nb|pkqD!Tnr*A`hb7#=94tj8jT z;Z)3Rjf{~HsDfAn%>$!4%EYMlpg@fZ4eCo)sz@<#XM<#}U@&z_j5Am9tN0i+*d$AH zAi`2XO`zsmF0|t26|Yi4r6lABjp z0SJv7TBo|&R>=WS=$a!BE(iCgYFLG zBZ*QZZ_Gx7IwtTb$ebnc0Rp7Tjobhmkq9_g4Tz0rjFv4cU)0!I074~-0Zo?FJ>p?UI`UB}+NXx9HCC&s=2{vzX;}U(Hghet+)Ag8R_dW^ zkKKCextD=n8#2Ps5r>U5@+dcIo9ZJqdLs8VHQLlBv9RpyP7P|as|%XjiB4u9#(_ZG zCIK`wPi8(*2t3J6Wda&DsA ziy{lqT!piyUIsR{<_jnMd66<@nct>glSd9p0be((u3zW$O=-tyn7H|Y z)1`b(zSR-+f6Mz9yt%ie^FMd=1&IggzTbk6QQb>voTPgxjgxdQrE!w(r8G`ixR=s6 zN%vA3r!3rC(m6T$UK%Iq-jUAC!o8HnNxE0}>OZp8{&yYWf7+Ja-{6rD7*U}F(f|Me z24YJ`L;(K){{a7>y{D4^000SaNLh0L01m?d01m?e$8V@)00007bV*G`2jvS07Y+ti zumm9h005RrL_t(2&tsf0VS@Ji_wTPTU;#WlJb|U9r5hOV0LJ?I`u|Y23IhYfetW2(c=Z2Ac0nVWBohMz12Z1YLJSNH>BugqXCMP08z9`+ z*!YfEjrH~Qo(v2O+nA8V-jitgJ(xc!0;2|?Mxzo^q9O*!!VFl*z`zjCz`$^a3f}5kN^Mx07*qoM6N<$f)xM!nE(I) literal 0 HcmV?d00001 diff --git a/test_data/new_dir_icon.png b/test_data/new_dir_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2d6876f4d97befb87f220218a2359e677ed7605b GIT binary patch literal 1201 zcmV;i1Wx;jP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+U1vFa`Pw*hX3mndjupQ1joS!X7&bq{C%-W+N5c} zHtCO@SyNkB0U^JSu&31j`Fqe`I5@{Z(LAS;Qos>QEKuAs*Lt6E2+_KZU-2 z5V#ap&)c;8T;aF32_Bb!yY-XrD^6a4E=#U}I#(8S*0{U^shoaZ_f*!r8s9z6I){aA zx@-l%zmE!B_sa`Z3|qDDQYB{aur z==;$MTE-*pAl5*0L)(c93)CJQxpC#e{jfXlNHIzmhZWhuVEVGU6xks^Yn+XTxE5`s z864+&S%;+&=|9HnWvcj)CzgYtQw>-$;~T9 z0SJ$!sU7+rjox-)2=Blu2-+cY#sk){ogucfsL$HcgR^3sg-GGt8tiQafJ9i^kPNCj za!9PkiGzzcGjz=0Q<1r7lnn&fOd8Vw8`T6j*bP`4bDWQ{Vsg6iY5@q-qz*JiD`3TF zs2`6RHB?P4l9?rQ%T}x=O)*(Y$fq@tQTqY&m7mIhS0E6vZpdN-9nz zmr`oQq7|VlW><`;b(2lExY?FA-||+rx}$vdxZ9p~-}7Gf+N5d2_HU^wt!Si)ulNpFf zWDxgB00k|RS;!otOmdT1D1uF40%~j%PUA@o1Y>8h!B=)~dNB1BB|K@hr8GNK z=eO|;`;z_DKts|W^8Q9N`jvG4=Yc*aack-M61-iNewfCm1wr3W{_C=$*K;{1(d)UK zmLBBtgN^ZK>4hiyV$(jA%PPoU+4=}V&oTNqiC)j;oJ6nZa$0&imrv{HMf+b2H1HY5 zKW+3cRETD^o!mHp00006VoOIv0RI600RN!9r;`8x010qNS#tmY4#NNd4#NS*Z>VGd z000McNliru35L3`v7m^fqQZ_zE5$f}FX`&)s#mX?=S5NUl_bfE9E4%mi{p4l z2|N)=T|5L*^R_Yh1K_*+-JK1f8JLD&ReceN$TM7*2uQ7hDbNO5z#q^BK7j?W1wP!= zK5%h8Gppd6a{QY$Ua$l`Rozh&A~FJ&mSCSLXaQ#cRrPW>PUJ~4@GC?F*%jUhRp@#qns`Hj0Np_ z+p45);?$`tmeaaxG?(;RqiSgRc?!Q=8eI1e;eode+jLg7e0KIQ-pf{*J6}A&`JU0L zSJxa!>Va)JTK>WT{tx>B;GDUbP0wc!2r2F#l1um33zvG_R|2zXmBsRtnU?dsYEXDr zt9&**Q!z{RYQ0HkxJfq6r#j7~GF%1#s1}#A5c+gLs2HdF0}O4q8hQ!pO|#-zKt3Nt zdR%f1)|6UyY1L?9e7#a>8P3la#OU8Cm)RSN>!6r=ttK`knareS=Vs^T+{AdxPXl^- zGIZYs!3(Y(-g2#I)hU6%ic6@hEM|D`tXj(-}4DH zIO{fI3}apko{`{&%5`*uklrktEv3e^2$n2Wvr_}Oqj3~ zY*Fy)0u>+-@Ae|RmylUkB5IJygt>S`U>npEl*bB4ADi~*(MXAYMR*&==gQf%aAz! zF780FoE_lYdmTsn_xfSaI)xZ9Cw_$?##np>FU>Om@oM*rxRTM;C+ZjIh`0B~)E%FL O7y~Md-*;Rfd-emUyTw5O literal 0 HcmV?d00001 diff --git a/test_data/up_arrow_icon.png b/test_data/up_arrow_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..efbca06d702e573f092bb341667b29cb93b92aa5 GIT binary patch literal 698 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP*AVL zHKN2VGbfe7#VRJCC_gJTxuiHI-YF+FFD11|0WRWLRFq$?mjn{_T4a_1)F}bd>5^EI z%3x$*WT{QtkaTyOf76wgUl{FV!68hpEvwe^*z zNz~eFCDK(Cwq<`Om#SLvvm!Em^fR(@J2&`lxWbHIsUp zg<00kzWsKa?gfu~0qgZ1ye`_ghdo^<<&(k#-c!5p@?G+{x2#X|8Q0^ZNf#G9v)WtH z?A3gbcguQagC7+(c8}(C?G{gWDBOQPJ*&rc7cYmdSXj=f`_DdCzKB$@V_fhi@}Bzj z%!xkC!FyN#`Y`GL`3FT-`En;8TMHi2U3S7De%a+r2@P8=j-;5}MSrKu2;Qr4cmC=2 zZ{Ze~o?YLJ`KHZhXSxt3;u`#OdBfgKI*MmpDqfUW?N69;-0_&FvJ3NL5%;V&ca3iv za{LezTgf#er*T0| zaB^>EX>4U6ba`-PAZ2)IW&i+q+U1u~a^oNjhTnCHJpz&tg5zLgX7&bq{Qa>>(xgqA zw0+sxP#Y|R5dSCel(s*=JN?3svk67>oJvXozgS|Cibf8{@2EM%{XQSUsc~heu+0|& zlcLt+F-<#H*zGdFLlXSP*g*toawG&mAJ4#?aq-KSMI#+^~x(kjIxQN7Foey*s{75Ss_1b9EH2M7Hy;% zR8~+^nB^P`@AH;@zLhg(Zj7G7aK;$%uFyxuuavW+-bd;(4>7kxE5s!urxC^=H}2>K zAl#>>w(GkWy6nOb-hfpQv|Z+m1=g?}E|w#ykKEFov!b7cNa5HT>}@)LM3~zU46-|N zNUX+*gNrybbj;vW5xHoT1q9ek8dC=wWIy0wH(+jzao+oi$zjIP0uY8R;y_ch0#=NM z{PCEPL)FwGnOQQoY{hEQ6qBWtJdHD|x_Wf;1i64y07MDzkr`c5owz0r#+xSLz_Tr8hwCcUtt zhu-L(mlt-&-A((Im0j}aVJuI)q^C3Jlc?y^iJx-Wg&y>JDJuG2K3@jW_wxCvz4luD zPdUQp8UM`DUrKmwf*x9Nxc~qF24YJ`L;(K){{a7>y{D4^000SaNLh0L01m?d01m?e z$8V@)00007bV*G`2jvS10VEV^bphr80087kL_t(2&z;h}N&`U@1>i3shD1apR<>d% zHa>yiQ%o&+u9Ua874ado^Dp=S24xWxV`CgdR`R8lLm}N zqseSGTWCQqrBqh+8On@%t9ZIyqqtm-9>8b16KcLQAG9xprW(ak>|<4%~P Sln>wl00005aJrdF8e_{q

(L>8;Iiph?uJ&#&7Q>jfnD~UuO8!k z&urDJ&s>)q8}ZSpE;}-m3lDL0;_44Mfq8(<-Vwz z9N*P0Uy7$HR;gaCH+jkz>87=A@N6dQQx|~U^63(Wu6GF)c78OV(9KrEC}F>OPCN^h zFS;oMUHTc;l-o{e!)#G}y;5nJ?!gOE^q;DZ=GRcFL)0>AHL)StNH#M&H#;}iNz5Pp z(?Fg9MOHgNz0#4rbRa?$3+%p1oFd%ptk*8p9X{!=7sWXp8bF>PTzriq`XG~@D26Ps z7Q*8p91me0!hIopDunw(cmOdyPf!m6dLFHRA;2iu6k$L@IT^y@;EQ)e6Oqy_|G-CS zbbS0#)P`c-2t6=$E9P@z3s_>S%|5hGs*_km1}Vn8)Mxn%xCmxE8Q@)X^atKvXWH^> zcoluQw;@TzzB_wGN#5>*y>0oggttw1oJfJ%+h4YuPpHM2hGS7WuPOLn>9xqo zPYhGL761~W7(uV2kGt4V6?KIkOjY-cw^25%%WPU#+mbG~6+OZvU15qYF%|WS4y*^U zqVFBv}D=Vo- zcgm6L-m*6YFsw2!!DR;5a7#;ABTyJ