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 0000000..069d2f2 Binary files /dev/null and b/imgui-filebrowser/screenshots/0.png differ diff --git a/meson.build b/meson.build index 328ff39..a65453c 100644 --- a/meson.build +++ b/meson.build @@ -34,7 +34,7 @@ include_dirs = [ 'assets', ] -res = run_command('./assets/update-assets.sh') +res = run_command('./assets/update-assets.sh', check: true) exe = executable('player', srcs, dependencies: deps,