From 8c6300549ff5c7ee090190f7dd63cdfa5f877aa7 Mon Sep 17 00:00:00 2001 From: Zachary Hall Date: Mon, 24 Apr 2023 15:28:39 -0700 Subject: [PATCH] Add icons to imgui-filebrowser --- .gitmodules | 3 - .vscode/settings.json | 2 +- assets/update-assets.sh | 4 +- imgui-filebrowser | 1 - imgui-filebrowser/.gitattributes | 63 ++ imgui-filebrowser/.gitignore | 268 ++++++++ imgui-filebrowser/LICENSE | 21 + imgui-filebrowser/README.md | 115 ++++ imgui-filebrowser/imfilebrowser.h | 982 ++++++++++++++++++++++++++++ imgui-filebrowser/screenshots/0.png | Bin 0 -> 23418 bytes meson.build | 2 +- 11 files changed, 1454 insertions(+), 7 deletions(-) delete mode 160000 imgui-filebrowser create mode 100644 imgui-filebrowser/.gitattributes create mode 100644 imgui-filebrowser/.gitignore create mode 100644 imgui-filebrowser/LICENSE create mode 100644 imgui-filebrowser/README.md create mode 100644 imgui-filebrowser/imfilebrowser.h create mode 100644 imgui-filebrowser/screenshots/0.png diff --git a/.gitmodules b/.gitmodules index 2fc4c4b..f7b35c4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,9 +4,6 @@ [submodule "IconFontCppHeaders"] path = IconFontCppHeaders url = https://github.com/juliettef/IconFontCppHeaders.git -[submodule "imgui-filebrowser"] - path = imgui-filebrowser - url = https://github.com/AirGuanZ/imgui-filebrowser.git [submodule "imgui"] path = imgui url = https://github.com/ocornut/imgui.git diff --git a/.vscode/settings.json b/.vscode/settings.json index 1faa897..a68ca29 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "C_Cpp.default.compileCommands": "builddir\\vscode_compile_commands.json", + "C_Cpp.default.compileCommands": "builddir/vscode_compile_commands.json", "files.associations": { "array": "cpp", "atomic": "cpp", diff --git a/assets/update-assets.sh b/assets/update-assets.sh index 1a93855..7bc0dd8 100755 --- a/assets/update-assets.sh +++ b/assets/update-assets.sh @@ -1,4 +1,5 @@ #!/bin/sh +pushd "$(dirname "$0")" if [ -z "$CXX" ]; then export CXX=g++ fi @@ -8,7 +9,8 @@ add_image() { add_font() { ./btcc -base85 $1 $2 > $2.h } -$CXX imgui/misc/fonts/binary_to_compressed_c.cpp -o btcc +$CXX ../imgui/misc/fonts/binary_to_compressed_c.cpp -o btcc add_font forkawesome-webfont.ttf forkawesome add_image icon.png icon rm btcc +popd \ No newline at end of file diff --git a/imgui-filebrowser b/imgui-filebrowser deleted file mode 160000 index cfccc2a..0000000 --- a/imgui-filebrowser +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cfccc2aab651cb19cbc2c3ad36be78c36078ec76 diff --git a/imgui-filebrowser/.gitattributes b/imgui-filebrowser/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/imgui-filebrowser/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/imgui-filebrowser/.gitignore b/imgui-filebrowser/.gitignore new file mode 100644 index 0000000..ab5fbbf --- /dev/null +++ b/imgui-filebrowser/.gitignore @@ -0,0 +1,268 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +*.o +*.d +*.dtmp +testProg +Doc/ +.vscode/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/imgui-filebrowser/LICENSE b/imgui-filebrowser/LICENSE new file mode 100644 index 0000000..4569127 --- /dev/null +++ b/imgui-filebrowser/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-2022 Zhuang Guan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/imgui-filebrowser/README.md b/imgui-filebrowser/README.md new file mode 100644 index 0000000..bbf9e48 --- /dev/null +++ b/imgui-filebrowser/README.md @@ -0,0 +1,115 @@ +# imgui-filebrowser + +[imgui-filebrowser](https://github.com/AirGuanZ/imgui-filebrowser) is a header-only file browser implementation for [dear-imgui](https://github.com/ocornut/imgui). C++ 17 is required. + +![IMG](./screenshots/0.png) + +## Getting Started + +`imfilebrowser.h` should be included after `imgui.h`: + +```cpp +#include +#include +``` + +Instead of creating a file dialog with an immediate function call, you need to create a `ImGui::FileBrowser` instance, open it with member function `Open()`, and call `Display()` in each frame. Here is a simple example: + +```cpp +#include +#include + +int main() +{ + //...initialize rendering window and imgui + + // create a file browser instance + ImGui::FileBrowser fileDialog; + + // (optional) set browser properties + fileDialog.SetTitle("title"); + fileDialog.SetTypeFilters({ ".h", ".cpp" }); + + // mainloop + while(continueRendering) + { + //...do other stuff like ImGui::NewFrame(); + + if(ImGui::Begin("dummy window")) + { + // open file dialog when user clicks this button + if(ImGui::Button("open file dialog")) + fileDialog.Open(); + } + ImGui::End(); + + fileDialog.Display(); + + if(fileDialog.HasSelected()) + { + std::cout << "Selected filename" << fileDialog.GetSelected().string() << std::endl; + fileDialog.ClearSelected(); + } + + //...do other stuff like ImGui::Render(); + } + + //...shutdown +} +``` + +## Options + +Various options can be combined with '|' and passed to the constructor: + +```cpp +enum ImGuiFileBrowserFlags_ +{ + ImGuiFileBrowserFlags_SelectDirectory = 1 << 0, // select directory instead of regular file + ImGuiFileBrowserFlags_EnterNewFilename = 1 << 1, // allow user to enter new filename when selecting regular file + ImGuiFileBrowserFlags_NoModal = 1 << 2, // file browsing window is modal by default. specify this to use a popup window + ImGuiFileBrowserFlags_NoTitleBar = 1 << 3, // hide window title bar + ImGuiFileBrowserFlags_NoStatusBar = 1 << 4, // hide status bar at the bottom of browsing window + ImGuiFileBrowserFlags_CloseOnEsc = 1 << 5, // close file browser when pressing 'ESC' + ImGuiFileBrowserFlags_CreateNewDir = 1 << 6, // allow user to create new directory + ImGuiFileBrowserFlags_MultipleSelection = 1 << 7, // allow user to select multiple files. this will hide ImGuiFileBrowserFlags_EnterNewFilename +}; +``` + +When `ImGuiFileBrowserFlags_MultipleSelection` is enabled, use `fileBrowser.GetMultiSelected()` to get all selected filenames (instead of `fileBrowser.GetSelected()`, which returns only one of them). + +Here are some common examples: + +```cpp +// select single regular file for opening +0 +// select multiple regular files for opening +ImGuiFileBrowserFlags_MultipleSelection +// select single directory for opening +ImGuiFileBrowserFlags_SelectDirectory +// select multiple directories for opening +ImGuiFileBrowserFlags_SelectDirectory | ImGuiFileBrowserFlags_MultipleSelection +// select single regular file for saving +ImGuiFileBrowserFlags_EnterNewFilename | ImGuiFileBrowserFlags_CreateNewDir +// select single directory for saving +ImGuiFileBrowserFlags_SelectDirectory | ImGuiFileBrowserFlags_CreateNewDir +``` + +## Usage + +* double click to enter a directory +* single click to (de)select a regular file (or directory, when `ImGuiFileBrowserFlags_SelectDirectory` is enabled) +* When `ImGuiFileBrowserFlags_SelectDirectory` is enabled and no directory is selected, click `ok` to choose the current directory as selected result +* When `ImGuiFileBrowserFlags_MultipleSelection` is enabled, hold `Shift` or `Ctrl` to select more than one file +* When `ImGuiFileBrowserFlags_CreateNewDir` is enabled, click the top-right little button "+" to create a new directory +* When `ImGuiFileBrowserFlags_SelectDirectory` is not specified, double click to choose a regular file as selected result. + +## Type Filters + +* (optionally) use `browser.SetTypeFilters({".h", ".cpp"})` to set file extension filters. +* ".*" matches with any extension +* filters are case-insensitive on Windows platform + +## Note + +The filebrowser implementation queries drive list via Win32 API (only on Windows). Thus `` is included in ``, which may pollute the global namespace. This can be solved by simply moving the `GetDrivesBitMask()` definition into a cpp file. diff --git a/imgui-filebrowser/imfilebrowser.h b/imgui-filebrowser/imfilebrowser.h new file mode 100644 index 0000000..5312b50 --- /dev/null +++ b/imgui-filebrowser/imfilebrowser.h @@ -0,0 +1,982 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "IconsForkAwesome.h" +#ifndef IMGUI_VERSION +# error "include imgui.h before this header" +#endif + +using ImGuiFileBrowserFlags = int; + +enum ImGuiFileBrowserFlags_ +{ + ImGuiFileBrowserFlags_SelectDirectory = 1 << 0, // select directory instead of regular file + ImGuiFileBrowserFlags_EnterNewFilename = 1 << 1, // allow user to enter new filename when selecting regular file + ImGuiFileBrowserFlags_NoModal = 1 << 2, // file browsing window is modal by default. specify this to use a popup window + ImGuiFileBrowserFlags_NoTitleBar = 1 << 3, // hide window title bar + ImGuiFileBrowserFlags_NoStatusBar = 1 << 4, // hide status bar at the bottom of browsing window + ImGuiFileBrowserFlags_CloseOnEsc = 1 << 5, // close file browser when pressing 'ESC' + ImGuiFileBrowserFlags_CreateNewDir = 1 << 6, // allow user to create new directory + ImGuiFileBrowserFlags_MultipleSelection = 1 << 7, // allow user to select multiple files. this will hide ImGuiFileBrowserFlags_EnterNewFilename +}; + +namespace ImGui +{ + class FileBrowser + { + public: + + // pwd is set to current working directory by default + explicit FileBrowser(ImGuiFileBrowserFlags flags = 0); + + FileBrowser(const FileBrowser ©From); + + FileBrowser &operator=(const FileBrowser ©From); + + // set the window position (in pixels) + // default is centered + void SetWindowPos(int posx, int posy) noexcept; + + // set the window size (in pixels) + // default is (700, 450) + void SetWindowSize(int width, int height) noexcept; + + // set the window title text + void SetTitle(std::string title); + + // open the browsing window + void Open(); + + // close the browsing window + void Close(); + + // the browsing window is opened or not + bool IsOpened() const noexcept; + + // display the browsing window if opened + void Display(); + + // returns true when there is a selected filename and the "ok" button was clicked + bool HasSelected() const noexcept; + + // set current browsing directory + bool SetPwd(const std::filesystem::path &pwd = + std::filesystem::current_path()); + + // get current browsing directory + const std::filesystem::path &GetPwd() const noexcept; + + // returns selected filename. make sense only when HasSelected returns true + // when ImGuiFileBrowserFlags_MultipleSelection is enabled, only one of + // selected filename will be returned + std::filesystem::path GetSelected() const; + + // returns all selected filenames. + // when ImGuiFileBrowserFlags_MultipleSelection is enabled, use this + // instead of GetSelected + std::vector GetMultiSelected() const; + + // set selected filename to empty + void ClearSelected(); + + // (optional) set file type filters. eg. { ".h", ".cpp", ".hpp" } + // ".*" matches any file types + void SetTypeFilters(const std::vector &typeFilters); + + // set currently applied type filter + // default value is 0 (the first type filter) + void SetCurrentTypeFilterIndex(int index); + + // when ImGuiFileBrowserFlags_EnterNewFilename is set + // this function will pre-fill the input dialog with a filename. + void SetInputName(std::string_view input); + + private: + + template + struct ScopeGuard + { + ScopeGuard(Functor&& t) : func(std::move(t)) { } + + ~ScopeGuard() { func(); } + + private: + + Functor func; + }; + + struct FileRecord + { + bool isDir = false; + std::filesystem::path name; + std::string showName; + std::filesystem::path extension; + }; + + static std::string ToLower(const std::string &s); + + void UpdateFileRecords(); + + void SetPwdUncatched(const std::filesystem::path &pwd); + + bool IsExtensionMatched(const std::filesystem::path &extension) const; + +#ifdef _WIN32 + static std::uint32_t GetDrivesBitMask(); +#endif + + // for c++17 compatibility + +#if defined(__cpp_lib_char8_t) + static std::string u8StrToStr(std::u8string s); +#endif + static std::string u8StrToStr(std::string s); + + int width_; + int height_; + int posX_; + int posY_; + ImGuiFileBrowserFlags flags_; + + std::string title_; + std::string openLabel_; + + bool openFlag_; + bool closeFlag_; + bool isOpened_; + bool ok_; + bool posIsSet_; + + std::string statusStr_; + + std::vector typeFilters_; + unsigned int typeFilterIndex_; + bool hasAllFilter_; + + std::filesystem::path pwd_; + std::set selectedFilenames_; + + std::vector fileRecords_; + + // IMPROVE: truncate when selectedFilename_.length() > inputNameBuf_.size() - 1 + static constexpr size_t INPUT_NAME_BUF_SIZE = 512; + std::unique_ptr> inputNameBuf_; + + std::string openNewDirLabel_; + std::unique_ptr> newDirNameBuf_; + +#ifdef _WIN32 + uint32_t drives_; +#endif + }; +} // namespace ImGui + +inline ImGui::FileBrowser::FileBrowser(ImGuiFileBrowserFlags flags) + : width_(700), height_(450), posX_(0), posY_(0), flags_(flags), + openFlag_(false), closeFlag_(false), isOpened_(false), ok_(false), posIsSet_(false), + inputNameBuf_(std::make_unique>()) +{ + if(flags_ & ImGuiFileBrowserFlags_CreateNewDir) + { + newDirNameBuf_ = + std::make_unique>(); + } + + inputNameBuf_->front() = '\0'; + inputNameBuf_->back() = '\0'; + SetTitle("file browser"); + SetPwd(std::filesystem::current_path()); + + typeFilters_.clear(); + typeFilterIndex_ = 0; + hasAllFilter_ = false; + +#ifdef _WIN32 + drives_ = GetDrivesBitMask(); +#endif +} + +inline ImGui::FileBrowser::FileBrowser(const FileBrowser ©From) + : FileBrowser() +{ + *this = copyFrom; +} + +inline ImGui::FileBrowser &ImGui::FileBrowser::operator=( + const FileBrowser ©From) +{ + width_ = copyFrom.width_; + height_ = copyFrom.height_; + + posX_ = copyFrom.posX_; + posY_ = copyFrom.posY_; + + flags_ = copyFrom.flags_; + SetTitle(copyFrom.title_); + + openFlag_ = copyFrom.openFlag_; + closeFlag_ = copyFrom.closeFlag_; + isOpened_ = copyFrom.isOpened_; + ok_ = copyFrom.ok_; + posIsSet_ = copyFrom.posIsSet_; + + statusStr_ = ""; + + typeFilters_ = copyFrom.typeFilters_; + typeFilterIndex_ = copyFrom.typeFilterIndex_; + hasAllFilter_ = copyFrom.hasAllFilter_; + + pwd_ = copyFrom.pwd_; + selectedFilenames_ = copyFrom.selectedFilenames_; + + fileRecords_ = copyFrom.fileRecords_; + + *inputNameBuf_ = *copyFrom.inputNameBuf_; + + openNewDirLabel_ = copyFrom.openNewDirLabel_; + if(flags_ & ImGuiFileBrowserFlags_CreateNewDir) + { + newDirNameBuf_ = std::make_unique< + std::array>(); + *newDirNameBuf_ = *copyFrom.newDirNameBuf_; + } + +#ifdef _WIN32 + drives_ = copyFrom.drives_; +#endif + + return *this; +} + +inline void ImGui::FileBrowser::SetWindowPos(int posx, int posy) noexcept +{ + posX_ = posx; + posY_ = posy; + posIsSet_ = true; +} + +inline void ImGui::FileBrowser::SetWindowSize(int width, int height) noexcept +{ + assert(width > 0 && height > 0); + width_ = width; + height_ = height; +} + +inline void ImGui::FileBrowser::SetTitle(std::string title) +{ + title_ = std::move(title); + openLabel_ = title_ + "##filebrowser_" + + std::to_string(reinterpret_cast(this)); + openNewDirLabel_ = "new dir##new_dir_" + + std::to_string(reinterpret_cast(this)); +} + +inline void ImGui::FileBrowser::Open() +{ + ClearSelected(); + UpdateFileRecords(); + statusStr_ = std::string(); + openFlag_ = true; + closeFlag_ = false; +} + +inline void ImGui::FileBrowser::Close() +{ + ClearSelected(); + statusStr_ = std::string(); + closeFlag_ = true; + openFlag_ = false; +} + +inline bool ImGui::FileBrowser::IsOpened() const noexcept +{ + return isOpened_; +} + +inline void ImGui::FileBrowser::Display() +{ + PushID(this); + ScopeGuard exitThis([this] + { + openFlag_ = false; + closeFlag_ = false; + PopID(); + }); + + if(openFlag_) + { + OpenPopup(openLabel_.c_str()); + } + isOpened_ = false; + + // open the popup window + + if(openFlag_ && (flags_ & ImGuiFileBrowserFlags_NoModal)) + { + if (posIsSet_) + SetNextWindowPos( + ImVec2(static_cast(posX_), static_cast(posY_))); + SetNextWindowSize( + ImVec2(static_cast(width_), static_cast(height_))); + } + else + { + if (posIsSet_) + SetNextWindowPos( + ImVec2(static_cast(posX_), static_cast(posY_)), + ImGuiCond_FirstUseEver); + SetNextWindowSize( + ImVec2(static_cast(width_), static_cast(height_)), + ImGuiCond_FirstUseEver); + } + if(flags_ & ImGuiFileBrowserFlags_NoModal) + { + if(!BeginPopup(openLabel_.c_str())) + { + return; + } + } + else if(!BeginPopupModal(openLabel_.c_str(), nullptr, + flags_ & ImGuiFileBrowserFlags_NoTitleBar ? + ImGuiWindowFlags_NoTitleBar : 0)) + { + return; + } + + isOpened_ = true; + ScopeGuard endPopup([] { EndPopup(); }); + + // display elements in pwd + +#ifdef _WIN32 + char currentDrive = static_cast(pwd_.c_str()[0]); + char driveStr[] = { currentDrive, ':', '\0' }; + + PushItemWidth(4 * GetFontSize()); + if(BeginCombo("##select_drive", driveStr)) + { + ScopeGuard guard([&] { EndCombo(); }); + + for(int i = 0; i < 26; ++i) + { + if(!(drives_ & (1 << i))) + { + continue; + } + + char driveCh = static_cast('A' + i); + char selectableStr[] = { driveCh, ':', '\0' }; + bool selected = currentDrive == driveCh; + + if(Selectable(selectableStr, selected) && !selected) + { + char newPwd[] = { driveCh, ':', '\\', '\0' }; + SetPwd(newPwd); + } + } + } + PopItemWidth(); + + SameLine(); +#endif + + int secIdx = 0, newPwdLastSecIdx = -1; + for(const auto &sec : pwd_) + { +#ifdef _WIN32 + if(secIdx == 1) + { + ++secIdx; + continue; + } +#endif + + PushID(secIdx); + if(secIdx > 0) + { + SameLine(); + } + if(SmallButton(u8StrToStr(sec.u8string()).c_str())) + { + newPwdLastSecIdx = secIdx; + } + PopID(); + + ++secIdx; + } + + if(newPwdLastSecIdx >= 0) + { + int i = 0; + std::filesystem::path newPwd; + for(const auto &sec : pwd_) + { + if(i++ > newPwdLastSecIdx) + { + break; + } + newPwd /= sec; + } + +#ifdef _WIN32 + if(newPwdLastSecIdx == 0) + { + newPwd /= "\\"; + } +#endif + + SetPwd(newPwd); + } + + SameLine(); + + if(SmallButton("*")) + { + UpdateFileRecords(); + + std::set newSelectedFilenames; + for(auto &name : selectedFilenames_) + { + auto it = std::find_if( + fileRecords_.begin(), fileRecords_.end(), + [&](const FileRecord &record) + { + return name == record.name; + }); + + if(it != fileRecords_.end()) + { + newSelectedFilenames.insert(name); + } + } + + if(inputNameBuf_ && (*inputNameBuf_)[0]) + { + newSelectedFilenames.insert(inputNameBuf_->data()); + } + } + + if(newDirNameBuf_) + { + SameLine(); + if(SmallButton("+")) + { + OpenPopup(openNewDirLabel_.c_str()); + (*newDirNameBuf_)[0] = '\0'; + } + + if(BeginPopup(openNewDirLabel_.c_str())) + { + ScopeGuard endNewDirPopup([] { EndPopup(); }); + + InputText("Name:", newDirNameBuf_->data(), newDirNameBuf_->size()); + SameLine(); + + if(Button("OK") && (*newDirNameBuf_)[0] != '\0') + { + ScopeGuard closeNewDirPopup([] { CloseCurrentPopup(); }); + if(create_directory(pwd_ / newDirNameBuf_->data())) + { + UpdateFileRecords(); + } + else + { + statusStr_ = "failed to create " + + std::string(newDirNameBuf_->data()); + } + } + } + } + + // browse files in a child window + + float reserveHeight = GetFrameHeightWithSpacing(); + std::filesystem::path newPwd; bool setNewPwd = false; + if(!(flags_ & ImGuiFileBrowserFlags_SelectDirectory) && + (flags_ & ImGuiFileBrowserFlags_EnterNewFilename)) + reserveHeight += GetFrameHeightWithSpacing(); + { + BeginChild("ch", ImVec2(0, -reserveHeight), true, + (flags_ & ImGuiFileBrowserFlags_NoModal) ? + ImGuiWindowFlags_AlwaysHorizontalScrollbar : 0); + ScopeGuard endChild([] { EndChild(); }); + + for(auto &rsc : fileRecords_) + { + if(!rsc.isDir && !IsExtensionMatched(rsc.extension)) + { + continue; + } + + if(!rsc.name.empty() && rsc.name.c_str()[0] == '$') + { + continue; + } + + bool selected = selectedFilenames_.find(rsc.name) + != selectedFilenames_.end(); + + if(Selectable(rsc.showName.c_str(), selected, + ImGuiSelectableFlags_DontClosePopups)) + { + const bool multiSelect = + (flags_ & ImGuiFileBrowserFlags_MultipleSelection) && + IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && + (GetIO().KeyCtrl || GetIO().KeyShift); + + if(selected) + { + if(!multiSelect) + { + selectedFilenames_.clear(); + } + else + { + selectedFilenames_.erase(rsc.name); + } + + (*inputNameBuf_)[0] = '\0'; + } + else if(rsc.name != "..") + { + if((rsc.isDir && (flags_ & ImGuiFileBrowserFlags_SelectDirectory)) || + (!rsc.isDir && !(flags_ & ImGuiFileBrowserFlags_SelectDirectory))) + { + if(multiSelect) + { + selectedFilenames_.insert(rsc.name); + } + else + { + selectedFilenames_ = { rsc.name }; + } + + if(!(flags_ & ImGuiFileBrowserFlags_SelectDirectory)) + { +#ifdef _MSC_VER + strcpy_s( + inputNameBuf_->data(), inputNameBuf_->size(), + u8StrToStr(rsc.name.u8string()).c_str()); +#else + std::strncpy(inputNameBuf_->data(), + u8StrToStr(rsc.name.u8string()).c_str(), + inputNameBuf_->size() - 1); +#endif + } + } + } + else + { + if(!multiSelect) + { + selectedFilenames_.clear(); + } + } + } + + if(IsItemClicked(0) && IsMouseDoubleClicked(0)) + { + if(rsc.isDir) + { + setNewPwd = true; + newPwd = (rsc.name != "..") ? (pwd_ / rsc.name) : + pwd_.parent_path(); + } + else if(!(flags_ & ImGuiFileBrowserFlags_SelectDirectory)) + { + selectedFilenames_ = { rsc.name }; + ok_ = true; + CloseCurrentPopup(); + } + } + } + } + + if(setNewPwd) + { + SetPwd(newPwd); + } + + if(!(flags_ & ImGuiFileBrowserFlags_SelectDirectory) && + (flags_ & ImGuiFileBrowserFlags_EnterNewFilename)) + { + PushID(this); + ScopeGuard popTextID([] { PopID(); }); + + PushItemWidth(-1); + if(InputText("", inputNameBuf_->data(), inputNameBuf_->size()) && + inputNameBuf_->at(0) != '\0') + { + selectedFilenames_ = { inputNameBuf_->data() }; + } + PopItemWidth(); + } + + if(!(flags_ & ImGuiFileBrowserFlags_SelectDirectory)) + { + if(Button(" OK ") && !selectedFilenames_.empty()) + { + ok_ = true; + CloseCurrentPopup(); + } + } + else + { + if(Button(" OK ")) + { + ok_ = true; + CloseCurrentPopup(); + } + } + + SameLine(); + + bool shouldExit = + Button("Cancel") || closeFlag_ || + ((flags_ & ImGuiFileBrowserFlags_CloseOnEsc) && + IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && + IsKeyPressed(ImGuiKey_Escape)); + if(shouldExit) + { + CloseCurrentPopup(); + } + + if(!statusStr_.empty() && !(flags_ & ImGuiFileBrowserFlags_NoStatusBar)) + { + SameLine(); + Text("%s", statusStr_.c_str()); + } + + if(!typeFilters_.empty()) + { + SameLine(); + PushItemWidth(8 * GetFontSize()); + if(BeginCombo( + "##type_filters", typeFilters_[typeFilterIndex_].c_str())) + { + ScopeGuard guard([&] { EndCombo(); }); + + for(size_t i = 0; i < typeFilters_.size(); ++i) + { + bool selected = i == typeFilterIndex_; + if(Selectable(typeFilters_[i].c_str(), selected) && !selected) + { + typeFilterIndex_ = static_cast(i); + } + } + } + PopItemWidth(); + } +} + +inline bool ImGui::FileBrowser::HasSelected() const noexcept +{ + return ok_; +} + +inline bool ImGui::FileBrowser::SetPwd(const std::filesystem::path &pwd) +{ + try + { + SetPwdUncatched(pwd); + return true; + } + catch(const std::exception &err) + { + statusStr_ = std::string("last error: ") + err.what(); + } + catch(...) + { + statusStr_ = "last error: unknown"; + } + + SetPwdUncatched(std::filesystem::current_path()); + return false; +} + +inline const class std::filesystem::path &ImGui::FileBrowser::GetPwd() const noexcept +{ + return pwd_; +} + +inline std::filesystem::path ImGui::FileBrowser::GetSelected() const +{ + // when ok_ is true, selectedFilenames_ may be empty if SelectDirectory + // is enabled. return pwd in that case. + if(selectedFilenames_.empty()) + { + return pwd_; + } + return pwd_ / *selectedFilenames_.begin(); +} + +inline std::vector + ImGui::FileBrowser::GetMultiSelected() const +{ + if(selectedFilenames_.empty()) + { + return { pwd_ }; + } + + std::vector ret; + ret.reserve(selectedFilenames_.size()); + for(auto &s : selectedFilenames_) + { + ret.push_back(pwd_ / s); + } + + return ret; +} + +inline void ImGui::FileBrowser::ClearSelected() +{ + selectedFilenames_.clear(); + (*inputNameBuf_)[0] = '\0'; + ok_ = false; +} + +inline void ImGui::FileBrowser::SetTypeFilters( + const std::vector &_typeFilters) +{ + typeFilters_.clear(); + + // remove duplicate filter names due to case unsensitivity on windows + +#ifdef _WIN32 + + std::vector typeFilters; + for(auto &rawFilter : _typeFilters) + { + std::string lowerFilter = ToLower(rawFilter); + auto it = std::find(typeFilters.begin(), typeFilters.end(), lowerFilter); + if(it == typeFilters.end()) + { + typeFilters.push_back(std::move(lowerFilter)); + } + } + +#else + + auto &typeFilters = _typeFilters; + +#endif + + // insert auto-generated filter + + if(typeFilters.size() > 1) + { + hasAllFilter_ = true; + std::string allFiltersName = std::string(); + for(size_t i = 0; i < typeFilters.size(); ++i) + { + if(typeFilters[i] == std::string_view(".*")) + { + hasAllFilter_ = false; + break; + } + + if(i > 0) + { + allFiltersName += ","; + } + allFiltersName += typeFilters[i]; + } + + if(hasAllFilter_) + { + typeFilters_.push_back(std::move(allFiltersName)); + } + } + + std::copy( + typeFilters.begin(), typeFilters.end(), + std::back_inserter(typeFilters_)); + + typeFilterIndex_ = 0; +} + +inline void ImGui::FileBrowser::SetCurrentTypeFilterIndex(int index) +{ + typeFilterIndex_ = static_cast(index); +} + +inline void ImGui::FileBrowser::SetInputName(std::string_view input) +{ + if(flags_ & ImGuiFileBrowserFlags_EnterNewFilename) + { + if(input.size() >= static_cast(INPUT_NAME_BUF_SIZE)) + { + // If input doesn't fit trim off characters + input = input.substr(0, INPUT_NAME_BUF_SIZE - 1); + } + std::copy(input.begin(), input.end(), inputNameBuf_->begin()); + inputNameBuf_->at(input.size()) = '\0'; + selectedFilenames_ = { inputNameBuf_->data() }; + } +} + +inline std::string ImGui::FileBrowser::ToLower(const std::string &s) +{ + std::string ret = s; + for(char &c : ret) + { + c = static_cast(std::tolower(c)); + } + return ret; +} + +inline void ImGui::FileBrowser::UpdateFileRecords() +{ + fileRecords_ = { FileRecord{ true, "..", ICON_FK_FOLDER " ..", "" } }; + + for(auto &p : std::filesystem::directory_iterator(pwd_)) + { + FileRecord rcd; + + if(p.is_regular_file()) + { + rcd.isDir = false; + } + else if(p.is_directory()) + { + rcd.isDir = true; + } + else + { + continue; + } + + rcd.name = p.path().filename(); + if(rcd.name.empty()) + { + continue; + } + + rcd.extension = p.path().filename().extension(); + + rcd.showName = (rcd.isDir ? ICON_FK_FOLDER " " : ICON_FK_FILE " ") + + u8StrToStr(p.path().filename().u8string()); + fileRecords_.push_back(rcd); + } + + std::sort(fileRecords_.begin(), fileRecords_.end(), + [](const FileRecord &L, const FileRecord &R) + { + return (L.isDir ^ R.isDir) ? L.isDir : (L.name < R.name); + }); +} + +inline void ImGui::FileBrowser::SetPwdUncatched(const std::filesystem::path &pwd) +{ + pwd_ = absolute(pwd); + UpdateFileRecords(); + selectedFilenames_.clear(); + (*inputNameBuf_)[0] = '\0'; +} + +inline bool ImGui::FileBrowser::IsExtensionMatched( + const std::filesystem::path &_extension) const +{ +#ifdef _WIN32 + std::filesystem::path extension = ToLower(_extension.string()); +#else + auto &extension = _extension; +#endif + + // no type filters + if(typeFilters_.empty()) + { + return true; + } + + // invalid type filter index + if(static_cast(typeFilterIndex_) >= typeFilters_.size()) + { + return true; + } + + // all type filters + if(hasAllFilter_ && typeFilterIndex_ == 0) + { + for(size_t i = 1; i < typeFilters_.size(); ++i) + { + if(extension == typeFilters_[i]) + { + return true; + } + } + return false; + } + + // universal filter + if(typeFilters_[typeFilterIndex_] == std::string_view(".*")) + { + return true; + } + + // regular filter + return extension == typeFilters_[typeFilterIndex_]; +} + +#if defined(__cpp_lib_char8_t) +inline std::string ImGui::FileBrowser::u8StrToStr(std::u8string s) +{ + return std::string(s.begin(), s.end()); +} +#endif + +inline std::string ImGui::FileBrowser::u8StrToStr(std::string s) +{ + return s; +} + +#ifdef _WIN32 + +#ifndef _INC_WINDOWS + +#ifndef WIN32_LEAN_AND_MEAN + +#define IMGUI_FILEBROWSER_UNDEF_WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN + +#endif // #ifndef WIN32_LEAN_AND_MEAN + +#include + +#ifdef IMGUI_FILEBROWSER_UNDEF_WIN32_LEAN_AND_MEAN +#undef IMGUI_FILEBROWSER_UNDEF_WIN32_LEAN_AND_MEAN +#undef WIN32_LEAN_AND_MEAN +#endif // #ifdef IMGUI_FILEBROWSER_UNDEF_WIN32_LEAN_AND_MEAN + +#endif // #ifdef _INC_WINDOWS + +inline std::uint32_t ImGui::FileBrowser::GetDrivesBitMask() +{ + DWORD mask = GetLogicalDrives(); + uint32_t ret = 0; + for(int i = 0; i < 26; ++i) + { + if(!(mask & (1 << i))) + { + continue; + } + char rootName[4] = { static_cast('A' + i), ':', '\\', '\0' }; + UINT type = GetDriveTypeA(rootName); + if(type == DRIVE_REMOVABLE || type == DRIVE_FIXED || type == DRIVE_REMOTE) + { + ret |= (1 << i); + } + } + return ret; +} + +#endif diff --git a/imgui-filebrowser/screenshots/0.png b/imgui-filebrowser/screenshots/0.png new file mode 100644 index 0000000000000000000000000000000000000000..069d2f23abebca3605a750460364cf1eb8cddeb5 GIT binary patch literal 23418 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 K8x