├── screenshots └── 0.png ├── LICENSE ├── .gitattributes ├── .gitignore ├── README.md └── imfilebrowser.h /screenshots/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirGuanZ/imgui-filebrowser/HEAD/screenshots/0.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2025 Zhuang Guan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | *.o 26 | *.d 27 | *.dtmp 28 | testProg 29 | Doc/ 30 | .vscode/ 31 | 32 | # Visual Studio 2015 cache/options directory 33 | .vs/ 34 | # Uncomment if you have tasks that create the project's static files in wwwroot 35 | #wwwroot/ 36 | 37 | # MSTest test Results 38 | [Tt]est[Rr]esult*/ 39 | [Bb]uild[Ll]og.* 40 | 41 | # NUNIT 42 | *.VisualState.xml 43 | TestResult.xml 44 | 45 | # Build Results of an ATL Project 46 | [Dd]ebugPS/ 47 | [Rr]eleasePS/ 48 | dlldata.c 49 | 50 | # DNX 51 | project.lock.json 52 | project.fragment.lock.json 53 | artifacts/ 54 | 55 | *_i.c 56 | *_p.c 57 | *_i.h 58 | *.ilk 59 | *.meta 60 | *.obj 61 | *.pch 62 | *.pdb 63 | *.pgc 64 | *.pgd 65 | *.rsp 66 | *.sbr 67 | *.tlb 68 | *.tli 69 | *.tlh 70 | *.tmp 71 | *.tmp_proj 72 | *.log 73 | *.vspscc 74 | *.vssscc 75 | .builds 76 | *.pidb 77 | *.svclog 78 | *.scc 79 | 80 | # Chutzpah Test files 81 | _Chutzpah* 82 | 83 | # Visual C++ cache files 84 | ipch/ 85 | *.aps 86 | *.ncb 87 | *.opendb 88 | *.opensdf 89 | *.sdf 90 | *.cachefile 91 | *.VC.db 92 | *.VC.VC.opendb 93 | 94 | # Visual Studio profiler 95 | *.psess 96 | *.vsp 97 | *.vspx 98 | *.sap 99 | 100 | # TFS 2012 Local Workspace 101 | $tf/ 102 | 103 | # Guidance Automation Toolkit 104 | *.gpState 105 | 106 | # ReSharper is a .NET coding add-in 107 | _ReSharper*/ 108 | *.[Rr]e[Ss]harper 109 | *.DotSettings.user 110 | 111 | # JustCode is a .NET coding add-in 112 | .JustCode 113 | 114 | # TeamCity is a build add-in 115 | _TeamCity* 116 | 117 | # DotCover is a Code Coverage Tool 118 | *.dotCover 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | #*.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignoreable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | node_modules/ 203 | orleans.codegen.cs 204 | 205 | # Since there are multiple workflows, uncomment next line to ignore bower_components 206 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 207 | #bower_components/ 208 | 209 | # RIA/Silverlight projects 210 | Generated_Code/ 211 | 212 | # Backup & report files from converting an old project file 213 | # to a newer Visual Studio version. Backup files are not needed, 214 | # because we have git ;-) 215 | _UpgradeReport_Files/ 216 | Backup*/ 217 | UpgradeLog*.XML 218 | UpgradeLog*.htm 219 | 220 | # SQL Server files 221 | *.mdf 222 | *.ldf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | 238 | # Visual Studio 6 build log 239 | *.plg 240 | 241 | # Visual Studio 6 workspace options file 242 | *.opt 243 | 244 | # Visual Studio LightSwitch build output 245 | **/*.HTMLClient/GeneratedArtifacts 246 | **/*.DesktopClient/GeneratedArtifacts 247 | **/*.DesktopClient/ModelManifest.xml 248 | **/*.Server/GeneratedArtifacts 249 | **/*.Server/ModelManifest.xml 250 | _Pvt_Extensions 251 | 252 | # Paket dependency manager 253 | .paket/paket.exe 254 | paket-files/ 255 | 256 | # FAKE - F# Make 257 | .fake/ 258 | 259 | # JetBrains Rider 260 | .idea/ 261 | *.sln.iml 262 | 263 | # CodeRush 264 | .cr/ 265 | 266 | # Python Tools for Visual Studio (PTVS) 267 | __pycache__/ 268 | *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # imgui-filebrowser 2 | 3 | [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. 4 | 5 | ![IMG](./screenshots/0.png) 6 | 7 | ## Getting Started 8 | 9 | `imfilebrowser.h` should be included after `imgui.h`: 10 | 11 | ```cpp 12 | #include 13 | #include 14 | ``` 15 | 16 | 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: 17 | 18 | ```cpp 19 | #include 20 | #include 21 | 22 | int main() 23 | { 24 | //...initialize rendering window and imgui 25 | 26 | // create a file browser instance 27 | ImGui::FileBrowser fileDialog; 28 | 29 | // (optional) set browser properties 30 | fileDialog.SetTitle("title"); 31 | fileDialog.SetTypeFilters({ ".h", ".cpp" }); 32 | 33 | // mainloop 34 | while(continueRendering) 35 | { 36 | //...do other stuff like ImGui::NewFrame(); 37 | 38 | if(ImGui::Begin("dummy window")) 39 | { 40 | // open file dialog when user clicks this button 41 | if(ImGui::Button("open file dialog")) 42 | fileDialog.Open(); 43 | } 44 | ImGui::End(); 45 | 46 | fileDialog.Display(); 47 | 48 | if(fileDialog.HasSelected()) 49 | { 50 | std::cout << "Selected filename" << fileDialog.GetSelected().string() << std::endl; 51 | fileDialog.ClearSelected(); 52 | } 53 | 54 | //...do other stuff like ImGui::Render(); 55 | } 56 | 57 | //...shutdown 58 | } 59 | ``` 60 | 61 | ## Options 62 | 63 | Various options can be combined with '|' and passed to the constructor: 64 | 65 | ```cpp 66 | enum ImGuiFileBrowserFlags_ 67 | { 68 | ImGuiFileBrowserFlags_SelectDirectory = 1 << 0, // select directory instead of regular file 69 | ImGuiFileBrowserFlags_EnterNewFilename = 1 << 1, // allow user to enter new filename when selecting regular file 70 | ImGuiFileBrowserFlags_NoModal = 1 << 2, // file browsing window is modal by default. specify this to use a popup window 71 | ImGuiFileBrowserFlags_NoTitleBar = 1 << 3, // hide window title bar 72 | ImGuiFileBrowserFlags_NoStatusBar = 1 << 4, // hide status bar at the bottom of browsing window 73 | ImGuiFileBrowserFlags_CloseOnEsc = 1 << 5, // close file browser when pressing 'ESC' 74 | ImGuiFileBrowserFlags_CreateNewDir = 1 << 6, // allow user to create new directory 75 | ImGuiFileBrowserFlags_MultipleSelection = 1 << 7, // allow user to select multiple files. this will hide ImGuiFileBrowserFlags_EnterNewFilename 76 | ImGuiFileBrowserFlags_HideRegularFiles = 1 << 8, // hide regular files when ImGuiFileBrowserFlags_SelectDirectory is enabled 77 | ImGuiFileBrowserFlags_ConfirmOnEnter = 1 << 9, // confirm selection when pressnig 'ENTER' 78 | ImGuiFileBrowserFlags_SkipItemsCausingError = 1 << 10, // when entering a new directory, any error will interrupt the process, causing the file browser to fall back to the working directory. 79 | // with this flag, if an error is caused by a specific item in the directory, that item will be skipped, allowing the process to continue. 80 | ImGuiFileBrowserFlags_EditPathString = 1 << 11, // allow user to directly edit the whole path string 81 | }; 82 | ``` 83 | 84 | When `ImGuiFileBrowserFlags_MultipleSelection` is enabled, use `fileBrowser.GetMultiSelected()` to get all selected filenames (instead of `fileBrowser.GetSelected()`, which returns only one of them). 85 | 86 | Here are some common examples: 87 | 88 | ```cpp 89 | // (default) select single regular file for opening 90 | 0 91 | // select multiple regular files for opening 92 | ImGuiFileBrowserFlags_MultipleSelection 93 | // select single directory for opening 94 | ImGuiFileBrowserFlags_SelectDirectory 95 | // select multiple directories for opening 96 | ImGuiFileBrowserFlags_SelectDirectory | ImGuiFileBrowserFlags_MultipleSelection 97 | // select single regular file for saving 98 | ImGuiFileBrowserFlags_EnterNewFilename | ImGuiFileBrowserFlags_CreateNewDir 99 | // select single directory for saving 100 | ImGuiFileBrowserFlags_SelectDirectory | ImGuiFileBrowserFlags_CreateNewDir 101 | // select single directory and hide regular files in browser 102 | ImGuiFileBrowserFlags_SelectDirectory | ImGuiFileBrowserFlags_HideRegularFiles 103 | ``` 104 | 105 | ## Usage 106 | 107 | * When `ImGuiFileBrowserFlags_EditPathString` is set, click the top-right button `#` to directly edit the current directory. 108 | * Click the top-right button `*` to refresh. 109 | * Double click to enter a directory. 110 | * Single click to (de)select a regular file (or directory, if `ImGuiFileBrowserFlags_SelectDirectory` is enabled). 111 | * When `ImGuiFileBrowserFlags_SelectDirectory` is enabled and no item is selected, click `ok` to choose the current directory as selected result. 112 | * When `ImGuiFileBrowserFlags_MultipleSelection` is enabled, hold `Ctrl` for multi selection and `Shift` for range selection. 113 | * When `ImGuiFileBrowserFlags_MultipleSelection` is enabled, use `Ctrl + A` to select all (filtered) items. 114 | * When `ImGuiFileBrowserFlags_CreateNewDir` is enabled, click the top-right button `+` to create a new directory. 115 | * When `ImGuiFileBrowserFlags_SelectDirectory` is not specified, double click to choose a regular file as selected result. 116 | 117 | ## Type Filters 118 | 119 | * Use `SetTypeFilters({".h", ".cpp"})` to set file extension filters. 120 | * `.*` matches with any extension 121 | * Filters are case-insensitive on Windows platform 122 | -------------------------------------------------------------------------------- /imfilebrowser.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #ifndef IMGUI_VERSION 14 | # error "include imgui.h before this header" 15 | #endif 16 | 17 | using ImGuiFileBrowserFlags = std::uint32_t; 18 | 19 | enum ImGuiFileBrowserFlags_ : std::uint32_t 20 | { 21 | ImGuiFileBrowserFlags_SelectDirectory = 1 << 0, // select directory instead of regular file 22 | ImGuiFileBrowserFlags_EnterNewFilename = 1 << 1, // allow user to enter new filename when selecting regular file 23 | ImGuiFileBrowserFlags_NoModal = 1 << 2, // file browsing window is modal by default. specify this to use a popup window 24 | ImGuiFileBrowserFlags_NoTitleBar = 1 << 3, // hide window title bar 25 | ImGuiFileBrowserFlags_NoStatusBar = 1 << 4, // hide status bar at the bottom of browsing window 26 | ImGuiFileBrowserFlags_CloseOnEsc = 1 << 5, // close file browser when pressing 'ESC' 27 | ImGuiFileBrowserFlags_CreateNewDir = 1 << 6, // allow user to create new directory 28 | ImGuiFileBrowserFlags_MultipleSelection = 1 << 7, // allow user to select multiple files. this will hide ImGuiFileBrowserFlags_EnterNewFilename 29 | ImGuiFileBrowserFlags_HideRegularFiles = 1 << 8, // hide regular files when ImGuiFileBrowserFlags_SelectDirectory is enabled 30 | ImGuiFileBrowserFlags_ConfirmOnEnter = 1 << 9, // confirm selection when pressing 'ENTER' 31 | ImGuiFileBrowserFlags_SkipItemsCausingError = 1 << 10, // when entering a new directory, any error will interrupt the process, causing the file browser to fall back to the working directory. 32 | // with this flag, if an error is caused by a specific item in the directory, that item will be skipped, allowing the process to continue. 33 | ImGuiFileBrowserFlags_EditPathString = 1 << 11, // allow user to directly edit the whole path string 34 | }; 35 | 36 | namespace ImGui 37 | { 38 | class FileBrowser 39 | { 40 | public: 41 | 42 | explicit FileBrowser( 43 | ImGuiFileBrowserFlags flags = 0, 44 | std::filesystem::path defaultDirectory = std::filesystem::current_path()); 45 | 46 | FileBrowser(const FileBrowser ©From); 47 | 48 | FileBrowser &operator=(const FileBrowser ©From); 49 | 50 | // set the window position (in pixels) 51 | // default is centered 52 | void SetWindowPos(int posX, int posY) noexcept; 53 | 54 | // set the window size (in pixels) 55 | // default is (700, 450) 56 | void SetWindowSize(int width, int height) noexcept; 57 | 58 | // set the window title text 59 | void SetTitle(std::string title); 60 | 61 | // open the browsing window 62 | void Open(); 63 | 64 | // close the browsing window 65 | void Close(); 66 | 67 | // the browsing window is opened or not 68 | bool IsOpened() const noexcept; 69 | 70 | // display the browsing window if opened 71 | void Display(); 72 | 73 | // returns true when there is a selected filename 74 | bool HasSelected() const noexcept; 75 | 76 | // set current browsing directory 77 | bool SetDirectory(const std::filesystem::path &dir = std::filesystem::current_path()); 78 | 79 | // legacy interface. use SetDirectory instead. 80 | bool SetPwd(const std::filesystem::path &dir = std::filesystem::current_path()) 81 | { 82 | return SetDirectory(dir); 83 | } 84 | 85 | // get current browsing directory 86 | const std::filesystem::path &GetDirectory() const noexcept; 87 | 88 | // legacy interface. use GetDirectory instead. 89 | const std::filesystem::path &GetPwd() const noexcept 90 | { 91 | return GetDirectory(); 92 | } 93 | 94 | // returns selected filename. make sense only when HasSelected returns true 95 | // when ImGuiFileBrowserFlags_MultipleSelection is enabled, only one of 96 | // selected filename will be returned 97 | std::filesystem::path GetSelected() const; 98 | 99 | // returns all selected filenames. 100 | // when ImGuiFileBrowserFlags_MultipleSelection is enabled, use this 101 | // instead of GetSelected 102 | std::vector GetMultiSelected() const; 103 | 104 | // set selected filename to empty 105 | void ClearSelected(); 106 | 107 | // (optional) set file type filters. eg. { ".h", ".cpp", ".hpp" } 108 | // ".*" matches any file types 109 | void SetTypeFilters(const std::vector &typeFilters); 110 | 111 | // set currently applied type filter 112 | // default value is 0 (the first type filter) 113 | void SetCurrentTypeFilterIndex(int index); 114 | 115 | // when ImGuiFileBrowserFlags_EnterNewFilename is set 116 | // this function will pre-fill the input dialog with a filename. 117 | void SetInputName(std::string_view input); 118 | 119 | private: 120 | 121 | template 122 | struct ScopeGuard 123 | { 124 | ScopeGuard(Functor&& t) : func(std::move(t)) { } 125 | 126 | ~ScopeGuard() { func(); } 127 | 128 | private: 129 | 130 | Functor func; 131 | }; 132 | 133 | struct FileRecord 134 | { 135 | bool isDir = false; 136 | std::filesystem::path name; 137 | std::string showName; 138 | std::filesystem::path extension; 139 | }; 140 | 141 | static std::string ToLower(const std::string &s); 142 | 143 | void ToolTip(const std::string_view &s); 144 | 145 | void UpdateFileRecords(); 146 | 147 | void SetCurrentDirectoryUncatched(const std::filesystem::path &pwd); 148 | 149 | bool SetCurrentDirectoryInternal( 150 | const std::filesystem::path &dir, 151 | const std::filesystem::path &preferredFallback); 152 | 153 | bool IsExtensionMatched(const std::filesystem::path &extension) const; 154 | 155 | void ClearRangeSelectionState(); 156 | 157 | static void AssignToArrayStyleString(std::vector &arr, std::string_view content); 158 | 159 | static int ExpandInputBuffer(ImGuiInputTextCallbackData *callbackData); 160 | 161 | #ifdef _WIN32 162 | static std::uint32_t GetDrivesBitMask(); 163 | #endif 164 | 165 | // for c++17 compatibility 166 | 167 | #if defined(__cpp_lib_char8_t) 168 | static std::string u8StrToStr(std::u8string s); 169 | #endif 170 | static std::string u8StrToStr(std::string s); 171 | 172 | static std::filesystem::path u8StrToPath(const char *str); 173 | 174 | int width_; 175 | int height_; 176 | int posX_; 177 | int posY_; 178 | ImGuiFileBrowserFlags flags_; 179 | std::filesystem::path defaultDirectory_; 180 | 181 | std::string title_; 182 | std::string openLabel_; 183 | 184 | bool shouldOpen_; 185 | bool shouldClose_; 186 | bool isOpened_; 187 | bool isOk_; 188 | bool isPosSet_; 189 | 190 | std::string statusStr_; 191 | 192 | std::vector typeFilters_; 193 | unsigned int typeFilterIndex_; 194 | bool hasAllFilter_; 195 | 196 | std::filesystem::path currentDirectory_; 197 | std::vector fileRecords_; 198 | 199 | unsigned int rangeSelectionStart_; // enable range selection when shift is pressed 200 | std::set selectedFilenames_; 201 | 202 | std::string openNewDirLabel_; 203 | std::vector newDirNameBuffer_; 204 | std::vector inputNameBuffer_; 205 | std::string customizedInputName_; 206 | 207 | bool editDir_; 208 | bool setFocusToEditDir_; 209 | std::vector currDirBuffer_; 210 | 211 | #ifdef _WIN32 212 | std::uint32_t drives_; 213 | #endif 214 | }; 215 | } // namespace ImGui 216 | 217 | inline ImGui::FileBrowser::FileBrowser(ImGuiFileBrowserFlags flags, std::filesystem::path defaultDirectory) 218 | : width_(700) 219 | , height_(450) 220 | , posX_(0) 221 | , posY_(0) 222 | , flags_(flags) 223 | , defaultDirectory_(std::move(defaultDirectory)) 224 | , shouldOpen_(false) 225 | , shouldClose_(false) 226 | , isOpened_(false) 227 | , isOk_(false) 228 | , isPosSet_(false) 229 | , rangeSelectionStart_(0) 230 | , editDir_(false) 231 | , setFocusToEditDir_(false) 232 | { 233 | assert(!((flags_ & ImGuiFileBrowserFlags_SelectDirectory) && (flags_ & ImGuiFileBrowserFlags_EnterNewFilename)) && 234 | "'EnterNewFilename' doesn't work when 'SelectDirectory' is enabled"); 235 | if(flags_ & ImGuiFileBrowserFlags_CreateNewDir) 236 | { 237 | newDirNameBuffer_.resize(32, '\0'); 238 | } 239 | 240 | SetTitle("file browser"); 241 | SetDirectory(defaultDirectory_); 242 | 243 | typeFilters_.clear(); 244 | typeFilterIndex_ = 0; 245 | hasAllFilter_ = false; 246 | 247 | #ifdef _WIN32 248 | drives_ = GetDrivesBitMask(); 249 | #endif 250 | } 251 | 252 | inline ImGui::FileBrowser::FileBrowser(const FileBrowser ©From) 253 | : FileBrowser() 254 | { 255 | *this = copyFrom; 256 | } 257 | 258 | inline ImGui::FileBrowser &ImGui::FileBrowser::operator=( 259 | const FileBrowser ©From) 260 | { 261 | width_ = copyFrom.width_; 262 | height_ = copyFrom.height_; 263 | 264 | posX_ = copyFrom.posX_; 265 | posY_ = copyFrom.posY_; 266 | 267 | flags_ = copyFrom.flags_; 268 | SetTitle(copyFrom.title_); 269 | 270 | shouldOpen_ = copyFrom.shouldOpen_; 271 | shouldClose_ = copyFrom.shouldClose_; 272 | isOpened_ = copyFrom.isOpened_; 273 | isOk_ = copyFrom.isOk_; 274 | isPosSet_ = copyFrom.isPosSet_; 275 | 276 | statusStr_ = ""; 277 | 278 | typeFilters_ = copyFrom.typeFilters_; 279 | typeFilterIndex_ = copyFrom.typeFilterIndex_; 280 | hasAllFilter_ = copyFrom.hasAllFilter_; 281 | 282 | selectedFilenames_ = copyFrom.selectedFilenames_; 283 | rangeSelectionStart_ = copyFrom.rangeSelectionStart_; 284 | 285 | currentDirectory_ = copyFrom.currentDirectory_; 286 | fileRecords_ = copyFrom.fileRecords_; 287 | 288 | openNewDirLabel_ = copyFrom.openNewDirLabel_; 289 | newDirNameBuffer_ = copyFrom.newDirNameBuffer_; 290 | inputNameBuffer_ = copyFrom.inputNameBuffer_; 291 | customizedInputName_ = copyFrom.customizedInputName_; 292 | 293 | editDir_ = copyFrom.editDir_; 294 | currDirBuffer_ = copyFrom.currDirBuffer_; 295 | 296 | #ifdef _WIN32 297 | drives_ = copyFrom.drives_; 298 | #endif 299 | 300 | return *this; 301 | } 302 | 303 | inline void ImGui::FileBrowser::SetWindowPos(int posX, int posY) noexcept 304 | { 305 | posX_ = posX; 306 | posY_ = posY; 307 | isPosSet_ = true; 308 | } 309 | 310 | inline void ImGui::FileBrowser::SetWindowSize(int width, int height) noexcept 311 | { 312 | assert(width > 0 && height > 0); 313 | width_ = width; 314 | height_ = height; 315 | } 316 | 317 | inline void ImGui::FileBrowser::SetTitle(std::string title) 318 | { 319 | title_ = std::move(title); 320 | 321 | const std::string thisPtrStr = std::to_string(reinterpret_cast(this)); 322 | openLabel_ = title_ + "##filebrowser_" + thisPtrStr; 323 | openNewDirLabel_ = "new dir##new_dir_" + thisPtrStr; 324 | } 325 | 326 | inline void ImGui::FileBrowser::Open() 327 | { 328 | UpdateFileRecords(); 329 | ClearSelected(); 330 | statusStr_ = std::string(); 331 | shouldOpen_ = true; 332 | shouldClose_ = false; 333 | if((flags_ & ImGuiFileBrowserFlags_EnterNewFilename) && !customizedInputName_.empty()) 334 | { 335 | AssignToArrayStyleString(inputNameBuffer_, customizedInputName_); 336 | selectedFilenames_ = { u8StrToPath(inputNameBuffer_.data()) }; 337 | } 338 | } 339 | 340 | inline void ImGui::FileBrowser::Close() 341 | { 342 | ClearSelected(); 343 | statusStr_ = std::string(); 344 | shouldClose_ = true; 345 | shouldOpen_ = false; 346 | } 347 | 348 | inline bool ImGui::FileBrowser::IsOpened() const noexcept 349 | { 350 | return isOpened_; 351 | } 352 | 353 | inline void ImGui::FileBrowser::Display() 354 | { 355 | PushID(this); 356 | ScopeGuard exitThis([this] 357 | { 358 | shouldOpen_ = false; 359 | shouldClose_ = false; 360 | PopID(); 361 | }); 362 | 363 | if(shouldOpen_) 364 | { 365 | OpenPopup(openLabel_.c_str()); 366 | } 367 | isOpened_ = false; 368 | 369 | // open the popup window 370 | 371 | if(shouldOpen_ && (flags_ & ImGuiFileBrowserFlags_NoModal)) 372 | { 373 | if(isPosSet_) 374 | { 375 | SetNextWindowPos(ImVec2(static_cast(posX_), static_cast(posY_))); 376 | } 377 | SetNextWindowSize(ImVec2(static_cast(width_), static_cast(height_))); 378 | } 379 | else 380 | { 381 | if(isPosSet_) 382 | { 383 | SetNextWindowPos(ImVec2(static_cast(posX_), static_cast(posY_)), ImGuiCond_FirstUseEver); 384 | } 385 | SetNextWindowSize(ImVec2(static_cast(width_), static_cast(height_)), ImGuiCond_FirstUseEver); 386 | } 387 | if(flags_ & ImGuiFileBrowserFlags_NoModal) 388 | { 389 | if(!BeginPopup(openLabel_.c_str())) 390 | { 391 | return; 392 | } 393 | } 394 | else if(!BeginPopupModal(openLabel_.c_str(), nullptr, 395 | flags_ & ImGuiFileBrowserFlags_NoTitleBar ? ImGuiWindowFlags_NoTitleBar : 0)) 396 | { 397 | return; 398 | } 399 | 400 | isOpened_ = true; 401 | ScopeGuard endPopup([] { EndPopup(); }); 402 | 403 | std::filesystem::path newDir; bool shouldSetNewDir = false; 404 | 405 | if(editDir_) 406 | { 407 | if(setFocusToEditDir_) // Automatically set the text box to be focused on appearing 408 | { 409 | SetKeyboardFocusHere(); 410 | } 411 | 412 | PushItemWidth(-1); 413 | const bool enter = InputText( 414 | "##directory", currDirBuffer_.data(), currDirBuffer_.size(), 415 | ImGuiInputTextFlags_CallbackResize | ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll, 416 | ExpandInputBuffer, &currDirBuffer_); 417 | PopItemWidth(); 418 | 419 | if(!IsItemActive() && !setFocusToEditDir_) 420 | { 421 | editDir_ = false; 422 | } 423 | setFocusToEditDir_ = false; 424 | 425 | if(enter) 426 | { 427 | std::filesystem::path enteredDir = u8StrToPath(currDirBuffer_.data()); 428 | if(is_directory(enteredDir)) 429 | { 430 | newDir = std::move(enteredDir); 431 | shouldSetNewDir = true; 432 | } 433 | else if(is_directory(enteredDir.parent_path())) 434 | { 435 | newDir = enteredDir.parent_path(); 436 | shouldSetNewDir = true; 437 | } 438 | else 439 | { 440 | statusStr_ = "[" + std::string(currDirBuffer_.data()) + "] is not a valid directory"; 441 | } 442 | } 443 | } 444 | else 445 | { 446 | // display elements in pwd 447 | 448 | #ifdef _WIN32 449 | const char currentDrive = static_cast(currentDirectory_.c_str()[0]); 450 | const char driveStr[] = { currentDrive, ':', '\0' }; 451 | 452 | PushItemWidth(4 * GetFontSize()); 453 | if(BeginCombo("##select_drive", driveStr)) 454 | { 455 | ScopeGuard guard([&] { EndCombo(); }); 456 | 457 | for(int i = 0; i < 26; ++i) 458 | { 459 | if(!(drives_ & (1 << i))) 460 | { 461 | continue; 462 | } 463 | 464 | const char driveCh = static_cast('A' + i); 465 | const char selectableStr[] = { driveCh, ':', '\0' }; 466 | const bool selected = currentDrive == driveCh; 467 | 468 | if(Selectable(selectableStr, selected) && !selected) 469 | { 470 | char newPwd[] = { driveCh, ':', '\\', '\0' }; 471 | SetDirectory(newPwd); 472 | } 473 | } 474 | } 475 | PopItemWidth(); 476 | 477 | SameLine(); 478 | #endif 479 | 480 | int secIdx = 0, newDirLastSecIdx = -1; 481 | for(const auto &sec : currentDirectory_) 482 | { 483 | #ifdef _WIN32 484 | if(secIdx == 1) 485 | { 486 | ++secIdx; 487 | continue; 488 | } 489 | #endif 490 | 491 | PushID(secIdx); 492 | if(secIdx > 0) 493 | { 494 | SameLine(); 495 | } 496 | if(SmallButton(u8StrToStr(sec.u8string()).c_str())) 497 | { 498 | newDirLastSecIdx = secIdx; 499 | } 500 | PopID(); 501 | 502 | ++secIdx; 503 | } 504 | 505 | if(newDirLastSecIdx >= 0) 506 | { 507 | int i = 0; 508 | std::filesystem::path dstDir; 509 | for(const auto &sec : currentDirectory_) 510 | { 511 | if(i++ > newDirLastSecIdx) 512 | { 513 | break; 514 | } 515 | dstDir /= sec; 516 | } 517 | 518 | #ifdef _WIN32 519 | if(newDirLastSecIdx == 0) 520 | { 521 | dstDir /= "\\"; 522 | } 523 | #endif 524 | 525 | SetDirectory(dstDir); 526 | } 527 | 528 | if(flags_ & ImGuiFileBrowserFlags_EditPathString) 529 | { 530 | SameLine(); 531 | 532 | if(SmallButton("#")) 533 | { 534 | const auto currDirStr = u8StrToStr(currentDirectory_.u8string()); 535 | currDirBuffer_.resize(currDirStr.size() + 1); 536 | std::memcpy(currDirBuffer_.data(), currDirStr.data(), currDirStr.size()); 537 | currDirBuffer_.back() = '\0'; 538 | 539 | editDir_ = true; 540 | setFocusToEditDir_ = true; 541 | } 542 | else 543 | { 544 | ToolTip("Edit the current path"); 545 | } 546 | } 547 | } 548 | 549 | SameLine(); 550 | if(SmallButton("*")) 551 | { 552 | #ifdef _WIN32 553 | drives_ = GetDrivesBitMask(); 554 | #endif 555 | 556 | UpdateFileRecords(); 557 | 558 | std::set newSelectedFilenames; 559 | for(auto &name : selectedFilenames_) 560 | { 561 | const auto it = std::find_if( 562 | fileRecords_.begin(), fileRecords_.end(), [&](const FileRecord &record) 563 | { 564 | return name == record.name; 565 | }); 566 | if(it != fileRecords_.end()) 567 | { 568 | newSelectedFilenames.insert(name); 569 | } 570 | } 571 | 572 | if((flags_ & ImGuiFileBrowserFlags_EnterNewFilename) && !inputNameBuffer_.empty() && inputNameBuffer_[0]) 573 | { 574 | newSelectedFilenames.insert(u8StrToPath(inputNameBuffer_.data())); 575 | } 576 | } 577 | else 578 | { 579 | ToolTip("Refresh"); 580 | } 581 | 582 | bool focusOnInputText = false; 583 | if(flags_ & ImGuiFileBrowserFlags_CreateNewDir) 584 | { 585 | SameLine(); 586 | if(SmallButton("+")) 587 | { 588 | OpenPopup(openNewDirLabel_.c_str()); 589 | newDirNameBuffer_[0] = '\0'; 590 | } 591 | else 592 | { 593 | ToolTip("Create a new directory"); 594 | } 595 | 596 | if(BeginPopup(openNewDirLabel_.c_str())) 597 | { 598 | ScopeGuard endNewDirPopup([] { EndPopup(); }); 599 | 600 | InputText( 601 | "name", newDirNameBuffer_.data(), newDirNameBuffer_.size(), 602 | ImGuiInputTextFlags_CallbackResize, ExpandInputBuffer, &newDirNameBuffer_); 603 | focusOnInputText |= IsItemFocused(); 604 | SameLine(); 605 | 606 | if(Button("ok") && newDirNameBuffer_[0] != '\0') 607 | { 608 | ScopeGuard closeNewDirPopup([] { CloseCurrentPopup(); }); 609 | if(create_directory(currentDirectory_ / u8StrToPath(newDirNameBuffer_.data()))) 610 | { 611 | UpdateFileRecords(); 612 | } 613 | else 614 | { 615 | statusStr_ = "failed to create " + std::string(newDirNameBuffer_.data()); 616 | } 617 | } 618 | } 619 | } 620 | 621 | // browse files in a child window 622 | 623 | float reserveHeight = GetFrameHeightWithSpacing(); 624 | if(flags_ & ImGuiFileBrowserFlags_EnterNewFilename) 625 | { 626 | reserveHeight += GetFrameHeightWithSpacing(); 627 | } 628 | 629 | { 630 | BeginChild("ch", ImVec2(0, -reserveHeight), true, 631 | (flags_ & ImGuiFileBrowserFlags_NoModal) ? ImGuiWindowFlags_AlwaysHorizontalScrollbar : 0); 632 | ScopeGuard endChild([] { EndChild(); }); 633 | 634 | const bool shouldHideRegularFiles = 635 | (flags_ & ImGuiFileBrowserFlags_HideRegularFiles) && (flags_ & ImGuiFileBrowserFlags_SelectDirectory); 636 | 637 | for(unsigned int rscIndex = 0; rscIndex < fileRecords_.size(); ++rscIndex) 638 | { 639 | const auto &rsc = fileRecords_[rscIndex]; 640 | if(!rsc.isDir && shouldHideRegularFiles) 641 | { 642 | continue; 643 | } 644 | if(!rsc.isDir && !IsExtensionMatched(rsc.extension)) 645 | { 646 | continue; 647 | } 648 | if(!rsc.name.empty() && rsc.name.c_str()[0] == '$') 649 | { 650 | continue; 651 | } 652 | 653 | const bool selected = selectedFilenames_.find(rsc.name) != selectedFilenames_.end(); 654 | 655 | #if IMGUI_VERSION_NUM >= 19100 656 | const ImGuiSelectableFlags selectableFlag = ImGuiSelectableFlags_NoAutoClosePopups; 657 | #else 658 | const ImGuiSelectableFlags selectableFlag = ImGuiSelectableFlags_DontClosePopups; 659 | #endif 660 | 661 | if(Selectable(rsc.showName.c_str(), selected, selectableFlag)) 662 | { 663 | const bool wantDir = flags_ & ImGuiFileBrowserFlags_SelectDirectory; 664 | const bool canSelect = rsc.name != ".." && rsc.isDir == wantDir; 665 | const bool rangeSelect = 666 | canSelect && GetIO().KeyShift && 667 | rangeSelectionStart_ < fileRecords_.size() && 668 | (flags_ & ImGuiFileBrowserFlags_MultipleSelection) && 669 | IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); 670 | const bool multiSelect = 671 | !rangeSelect && GetIO().KeyCtrl && 672 | (flags_ & ImGuiFileBrowserFlags_MultipleSelection) && 673 | IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); 674 | 675 | if(rangeSelect) 676 | { 677 | const unsigned int first = (std::min)(rangeSelectionStart_, rscIndex); 678 | const unsigned int last = (std::max)(rangeSelectionStart_, rscIndex); 679 | selectedFilenames_.clear(); 680 | for(unsigned int i = first; i <= last; ++i) 681 | { 682 | if(fileRecords_[i].isDir != wantDir) 683 | { 684 | continue; 685 | } 686 | if(!wantDir && !IsExtensionMatched(fileRecords_[i].extension)) 687 | { 688 | continue; 689 | } 690 | selectedFilenames_.insert(fileRecords_[i].name); 691 | } 692 | } 693 | else if(selected) 694 | { 695 | if(!multiSelect) 696 | { 697 | selectedFilenames_ = { rsc.name }; 698 | rangeSelectionStart_ = rscIndex; 699 | } 700 | else 701 | { 702 | selectedFilenames_.erase(rsc.name); 703 | } 704 | if(flags_ & ImGuiFileBrowserFlags_EnterNewFilename) 705 | { 706 | AssignToArrayStyleString(inputNameBuffer_, ""); 707 | } 708 | } 709 | else if(canSelect) 710 | { 711 | if(multiSelect) 712 | { 713 | selectedFilenames_.insert(rsc.name); 714 | } 715 | else 716 | { 717 | selectedFilenames_ = { rsc.name }; 718 | } 719 | if(flags_ & ImGuiFileBrowserFlags_EnterNewFilename) 720 | { 721 | const auto rscName = u8StrToStr(rsc.name.u8string()); 722 | AssignToArrayStyleString(inputNameBuffer_, rscName); 723 | } 724 | rangeSelectionStart_ = rscIndex; 725 | } 726 | } 727 | 728 | if(IsMouseDoubleClicked(ImGuiMouseButton_Left) && IsItemHovered(ImGuiHoveredFlags_None)) 729 | { 730 | if(rsc.isDir) 731 | { 732 | shouldSetNewDir = true; 733 | newDir = (rsc.name != "..") ? (currentDirectory_ / rsc.name) : currentDirectory_.parent_path(); 734 | } 735 | else if(!(flags_ & ImGuiFileBrowserFlags_SelectDirectory)) 736 | { 737 | selectedFilenames_ = { rsc.name }; 738 | isOk_ = true; 739 | CloseCurrentPopup(); 740 | } 741 | } 742 | else if(IsKeyPressed(ImGuiKey_GamepadFaceDown) && IsItemHovered()) 743 | { 744 | if(rsc.isDir) 745 | { 746 | shouldSetNewDir = true; 747 | newDir = (rsc.name != "..") ? (currentDirectory_ / rsc.name) : currentDirectory_.parent_path(); 748 | SetKeyboardFocusHere(-1); 749 | } 750 | else if(!(flags_ & ImGuiFileBrowserFlags_SelectDirectory)) 751 | { 752 | selectedFilenames_ = { rsc.name }; 753 | isOk_ = true; 754 | CloseCurrentPopup(); 755 | } 756 | } 757 | } 758 | } 759 | 760 | if(shouldSetNewDir) 761 | { 762 | SetDirectory(newDir); 763 | } 764 | 765 | if(flags_ & ImGuiFileBrowserFlags_EnterNewFilename) 766 | { 767 | PushID(this); 768 | ScopeGuard popTextID([] { PopID(); }); 769 | 770 | if(inputNameBuffer_.empty()) 771 | { 772 | inputNameBuffer_.resize(1, '\0'); 773 | } 774 | 775 | PushItemWidth(-1); 776 | if(InputText( 777 | "", inputNameBuffer_.data(), inputNameBuffer_.size(), 778 | ImGuiInputTextFlags_CallbackResize, ExpandInputBuffer, &inputNameBuffer_)) 779 | { 780 | if(inputNameBuffer_[0] != '\0') 781 | { 782 | selectedFilenames_ = { u8StrToPath(inputNameBuffer_.data()) }; 783 | } 784 | else 785 | { 786 | selectedFilenames_.clear(); 787 | } 788 | } 789 | focusOnInputText |= IsItemFocused(); 790 | PopItemWidth(); 791 | } 792 | 793 | if(!focusOnInputText && !editDir_) 794 | { 795 | const bool selectAll = (flags_ & ImGuiFileBrowserFlags_MultipleSelection) && 796 | IsKeyPressed(ImGuiKey_A) && (IsKeyDown(ImGuiKey_LeftCtrl) || 797 | IsKeyDown(ImGuiKey_RightCtrl)); 798 | if(selectAll) 799 | { 800 | const bool needDir = flags_ & ImGuiFileBrowserFlags_SelectDirectory; 801 | selectedFilenames_.clear(); 802 | for(size_t i = 1; i < fileRecords_.size(); ++i) 803 | { 804 | auto &record = fileRecords_[i]; 805 | if(record.isDir == needDir && 806 | (needDir || IsExtensionMatched(record.extension))) 807 | { 808 | selectedFilenames_.insert(record.name); 809 | } 810 | } 811 | } 812 | } 813 | 814 | const bool isEnterPressed = 815 | (flags_ & ImGuiFileBrowserFlags_ConfirmOnEnter) && 816 | IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && 817 | IsKeyPressed(ImGuiKey_Enter); 818 | if(!(flags_ & ImGuiFileBrowserFlags_SelectDirectory)) 819 | { 820 | BeginDisabled(selectedFilenames_.empty()); 821 | const bool ok = Button("ok"); 822 | EndDisabled(); 823 | if((ok || isEnterPressed) && !selectedFilenames_.empty()) 824 | { 825 | isOk_ = true; 826 | CloseCurrentPopup(); 827 | } 828 | } 829 | else 830 | { 831 | if(Button(" ok ") || isEnterPressed) 832 | { 833 | isOk_ = true; 834 | CloseCurrentPopup(); 835 | } 836 | } 837 | 838 | SameLine(); 839 | 840 | const bool shouldClose = 841 | Button("cancel") || shouldClose_ || 842 | ((flags_ & ImGuiFileBrowserFlags_CloseOnEsc) && 843 | IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && 844 | IsKeyPressed(ImGuiKey_Escape)); 845 | if(shouldClose) 846 | { 847 | CloseCurrentPopup(); 848 | } 849 | 850 | if(!statusStr_.empty() && !(flags_ & ImGuiFileBrowserFlags_NoStatusBar)) 851 | { 852 | SameLine(); 853 | Text("%s", statusStr_.c_str()); 854 | if (ImGui::IsItemHovered()) 855 | { 856 | ImGui::BeginTooltip(); 857 | ImGui::PushTextWrapPos(300.0f); 858 | ImGui::Text("%s", statusStr_.c_str()); 859 | ImGui::PopTextWrapPos(); 860 | ImGui::EndTooltip(); 861 | } 862 | } 863 | 864 | if(!typeFilters_.empty()) 865 | { 866 | SameLine(); 867 | PushItemWidth(8 * GetFontSize()); 868 | if(BeginCombo( 869 | "##type_filters", typeFilters_[typeFilterIndex_].c_str())) 870 | { 871 | ScopeGuard guard([&] { EndCombo(); }); 872 | 873 | for(size_t i = 0; i < typeFilters_.size(); ++i) 874 | { 875 | bool selected = i == typeFilterIndex_; 876 | if(Selectable(typeFilters_[i].c_str(), selected) && !selected) 877 | { 878 | typeFilterIndex_ = static_cast(i); 879 | } 880 | } 881 | } 882 | PopItemWidth(); 883 | } 884 | } 885 | 886 | inline bool ImGui::FileBrowser::HasSelected() const noexcept 887 | { 888 | return isOk_; 889 | } 890 | 891 | inline bool ImGui::FileBrowser::SetDirectory(const std::filesystem::path &dir) 892 | { 893 | const std::filesystem::path preferredFallback = this->GetDirectory(); 894 | return SetCurrentDirectoryInternal(dir, preferredFallback); 895 | } 896 | 897 | inline const std::filesystem::path &ImGui::FileBrowser::GetDirectory() const noexcept 898 | { 899 | return currentDirectory_; 900 | } 901 | 902 | inline std::filesystem::path ImGui::FileBrowser::GetSelected() const 903 | { 904 | // when isOk_ is true, selectedFilenames_ may be empty if SelectDirectory 905 | // is enabled. return pwd in that case. 906 | if(selectedFilenames_.empty()) 907 | { 908 | return currentDirectory_; 909 | } 910 | return currentDirectory_ / *selectedFilenames_.begin(); 911 | } 912 | 913 | inline std::vector ImGui::FileBrowser::GetMultiSelected() const 914 | { 915 | if(selectedFilenames_.empty()) 916 | { 917 | return { currentDirectory_ }; 918 | } 919 | 920 | std::vector ret; 921 | ret.reserve(selectedFilenames_.size()); 922 | for(auto &s : selectedFilenames_) 923 | { 924 | ret.push_back(currentDirectory_ / s); 925 | } 926 | 927 | return ret; 928 | } 929 | 930 | inline void ImGui::FileBrowser::ClearSelected() 931 | { 932 | selectedFilenames_.clear(); 933 | if((flags_ & ImGuiFileBrowserFlags_EnterNewFilename)) 934 | { 935 | AssignToArrayStyleString(inputNameBuffer_, ""); 936 | } 937 | isOk_ = false; 938 | } 939 | 940 | inline void ImGui::FileBrowser::SetTypeFilters(const std::vector &_typeFilters) 941 | { 942 | typeFilters_.clear(); 943 | 944 | // remove duplicate filter names due to case unsensitivity on windows 945 | 946 | #ifdef _WIN32 947 | 948 | std::vector typeFilters; 949 | for(auto &rawFilter : _typeFilters) 950 | { 951 | std::string lowerFilter = ToLower(rawFilter); 952 | const auto it = std::find(typeFilters.begin(), typeFilters.end(), lowerFilter); 953 | if(it == typeFilters.end()) 954 | { 955 | typeFilters.push_back(std::move(lowerFilter)); 956 | } 957 | } 958 | 959 | #else 960 | 961 | auto &typeFilters = _typeFilters; 962 | 963 | #endif 964 | 965 | // insert auto-generated filter 966 | hasAllFilter_ = false; 967 | if(typeFilters.size() > 1) 968 | { 969 | hasAllFilter_ = true; 970 | std::string allFiltersName = std::string(); 971 | for(size_t i = 0; i < typeFilters.size(); ++i) 972 | { 973 | if(typeFilters[i] == std::string_view(".*")) 974 | { 975 | hasAllFilter_ = false; 976 | break; 977 | } 978 | 979 | if(i > 0) 980 | { 981 | allFiltersName += ","; 982 | } 983 | allFiltersName += typeFilters[i]; 984 | } 985 | 986 | if(hasAllFilter_) 987 | { 988 | typeFilters_.push_back(std::move(allFiltersName)); 989 | } 990 | } 991 | 992 | std::copy(typeFilters.begin(), typeFilters.end(), std::back_inserter(typeFilters_)); 993 | typeFilterIndex_ = 0; 994 | } 995 | 996 | inline void ImGui::FileBrowser::SetCurrentTypeFilterIndex(int index) 997 | { 998 | typeFilterIndex_ = static_cast(index); 999 | } 1000 | 1001 | inline void ImGui::FileBrowser::SetInputName(std::string_view input) 1002 | { 1003 | assert((flags_ & ImGuiFileBrowserFlags_EnterNewFilename) && 1004 | "SetInputName can only be called when ImGuiFileBrowserFlags_EnterNewFilename is enabled"); 1005 | customizedInputName_ = input; 1006 | } 1007 | 1008 | inline std::string ImGui::FileBrowser::ToLower(const std::string &s) 1009 | { 1010 | std::string ret = s; 1011 | for(char &c : ret) 1012 | { 1013 | c = static_cast(std::tolower(c)); 1014 | } 1015 | return ret; 1016 | } 1017 | 1018 | inline void ImGui::FileBrowser::ToolTip(const std::string_view &s) 1019 | { 1020 | if (!ImGui::IsItemHovered()) 1021 | { 1022 | return; 1023 | } 1024 | ImGui::SetTooltip("%s", s.data()); 1025 | } 1026 | 1027 | inline void ImGui::FileBrowser::UpdateFileRecords() 1028 | { 1029 | fileRecords_ = { FileRecord{ true, "..", "[D] ..", "" } }; 1030 | 1031 | const auto getDirectoryIterator = [&]() -> std::filesystem::directory_iterator 1032 | { 1033 | try 1034 | { 1035 | return std::filesystem::directory_iterator(currentDirectory_); 1036 | } 1037 | catch (const std::filesystem::filesystem_error& err) 1038 | { 1039 | statusStr_ = std::string("error: ") + err.what(); 1040 | if (!(flags_ & ImGuiFileBrowserFlags_SkipItemsCausingError)) 1041 | { 1042 | throw; 1043 | } 1044 | return {}; 1045 | } 1046 | }; 1047 | 1048 | for(auto &p : getDirectoryIterator()) 1049 | { 1050 | FileRecord rcd; 1051 | try 1052 | { 1053 | if(p.is_regular_file()) 1054 | { 1055 | rcd.isDir = false; 1056 | } 1057 | else if(p.is_directory()) 1058 | { 1059 | rcd.isDir = true; 1060 | } 1061 | else 1062 | { 1063 | continue; 1064 | } 1065 | 1066 | rcd.name = p.path().filename(); 1067 | if(rcd.name.empty()) 1068 | { 1069 | continue; 1070 | } 1071 | 1072 | rcd.extension = p.path().filename().extension(); 1073 | rcd.showName = (rcd.isDir ? "[D] " : "[F] ") + u8StrToStr(p.path().filename().u8string()); 1074 | } 1075 | catch(...) 1076 | { 1077 | if(!(flags_ & ImGuiFileBrowserFlags_SkipItemsCausingError)) 1078 | { 1079 | throw; 1080 | } 1081 | continue; 1082 | } 1083 | fileRecords_.push_back(rcd); 1084 | } 1085 | 1086 | // The default lexicographical order does not meet our sorting requirements. 1087 | // We want [b0, a0, A1] to be sorted into something like [a0, A1, b0] instead of [a0, b0, A1]. 1088 | // Therefore, here we compute a custom key for each filename for sorting. 1089 | if(fileRecords_.size() > 2) 1090 | { 1091 | std::vector> keys; 1092 | keys.reserve(fileRecords_.size()); 1093 | for(auto &fileRecord : fileRecords_) 1094 | { 1095 | const auto name = u8StrToStr(fileRecord.name.u8string()); 1096 | auto& key = keys.emplace_back(); 1097 | key.reserve(name.size() + 1); 1098 | key.emplace_back(!fileRecord.isDir); 1099 | for(char c : name) 1100 | { 1101 | if('A' <= c && c <= 'Z') 1102 | { 1103 | key.emplace_back(2 * (c + 'a' - 'A') + 1); 1104 | } 1105 | else 1106 | { 1107 | key.emplace_back(2 * c); 1108 | } 1109 | } 1110 | } 1111 | 1112 | std::vector fileRecordRemapIndices; 1113 | fileRecordRemapIndices.reserve(fileRecords_.size()); 1114 | for(uint32_t i = 0; i < fileRecords_.size(); ++i) 1115 | { 1116 | fileRecordRemapIndices.push_back(i); 1117 | } 1118 | 1119 | std::sort( 1120 | fileRecordRemapIndices.begin() + 1, fileRecordRemapIndices.end(), [&](uint32_t li, uint32_t ri) 1121 | { 1122 | return keys[li] < keys[ri]; 1123 | }); 1124 | 1125 | std::vector remappedFileRecords; 1126 | remappedFileRecords.reserve(fileRecords_.size()); 1127 | for(const uint32_t index : fileRecordRemapIndices) 1128 | { 1129 | remappedFileRecords.emplace_back(std::move(fileRecords_[index])); 1130 | } 1131 | 1132 | fileRecords_ = std::move(remappedFileRecords); 1133 | } 1134 | 1135 | ClearRangeSelectionState(); 1136 | } 1137 | 1138 | inline void ImGui::FileBrowser::SetCurrentDirectoryUncatched(const std::filesystem::path &pwd) 1139 | { 1140 | currentDirectory_ = absolute(pwd); 1141 | UpdateFileRecords(); 1142 | 1143 | bool shouldClearInputNameBuffer = true; 1144 | 1145 | if((flags_ & ImGuiFileBrowserFlags_EnterNewFilename) && 1146 | selectedFilenames_.size() == 1 && 1147 | !customizedInputName_.empty() && 1148 | !inputNameBuffer_.empty() && 1149 | std::strcmp(inputNameBuffer_.data(), customizedInputName_.data()) == 0) 1150 | { 1151 | shouldClearInputNameBuffer = false; 1152 | } 1153 | 1154 | if(shouldClearInputNameBuffer) 1155 | { 1156 | selectedFilenames_.clear(); 1157 | AssignToArrayStyleString(inputNameBuffer_, ""); 1158 | } 1159 | } 1160 | 1161 | inline bool ImGui::FileBrowser::SetCurrentDirectoryInternal( 1162 | const std::filesystem::path &dir, const std::filesystem::path &preferredFallback) 1163 | { 1164 | try 1165 | { 1166 | SetCurrentDirectoryUncatched(dir); 1167 | return true; 1168 | } 1169 | catch(const std::exception &err) 1170 | { 1171 | statusStr_ = std::string("error: ") + err.what(); 1172 | } 1173 | catch(...) 1174 | { 1175 | statusStr_ = "unknown error"; 1176 | } 1177 | 1178 | if(preferredFallback != defaultDirectory_) 1179 | { 1180 | try 1181 | { 1182 | SetCurrentDirectoryUncatched(preferredFallback); 1183 | } 1184 | catch(...) 1185 | { 1186 | SetCurrentDirectoryUncatched(defaultDirectory_); 1187 | } 1188 | } 1189 | else 1190 | { 1191 | SetCurrentDirectoryUncatched(defaultDirectory_); 1192 | } 1193 | 1194 | return false; 1195 | } 1196 | 1197 | inline bool ImGui::FileBrowser::IsExtensionMatched(const std::filesystem::path &_extension) const 1198 | { 1199 | #ifdef _WIN32 1200 | std::filesystem::path extension = ToLower(u8StrToStr(_extension.u8string())); 1201 | #else 1202 | auto &extension = _extension; 1203 | #endif 1204 | 1205 | // no type filters 1206 | if(typeFilters_.empty()) 1207 | { 1208 | return true; 1209 | } 1210 | 1211 | // invalid type filter index 1212 | if(static_cast(typeFilterIndex_) >= typeFilters_.size()) 1213 | { 1214 | return true; 1215 | } 1216 | 1217 | // all type filters 1218 | if(hasAllFilter_ && typeFilterIndex_ == 0) 1219 | { 1220 | for(size_t i = 1; i < typeFilters_.size(); ++i) 1221 | { 1222 | if(extension == typeFilters_[i]) 1223 | { 1224 | return true; 1225 | } 1226 | } 1227 | return false; 1228 | } 1229 | 1230 | // universal filter 1231 | if(typeFilters_[typeFilterIndex_] == std::string_view(".*")) 1232 | { 1233 | return true; 1234 | } 1235 | 1236 | // regular filter 1237 | return extension == typeFilters_[typeFilterIndex_]; 1238 | } 1239 | 1240 | inline void ImGui::FileBrowser::ClearRangeSelectionState() 1241 | { 1242 | rangeSelectionStart_ = 9999999; 1243 | const bool dir = flags_ & ImGuiFileBrowserFlags_SelectDirectory; 1244 | for(unsigned int i = 1; i < fileRecords_.size(); ++i) 1245 | { 1246 | if(fileRecords_[i].isDir == dir) 1247 | { 1248 | if(!dir && !IsExtensionMatched(fileRecords_[i].extension)) 1249 | { 1250 | continue; 1251 | } 1252 | rangeSelectionStart_ = i; 1253 | break; 1254 | } 1255 | } 1256 | } 1257 | 1258 | inline void ImGui::FileBrowser::AssignToArrayStyleString(std::vector &arr, std::string_view content) 1259 | { 1260 | if(content.empty()) 1261 | { 1262 | if(!arr.empty()) 1263 | { 1264 | arr[0] = '\0'; 1265 | } 1266 | return; 1267 | } 1268 | 1269 | if(arr.size() < content.size() + 1) 1270 | { 1271 | arr.resize(content.size() + 1); 1272 | } 1273 | std::memcpy(arr.data(), content.data(), content.size()); 1274 | arr[content.size()] = '\0'; 1275 | } 1276 | 1277 | inline int ImGui::FileBrowser::ExpandInputBuffer(ImGuiInputTextCallbackData *callbackData) 1278 | { 1279 | if(callbackData && callbackData->EventFlag & ImGuiInputTextFlags_CallbackResize) 1280 | { 1281 | auto buffer = static_cast*>(callbackData->UserData); 1282 | size_t newSize = buffer->size(); 1283 | while(newSize < static_cast(callbackData->BufSize)) 1284 | { 1285 | newSize <<= 1; 1286 | } 1287 | buffer->resize(newSize, '\0'); 1288 | callbackData->Buf = buffer->data(); 1289 | callbackData->BufDirty = true; 1290 | } 1291 | return 0; 1292 | } 1293 | 1294 | #if defined(__cpp_lib_char8_t) 1295 | inline std::string ImGui::FileBrowser::u8StrToStr(std::u8string s) 1296 | { 1297 | std::string result; 1298 | result.resize(s.length()); 1299 | std::memcpy(result.data(), s.data(), s.length()); 1300 | return result; 1301 | } 1302 | #endif 1303 | 1304 | inline std::string ImGui::FileBrowser::u8StrToStr(std::string s) 1305 | { 1306 | return s; 1307 | } 1308 | 1309 | inline std::filesystem::path ImGui::FileBrowser::u8StrToPath(const char *str) 1310 | { 1311 | #if defined(__cpp_lib_char8_t) 1312 | // With C++20/23, it's impossible to efficiently convert a `char*` string to a `char8_t*` string without violating 1313 | // the strict aliasing rule. Bad joke! 1314 | const size_t len = std::strlen(str); 1315 | std::u8string u8Str; 1316 | u8Str.resize(len); 1317 | std::memcpy(u8Str.data(), str, len); 1318 | return std::filesystem::path(u8Str); 1319 | #else 1320 | // u8path is deprecated in C++20 1321 | return std::filesystem::u8path(str); 1322 | #endif 1323 | } 1324 | 1325 | #ifdef _WIN32 1326 | 1327 | inline std::uint32_t ImGui::FileBrowser::GetDrivesBitMask() 1328 | { 1329 | std::uint32_t ret = 0; 1330 | for(int i = 0; i < 26; ++i) 1331 | { 1332 | const char rootName[4] = { static_cast('A' + i), ':', '\\', '\0' }; 1333 | try{ 1334 | if (std::filesystem::exists(rootName)) 1335 | { 1336 | ret |= (1 << i); 1337 | } 1338 | } 1339 | catch (const std::filesystem::filesystem_error &) 1340 | { 1341 | // Ignore invalid paths or inaccessible drives, e.g., empty CD drives or network shares 1342 | } 1343 | } 1344 | return ret; 1345 | } 1346 | 1347 | #endif 1348 | --------------------------------------------------------------------------------