diff --git a/.gitignore b/.gitignore index 6a42dc76..2d38f50b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ build/ code/ .vscode/ tools/ -modding/* \ No newline at end of file +modding/* +imgui.ini \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 05ebc206..076e5b6f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,8 +17,10 @@ option(BUILD_FZERO "Build with F-Zero X support" ON) option(BUILD_MARIO_ARTIST "Build with Mario Artist support" ON) option(BUILD_NAUDIO "Build with NAudio support" ON) +option(BUILD_UI "Build with UI support" ON) + if(EMSCRIPTEN) - set(BUILD_SM64 OFF) # TODO: This is broken for some reason + set(BUILD_SM64 OFF) add_definitions(-D__EMSCRIPTEN__) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --bind -lidbfs.js -s MODULARIZE=1 -s EXPORT_ES6=1 -s NO_DISABLE_EXCEPTION_CATCHING -s ALLOW_MEMORY_GROWTH=1 -s EXPORT_NAME=createModule -s EXPORTED_RUNTIME_METHODS='[\"FS\", \"IDBFS\"]'") if(EMSCRIPTEN_PRELOAD) @@ -61,14 +63,53 @@ if(USE_STANDALONE) endforeach() endif() endif() + +if(BUILD_UI) + add_definitions(-DBUILD_UI) + FetchContent_Declare( + raylib + GIT_REPOSITORY https://github.com/raysan5/raylib.git + GIT_TAG 767df4cf526b78a64393a790d252bea21b71627c + ) + FetchContent_MakeAvailable(raylib) + + FetchContent_Declare( + ImGui + GIT_REPOSITORY https://github.com/ocornut/imgui.git + GIT_TAG v1.90.6-docking + ) + FetchContent_MakeAvailable(ImGui) + + set(UI_FILES + ${imgui_SOURCE_DIR}/imgui_demo.cpp + ${imgui_SOURCE_DIR}/imgui_draw.cpp + ${imgui_SOURCE_DIR}/imgui_tables.cpp + ${imgui_SOURCE_DIR}/imgui_widgets.cpp + ${imgui_SOURCE_DIR}/misc/cpp/imgui_stdlib.cpp + ${imgui_SOURCE_DIR}/imgui.cpp + ) + + include_directories(${imgui_SOURCE_DIR}) + + FetchContent_Declare( + rlImGui + GIT_REPOSITORY https://github.com/raylib-extras/rlImGui.git + GIT_TAG 3c6986358c001851d9bc20e55acf9de803c73b5d + ) + FetchContent_MakeAvailable(rlImGui) + + list(APPEND UI_FILES ${rlimgui_SOURCE_DIR}/rlImGui.cpp) + include_directories(${rlimgui_SOURCE_DIR}) +endif() + # Source files include_directories(${CMAKE_CURRENT_SOURCE_DIR}) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/lib) -include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/src/lib) file(GLOB_RECURSE CXX_FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/**/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/lib/strhash64/*.cpp) file(GLOB C_FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/*.c ${CMAKE_CURRENT_SOURCE_DIR}/src/**/*.c ${CMAKE_CURRENT_SOURCE_DIR}/lib/libmio0/*.c ${CMAKE_CURRENT_SOURCE_DIR}/lib/libyay0/*.c) -set(SRC_DIR ${CXX_FILES} ${C_FILES} ${LGFXD_FILES}) +set(SRC_DIR ${CXX_FILES} ${C_FILES} ${LGFXD_FILES} ${UI_FILES}) if(BUILD_SM64) add_definitions(-DSM64_SUPPORT) @@ -278,4 +319,15 @@ if(NOT USE_STANDALONE) target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/lib) target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) target_include_directories(${PROJECT_NAME} PUBLIC ${yaml-cpp_SOURCE_DIR}/include) +elseif(BUILD_UI) + target_link_libraries(${PROJECT_NAME} PRIVATE raylib) endif() + +if(EMSCRIPTEN) + # Copy builded .js and .wasm to docs/ + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_CURRENT_SOURCE_DIR}/docs/torch.js + COMMAND ${CMAKE_COMMAND} -E copy $/$.wasm ${CMAKE_CURRENT_SOURCE_DIR}/docs/torch.wasm + COMMAND ${CMAKE_COMMAND} -E copy $/$.data ${CMAKE_CURRENT_SOURCE_DIR}/docs/torch.data + ) +endif() \ No newline at end of file diff --git a/lib/n64graphics/n64graphics.c b/lib/n64graphics/n64graphics.c index 46a1b8a1..e1d9f7de 100644 --- a/lib/n64graphics/n64graphics.c +++ b/lib/n64graphics/n64graphics.c @@ -506,7 +506,9 @@ int ci2png(unsigned char **png_output, int *size_output, const ci *img, int widt } } + #ifndef BUILD_UI (*png_output) = stbi_write_plte_png_to_mem(data, 0, width, height, 1, NULL, 0, size_output); + #endif free(data); } diff --git a/lib/n64graphics/stb_image.c b/lib/n64graphics/stb_image.c index 8ddfd1f5..eb0dc55c 100644 --- a/lib/n64graphics/stb_image.c +++ b/lib/n64graphics/stb_image.c @@ -1,2 +1,4 @@ -#define STB_IMAGE_IMPLEMENTATION +#ifndef BUILD_UI +#define STB_IMAGE_WRITE_IMPLEMENTATION +#endif #include "stb_image.h" diff --git a/lib/n64graphics/stb_image_write.c b/lib/n64graphics/stb_image_write.c index 2f540c3c..84349524 100644 --- a/lib/n64graphics/stb_image_write.c +++ b/lib/n64graphics/stb_image_write.c @@ -1,2 +1,4 @@ +#ifndef BUILD_UI #define STB_IMAGE_WRITE_IMPLEMENTATION +#endif #include \ No newline at end of file diff --git a/src/Companion.cpp b/src/Companion.cpp index 231b24c2..c912c5df 100644 --- a/src/Companion.cpp +++ b/src/Companion.cpp @@ -125,102 +125,105 @@ static std::string GetTypeNode(YAML::Node& node) { return type; } -void Companion::Init(const ExportType type) { +void Companion::Init(const ExportType type, const bool runProcess) { spdlog::set_level(spdlog::level::debug); spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] %v"); this->gConfig.exporterType = type; - this->RegisterFactory("BLOB", std::make_shared()); - this->RegisterFactory("TEXTURE", std::make_shared()); - this->RegisterFactory("VTX", std::make_shared()); - this->RegisterFactory("MTX", std::make_shared()); - this->RegisterFactory("F32", std::make_shared()); - this->RegisterFactory("INC", std::make_shared()); - this->RegisterFactory("LIGHTS", std::make_shared()); - this->RegisterFactory("GFX", std::make_shared()); - this->RegisterFactory("VEC3F", std::make_shared()); - this->RegisterFactory("VEC3S", std::make_shared()); - this->RegisterFactory("ARRAY", std::make_shared()); - this->RegisterFactory("ASSET_ARRAY", std::make_shared()); - this->RegisterFactory("VP", std::make_shared()); - this->RegisterFactory("COMPRESSED_TEXTURE", std::make_shared()); + REGISTER_FACTORY("BLOB", std::make_shared()); + REGISTER_FACTORY("TEXTURE", std::make_shared(), std::make_shared()); + REGISTER_FACTORY("VTX", std::make_shared()); + REGISTER_FACTORY("MTX", std::make_shared()); + REGISTER_FACTORY("F32", std::make_shared()); + REGISTER_FACTORY("INC", std::make_shared()); + REGISTER_FACTORY("LIGHTS", std::make_shared()); + REGISTER_FACTORY("GFX", std::make_shared()); + REGISTER_FACTORY("VEC3F", std::make_shared(), std::make_shared()); + REGISTER_FACTORY("VEC3S", std::make_shared(), std::make_shared()); + REGISTER_FACTORY("ARRAY", std::make_shared()); + REGISTER_FACTORY("ASSET_ARRAY", std::make_shared()); + REGISTER_FACTORY("VP", std::make_shared()); + REGISTER_FACTORY("COMPRESSED_TEXTURE", std::make_shared()); #ifdef SM64_SUPPORT - this->RegisterFactory("SM64:DIALOG", std::make_shared()); - this->RegisterFactory("SM64:TEXT", std::make_shared()); - this->RegisterFactory("SM64:DICTIONARY", std::make_shared()); - this->RegisterFactory("SM64:ANIM", std::make_shared()); - this->RegisterFactory("SM64:BEHAVIOR_SCRIPT", std::make_shared()); - this->RegisterFactory("SM64:COLLISION", std::make_shared()); - this->RegisterFactory("SM64:GEO_LAYOUT", std::make_shared()); - this->RegisterFactory("SM64:LEVEL_SCRIPT", std::make_shared()); - this->RegisterFactory("SM64:MACRO", std::make_shared()); - this->RegisterFactory("SM64:MOVTEX_QUAD", std::make_shared()); - this->RegisterFactory("SM64:MOVTEX", std::make_shared()); - this->RegisterFactory("SM64:PAINTING", std::make_shared()); - this->RegisterFactory("SM64:PAINTING_MAP", std::make_shared()); - this->RegisterFactory("SM64:TRAJECTORY", std::make_shared()); - this->RegisterFactory("SM64:WATER_DROPLET", std::make_shared()); + REGISTER_FACTORY("SM64:DIALOG", std::make_shared()); + REGISTER_FACTORY("SM64:TEXT", std::make_shared()); + REGISTER_FACTORY("SM64:DICTIONARY", std::make_shared()); + REGISTER_FACTORY("SM64:ANIM", std::make_shared()); + REGISTER_FACTORY("SM64:BEHAVIOR_SCRIPT", std::make_shared()); + REGISTER_FACTORY("SM64:COLLISION", std::make_shared()); + REGISTER_FACTORY("SM64:GEO_LAYOUT", std::make_shared()); + REGISTER_FACTORY("SM64:LEVEL_SCRIPT", std::make_shared()); + REGISTER_FACTORY("SM64:MACRO", std::make_shared()); + REGISTER_FACTORY("SM64:MOVTEX_QUAD", std::make_shared()); + REGISTER_FACTORY("SM64:MOVTEX", std::make_shared()); + REGISTER_FACTORY("SM64:PAINTING", std::make_shared()); + REGISTER_FACTORY("SM64:PAINTING_MAP", std::make_shared()); + REGISTER_FACTORY("SM64:TRAJECTORY", std::make_shared()); + REGISTER_FACTORY("SM64:WATER_DROPLET", std::make_shared()); #endif #ifdef MK64_SUPPORT - this->RegisterFactory("MK64:COURSE_VTX", std::make_shared()); - this->RegisterFactory("MK64:TRACK_PATH", std::make_shared()); - this->RegisterFactory("MK64:TRACK_SECTIONS", std::make_shared()); - this->RegisterFactory("MK64:SPAWN_DATA", std::make_shared()); - this->RegisterFactory("MK64:UNK_SPAWN_DATA", std::make_shared()); - this->RegisterFactory("MK64:DRIVING_BEHAVIOUR", std::make_shared()); - this->RegisterFactory("MK64:ITEM_CURVE", std::make_shared()); // Item curve for decomp only - this->RegisterFactory("MK64:METADATA", std::make_shared()); + REGISTER_FACTORY("MK64:COURSE_VTX", std::make_shared()); + REGISTER_FACTORY("MK64:TRACK_PATH", std::make_shared()); + REGISTER_FACTORY("MK64:TRACK_SECTIONS", std::make_shared()); + REGISTER_FACTORY("MK64:SPAWN_DATA", std::make_shared()); + REGISTER_FACTORY("MK64:UNK_SPAWN_DATA", std::make_shared()); + REGISTER_FACTORY("MK64:DRIVING_BEHAVIOUR", std::make_shared()); + REGISTER_FACTORY("MK64:ITEM_CURVE", std::make_shared()); // Item curve for decomp only + REGISTER_FACTORY("MK64:METADATA", std::make_shared()); #endif #ifdef SF64_SUPPORT - this->RegisterFactory("SF64:ANIM", std::make_shared()); - this->RegisterFactory("SF64:SKELETON", std::make_shared()); - this->RegisterFactory("SF64:MESSAGE", std::make_shared()); - this->RegisterFactory("SF64:MSG_TABLE", std::make_shared()); - this->RegisterFactory("SF64:SCRIPT", std::make_shared()); - this->RegisterFactory("SF64:HITBOX", std::make_shared()); - this->RegisterFactory("SF64:ENVIRONMENT", std::make_shared()); - this->RegisterFactory("SF64:OBJECT_INIT", std::make_shared()); - this->RegisterFactory("SF64:COLPOLY", std::make_shared()); - this->RegisterFactory("SF64:TRIANGLE", std::make_shared()); + REGISTER_FACTORY("SF64:ANIM", std::make_shared()); + REGISTER_FACTORY("SF64:SKELETON", std::make_shared()); + REGISTER_FACTORY("SF64:MESSAGE", std::make_shared(), std::make_shared()); + REGISTER_FACTORY("SF64:MSG_TABLE", std::make_shared()); + REGISTER_FACTORY("SF64:SCRIPT", std::make_shared()); + REGISTER_FACTORY("SF64:HITBOX", std::make_shared()); + REGISTER_FACTORY("SF64:ENVIRONMENT", std::make_shared()); + REGISTER_FACTORY("SF64:OBJECT_INIT", std::make_shared()); + REGISTER_FACTORY("SF64:COLPOLY", std::make_shared()); + REGISTER_FACTORY("SF64:TRIANGLE", std::make_shared()); #endif #ifdef FZERO_SUPPORT - this->RegisterFactory("FZX:ANIM", std::make_shared()); - this->RegisterFactory("FZX:COURSE", std::make_shared()); - this->RegisterFactory("FZX:GHOST", std::make_shared()); - this->RegisterFactory("FZX:LIMB", std::make_shared()); - this->RegisterFactory("FZX:SEQUENCE", std::make_shared()); - this->RegisterFactory("FZX:SOUNDFONT", std::make_shared()); + REGISTER_FACTORY("FZX:ANIM", std::make_shared()); + REGISTER_FACTORY("FZX:COURSE", std::make_shared()); + REGISTER_FACTORY("FZX:GHOST", std::make_shared()); + REGISTER_FACTORY("FZX:LIMB", std::make_shared()); + REGISTER_FACTORY("FZX:SEQUENCE", std::make_shared()); + REGISTER_FACTORY("FZX:SOUNDFONT", std::make_shared()); #endif #ifdef MARIO_ARTIST_SUPPORT - this->RegisterFactory("MA:MA2D1", std::make_shared()); + REGISTER_FACTORY("MA:MA2D1", std::make_shared()); #endif #ifdef NAUDIO_SUPPORT - this->RegisterFactory("NAUDIO:V0:AUDIO_HEADER", std::make_shared()); - this->RegisterFactory("NAUDIO:V0:SEQUENCE", std::make_shared()); - this->RegisterFactory("NAUDIO:V0:SAMPLE", std::make_shared()); - this->RegisterFactory("NAUDIO:V0:BANK", std::make_shared()); - - this->RegisterFactory("NAUDIO:V1:AUDIO_SETUP", std::make_shared()); - this->RegisterFactory("NAUDIO:V1:AUDIO_TABLE", std::make_shared()); - this->RegisterFactory("NAUDIO:V1:SOUND_FONT", std::make_shared()); - this->RegisterFactory("NAUDIO:V1:INSTRUMENT", std::make_shared()); - this->RegisterFactory("NAUDIO:V1:DRUM", std::make_shared()); - this->RegisterFactory("NAUDIO:V1:SAMPLE", std::make_shared()); - this->RegisterFactory("NAUDIO:V1:ENVELOPE", std::make_shared()); - this->RegisterFactory("NAUDIO:V1:ADPCM_LOOP", std::make_shared()); - this->RegisterFactory("NAUDIO:V1:ADPCM_BOOK", std::make_shared()); - this->RegisterFactory("NAUDIO:V1:SEQUENCE", std::make_shared()); -#endif -#ifndef __EMSCRIPTEN__ // We call this manually - this->Process(); + REGISTER_FACTORY("NAUDIO:V0:AUDIO_HEADER", std::make_shared()); + REGISTER_FACTORY("NAUDIO:V0:SEQUENCE", std::make_shared()); + REGISTER_FACTORY("NAUDIO:V0:SAMPLE", std::make_shared()); + REGISTER_FACTORY("NAUDIO:V0:BANK", std::make_shared()); + + REGISTER_FACTORY("NAUDIO:V1:AUDIO_SETUP", std::make_shared()); + REGISTER_FACTORY("NAUDIO:V1:AUDIO_TABLE", std::make_shared()); + REGISTER_FACTORY("NAUDIO:V1:SOUND_FONT", std::make_shared()); + REGISTER_FACTORY("NAUDIO:V1:INSTRUMENT", std::make_shared()); + REGISTER_FACTORY("NAUDIO:V1:DRUM", std::make_shared()); + REGISTER_FACTORY("NAUDIO:V1:SAMPLE", std::make_shared(), std::make_shared()); + REGISTER_FACTORY("NAUDIO:V1:ENVELOPE", std::make_shared()); + REGISTER_FACTORY("NAUDIO:V1:ADPCM_LOOP", std::make_shared()); + REGISTER_FACTORY("NAUDIO:V1:ADPCM_BOOK", std::make_shared()); + REGISTER_FACTORY("NAUDIO:V1:SEQUENCE", std::make_shared()); #endif + if(runProcess) { + auto start = duration_cast(system_clock::now().time_since_epoch()); + this->Process(); + this->Finalize(start); + this->Exit(); + } } void Companion::ParseEnums(std::string& header) { @@ -392,7 +395,6 @@ void Companion::ParseCurrentFileConfig(YAML::Node node) { this->gCurrentFile = currentFile; this->gCurrentDirectory = currentDirectory; this->gCurrentExternalFiles = currentExternalFiles; - this->gFileHeader.clear(); } else { SPDLOG_INFO("Skipping external file {} as it has already been processed", externalFileName); } @@ -435,14 +437,15 @@ void Companion::ParseCurrentFileConfig(YAML::Node node) { auto virtualAddrMap = node["virtual"]; gVirtualAddrMap[gCurrentFile] = std::make_tuple(virtualAddrMap[0].as(), virtualAddrMap[1].as()); } - + if(node["header"]) { auto header = node["header"]; + std::string tempHeader = ""; switch (this->gConfig.exporterType) { case ExportType::Header: { if(header["header"].IsSequence()) { for(auto line = header["header"].begin(); line != header["header"].end(); ++line) { - this->gFileHeader += line->as() + "\n"; + tempHeader += line->as() + "\n"; } } break; @@ -450,13 +453,14 @@ void Companion::ParseCurrentFileConfig(YAML::Node node) { case ExportType::Code: { if(header["code"].IsSequence()) { for(auto line = header["code"].begin(); line != header["code"].end(); ++line) { - this->gFileHeader += line->as() + "\n"; + tempHeader += line->as() + "\n"; } } break; } default: break; } + this->gFileHeaders[this->gCurrentFile] = tempHeader; } if(node["tables"]){ @@ -468,7 +472,9 @@ void Companion::ParseCurrentFileConfig(YAML::Node node) { auto mode = GetSafeNode(table->second, "mode", "APPEND"); TableMode tMode = mode == "REFERENCE" ? TableMode::Reference : TableMode::Append; auto index_size = GetSafeNode(table->second, "index_size", -1); - this->gTables.push_back({name, start, end, tMode, index_size}); + if(!this->gFileTables[this->gCurrentFile].contains(name)){ + this->gFileTables[this->gCurrentFile][name] = {name, start, end, tMode, index_size}; + } } } @@ -618,6 +624,7 @@ void Companion::ProcessFile(YAML::Node root) { } if(!node["offset"]) { + SPDLOG_WARN("No offset found for {}, skipping address map entry", entryName); continue; } @@ -637,14 +644,12 @@ void Companion::ProcessFile(YAML::Node root) { // Stupid hack because the iteration broke the assets root = YAML::LoadFile(this->gCurrentFile); this->gConfig.segment.local.clear(); - this->gFileHeader.clear(); this->gCurrentPad = 0; this->gCurrentVram = std::nullopt; this->gCurrentVirtualPath = ""; this->gCurrentSegmentNumber = 0; this->gCurrentCompressionType = CompressionType::None; this->gCurrentFileOffset = 0; - this->gTables.clear(); this->gCurrentExternalFiles.clear(); GFXDOverride::ClearVtx(); @@ -653,6 +658,7 @@ void Companion::ProcessFile(YAML::Node root) { } if(!this->NodeHasChanges(this->gCurrentFile) && !this->gNodeForceProcessing) { + SPDLOG_INFO("Skipping file {} as it has not changed", RelativePathToSrcDir(this->gCurrentFile)); return; } @@ -661,11 +667,11 @@ void Companion::ProcessFile(YAML::Node root) { spdlog::set_pattern(line); for(auto asset = root.begin(); asset != root.end(); ++asset){ - auto entryName = asset->first.as(); auto assetNode = asset->second; if(entryName.find(":config") != std::string::npos) { + SPDLOG_INFO("Skipping :config node"); continue; } @@ -692,7 +698,10 @@ void Companion::ProcessFile(YAML::Node root) { SPDLOG_INFO("------------------------------------------------"); spdlog::set_pattern(line); } +} +void Companion::WriteFile(YAML::Node root) { + // Start for(auto& result : this->gParseResults[this->gCurrentFile]){ std::ostringstream stream; ExportResult endptr = std::nullopt; @@ -812,7 +821,7 @@ void Companion::ProcessFile(YAML::Node root) { this->gWriteMap[this->gCurrentFile][result.type].push_back(wEntry); } - + auto fsout = fs::path(this->gConfig.outputPath); if(this->gConfig.exporterType == ExportType::Modding || this->gConfig.exporterType == ExportType::XML) { @@ -933,8 +942,8 @@ void Companion::ProcessFile(YAML::Node root) { std::ofstream file(outinc, std::ios::binary); - if(!this->gFileHeader.empty()) { - file << this->gFileHeader << std::endl; + if(this->gFileHeaders.contains(this->gCurrentFile) && !this->gFileHeaders[this->gCurrentFile].empty()) { + file << this->gFileHeaders[this->gCurrentFile] << std::endl; } file << stream.str(); stream.str(""); @@ -972,16 +981,16 @@ void Companion::ProcessFile(YAML::Node root) { file << "#ifndef " << symbol << "_H" << std::endl; file << "#define " << symbol << "_H" << std::endl << std::endl; } - if(!this->gFileHeader.empty()) { - file << this->gFileHeader << std::endl; + if(!this->gFileHeaders[this->gCurrentFile].empty()) { + file << this->gFileHeaders[this->gCurrentFile] << std::endl; } file << buffer; if(!this->IsOTRMode()){ file << std::endl << "#endif" << std::endl; } } else { - if(!this->gFileHeader.empty()) { - file << this->gFileHeader << std::endl; + if(!this->gFileHeaders[this->gCurrentFile].empty()) { + file << this->gFileHeaders[this->gCurrentFile] << std::endl; } file << buffer; } @@ -993,10 +1002,10 @@ void Companion::ProcessFile(YAML::Node root) { if(this->gConfig.exporterType != ExportType::Binary) { this->gHashNode[RelativePathToSrcDir(this->gCurrentFile)]["extracted"][ExportTypeToString(this->gConfig.exporterType)] = true; } +// End } void Companion::Process() { - auto configPath = this->gSourceDirectory / "config.yml"; if(!fs::exists(configPath)) { @@ -1004,7 +1013,6 @@ void Companion::Process() { return; } - auto start = duration_cast(system_clock::now().time_since_epoch()); YAML::Node config = YAML::LoadFile(configPath.string()); bool isDirectoryMode = config["mode"] && config["mode"].as() == "directory"; @@ -1253,17 +1261,8 @@ void Companion::Process() { if (wrapper) { wrapper->CreateArchive(); } - this->gCurrentWrapper = wrapper; - auto vWriter = LUS::BinaryWriter(); - vWriter.SetEndianness(Torch::Endianness::Big); - vWriter.Write(static_cast(Torch::Endianness::Big)); - - if(this->gConfig.parseMode == ParseMode::Default) { - vWriter.Write(this->gCartridge->GetCRC()); - } else { - vWriter.Write((uint32_t) 0); - } + this->gCurrentWrapper = wrapper; for (const auto & entry : Torch::getRecursiveEntries(this->gAssetPath)){ if(entry.is_directory()) { @@ -1289,6 +1288,20 @@ void Companion::Process() { this->gProcessedFiles.insert(this->gCurrentFile); } } +} + +void Companion::Finalize(std::chrono::milliseconds start) { + BinaryWrapper* wrapper = this->gCurrentWrapper; + + auto vWriter = LUS::BinaryWriter(); + vWriter.SetEndianness(Torch::Endianness::Big); + vWriter.Write(static_cast(Torch::Endianness::Big)); + + if(this->gConfig.parseMode == ParseMode::Default) { + vWriter.Write(this->gCartridge->GetCRC()); + } else { + vWriter.Write((uint32_t) 0); + } if(wrapper != nullptr) { SPDLOG_CRITICAL("Writing version file"); @@ -1297,6 +1310,12 @@ void Companion::Process() { wrapper->Close(); } + for(const auto& file : this->gProcessedFiles) { + this->gCurrentFile = file; + this->gCurrentDirectory = relative(fs::path(file), this->gAssetPath).replace_extension(""); + WriteFile(YAML::LoadFile(file)); + } + // Write entries hash std::ofstream file(this->gDestinationDirectory / "torch.hash.yml", std::ios::binary); file << this->gHashNode; @@ -1309,6 +1328,13 @@ void Companion::Process() { SPDLOG_CRITICAL("------------------------------------------------"); spdlog::set_level(level); spdlog::set_pattern(regular); +} + +void Companion::Exit() { + if(AudioManager::Instance) { + delete AudioManager::Instance; + AudioManager::Instance = nullptr; + } Decompressor::ClearCache(); this->gCartridge = nullptr; @@ -1391,8 +1417,19 @@ std::optional> Companion::RegisterAsset(cons return entry; } -void Companion::RegisterFactory(const std::string& type, const std::shared_ptr& factory) { +void Companion::RegisterFactory( + const std::string& type, + const std::shared_ptr& factory +#ifdef BUILD_UI + , const std::shared_ptr& uiFactory +#endif +) { this->gFactories[type] = factory; +#ifdef BUILD_UI + if(uiFactory != nullptr) { + this->gUIFactories[type] = uiFactory; + } +#endif SPDLOG_INFO("Registered factory for {}", type); } @@ -1404,8 +1441,18 @@ std::optional> Companion::GetFactory(const std::str return this->gFactories[type]; } +#ifdef BUILD_UI +std::optional> Companion::GetUIFactory(const std::string &type) { + if(!this->gUIFactories.contains(type)){ + return std::nullopt; + } + + return this->gUIFactories[type]; +} +#endif + std::optional Companion::SearchTable(uint32_t addr){ - for(auto& table : this->gTables){ + for(auto& [name, table] : this->gFileTables[this->gCurrentFile]){ if(addr >= table.start && addr <= table.end){ return table; } diff --git a/src/Companion.h b/src/Companion.h index 95da2a48..8f16fe42 100644 --- a/src/Companion.h +++ b/src/Companion.h @@ -93,20 +93,13 @@ struct TorchConfig { bool textureDefines; }; -struct ParseResultData { - std::string name; - std::string type; - YAML::Node node; - std::optional> data; - - uint32_t GetOffset() { - return GetSafeNode(node, "offset"); - } - - std::optional GetSymbol() { - return GetSafeNode(node, "symbol"); - } -}; +#ifdef BUILD_UI +#define REGISTER_FACTORY(type, ...) \ + this->RegisterFactory(type, __VA_ARGS__); +#else +#define REGISTER_FACTORY(type, factory) \ + this->RegisterFactory(type, factory); +#endif class Companion { public: @@ -138,11 +131,13 @@ class Companion { explicit Companion(std::vector rom, const ArchiveType otr, const bool debug, const std::string& srcDir = "", const std::string& destPath = "") : Companion(rom, otr, debug, false, srcDir, destPath) {} - void Init(ExportType type); + void Init(ExportType type, const bool runProcess = true); bool NodeHasChanges(const std::string& string); void Process(); + void Finalize(std::chrono::milliseconds start); + void Exit(); bool IsOTRMode() const { return (this->gConfig.otrMode != ArchiveType::None); } bool IsDebug() const { return this->gConfig.debug; } @@ -152,6 +147,7 @@ class Companion { std::vector& GetRomData() { return this->gRomData; } std::string GetOutputPath() { return this->gConfig.outputPath; } std::string GetDestRelativeOutputPath() { return RelativePathToDestDir(GetOutputPath()); } + std::string GetAssetPath() { return this->gAssetPath; } GBIVersion GetGBIVersion() const { return this->gConfig.gbi.version; } GBIMinorVersion GetGBIMinorVersion() const { return this->gConfig.gbi.subversion; } @@ -159,11 +155,15 @@ class Companion { std::optional GetEnumFromValue(const std::string& key, int id); bool IsUsingIndividualIncludes() const { return this->gIndividualIncludes; } + std::unordered_map> GetParseResults() const { return this->gParseResults; } std::optional GetParseDataByAddr(uint32_t addr); std::optional GetParseDataBySymbol(const std::string& symbol); std::optional GetFileOffsetFromSegmentedAddr(uint8_t segment) const; std::optional> GetFactory(const std::string& type); +#ifdef BUILD_UI + std::optional> GetUIFactory(const std::string& type); +#endif uint32_t PatchVirtualAddr(uint32_t addr); std::optional> GetNodeByAddr(uint32_t addr); std::optional> GetSafeNodeByAddr(const uint32_t addr, std::string type); @@ -183,12 +183,13 @@ class Companion { std::string RelativePathToSrcDir(const std::string& path) const; std::string RelativePathToDestDir(const std::string& path) const; void RegisterCompanionFile(const std::string path, std::vector data); - + TorchConfig& GetConfig() { return this->gConfig; } BinaryWrapper* GetCurrentWrapper() { return this->gCurrentWrapper; } std::optional> RegisterAsset(const std::string& name, YAML::Node& node); std::optional AddAsset(YAML::Node asset); + std::unordered_map>> GetAddrMap() const { return this->gAddrMap; } private: TorchConfig gConfig; YAML::Node gModdingConfig; @@ -210,33 +211,41 @@ class Companion { // Temporal Variables std::string gCurrentFile; std::string gCurrentVirtualPath; - std::string gFileHeader; bool gEnablePadGen = false; uint32_t gCurrentPad = 0; uint32_t gCurrentFileOffset; uint32_t gCurrentSegmentNumber; std::optional gCurrentVram; CompressionType gCurrentCompressionType = CompressionType::None; - std::vector
gTables; std::vector gCurrentExternalFiles; std::unordered_set gProcessedFiles; std::unordered_map> gCompanionFiles; std::unordered_map> gParseResults; + std::unordered_map gFileHeaders; + std::unordered_map> gFileTables; std::unordered_map gModdedAssetPaths; std::variant, std::string> gWriteOrder; std::unordered_map> gFactories; + std::unordered_map> gUIFactories; std::unordered_map>> gWriteMap; std::unordered_map> gVirtualAddrMap; std::unordered_map>> gAddrMap; void ProcessFile(YAML::Node root); + void WriteFile(YAML::Node root); void ParseEnums(std::string& file); void ParseHash(); void ParseModdingConfig(); void ParseCurrentFileConfig(YAML::Node node); - void RegisterFactory(const std::string& type, const std::shared_ptr& factory); + void RegisterFactory( + const std::string& type, + const std::shared_ptr& factory +#ifdef BUILD_UI + , const std::shared_ptr& factoryUI = nullptr +#endif + ); void ExtractNode(YAML::Node& node, std::string& name, BinaryWrapper* binary); void ProcessTables(YAML::Node& rom); void LoadYAMLRecursively(const std::string &dirPath, std::vector &result, bool skipRoot); diff --git a/src/factories/BaseFactory.h b/src/factories/BaseFactory.h index 3fdb1cf5..812da97f 100644 --- a/src/factories/BaseFactory.h +++ b/src/factories/BaseFactory.h @@ -13,9 +13,18 @@ #include #include #include +#include "utils/TorchUtils.h" #include "lib/binarytools/BinaryWriter.h" #include "lib/binarytools/BinaryReader.h" +#ifdef BUILD_UI +#define IMGUI_DEFINE_MATH_OPERATORS +#include "imgui.h" +#include "raylib.h" +#include "rlImGui.h" +#include "misc/cpp/imgui_stdlib.h" +#endif + namespace fs = std::filesystem; #define REGISTER(type, c) { ExportType::type, std::make_shared() }, @@ -85,6 +94,21 @@ class GenericData : public IParsedData { T mData; }; +struct ParseResultData { + std::string name; + std::string type; + YAML::Node node; + std::optional> data; + + uint32_t GetOffset() { + return GetSafeNode(node, "offset"); + } + + std::optional GetSymbol() { + return GetSafeNode(node, "symbol"); + } +}; + class BaseExporter { public: virtual ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, YAML::Node& node, std::string* replacement) = 0; @@ -120,4 +144,12 @@ class BaseFactory { virtual std::unordered_map> GetExporters() { return {}; } +}; + +class BaseFactoryUI { +public: + virtual float GetItemHeight(const ParseResultData& data) { + return ImGui::GetTextLineHeightWithSpacing(); + }; + virtual void DrawUI(const ParseResultData& data) = 0; }; \ No newline at end of file diff --git a/src/factories/TextureFactory.cpp b/src/factories/TextureFactory.cpp index 8e9d53ca..d138f3cf 100644 --- a/src/factories/TextureFactory.cpp +++ b/src/factories/TextureFactory.cpp @@ -190,23 +190,16 @@ ExportResult TextureBinaryExporter::Export(std::ostream &write, std::shared_ptr< return std::nullopt; } -ExportResult TextureModdingExporter::Export(std::ostream&write, std::shared_ptr data, std::string&entryName, YAML::Node&node, std::string* replacement) { - auto texture = std::static_pointer_cast(data); +uint8_t* ConvertTextureToRGBA(std::shared_ptr texture, YAML::Node &node, int* size) { auto format = texture->mFormat; uint8_t* raw = new uint8_t[TextureUtils::CalculateTextureSize(format.type, texture->mWidth, texture->mHeight) * 2]; - int size = 0; - - auto ext = GetSafeNode(node, "format"); - - std::transform(ext.begin(), ext.end(), ext.begin(), tolower); - *replacement += "." + ext + ".png"; switch (format.type) { case TextureType::TLUT: case TextureType::RGBA16bpp: case TextureType::RGBA32bpp: { rgba* imgr = raw2rgba(texture->mBuffer.data(), texture->mWidth, texture->mHeight, format.depth); - if(rgba2png(&raw, &size, imgr, texture->mWidth, texture->mHeight)) { + if(rgba2png(&raw, size, imgr, texture->mWidth, texture->mHeight)) { throw std::runtime_error("Failed to convert texture to PNG"); } break; @@ -216,7 +209,7 @@ ExportResult TextureModdingExporter::Export(std::ostream&write, std::shared_ptr< case TextureType::GrayscaleAlpha4bpp: case TextureType::GrayscaleAlpha1bpp: { ia* imgia = raw2ia(texture->mBuffer.data(), texture->mWidth, texture->mHeight, format.depth); - if(ia2png(&raw, &size, imgia, texture->mWidth, texture->mHeight)) { + if(ia2png(&raw, size, imgia, texture->mWidth, texture->mHeight)) { throw std::runtime_error("Failed to convert texture to PNG"); } break; @@ -229,10 +222,11 @@ ExportResult TextureModdingExporter::Export(std::ostream&write, std::shared_ptr< if (palette.has_value()) { auto palTexture = std::static_pointer_cast(palette.value().data.value()); - convert_raw_to_ci8(&raw, &size, texture->mBuffer.data(), (uint8_t *)palTexture->mBuffer.data(), 0, texture->mWidth, texture->mHeight, texture->mFormat.depth, palTexture->mFormat.depth); + convert_raw_to_ci8(&raw, size, texture->mBuffer.data(), (uint8_t *)palTexture->mBuffer.data(), 0, texture->mWidth, texture->mHeight, texture->mFormat.depth, palTexture->mFormat.depth); } else { auto symbol = GetSafeNode(node, "symbol"); - throw std::runtime_error("Could not convert ci8 '"+symbol+"' the tlut symbol name is probably wrong for tlut_symbol node"); + SPDLOG_ERROR("Could not convert ci8 '{}' the tlut symbol name is probably wrong for tlut_symbol node", symbol); + return nullptr; } break; } @@ -243,10 +237,11 @@ ExportResult TextureModdingExporter::Export(std::ostream&write, std::shared_ptr< if (palette.has_value()) { auto palTexture = std::static_pointer_cast(palette.value().data.value()); - convert_raw_to_ci8(&raw, &size, texture->mBuffer.data(), (uint8_t *)palTexture->mBuffer.data(), 0, texture->mWidth, texture->mHeight, texture->mFormat.depth, palTexture->mFormat.depth); + convert_raw_to_ci8(&raw, size, texture->mBuffer.data(), (uint8_t *)palTexture->mBuffer.data(), 0, texture->mWidth, texture->mHeight, texture->mFormat.depth, palTexture->mFormat.depth); } else { auto symbol = GetSafeNode(node, "symbol"); - throw std::runtime_error("Could not convert ci8 '"+symbol+"' the address is probably wrong for tlut address node"); + SPDLOG_ERROR("Could not convert ci8 '{}' the address is probably wrong for tlut address node", symbol); + return nullptr; } break; } @@ -254,20 +249,39 @@ ExportResult TextureModdingExporter::Export(std::ostream&write, std::shared_ptr< case TextureType::Grayscale8bpp: case TextureType::Grayscale4bpp: { ia* imgi = raw2i(texture->mBuffer.data(), texture->mWidth, texture->mHeight, format.depth); - if(ia2png(&raw, &size, imgi, texture->mWidth, texture->mHeight)) { + if(ia2png(&raw, size, imgi, texture->mWidth, texture->mHeight)) { throw std::runtime_error("Failed to convert texture to PNG"); } break; } default: { - SPDLOG_ERROR("Unsupported texture format for modding: {}", ext); + return nullptr; } } - write.write(reinterpret_cast(raw), size); - return std::nullopt; + return raw; } +ExportResult TextureModdingExporter::Export(std::ostream&write, std::shared_ptr data, std::string&entryName, YAML::Node&node, std::string* replacement) { + auto texture = std::static_pointer_cast(data); + auto format = texture->mFormat; + int size = 0; + + auto ext = GetSafeNode(node, "format"); + std::transform(ext.begin(), ext.end(), ext.begin(), tolower); + *replacement += "." + ext + ".png"; + + uint8_t* raw = ConvertTextureToRGBA(texture, node, &size); + + if(raw == nullptr) { + write.write(reinterpret_cast(raw), size); + SPDLOG_ERROR("Unsupported texture format for modding: {}", ext); + } + + delete[] raw; + + return std::nullopt; +} std::optional> TextureFactory::parse(std::vector& buffer, YAML::Node& node) { auto offset = GetSafeNode(node, "offset"); @@ -464,3 +478,73 @@ std::optional> TextureFactory::parse_modding(std::v return std::make_shared(fmt, width, height, result); } + +std::unordered_map sLoadedTextures = {}; + +Texture2D* GetOrLoadTexture(const ParseResultData& item) { + auto texture = std::static_pointer_cast(item.data.value()); + auto symbol = GetSafeNode(const_cast(item.node), "symbol", item.name); + auto format = GetSafeNode(const_cast(item.node), "format"); + auto offset = GetSafeNode(const_cast(item.node), "offset"); + + if (!sLoadedTextures.contains(symbol)) { + int size = 0; + uint8_t* raw = ConvertTextureToRGBA(texture, const_cast(item.node), &size); + if (raw) { + Image image = LoadImageFromMemory(".png", raw, size); + SPDLOG_INFO("Loaded texture: {} (0x{:X}) {}x{}, {} bytes)", symbol, offset, texture->mWidth, texture->mHeight, size); + sLoadedTextures[symbol] = LoadTextureFromImage(image); + delete[] raw; + } else { + SPDLOG_ERROR("Unsupported texture format: {}", format); + return nullptr; + } + } + + if (sLoadedTextures.contains(symbol)) { + return &sLoadedTextures[symbol]; + } else { + SPDLOG_ERROR("Failed to load texture: {}", symbol); + return nullptr; + } +} + +float TextureFactoryUI::GetItemHeight(const ParseResultData& item) { + return 150.0f; +} + +void TextureFactoryUI::DrawUI(const ParseResultData& item) { + auto texture = std::static_pointer_cast(item.data.value()); + auto symbol = GetSafeNode(const_cast(item.node), "symbol", item.name); + auto format = GetSafeNode(const_cast(item.node), "format"); + auto offset = GetSafeNode(const_cast(item.node), "offset"); + auto tex2d = GetOrLoadTexture(item); + + ImGui::Text("%s", item.name.c_str()); + auto cursorPos = ImGui::GetCursorPos(); + if (tex2d != nullptr) { + float aspectRatio = (float)tex2d->width / (float)tex2d->height; + float displayWidth = 128.0f; + float displayHeight = displayWidth / aspectRatio; + if (displayHeight > 128.0f) { + displayHeight = 128.0f; + displayWidth = displayHeight * aspectRatio; + } + ImGui::Image((ImTextureID)tex2d, ImVec2(displayWidth, displayHeight)); + } else { + // Draw a placeholder box + ImGui::SetCursorPos(cursorPos); + ImGui::Dummy(ImVec2(128, 128)); + ImGui::SetCursorPos(cursorPos); + ImGui::GetWindowDrawList()->AddRectFilled(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), IM_COL32(100, 100, 100, 255)); + ImGui::SetCursorPos(ImGui::GetCursorPos() + ImVec2(32, 56)); + ImGui::GetWindowDrawList()->AddText(ImGui::GetItemRectMin(), IM_COL32(0, 0, 0, 255), "No Preview"); + } + ImGui::SameLine(); + ImGui::BeginGroup(); + ImGui::Text("Format: %s (%dbpp)", format.c_str(), texture->mFormat.depth); + ImGui::Text("Resolution: %dx%d", texture->mWidth, texture->mHeight); + ImGui::Text("Size: %zu bytes", texture->mBuffer.size()); + ImGui::Text("Offset: %s", Torch::to_hex(offset).c_str()); + ImGui::EndGroup(); +} \ No newline at end of file diff --git a/src/factories/TextureFactory.h b/src/factories/TextureFactory.h index 9fa0006b..2a0398db 100644 --- a/src/factories/TextureFactory.h +++ b/src/factories/TextureFactory.h @@ -42,4 +42,10 @@ class TextureFactory : public BaseFactory { }; } bool SupportModdedAssets() override { return true; } +}; + +class TextureFactoryUI : public BaseFactoryUI { +public: + float GetItemHeight(const ParseResultData& data) override; + void DrawUI(const ParseResultData& data) override; }; \ No newline at end of file diff --git a/src/factories/naudio/v1/SampleFactory.cpp b/src/factories/naudio/v1/SampleFactory.cpp index 616d8837..75f9a2b6 100644 --- a/src/factories/naudio/v1/SampleFactory.cpp +++ b/src/factories/naudio/v1/SampleFactory.cpp @@ -185,3 +185,84 @@ std::optional> NSampleFactory::parse(std::vector(data.data.value()); + float height = 140.0f; + + if(sample->loop != 0) { + height += 20.0f; + auto ldata = Companion::Instance->GetParseDataByAddr(sample->loop); + if(ldata.has_value() && ldata->data.has_value()) { + auto loop = std::static_pointer_cast(ldata->data.value()); + if(loop->count != 0) { + height += 20.0f * 16; + } + } + } + + if(sample->book != 0) { + height += 20.0f; + auto bdata = Companion::Instance->GetParseDataByAddr(sample->book); + if(bdata.has_value() && bdata->data.has_value()) { + auto book = std::static_pointer_cast(bdata->data.value()); + height += 20.0f * book->book.size(); + } + } + + return height; +} + +void NSampleFactoryUI::DrawUI(const ParseResultData& item) { + auto sample = std::static_pointer_cast(item.data.value()); + auto symbol = GetSafeNode(const_cast(item.node), "symbol", item.name); + + ImGui::Text("%s", symbol.c_str()); + ImGui::Text("Codec: %s", AudioContext::GetCodecStr(sample->codec)); + ImGui::Text("Medium: %s", AudioContext::GetMediumStr(sample->medium)); + ImGui::Text("bit26: %d", sample->unk); + ImGui::Text("Size: %d bytes", sample->size); + ImGui::Text("Tuning: %.2f", sample->tuning); + ImGui::Text("Sample Rate: %d Hz", sample->sampleRate); + + if(sample->loop != 0) { + auto ldata = Companion::Instance->GetParseDataByAddr(sample->loop); + if(!ldata.has_value() || !ldata->data.has_value()) { + ImGui::TextDisabled("Invalid Loop"); + return; + } + auto loop = std::static_pointer_cast(Companion::Instance->GetParseDataByAddr(sample->loop)->data.value()); + if (ImGui::TreeNode("ADPCM Loop")) { + ImGui::Text("Start: %d", loop->start); + ImGui::Text("End: %d", loop->end); + ImGui::Text("Count: %d", loop->count); + if (loop->count != 0) { + for (size_t i = 0; i < 16; i++) { + ImGui::Text("Predictor %zu: %d", i, loop->predictorState[i]); + } + } + ImGui::TreePop(); + } + } else { + ImGui::TextDisabled("No Loop"); + } + + if(sample->book != 0) { + auto bdata = Companion::Instance->GetParseDataByAddr(sample->book); + if(!bdata.has_value() || !bdata->data.has_value()) { + ImGui::TextDisabled("Invalid Book"); + return; + } + auto book = std::static_pointer_cast(bdata->data.value()); + if (ImGui::TreeNode("ADPCM Book")) { + ImGui::Text("Order: %d", book->order); + ImGui::Text("Npredictors: %d", book->numPredictors); + for (size_t i = 0; i < book->book.size(); i++) { + ImGui::Text("Page %zu: %d", i, book->book[i]); + } + ImGui::TreePop(); + } + } else { + ImGui::TextDisabled("No Book"); + } +} \ No newline at end of file diff --git a/src/factories/naudio/v1/SampleFactory.h b/src/factories/naudio/v1/SampleFactory.h index eb7cff55..d1ea4e8f 100644 --- a/src/factories/naudio/v1/SampleFactory.h +++ b/src/factories/naudio/v1/SampleFactory.h @@ -54,3 +54,9 @@ class NSampleFactory : public BaseFactory { } bool SupportModdedAssets() override { return true; } }; + +class NSampleFactoryUI : public BaseFactoryUI { +public: + float GetItemHeight(const ParseResultData& data) override; + void DrawUI(const ParseResultData& data) override; +}; \ No newline at end of file diff --git a/src/factories/sf64/MessageFactory.cpp b/src/factories/sf64/MessageFactory.cpp index ca1a6243..8f5aacb6 100644 --- a/src/factories/sf64/MessageFactory.cpp +++ b/src/factories/sf64/MessageFactory.cpp @@ -186,9 +186,12 @@ std::optional> SF64::MessageFactory::parse(std::vec uint16_t c; std::string whitespace = ""; + std::string str; + do { c = reader.ReadUInt16(); message.push_back(c); + str += gASCIIFullTable[c]; std::string enumCode = gCharCodeEnums[c]; if((enumCode.find("SP") != std::string::npos) && whitespace.empty()) { @@ -209,7 +212,7 @@ std::optional> SF64::MessageFactory::parse(std::vec } while(c != END_CODE); - return std::make_shared(message, mesgStr.str()); + return std::make_shared(message, mesgStr.str(), str); } std::optional getCharByCode(const std::string& code) { @@ -292,5 +295,21 @@ std::optional> SF64::MessageFactory::parse_modding( message.push_back(END_CODE); - return std::make_shared(message, mesgStr.str()); + return std::make_shared(message, mesgStr.str(), ""); +} + +float SF64::MessageFactoryUI::GetItemHeight(const ParseResultData& item) { + auto msg = std::static_pointer_cast(item.data.value()); + ImVec2 textSize = ImGui::CalcTextSize(msg->mRawStr.c_str()); + return (std::max(textSize.y, ImGui::GetTextLineHeight() * 6) + ImGui::GetStyle().FramePadding.y * 2 + ImGui::GetStyle().ItemSpacing.y) + 30.0f; +} + +void SF64::MessageFactoryUI::DrawUI(const ParseResultData& item) { + auto msg = std::static_pointer_cast(item.data.value()); + auto symbol = GetSafeNode(const_cast(item.node), "symbol", item.name); + + ImVec2 textSize = ImGui::CalcTextSize(msg->mRawStr.c_str()); + + ImGui::Text("%s", symbol.c_str()); + ImGui::InputTextMultiline(("##" + symbol).c_str(), &msg->mRawStr, ImVec2(-FLT_MIN, std::max(textSize.y, ImGui::GetTextLineHeight() * 6)), ImGuiInputTextFlags_AllowTabInput, nullptr); } \ No newline at end of file diff --git a/src/factories/sf64/MessageFactory.h b/src/factories/sf64/MessageFactory.h index a20522c1..b475135a 100644 --- a/src/factories/sf64/MessageFactory.h +++ b/src/factories/sf64/MessageFactory.h @@ -7,9 +7,10 @@ namespace SF64 { class MessageData : public IParsedData { public: std::vector mMessage; + std::string mRawStr; std::string mMesgStr; - MessageData(std::vector message, std::string mesgStr) : mMessage(message), mMesgStr(mesgStr) {} + MessageData(std::vector message, std::string mesgStr, std::string rawStr) : mMessage(message), mMesgStr(mesgStr), mRawStr(rawStr) {} }; class MessageCodeExporter : public BaseExporter { @@ -47,4 +48,10 @@ class MessageFactory : public BaseFactory { } bool SupportModdedAssets() override { return true; } }; + +class MessageFactoryUI : public BaseFactoryUI { +public: + float GetItemHeight(const ParseResultData& data) override; + void DrawUI(const ParseResultData& data) override; +}; } \ No newline at end of file diff --git a/src/lib/ui/ImGuiFileDialog.cpp b/src/lib/ui/ImGuiFileDialog.cpp new file mode 100644 index 00000000..2d0a8b76 --- /dev/null +++ b/src/lib/ui/ImGuiFileDialog.cpp @@ -0,0 +1,5266 @@ + +// This is an independent project of an individual developer. Dear PVS-Studio, please check it. +// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + +/* +MIT License + +Copyright (c) 2019-2024 Stephane Cuillerdier (aka aiekick) + +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. +*/ + +#include "ImGuiFileDialog.h" + +#ifdef __cplusplus + +#include // stricmp / strcasecmp +#include // variadic +#include +#include +#include +#include +#include +#include +#include + +// this option need c++17 +#ifdef USE_STD_FILESYSTEM +#include +#include +#endif // USE_STD_FILESYSTEM + +#ifdef __EMSCRIPTEN__ +#include +#endif // __EMSCRIPTEN__ + +#ifdef _MSC_VER + +#define IGFD_DEBUG_BREAK \ + if (IsDebuggerPresent()) __debugbreak() +#else +#define IGFD_DEBUG_BREAK +#endif + +#if defined(__WIN32__) || \ + defined(WIN32) || \ + defined(_WIN32) || \ + defined(__WIN64__) || \ + defined(WIN64) || \ + defined(_WIN64) || \ + defined(_MSC_VER) +#define _IGFD_WIN_ +#define stat _stat64 +#define stricmp _stricmp +#include +// this option need c++17 +#ifdef USE_STD_FILESYSTEM +#include +#else // USE_STD_FILESYSTEM +#include "dirent/dirent.h" // directly open the dirent file attached to this lib +#endif // USE_STD_FILESYSTEM +#define PATH_SEP '\\' +#ifndef PATH_MAX +#define PATH_MAX 260 +#endif // PATH_MAX +#elif defined(__linux__) || \ + defined(__FreeBSD__) || \ + defined(__DragonFly__) || \ + defined(__NetBSD__) || \ + defined(__OpenBSD__) || \ + defined(__APPLE__) ||\ + defined(__EMSCRIPTEN__) +#define _IGFD_UNIX_ +#define stricmp strcasecmp +#include +// this option need c++17 +#ifndef USE_STD_FILESYSTEM +#include +#endif // USE_STD_FILESYSTEM +#define PATH_SEP '/' +#endif // _IGFD_UNIX_ + + +#ifdef IMGUI_INTERNAL_INCLUDE +#include IMGUI_INTERNAL_INCLUDE +#else // IMGUI_INTERNAL_INCLUDE +#include +#endif // IMGUI_INTERNAL_INCLUDE + +// legacy compatibility 1.89 +#ifndef IM_TRUNC +#define IM_TRUNC IM_FLOOR +#endif + +#include +#include +#include + +/////////////////////////////// +// STB IMAGE LIBS +/////////////////////////////// + +#ifdef USE_THUMBNAILS +#ifndef DONT_DEFINE_AGAIN__STB_IMAGE_IMPLEMENTATION +#ifndef STB_IMAGE_IMPLEMENTATION +#define STB_IMAGE_IMPLEMENTATION +#endif // STB_IMAGE_IMPLEMENTATION +#endif // DONT_DEFINE_AGAIN__STB_IMAGE_IMPLEMENTATION +#include "stb/stb_image.h" +#ifndef DONT_DEFINE_AGAIN__STB_IMAGE_RESIZE_IMPLEMENTATION +#ifndef STB_IMAGE_RESIZE_IMPLEMENTATION +#define STB_IMAGE_RESIZE_IMPLEMENTATION +#endif // STB_IMAGE_RESIZE_IMPLEMENTATION +#endif // DONT_DEFINE_AGAIN__STB_IMAGE_RESIZE_IMPLEMENTATION +#include "stb/stb_image_resize2.h" +#endif // USE_THUMBNAILS + +/////////////////////////////// +// FLOAT MACROS +/////////////////////////////// + +// float comparisons +#ifndef IS_FLOAT_DIFFERENT +#define IS_FLOAT_DIFFERENT(a, b) (fabs((a) - (b)) > FLT_EPSILON) +#endif // IS_FLOAT_DIFFERENT +#ifndef IS_FLOAT_EQUAL +#define IS_FLOAT_EQUAL(a, b) (fabs((a) - (b)) < FLT_EPSILON) +#endif // IS_FLOAT_EQUAL + +/////////////////////////////// +// COMBOBOX +/////////////////////////////// + +#ifndef FILTER_COMBO_AUTO_SIZE +#define FILTER_COMBO_AUTO_SIZE 1 +#endif // FILTER_COMBO_AUTO_SIZE +#ifndef FILTER_COMBO_MIN_WIDTH +#define FILTER_COMBO_MIN_WIDTH 150.0f +#endif // FILTER_COMBO_MIN_WIDTH +#ifndef IMGUI_BEGIN_COMBO +#define IMGUI_BEGIN_COMBO ImGui::BeginCombo +#endif // IMGUI_BEGIN_COMBO + +/////////////////////////////// +// BUTTON +/////////////////////////////// + +// for lets you define your button widget +// if you have like me a special bi-color button +#ifndef IMGUI_PATH_BUTTON +#define IMGUI_PATH_BUTTON ImGui::Button +#endif // IMGUI_PATH_BUTTON +#ifndef IMGUI_BUTTON +#define IMGUI_BUTTON ImGui::Button +#endif // IMGUI_BUTTON + +/////////////////////////////// +// locales +/////////////////////////////// + +#ifndef createDirButtonString +#define createDirButtonString "+" +#endif // createDirButtonString +#ifndef okButtonString +#define okButtonString "OK" +#endif // okButtonString +#ifndef okButtonWidth +#define okButtonWidth 0.0f +#endif // okButtonWidth +#ifndef cancelButtonString +#define cancelButtonString "Cancel" +#endif // cancelButtonString +#ifndef cancelButtonWidth +#define cancelButtonWidth 0.0f +#endif // cancelButtonWidth +#ifndef okCancelButtonAlignement +#define okCancelButtonAlignement 0.0f +#endif // okCancelButtonAlignement +#ifndef invertOkAndCancelButtons +// 0 => disabled, 1 => enabled +#define invertOkAndCancelButtons 0 +#endif // invertOkAndCancelButtons +#ifndef resetButtonString +#define resetButtonString "R" +#endif // resetButtonString +#ifndef devicesButtonString +#define devicesButtonString "Devices" +#endif // devicesButtonString +#ifndef editPathButtonString +#define editPathButtonString "E" +#endif // editPathButtonString +#ifndef searchString +#define searchString "Search :" +#endif // searchString +#ifndef dirEntryString +#define dirEntryString "[Dir]" +#endif // dirEntryString +#ifndef linkEntryString +#define linkEntryString "[Link]" +#endif // linkEntryString +#ifndef fileEntryString +#define fileEntryString "[File]" +#endif // fileEntryString +#ifndef fileNameString +#define fileNameString "File Name:" +#endif // fileNameString +#ifndef dirNameString +#define dirNameString "Directory Path:" +#endif // dirNameString +#ifndef buttonResetSearchString +#define buttonResetSearchString "Reset search" +#endif // buttonResetSearchString +#ifndef buttonDriveString +#define buttonDriveString "Devices" +#endif // buttonDriveString +#ifndef buttonEditPathString +#define buttonEditPathString "Edit path\nYou can also right click on path buttons" +#endif // buttonEditPathString +#ifndef buttonResetPathString +#define buttonResetPathString "Reset to current directory" +#endif // buttonResetPathString +#ifndef buttonCreateDirString +#define buttonCreateDirString "Create Directory" +#endif // buttonCreateDirString +#ifndef tableHeaderAscendingIcon +#define tableHeaderAscendingIcon "A|" +#endif // tableHeaderAscendingIcon +#ifndef tableHeaderDescendingIcon +#define tableHeaderDescendingIcon "D|" +#endif // tableHeaderDescendingIcon +#ifndef tableHeaderFileNameString +#define tableHeaderFileNameString "File name" +#endif // tableHeaderFileNameString +#ifndef tableHeaderFileTypeString +#define tableHeaderFileTypeString "Type" +#endif // tableHeaderFileTypeString +#ifndef tableHeaderFileSizeString +#define tableHeaderFileSizeString "Size" +#endif // tableHeaderFileSizeString +#ifndef tableHeaderFileDateString +#define tableHeaderFileDateString "Date" +#endif // tableHeaderFileDateString +#ifndef fileSizeBytes +#define fileSizeBytes "o" +#endif // fileSizeBytes +#ifndef fileSizeKiloBytes +#define fileSizeKiloBytes "Ko" +#endif // fileSizeKiloBytes +#ifndef fileSizeMegaBytes +#define fileSizeMegaBytes "Mo" +#endif // fileSizeMegaBytes +#ifndef fileSizeGigaBytes +#define fileSizeGigaBytes "Go" +#endif // fileSizeGigaBytes +#ifndef OverWriteDialogTitleString +#define OverWriteDialogTitleString "The selected file already exists!" +#endif // OverWriteDialogTitleString +#ifndef OverWriteDialogMessageString +#define OverWriteDialogMessageString "Are you sure you want to overwrite it?" +#endif // OverWriteDialogMessageString +#ifndef OverWriteDialogConfirmButtonString +#define OverWriteDialogConfirmButtonString "Confirm" +#endif // OverWriteDialogConfirmButtonString +#ifndef OverWriteDialogCancelButtonString +#define OverWriteDialogCancelButtonString "Cancel" +#endif // OverWriteDialogCancelButtonString +#ifndef DateTimeFormat +// see strftime functionin for customize +#define DateTimeFormat "%Y/%m/%d %H:%M" +#endif // DateTimeFormat + +/////////////////////////////// +//// SHORTCUTS => ctrl + KEY +/////////////////////////////// + +#ifndef SelectAllFilesKey +#define SelectAllFilesKey ImGuiKey_A +#endif // SelectAllFilesKey + +/////////////////////////////// +// THUMBNAILS +/////////////////////////////// + +#ifdef USE_THUMBNAILS +#ifndef tableHeaderFileThumbnailsString +#define tableHeaderFileThumbnailsString "Thumbnails" +#endif // tableHeaderFileThumbnailsString +#ifndef DisplayMode_FilesList_ButtonString +#define DisplayMode_FilesList_ButtonString "FL" +#endif // DisplayMode_FilesList_ButtonString +#ifndef DisplayMode_FilesList_ButtonHelp +#define DisplayMode_FilesList_ButtonHelp "File List" +#endif // DisplayMode_FilesList_ButtonHelp +#ifndef DisplayMode_ThumbailsList_ButtonString +#define DisplayMode_ThumbailsList_ButtonString "TL" +#endif // DisplayMode_ThumbailsList_ButtonString +#ifndef DisplayMode_ThumbailsList_ButtonHelp +#define DisplayMode_ThumbailsList_ButtonHelp "Thumbnails List" +#endif // DisplayMode_ThumbailsList_ButtonHelp +#ifndef DisplayMode_ThumbailsGrid_ButtonString +#define DisplayMode_ThumbailsGrid_ButtonString "TG" +#endif // DisplayMode_ThumbailsGrid_ButtonString +#ifndef DisplayMode_ThumbailsGrid_ButtonHelp +#define DisplayMode_ThumbailsGrid_ButtonHelp "Thumbnails Grid" +#endif // DisplayMode_ThumbailsGrid_ButtonHelp +#ifndef DisplayMode_ThumbailsList_ImageHeight +#define DisplayMode_ThumbailsList_ImageHeight 32.0f +#endif // DisplayMode_ThumbailsList_ImageHeight +#ifndef IMGUI_RADIO_BUTTON +inline bool inRadioButton(const char* vLabel, bool vToggled) { + bool pressed = false; + if (vToggled) { + ImVec4 bua = ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive); + ImVec4 te = ImGui::GetStyleColorVec4(ImGuiCol_Text); + ImGui::PushStyleColor(ImGuiCol_Button, te); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, te); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, te); + ImGui::PushStyleColor(ImGuiCol_Text, bua); + } + pressed = IMGUI_BUTTON(vLabel); + if (vToggled) { + ImGui::PopStyleColor(4); //-V112 + } + return pressed; +} +#define IMGUI_RADIO_BUTTON inRadioButton +#endif // IMGUI_RADIO_BUTTON +#endif // USE_THUMBNAILS + +/////////////////////////////// +// PLACES +/////////////////////////////// + +#ifdef USE_PLACES_FEATURE +#ifndef defaultPlacePaneWith +#define defaultPlacePaneWith 150.0f +#endif // defaultPlacePaneWith +#ifndef placesButtonString +#define placesButtonString "Places" +#endif // placesButtonString +#ifndef placesButtonHelpString +#define placesButtonHelpString "Places" +#endif // placesButtonHelpString +#ifndef placesBookmarksGroupName +#define placesBookmarksGroupName "Bookmarks" +#endif // placesBookmarksGroupName +#ifndef PLACES_BOOKMARK_DEFAULT_OPEPEND +#define PLACES_BOOKMARK_DEFAULT_OPEPEND true +#endif // PLACES_BOOKMARK_DEFAULT_OPEPEND +#ifndef PLACES_DEVICES_DEFAULT_OPEPEND +#define PLACES_DEVICES_DEFAULT_OPEPEND true +#endif // PLACES_DEVICES_DEFAULT_OPEPEND +#ifndef placesBookmarksDisplayOrder +#define placesBookmarksDisplayOrder 0 +#endif // placesBookmarksDisplayOrder +#ifndef placesDevicesGroupName +#define placesDevicesGroupName "Devices" +#endif // placesDevicesGroupName +#ifndef placesDevicesDisplayOrder +#define placesDevicesDisplayOrder 10 +#endif // placesDevicesDisplayOrder +#ifndef addPlaceButtonString +#define addPlaceButtonString "+" +#endif // addPlaceButtonString +#ifndef removePlaceButtonString +#define removePlaceButtonString "-" +#endif // removePlaceButtonString +#ifndef validatePlaceButtonString +#define validatePlaceButtonString "ok" +#endif // validatePlaceButtonString +#ifndef editPlaceButtonString +#define editPlaceButtonString "E" +#endif // editPlaceButtonString +#ifndef PLACES_PANE_DEFAULT_SHOWN +#define PLACES_PANE_DEFAULT_SHOWN false +#endif // PLACES_PANE_DEFAULT_SHOWN +#ifndef IMGUI_TOGGLE_BUTTON +inline bool inToggleButton(const char* vLabel, bool* vToggled) { + bool pressed = false; + + if (vToggled && *vToggled) { + ImVec4 bua = ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive); + // ImVec4 buh = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + // ImVec4 bu = ImGui::GetStyleColorVec4(ImGuiCol_Button); + ImVec4 te = ImGui::GetStyleColorVec4(ImGuiCol_Text); + ImGui::PushStyleColor(ImGuiCol_Button, te); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, te); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, te); + ImGui::PushStyleColor(ImGuiCol_Text, bua); + } + + pressed = IMGUI_BUTTON(vLabel); + + if (vToggled && *vToggled) { + ImGui::PopStyleColor(4); //-V112 + } + + if (vToggled && pressed) *vToggled = !*vToggled; + + return pressed; +} +#define IMGUI_TOGGLE_BUTTON inToggleButton +#endif // IMGUI_TOGGLE_BUTTON +#endif // USE_PLACES_FEATURE + +class IGFDException : public std::exception { +private: + char const* m_msg{}; + +public: + IGFDException() : std::exception() { + } + explicit IGFDException(char const* const vMsg) + : std::exception(), // std::exception(msg) is not availaiable on linux it seems... but on windos yes + m_msg(vMsg) { + } + char const* what() const noexcept override { + return m_msg; + } +}; + +#ifndef CUSTOM_FILESYSTEM_INCLUDE +#ifdef USE_STD_FILESYSTEM + +static std::filesystem::path stringToPath(const std::string& str) { +#ifdef _IGFD_WIN_ + return std::filesystem::path(IGFD::Utils::UTF8Decode(str)); +#else + return std::filesystem::path(str); +#endif +} + +static std::string pathToString(const std::filesystem::path& path) { +#ifdef _IGFD_WIN_ + return IGFD::Utils::UTF8Encode(path.wstring()); +#else + return path.string(); +#endif +} + +class FileSystemStd : public IGFD::IFileSystem { +public: + bool IsDirectoryCanBeOpened(const std::string& vName) override { + bool bExists = false; + if (!vName.empty()) { + namespace fs = std::filesystem; + auto pathName = stringToPath(vName); + try { + // interesting, in the case of a protected dir or for any reason the dir cant be opened + // this func will work but will say nothing more . not like the dirent version + bExists = fs::is_directory(pathName); + // test if can be opened, this function can thrown an exception if there is an issue with this dir + // here, the dir_iter is need else not exception is thrown.. + const auto dir_iter = fs::directory_iterator(pathName); + (void)dir_iter; // for avoid unused warnings + } catch (const std::exception& /*ex*/) { + // fail so this dir cant be opened + bExists = false; + } + } + return bExists; // this is not a directory! + } + bool IsDirectoryExist(const std::string& vName) override { + if (!vName.empty()) { + namespace fs = std::filesystem; + return fs::is_directory(stringToPath(vName)); + } + return false; // this is not a directory! + } + bool IsFileExist(const std::string& vName) override { + namespace fs = std::filesystem; + return fs::is_regular_file(stringToPath(vName)); + } + bool CreateDirectoryIfNotExist(const std::string& vName) override { + if (vName.empty()) return false; + if (IsDirectoryExist(vName)) return true; + +#if defined(__EMSCRIPTEN__) + std::string str = std::string("FS.mkdir('") + vName + "');"; + emscripten_run_script(str.c_str()); + bool res = true; +#else + namespace fs = std::filesystem; + bool res = fs::create_directory(stringToPath(vName)); +#endif // _IGFD_WIN_ + if (!res) { + std::cout << "Error creating directory " << vName << std::endl; + } + return res; + } + + std::vector GetDevicesList() override { + std::vector res; +#ifdef _IGFD_WIN_ + const DWORD mydevices = 2048; + char lpBuffer[2048]; +#define mini(a, b) (((a) < (b)) ? (a) : (b)) + const DWORD countChars = mini(GetLogicalDriveStringsA(mydevices, lpBuffer), 2047); +#undef mini + if (countChars > 0U && countChars < 2049U) { + std::string var = std::string(lpBuffer, (size_t)countChars); + IGFD::Utils::ReplaceString(var, "\\", ""); + auto arr = IGFD::Utils::SplitStringToVector(var, '\0', false); + wchar_t szVolumeName[2048]; + IGFD::PathDisplayedName path_name; + for (auto& a : arr) { + path_name.first = a; + path_name.second.clear(); + std::wstring wpath = IGFD::Utils::UTF8Decode(a); + if (GetVolumeInformationW(wpath.c_str(), szVolumeName, 2048, nullptr, nullptr, nullptr, nullptr, 0)) { + path_name.second = IGFD::Utils::UTF8Encode(szVolumeName); + res.push_back(path_name); + } + } + } +#endif // _IGFD_WIN_ + return res; + } + + IGFD::Utils::PathStruct ParsePathFileName(const std::string& vPathFileName) override { + // https://github.com/aiekick/ImGuiFileDialog/issues/54 + namespace fs = std::filesystem; + IGFD::Utils::PathStruct res; + if (vPathFileName.empty()) return res; + auto fsPath = stringToPath(vPathFileName); + if (fs::is_directory(fsPath)) { + res.name = ""; + res.path = pathToString(fsPath); + res.isOk = true; + } else if (fs::is_regular_file(fsPath)) { + res.name = pathToString(fsPath.filename()); + res.path = pathToString(fsPath.parent_path()); + res.isOk = true; + } + return res; + } + + std::vector ScanDirectory(const std::string& vPath) override { + std::vector res; + try { + namespace fs = std::filesystem; + auto fspath = stringToPath(vPath); + const auto dir_iter = fs::directory_iterator(fspath); + IGFD::FileType fstype = IGFD::FileType(IGFD::FileType::ContentType::Directory, fs::is_symlink(fs::status(fspath))); + { + IGFD::FileInfos file_two_dot; + file_two_dot.filePath = vPath; + file_two_dot.fileNameExt = ".."; + file_two_dot.fileType = fstype; + res.push_back(file_two_dot); + } + for (const auto& file : dir_iter) { + try { + IGFD::FileType fileType; + if (file.is_symlink()) { + fileType.SetSymLink(file.is_symlink()); + fileType.SetContent(IGFD::FileType::ContentType::LinkToUnknown); + } + if (file.is_directory()) { + fileType.SetContent(IGFD::FileType::ContentType::Directory); + } // directory or symlink to directory + else if (file.is_regular_file()) { + fileType.SetContent(IGFD::FileType::ContentType::File); + } + if (fileType.isValid()) { + auto fileNameExt = pathToString(file.path().filename()); + { + IGFD::FileInfos _file; + _file.filePath = vPath; + _file.fileNameExt = fileNameExt; + _file.fileType = fileType; + res.push_back(_file); + } + } + } catch (const std::exception& ex) { + std::cout << "IGFD : " << ex.what() << std::endl; + } + } + } catch (const std::exception& ex) { + std::cout << "IGFD : " << ex.what() << std::endl; + } + return res; + } + bool IsDirectory(const std::string& vFilePathName) override { + namespace fs = std::filesystem; + return fs::is_directory(stringToPath(vFilePathName)); + } + void GetFileDateAndSize(const std::string& vFilePathName, const IGFD::FileType& vFileType, std::string& voDate, size_t& voSize) override { + namespace fs = std::filesystem; + fs::path fpath(vFilePathName); + try { + // date + size_t len{}; + const auto lastWriteTime = fs::last_write_time(fpath); + const auto sctp = std::chrono::time_point_cast( // + lastWriteTime - fs::file_time_type::clock::now() + std::chrono::system_clock::now()); + const auto cftime = std::chrono::system_clock::to_time_t(sctp); + static char timebuf[100]; + std::strftime(timebuf, sizeof(timebuf), DateTimeFormat, std::localtime(&cftime)); + voDate = timebuf; + // size + if (!vFileType.isDir()) { + voSize = fs::file_size(fpath); + } + } catch (const fs::filesystem_error& e) { + voSize = 0; + voDate.clear(); + } + } +}; +#define FILE_SYSTEM_OVERRIDE FileSystemStd +#else +class FileSystemDirent : public IGFD::IFileSystem { +public: + bool IsDirectoryCanBeOpened(const std::string& vName) override { + if (!vName.empty()) { + DIR* pDir = nullptr; + // interesting, in the case of a protected dir or for any reason the dir cant be opened + // this func will fail + pDir = opendir(vName.c_str()); + if (pDir != nullptr) { + (void)closedir(pDir); + return true; + } + } + return false; + } + bool IsDirectoryExist(const std::string& vName) override { + bool bExists = false; + if (!vName.empty()) { + DIR* pDir = nullptr; + pDir = opendir(vName.c_str()); + if (pDir) { + bExists = true; + closedir(pDir); + } else if (ENOENT == errno) { + /* Directory does not exist. */ + // bExists = false; + } else { + /* opendir() failed for some other reason. + like if a dir is protected, or not accessable with user right + */ + bExists = true; + } + } + return bExists; + } + bool IsFileExist(const std::string& vName) override { + std::ifstream docFile(vName, std::ios::in); + if (docFile.is_open()) { + docFile.close(); + return true; + } + return false; + } + bool CreateDirectoryIfNotExist(const std::string& vName) override { + bool res = false; + if (!vName.empty()) { + if (!IsDirectoryExist(vName)) { +#ifdef _IGFD_WIN_ + std::wstring wname = IGFD::Utils::UTF8Decode(vName); + if (CreateDirectoryW(wname.c_str(), nullptr)) { + res = true; + } +#elif defined(__EMSCRIPTEN__) // _IGFD_WIN_ + std::string str = std::string("FS.mkdir('") + vName + "');"; + emscripten_run_script(str.c_str()); + res = true; +#elif defined(_IGFD_UNIX_) + char buffer[PATH_MAX] = {}; + snprintf(buffer, PATH_MAX, "mkdir -p \"%s\"", vName.c_str()); + const int dir_err = std::system(buffer); + if (dir_err != -1) { + res = true; + } +#endif // _IGFD_WIN_ + if (!res) { + std::cout << "Error creating directory " << vName << std::endl; + } + } + } + + return res; + } + + std::vector GetDevicesList() override { + std::vector res; +#ifdef _IGFD_WIN_ + const DWORD mydevices = 2048; + char lpBuffer[2048]; +#define mini(a, b) (((a) < (b)) ? (a) : (b)) + const DWORD countChars = mini(GetLogicalDriveStringsA(mydevices, lpBuffer), 2047); +#undef mini + if (countChars > 0U && countChars < 2049U) { + std::string var = std::string(lpBuffer, (size_t)countChars); + IGFD::Utils::ReplaceString(var, "\\", ""); + auto arr = IGFD::Utils::SplitStringToVector(var, '\0', false); + wchar_t szVolumeName[2048]; + IGFD::PathDisplayedName path_name; + for (auto& a : arr) { + path_name.first = a; + path_name.second.clear(); + std::wstring wpath = IGFD::Utils::UTF8Decode(a); + if (GetVolumeInformationW(wpath.c_str(), szVolumeName, 2048, nullptr, nullptr, nullptr, nullptr, 0)) { + path_name.second = IGFD::Utils::UTF8Encode(szVolumeName); + res.push_back(path_name); + } + } + } +#endif // _IGFD_WIN_ + return res; + } + + IGFD::Utils::PathStruct ParsePathFileName(const std::string& vPathFileName) override { + IGFD::Utils::PathStruct res; + if (!vPathFileName.empty()) { + std::string pfn = vPathFileName; + std::string separator(1u, PATH_SEP); + IGFD::Utils::ReplaceString(pfn, "\\", separator); + IGFD::Utils::ReplaceString(pfn, "/", separator); + size_t lastSlash = pfn.find_last_of(separator); + if (lastSlash != std::string::npos) { + res.name = pfn.substr(lastSlash + 1); + res.path = pfn.substr(0, lastSlash); + res.isOk = true; + } + size_t lastPoint = pfn.find_last_of('.'); + if (lastPoint != std::string::npos) { + if (!res.isOk) { + res.name = pfn; + res.isOk = true; + } + res.ext = pfn.substr(lastPoint + 1); + IGFD::Utils::ReplaceString(res.name, "." + res.ext, ""); + } + if (!res.isOk) { + res.name = std::move(pfn); + res.isOk = true; + } + } + return res; + } + + std::vector ScanDirectory(const std::string& vPath) override { + std::vector res; + struct dirent** files = nullptr; + size_t n = scandir(vPath.c_str(), &files, nullptr, // + [](const struct dirent** a, const struct dirent** b) { // + return strcoll((*a)->d_name, (*b)->d_name); + }); + if (n && files) { + for (size_t i = 0; i < n; ++i) { + struct dirent* ent = files[i]; + IGFD::FileType fileType; + switch (ent->d_type) { + case DT_DIR: fileType.SetContent(IGFD::FileType::ContentType::Directory); break; + case DT_REG: fileType.SetContent(IGFD::FileType::ContentType::File); break; +#if defined(_IGFD_UNIX_) || (DT_LNK != DT_UNKNOWN) + case DT_LNK: +#endif + case DT_UNKNOWN: { + struct stat sb = {}; +#ifdef _IGFD_WIN_ + const auto wfpn = IGFD::Utils::UTF8Decode(vPath + ent->d_name); + if (!_wstati64(wfpn.c_str(), &sb)) { +#else + const auto fpn = vPath + IGFD::Utils::GetPathSeparator() + ent->d_name; + if (!stat(fpn.c_str(), &sb)) { +#endif + if (sb.st_mode & S_IFLNK) { + fileType.SetSymLink(true); + // by default if we can't figure out the target type. + fileType.SetContent(IGFD::FileType::ContentType::LinkToUnknown); + } + if (sb.st_mode & S_IFREG) { + fileType.SetContent(IGFD::FileType::ContentType::File); + break; + } else if (sb.st_mode & S_IFDIR) { + fileType.SetContent(IGFD::FileType::ContentType::Directory); + break; + } + } + break; + } + default: break; // leave it invalid (devices, etc.) + } + if (fileType.isValid()) { + IGFD::FileInfos _file; + _file.filePath = vPath; + _file.fileNameExt = ent->d_name; + _file.fileType = fileType; + res.push_back(_file); + } + } + for (size_t i = 0; i < n; ++i) { + free(files[i]); + } + free(files); + } + return res; + } + bool IsDirectory(const std::string& vFilePathName) override { + DIR* pDir = opendir(vFilePathName.c_str()); + if (pDir) { + (void)closedir(pDir); + return true; + } + return false; + } + void GetFileDateAndSize(const std::string& vFilePathName, const IGFD::FileType& vFileType, std::string& voDate, size_t& voSize) override { + struct stat statInfos{}; + int32_t result{}; +#ifdef _IGFD_WIN_ + std::wstring wfpn = IGFD::Utils::UTF8Decode(vFilePathName); + result = _wstati64(wfpn.c_str(), &statInfos); +#else + result = stat(vFilePathName.c_str(), &statInfos); +#endif + static char timebuf[100]; + if (!result) { + // date + size_t len = 0; +#ifdef _MSC_VER + struct tm _tm; + errno_t err = localtime_s(&_tm, &statInfos.st_mtime); + if (!err) len = strftime(timebuf, 99, DateTimeFormat, &_tm); +#else // _MSC_VER + struct tm* _tm = localtime(&statInfos.st_mtime); + if (_tm) len = strftime(timebuf, 99, DateTimeFormat, _tm); +#endif // _MSC_VER + if (len) { + voDate = std::string(timebuf, len); + } + // size + if (!vFileType.isDir()) { + voSize = (size_t)statInfos.st_size; + } + } + } +}; +#define FILE_SYSTEM_OVERRIDE FileSystemDirent +#endif // USE_STD_FILESYSTEM +#else +#include CUSTOM_FILESYSTEM_INCLUDE +#endif // USE_CUSTOM_FILESYSTEM + +// https://github.com/ocornut/imgui/issues/1720 +bool IGFD::Utils::ImSplitter(bool split_vertically, float thickness, float* size1, float* size2, float min_size1, float min_size2, float splitter_long_axis_size) { + auto* window = ImGui::GetCurrentWindow(); + ImGuiID id = window->GetID("##Splitter"); + ImRect bb; + bb.Min = window->DC.CursorPos + (split_vertically ? ImVec2(*size1, 0.0f) : ImVec2(0.0f, *size1)); + bb.Max = bb.Min + ImGui::CalcItemSize(split_vertically ? ImVec2(thickness, splitter_long_axis_size) : ImVec2(splitter_long_axis_size, thickness), 0.0f, 0.0f); + return ImGui::SplitterBehavior(bb, id, split_vertically ? ImGuiAxis_X : ImGuiAxis_Y, size1, size2, min_size1, min_size2, 1.0f, 0.0, ImGui::GetColorU32(ImGuiCol_FrameBg)); +} + +// Convert a wide Unicode string to an UTF8 string +std::string IGFD::Utils::UTF8Encode(const std::wstring& wstr) { + std::string res; +#ifdef _IGFD_WIN_ + if (!wstr.empty()) { + int size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), nullptr, 0, nullptr, nullptr); + if (size_needed) { + res = std::string(size_needed, 0); + WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), &res[0], size_needed, nullptr, nullptr); + } + } +#else + // Suppress warnings from the compiler. + (void)wstr; +#endif // _IGFD_WIN_ + return res; +} + +// Convert an UTF8 string to a wide Unicode String +std::wstring IGFD::Utils::UTF8Decode(const std::string& str) { + std::wstring res; +#ifdef _IGFD_WIN_ + if (!str.empty()) { + int size_needed = MultiByteToWideChar(CP_UTF8, 0, &str[0], (int)str.size(), nullptr, 0); + if (size_needed) { + res = std::wstring(size_needed, 0); + MultiByteToWideChar(CP_UTF8, 0, &str[0], (int)str.size(), &res[0], size_needed); + } + } +#else + // Suppress warnings from the compiler. + (void)str; +#endif // _IGFD_WIN_ + return res; +} + +bool IGFD::Utils::ReplaceString(std::string& str, const ::std::string& oldStr, const ::std::string& newStr, const size_t& vMaxRecursion) { + if (!str.empty() && oldStr != newStr) { + bool res = false; + size_t pos = 0; + bool found = false; + size_t max_recursion = vMaxRecursion; + do { + pos = str.find(oldStr, pos); + if (pos != std::string::npos) { + found = res = true; + str.replace(pos, oldStr.length(), newStr); + pos += newStr.length(); + } else if (found && max_recursion > 0) { // recursion loop + found = false; + pos = 0; + --max_recursion; + } + } while (pos != std::string::npos); + return res; + } + return false; +} + +std::vector IGFD::Utils::SplitStringToVector(const std::string& vText, const std::string& vDelimiterPattern, const bool vPushEmpty) { + std::vector arr; + if (!vText.empty()) { + size_t start = 0; + size_t end = vText.find(vDelimiterPattern, start); + while (end != std::string::npos) { + auto token = vText.substr(start, end - start); + if (!token.empty() || (token.empty() && vPushEmpty)) { //-V728 + arr.push_back(token); + } + start = end + vDelimiterPattern.size(); + end = vText.find(vDelimiterPattern, start); + } + auto token = vText.substr(start); + if (!token.empty() || (token.empty() && vPushEmpty)) { //-V728 + arr.push_back(token); + } + } + return arr; +} + +std::vector IGFD::Utils::SplitStringToVector(const std::string& vText, const char& vDelimiter, const bool vPushEmpty) { + std::vector arr; + if (!vText.empty()) { + size_t start = 0; + size_t end = vText.find(vDelimiter, start); + while (end != std::string::npos) { + auto token = vText.substr(start, end - start); + if (!token.empty() || (token.empty() && vPushEmpty)) { //-V728 + arr.push_back(token); + } + start = end + 1; + end = vText.find(vDelimiter, start); + } + auto token = vText.substr(start); + if (!token.empty() || (token.empty() && vPushEmpty)) { //-V728 + arr.push_back(token); + } + } + return arr; +} + +void IGFD::Utils::AppendToBuffer(char* vBuffer, size_t vBufferLen, const std::string& vStr) { + std::string st = vStr; + size_t len = vBufferLen - 1u; + size_t slen = strlen(vBuffer); + + if (!st.empty() && st != "\n") { + IGFD::Utils::ReplaceString(st, "\n", ""); + IGFD::Utils::ReplaceString(st, "\r", ""); + } + vBuffer[slen] = '\0'; + std::string str = std::string(vBuffer); + // if (!str.empty()) str += "\n"; + str += vStr; + if (len > str.size()) { + len = str.size(); + } +#ifdef _MSC_VER + strncpy_s(vBuffer, vBufferLen, str.c_str(), len); +#else // _MSC_VER + strncpy(vBuffer, str.c_str(), len); +#endif // _MSC_VER + vBuffer[len] = '\0'; +} + +void IGFD::Utils::ResetBuffer(char* vBuffer) { + vBuffer[0] = '\0'; +} + +void IGFD::Utils::SetBuffer(char* vBuffer, size_t vBufferLen, const std::string& vStr) { + ResetBuffer(vBuffer); + AppendToBuffer(vBuffer, vBufferLen, vStr); +} + +std::string IGFD::Utils::LowerCaseString(const std::string& vString) { + auto str = vString; + + // convert to lower case + for (char& c : str) { + c = (char)std::tolower(c); + } + + return str; +} + +size_t IGFD::Utils::GetCharCountInString(const std::string& vString, const char& vChar) { + size_t res = 0U; + for (const auto& c : vString) { + if (c == vChar) { + ++res; + } + } + return res; +} + +size_t IGFD::Utils::GetLastCharPosWithMinCharCount(const std::string& vString, const char& vChar, const size_t& vMinCharCount) { + if (vMinCharCount) { + size_t last_dot_pos = vString.size() + 1U; + size_t count_dots = vMinCharCount; + while (count_dots > 0U && last_dot_pos > 0U && last_dot_pos != std::string::npos) { + auto new_dot = vString.rfind(vChar, last_dot_pos - 1U); + if (new_dot != std::string::npos) { + last_dot_pos = new_dot; + --count_dots; + } else { + break; + } + } + return last_dot_pos; + } + return std::string::npos; +} + +std::string IGFD::Utils::GetPathSeparator() { + return std::string(1U, PATH_SEP); +} + +std::string IGFD::Utils::RoundNumber(double vvalue, int n) { + std::stringstream tmp; + tmp << std::setprecision(n) << std::fixed << vvalue; + return tmp.str(); +} + +std::pair IGFD::Utils::FormatFileSize(size_t vByteSize) { + if (vByteSize != 0) { + static auto lo = 1024.0; + static auto ko = 1024.0 * 1024.0; + static auto mo = 1024.0 * 1024.0 * 1024.0; + const auto v = static_cast(vByteSize); + if (v < lo) { + return {RoundNumber(v, 0), fileSizeBytes}; // octet + } else if (v < ko) { + return {RoundNumber(v / lo, 2), fileSizeKiloBytes}; // ko + } else if (v < mo) { + return {RoundNumber(v / ko, 2), fileSizeMegaBytes}; // Mo + } else { + return {RoundNumber(v / mo, 2), fileSizeGigaBytes}; // Go + } + } + return {"0", fileSizeBytes}; +} + +// https://cplusplus.com/reference/cstdlib/strtod +bool IGFD::Utils::M_IsAValidCharExt(const char& c) { + return c == '.' || // .5 + c == '-' || c == '+'; // -2.5 or +2.5; +} + +// https://cplusplus.com/reference/cstdlib/strtod +bool IGFD::Utils::M_IsAValidCharSuffix(const char& c) { + return c == 'e' || c == 'E' || // 1e5 or 1E5 + c == 'x' || c == 'X' || // 0x14 or 0X14 + c == 'p' || c == 'P'; // 6.2p2 or 3.2P-5 +} + +bool IGFD::Utils::M_ExtractNumFromStringAtPos(const std::string& str, size_t& pos, double& vOutNum) { + if (!str.empty() && pos < str.size()) { + const char fc = str.at(pos); // first char + // if the first char is not possible for a number we quit + if (std::isdigit(fc) || M_IsAValidCharExt(fc)) { + static constexpr size_t COUNT_CHAR = 64; + char buf[COUNT_CHAR + 1]; + size_t buf_p = 0; + bool is_last_digit = false; + bool is_last_suffix = false; + const auto& ss = str.size(); + while (ss > 1 && pos < ss && buf_p < COUNT_CHAR) { + const char& c = str.at(pos); + // a suffix must be after a number + if (is_last_digit && M_IsAValidCharSuffix(c)) { + is_last_suffix = true; + buf[buf_p++] = c; + } else if (std::isdigit(c)) { + is_last_suffix = false; + is_last_digit = true; + buf[buf_p++] = c; + } else if (M_IsAValidCharExt(c)) { + is_last_digit = false; + buf[buf_p++] = c; + } else { + break; + } + ++pos; + } + // if the last char is a suffix so its not a number + if (buf_p != 0 && !is_last_suffix) { + buf[buf_p] = '\0'; + char* endPtr; + vOutNum = strtod(buf, &endPtr); + // the edge cases for numbers will be next filtered by strtod + if (endPtr != buf) { + return true; + } + } + } + } + return false; +} + +// Fonction de comparaison naturelle entre deux cha�nes +bool IGFD::Utils::NaturalCompare(const std::string& vA, const std::string& vB, bool vInsensitiveCase, bool vDescending) { + std::size_t ia = 0, ib = 0; + double nA, nB; + const auto& as = vA.size(); + const auto& bs = vB.size(); + while (ia < as && ib < bs) { + const char& ca = vInsensitiveCase ? std::tolower(vA[ia]) : vA[ia]; + const char& cb = vInsensitiveCase ? std::tolower(vB[ib]) : vB[ib]; + // we cannot start a number extraction from suffixs + const auto rA = M_ExtractNumFromStringAtPos(vA, ia, nA); + const auto rB = M_ExtractNumFromStringAtPos(vB, ib, nB); + if (rA && rB) { + if (nA != nB) { + return vDescending ? nA > nB : nA < nB; + } + } else { + if (ca != cb) { + return vDescending ? ca > cb : ca < cb; + } + ++ia; + ++ib; + } + } + return vDescending ? as > bs : as < bs; // toto1 < toto1+ +} + +IGFD::FileStyle::FileStyle() : color(0, 0, 0, 0) { +} + +IGFD::FileStyle::FileStyle(const FileStyle& vStyle) { + color = vStyle.color; + icon = vStyle.icon; + font = vStyle.font; + flags = vStyle.flags; +} + +IGFD::FileStyle::FileStyle(const ImVec4& vColor, const std::string& vIcon, ImFont* vFont) : color(vColor), icon(vIcon), font(vFont) { +} + +void IGFD::SearchManager::Clear() { + searchTag.clear(); + IGFD::Utils::ResetBuffer(searchBuffer); +} + +void IGFD::SearchManager::DrawSearchBar(FileDialogInternal& vFileDialogInternal) { + // search field + if (IMGUI_BUTTON(resetButtonString "##BtnImGuiFileDialogSearchField")) { + Clear(); + vFileDialogInternal.fileManager.ApplyFilteringOnFileList(vFileDialogInternal); + } + if (ImGui::IsItemHovered()) ImGui::SetTooltip(buttonResetSearchString); + ImGui::SameLine(); + ImGui::Text(searchString); + ImGui::SameLine(); + ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x); + bool edited = ImGui::InputText("##InputImGuiFileDialogSearchField", searchBuffer, MAX_FILE_DIALOG_NAME_BUFFER); + if (ImGui::GetItemID() == ImGui::GetActiveID()) searchInputIsActive = true; + ImGui::PopItemWidth(); + if (edited) { + searchTag = searchBuffer; + vFileDialogInternal.fileManager.ApplyFilteringOnFileList(vFileDialogInternal); + } +} + +void IGFD::FilterInfos::setCollectionTitle(const std::string& vTitle) { + title = vTitle; +} + +void IGFD::FilterInfos::addFilter(const std::string& vFilter, const bool vIsRegex) { + setCollectionTitle(vFilter); + addCollectionFilter(vFilter, vIsRegex); +} + +void IGFD::FilterInfos::addCollectionFilter(const std::string& vFilter, const bool vIsRegex) { + if (!vIsRegex) { + auto _count_dots = Utils::GetCharCountInString(vFilter, '.'); + if (_count_dots > IGFD::FilterInfos::count_dots) { + IGFD::FilterInfos::count_dots = _count_dots; + } + if (vFilter.find('*') != std::string::npos) { + const auto& regex_string = transformAsteriskBasedFilterToRegex(vFilter); + addCollectionFilter(regex_string, true); + return; + } + filters.try_add(vFilter); + filters_optimized.try_add(Utils::LowerCaseString(vFilter)); + } else { + try { + auto rx = std::regex(vFilter); + filters.try_add(vFilter); + filters_regex.emplace_back(rx); + } catch (std::exception& e) { + const std::string msg = "IGFD : The regex \"" + vFilter + "\" parsing was failed with msg : " + e.what(); + throw IGFDException(msg.c_str()); + } + } +} + +void IGFD::FilterInfos::clear() { + title.clear(); + filters.clear(); + filters_optimized.clear(); + filters_regex.clear(); +} + +bool IGFD::FilterInfos::empty() const { + return filters.empty() || filters.begin()->empty(); +} + +const std::string& IGFD::FilterInfos::getFirstFilter() const { + if (!filters.empty()) { + return *filters.begin(); + } + return empty_string; +} + +bool IGFD::FilterInfos::exist(const FileInfos& vFileInfos, bool vIsCaseInsensitive) const { + for (const auto& filter : filters) { + if (vFileInfos.SearchForExt(filter, vIsCaseInsensitive, count_dots)) { + return true; + } + } + return false; +} + +bool IGFD::FilterInfos::regexExist(const std::string& vFilter) const { + for (const auto& regex : filters_regex) { + if (std::regex_search(vFilter, regex)) { + return true; + } + } + return false; +} + +std::string IGFD::FilterInfos::transformAsteriskBasedFilterToRegex(const std::string& vFilter) { + std::string res; + if (!vFilter.empty() && vFilter.find('*') != std::string::npos) { + res = "(("; + for (const auto& c : vFilter) { + if (c == '.') { + res += "[.]"; // [.] => a dot + } else if (c == '*') { + res += ".*"; // .* => any char zero or many + } else { + res += c; // other chars + } + } + res += "$))"; // $ => end fo the string + } + return res; +} + +const IGFD::FilterInfos& IGFD::FilterManager::GetSelectedFilter() const { + return m_SelectedFilter; +} + +void IGFD::FilterManager::ParseFilters(const char* vFilters) { + m_ParsedFilters.clear(); + + if (vFilters) { + dLGFilters = vFilters; // file mode + } else { + dLGFilters.clear(); // directory mode + } + + if (!dLGFilters.empty()) { + /* Rules + 0) a filter must have 2 chars mini and the first must be a . + 1) a regex must be in (( and )) + 2) a , will separate filters except if between a ( and ) + 3) name{filter1, filter2} is a spetial form for collection filters + 3.1) the name can be composed of what you want except { and } + 3.2) the filter can be a regex + 4) the filters cannot integrate these chars '(' ')' '{' '}' ' ' except for a regex with respect to rule 1) + 5) the filters cannot integrate a ',' + */ + + bool current_filter_found = false; + bool started = false; + bool regex_started = false; + bool parenthesis_started = false; + + std::string word; + std::string filter_name; + + char last_char = 0; + for (char c : dLGFilters) { + if (c == '{') { + if (regex_started) { + word += c; + } else { + started = true; + m_ParsedFilters.emplace_back(); + m_ParsedFilters.back().setCollectionTitle(filter_name); + filter_name.clear(); + word.clear(); + } + } else if (c == '}') { + if (regex_started) { + word += c; + } else { + if (started) { + if (word.size() > 1U && word[0] == '.') { + if (m_ParsedFilters.empty()) { + m_ParsedFilters.emplace_back(); + } + m_ParsedFilters.back().addCollectionFilter(word, false); + } + word.clear(); + filter_name.clear(); + started = false; + } + } + } else if (c == '(') { + word += c; + if (last_char == '(') { + regex_started = true; + } + parenthesis_started = true; + if (!started) { + filter_name += c; + } + } else if (c == ')') { + word += c; + if (last_char == ')') { + if (regex_started) { + if (started) { + m_ParsedFilters.back().addCollectionFilter(word, true); + } else { + m_ParsedFilters.emplace_back(); + m_ParsedFilters.back().addFilter(word, true); + } + word.clear(); + filter_name.clear(); + regex_started = false; + } else { + if (!started) { + if (!m_ParsedFilters.empty()) { + m_ParsedFilters.erase(m_ParsedFilters.begin() + m_ParsedFilters.size() - 1U); + } else { + m_ParsedFilters.clear(); + } + } + word.clear(); + filter_name.clear(); + } + } + parenthesis_started = false; + if (!started) { + filter_name += c; + } + } else if (c == '.') { + word += c; + if (!started) { + filter_name += c; + } + } else if (c == ',') { + if (regex_started) { + word += c; + } else { + if (started) { + if (word.size() > 1U && word[0] == '.') { + m_ParsedFilters.back().addCollectionFilter(word, false); + word.clear(); + filter_name.clear(); + } + } else { + if (word.size() > 1U && word[0] == '.') { + m_ParsedFilters.emplace_back(); + m_ParsedFilters.back().addFilter(word, false); + word.clear(); + filter_name.clear(); + } + if (parenthesis_started) { + filter_name += c; + } + } + } + } else { + if (c != ' ') { + word += c; + } + if (!started) { + filter_name += c; + } + } + last_char = c; + } + + if (started) { + if (!m_ParsedFilters.empty()) { + m_ParsedFilters.erase(m_ParsedFilters.begin() + m_ParsedFilters.size() - 1U); + } else { + m_ParsedFilters.clear(); + } + } else if (word.size() > 1U && word[0] == '.') { + m_ParsedFilters.emplace_back(); + m_ParsedFilters.back().addFilter(word, false); + word.clear(); + } + + for (const auto& it : m_ParsedFilters) { + if (it.title == m_SelectedFilter.title) { + m_SelectedFilter = it; + current_filter_found = true; + break; + } + } + + if (!current_filter_found) { + if (!m_ParsedFilters.empty()) { + m_SelectedFilter = *m_ParsedFilters.begin(); + } + } + } +} + +void IGFD::FilterManager::SetSelectedFilterWithExt(const std::string& vFilter) { + if (!m_ParsedFilters.empty()) { + if (!vFilter.empty()) { + for (const auto& infos : m_ParsedFilters) { + for (const auto& filter : infos.filters) { + if (vFilter == filter) { + m_SelectedFilter = infos; + } + } + } + } + + if (m_SelectedFilter.empty()) { + m_SelectedFilter = *m_ParsedFilters.begin(); + } + } +} + +void IGFD::FilterManager::SetFileStyle(const IGFD_FileStyleFlags& vFlags, const char* vCriteria, const FileStyle& vInfos) { + std::string _criteria = (vCriteria != nullptr) ? std::string(vCriteria) : ""; + m_FilesStyle[vFlags][_criteria] = std::make_shared(vInfos); + m_FilesStyle[vFlags][_criteria]->flags = vFlags; +} + +// will be called internally +// will not been exposed to IGFD API +bool IGFD::FilterManager::FillFileStyle(std::shared_ptr vFileInfos) const { + // todo : better system to found regarding what style to priorize regarding other + // maybe with a lambda fucntion for let the user use his style + // according to his use case + if (vFileInfos.use_count() && !m_FilesStyle.empty()) { + for (const auto& _flag : m_FilesStyle) { + for (const auto& _file : _flag.second) { + if ((_flag.first & IGFD_FileStyleByTypeDir && _flag.first & IGFD_FileStyleByTypeLink && vFileInfos->fileType.isDir() && vFileInfos->fileType.isSymLink()) || + (_flag.first & IGFD_FileStyleByTypeFile && _flag.first & IGFD_FileStyleByTypeLink && vFileInfos->fileType.isFile() && vFileInfos->fileType.isSymLink()) || + (_flag.first & IGFD_FileStyleByTypeLink && vFileInfos->fileType.isSymLink()) || (_flag.first & IGFD_FileStyleByTypeDir && vFileInfos->fileType.isDir()) || + (_flag.first & IGFD_FileStyleByTypeFile && vFileInfos->fileType.isFile())) { + if (_file.first.empty()) { // for all links + vFileInfos->fileStyle = _file.second; + } else if (_file.first.find("((") != std::string::npos && std::regex_search(vFileInfos->fileNameExt, + std::regex(_file.first))) { // for links who are equal to style criteria + vFileInfos->fileStyle = _file.second; + } else if (_file.first == vFileInfos->fileNameExt) { // for links who are equal to style criteria + vFileInfos->fileStyle = _file.second; + } + } + + if (_flag.first & IGFD_FileStyleByExtention) { + if (_file.first.find("((") != std::string::npos && std::regex_search(vFileInfos->fileExtLevels[0], std::regex(_file.first))) { + vFileInfos->fileStyle = _file.second; + } else if (vFileInfos->SearchForExt(_file.first, false)) { + vFileInfos->fileStyle = _file.second; + } + } + + if (_flag.first & IGFD_FileStyleByFullName) { + if (_file.first.find("((") != std::string::npos && std::regex_search(vFileInfos->fileNameExt, std::regex(_file.first))) { + vFileInfos->fileStyle = _file.second; + } else if (_file.first == vFileInfos->fileNameExt) { + vFileInfos->fileStyle = _file.second; + } + } + + if (_flag.first & IGFD_FileStyleByContainedInFullName) { + if (_file.first.find("((") != std::string::npos && std::regex_search(vFileInfos->fileNameExt, std::regex(_file.first))) { + vFileInfos->fileStyle = _file.second; + } else if (vFileInfos->fileNameExt.find(_file.first) != std::string::npos) { + vFileInfos->fileStyle = _file.second; + } + } + + for (auto& functor : m_FilesStyleFunctors) { + if (functor) { + FileStyle result; + if (functor(*(vFileInfos.get()), result)) { + vFileInfos->fileStyle = std::make_shared(std::move(result)); + } + } + } + + if (vFileInfos->fileStyle.use_count()) { + return true; + } + } + } + } + + return false; +} + +void IGFD::FilterManager::SetFileStyle(const IGFD_FileStyleFlags& vFlags, const char* vCriteria, const ImVec4& vColor, const std::string& vIcon, ImFont* vFont) { + std::string _criteria; + if (vCriteria) _criteria = std::string(vCriteria); + m_FilesStyle[vFlags][_criteria] = std::make_shared(vColor, vIcon, vFont); + m_FilesStyle[vFlags][_criteria]->flags = vFlags; +} + +void IGFD::FilterManager::SetFileStyle(FileStyle::FileStyleFunctor vFunctor) { + if (vFunctor) { + m_FilesStyleFunctors.push_back(vFunctor); + } +} + +// todo : refactor this fucking function +bool IGFD::FilterManager::GetFileStyle(const IGFD_FileStyleFlags& vFlags, const std::string& vCriteria, ImVec4* vOutColor, std::string* vOutIcon, ImFont** vOutFont) { + if (vOutColor) { + if (!m_FilesStyle.empty()) { + if (m_FilesStyle.find(vFlags) != m_FilesStyle.end()) { // found + if (vFlags & IGFD_FileStyleByContainedInFullName) { + // search for vCriteria who are containing the criteria + for (const auto& _file : m_FilesStyle.at(vFlags)) { + if (vCriteria.find(_file.first) != std::string::npos) { + if (_file.second.use_count()) { + *vOutColor = _file.second->color; + if (vOutIcon) *vOutIcon = _file.second->icon; + if (vOutFont) *vOutFont = _file.second->font; + return true; + } + } + } + } else { + if (m_FilesStyle.at(vFlags).find(vCriteria) != m_FilesStyle.at(vFlags).end()) { // found + *vOutColor = m_FilesStyle[vFlags][vCriteria]->color; + if (vOutIcon) *vOutIcon = m_FilesStyle[vFlags][vCriteria]->icon; + if (vOutFont) *vOutFont = m_FilesStyle[vFlags][vCriteria]->font; + return true; + } + } + } else { + // search for flag composition + for (const auto& _flag : m_FilesStyle) { + if (_flag.first & vFlags) { + if (_flag.first & IGFD_FileStyleByContainedInFullName) { + // search for vCriteria who are containing the criteria + for (const auto& _file : m_FilesStyle.at(_flag.first)) { + if (vCriteria.find(_file.first) != std::string::npos) { + if (_file.second.use_count()) { + *vOutColor = _file.second->color; + if (vOutIcon) *vOutIcon = _file.second->icon; + if (vOutFont) *vOutFont = _file.second->font; + return true; + } + } + } + } else { + if (m_FilesStyle.at(_flag.first).find(vCriteria) != m_FilesStyle.at(_flag.first).end()) { // found + *vOutColor = m_FilesStyle[_flag.first][vCriteria]->color; + if (vOutIcon) *vOutIcon = m_FilesStyle[_flag.first][vCriteria]->icon; + if (vOutFont) *vOutFont = m_FilesStyle[_flag.first][vCriteria]->font; + return true; + } + } + } + } + } + } + } + return false; +} + +void IGFD::FilterManager::ClearFilesStyle() { + m_FilesStyle.clear(); +} + +bool IGFD::FilterManager::IsCoveredByFilters(const FileInfos& vFileInfos, bool vIsCaseInsensitive) const { + if (!dLGFilters.empty() && !m_SelectedFilter.empty()) { + return (m_SelectedFilter.exist(vFileInfos, vIsCaseInsensitive) || m_SelectedFilter.regexExist(vFileInfos.fileNameExt)); + } + + return false; +} + +float IGFD::FilterManager::GetFilterComboBoxWidth() const { +#if FILTER_COMBO_AUTO_SIZE + const auto& combo_width = ImGui::CalcTextSize(m_SelectedFilter.title.c_str()).x + ImGui::GetFrameHeight() + ImGui::GetStyle().ItemInnerSpacing.x; + return ImMax(combo_width, FILTER_COMBO_MIN_WIDTH); +#else + return FILTER_COMBO_MIN_WIDTH; +#endif +} + +bool IGFD::FilterManager::DrawFilterComboBox(FileDialogInternal& vFileDialogInternal) { + if (!dLGFilters.empty()) { + ImGui::SameLine(); + bool needToApllyNewFilter = false; + ImGui::PushItemWidth(GetFilterComboBoxWidth()); + if (IMGUI_BEGIN_COMBO("##Filters", m_SelectedFilter.title.c_str(), ImGuiComboFlags_None)) { + intptr_t i = 0; + for (const auto& filter : m_ParsedFilters) { + const bool item_selected = (filter.title == m_SelectedFilter.title); + ImGui::PushID((void*)(intptr_t)i++); + if (ImGui::Selectable(filter.title.c_str(), item_selected)) { + m_SelectedFilter = filter; + needToApllyNewFilter = true; + } + ImGui::PopID(); + } + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + if (needToApllyNewFilter) { + vFileDialogInternal.fileManager.OpenCurrentPath(vFileDialogInternal); + } + return needToApllyNewFilter; + } + return false; +} + +std::string IGFD::FilterManager::ReplaceExtentionWithCurrentFilterIfNeeded(const std::string& vFileName, IGFD_ResultMode vFlag) const { + auto result = vFileName; + if (!result.empty()) { + const auto& current_filter = m_SelectedFilter.getFirstFilter(); + if (!current_filter.empty()) { + Utils::ReplaceString(result, "..", "."); + + // is a regex => no change + if (current_filter.find("((") != std::string::npos) { + return result; + } + + // contain .* => no change + if (current_filter.find(".*") != std::string::npos) { + return result; + } + + switch (vFlag) { + case IGFD_ResultMode_KeepInputFile: { + return vFileName; + } + case IGFD_ResultMode_OverwriteFileExt: { + const auto& count_dots = Utils::GetCharCountInString(vFileName, '.'); + const auto& min_dots = ImMin(count_dots, m_SelectedFilter.count_dots); + const auto& lp = Utils::GetLastCharPosWithMinCharCount(vFileName, '.', min_dots); + if (lp != std::string::npos) { // there is a user extention + const auto& file_name_without_user_ext = vFileName.substr(0, lp); + result = file_name_without_user_ext + current_filter; + } else { // add extention + result = vFileName + current_filter; + } + break; + } + case IGFD_ResultMode_AddIfNoFileExt: { + const auto& count_dots = Utils::GetCharCountInString(vFileName, '.'); + const auto& min_dots = ImMin(count_dots, m_SelectedFilter.count_dots); + const auto& lp = Utils::GetLastCharPosWithMinCharCount(vFileName, '.', min_dots); + if (lp == std::string::npos || // there is no user extention + lp == (vFileName.size() - 1U)) { // or this pos is also the last char => considered like no user extention + const auto& file_name_without_user_ext = vFileName.substr(0, lp); + result = file_name_without_user_ext + current_filter; + } + break; + } + default: break; + } + + Utils::ReplaceString(result, "..", "."); + } + } + return result; +} + +void IGFD::FilterManager::SetDefaultFilterIfNotDefined() { + if (m_SelectedFilter.empty() && // no filter selected + !m_ParsedFilters.empty()) { // filter exist + m_SelectedFilter = *m_ParsedFilters.begin(); // we take the first filter + } +} + +IGFD::FileType::FileType() = default; +IGFD::FileType::FileType(const ContentType& vContentType, const bool vIsSymlink) : m_Content(vContentType), m_Symlink(vIsSymlink) { +} +void IGFD::FileType::SetContent(const ContentType& vContentType) { + m_Content = vContentType; +} +void IGFD::FileType::SetSymLink(const bool vIsSymlink) { + m_Symlink = vIsSymlink; +} +bool IGFD::FileType::isValid() const { + return m_Content != ContentType::Invalid; +} +bool IGFD::FileType::isDir() const { + return m_Content == ContentType::Directory; +} +bool IGFD::FileType::isFile() const { + return m_Content == ContentType::File; +} +bool IGFD::FileType::isLinkToUnknown() const { + return m_Content == ContentType::LinkToUnknown; +} +bool IGFD::FileType::isSymLink() const { + return m_Symlink; +} +// Comparisons only care about the content type, ignoring whether it's a symlink or not. +bool IGFD::FileType::operator==(const FileType& rhs) const { + return m_Content == rhs.m_Content; +} +bool IGFD::FileType::operator!=(const FileType& rhs) const { + return m_Content != rhs.m_Content; +} +bool IGFD::FileType::operator<(const FileType& rhs) const { + return m_Content < rhs.m_Content; +} +bool IGFD::FileType::operator>(const FileType& rhs) const { + return m_Content > rhs.m_Content; +} + +std::shared_ptr IGFD::FileInfos::create() { + return std::make_shared(); +} + +bool IGFD::FileInfos::SearchForTag(const std::string& vTag) const { + if (!vTag.empty()) { + if (fileNameExt_optimized == "..") return true; + return fileNameExt_optimized.find(vTag) != std::string::npos || // first try without case and accents + fileNameExt.find(vTag) != std::string::npos; // second if searched with case and accents + } + + // if tag is empty => its a special case but all is found + return true; +} + +bool IGFD::FileInfos::SearchForExt(const std::string& vExt, const bool vIsCaseInsensitive, const size_t& vMaxLevel) const { + if (!vExt.empty()) { + const auto& ext_to_check = vIsCaseInsensitive ? Utils::LowerCaseString(vExt) : vExt; + const auto& ext_levels = vIsCaseInsensitive ? fileExtLevels_optimized : fileExtLevels; + if (vMaxLevel >= 1 && countExtDot >= vMaxLevel) { + for (const auto& ext : ext_levels) { + if (!ext.empty() && ext == ext_to_check) { + return true; + } + } + } else { + return (fileExtLevels[0] == vExt); + } + } + return false; +} + +bool IGFD::FileInfos::SearchForExts(const std::string& vComaSepExts, const bool vIsCaseInsensitive, const size_t& vMaxLevel) const { + if (!vComaSepExts.empty()) { + const auto& arr = Utils::SplitStringToVector(vComaSepExts, ',', false); + for (const auto& a : arr) { + if (SearchForExt(a, vIsCaseInsensitive, vMaxLevel)) { + return true; + } + } + } + return false; +} + +bool IGFD::FileInfos::FinalizeFileTypeParsing(const size_t& vMaxDotToExtract) { + if (fileType.isFile() || fileType.isLinkToUnknown()) { // link can have the same extention of a file + countExtDot = Utils::GetCharCountInString(fileNameExt, '.'); + size_t lpt = 0U; + if (countExtDot > 1U) { // multi layer ext + size_t max_dot_to_extract = vMaxDotToExtract; + if (max_dot_to_extract > countExtDot) { + max_dot_to_extract = countExtDot; + } + lpt = Utils::GetLastCharPosWithMinCharCount(fileNameExt, '.', max_dot_to_extract); + } else { + lpt = fileNameExt.find_first_of('.'); + } + if (lpt != std::string::npos) { + size_t lvl = 0U; + fileNameLevels[lvl] = fileNameExt.substr(0, lpt); + fileNameLevels[lvl] = Utils::LowerCaseString(fileNameLevels[lvl]); + fileExtLevels[lvl] = fileNameExt.substr(lpt); + fileExtLevels_optimized[lvl] = Utils::LowerCaseString(fileExtLevels[lvl]); + if (countExtDot > 1U) { // multi layer ext + auto count = countExtDot; + while (count > 0 && lpt != std::string::npos && lvl < fileExtLevels.size()) { + ++lpt; + ++lvl; + if (fileNameExt.size() > lpt) { + lpt = fileNameExt.find_first_of('.', lpt); + if (lpt != std::string::npos) { + fileNameLevels[lvl] = fileNameExt.substr(0, lpt); + fileNameLevels[lvl] = Utils::LowerCaseString(fileNameLevels[lvl]); + fileExtLevels[lvl] = fileNameExt.substr(lpt); + fileExtLevels_optimized[lvl] = Utils::LowerCaseString(fileExtLevels[lvl]); + } + } + } + } + } + return true; + } + return false; +} + +IGFD::FileManager::FileManager() { + fsRoot = IGFD::Utils::GetPathSeparator(); +#define STR(x) #x +#define STR_AFTER_EXPAND(x) STR(x) + m_FileSystemName = STR_AFTER_EXPAND(FILE_SYSTEM_OVERRIDE); +#undef STR_AFTER_EXPAND +#undef STR + // std::make_unique is not available un cpp11 + m_FileSystemPtr = std::unique_ptr(new FILE_SYSTEM_OVERRIDE()); + // m_FileSystemPtr = std::make_unique(); +} + +void IGFD::FileManager::OpenCurrentPath(const FileDialogInternal& vFileDialogInternal) { + showDevices = false; + ClearComposer(); + ClearFileLists(); + if (dLGDirectoryMode) { // directory mode + SetDefaultFileName("."); + } else { + SetDefaultFileName(dLGDefaultFileName); + } + ScanDir(vFileDialogInternal, GetCurrentPath()); +} + +void IGFD::FileManager::SortFields(const FileDialogInternal& vFileDialogInternal) { + m_SortFields(vFileDialogInternal, m_FileList, m_FilteredFileList); +} + +bool IGFD::FileManager::M_SortStrings(const FileDialogInternal& vFileDialogInternal, const bool vInsensitiveCase, const bool vDescendingOrder, const std::string& vA, const std::string& vB) { + if (vFileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_NaturalSorting) { + return IGFD::Utils::NaturalCompare(vA, vB, vInsensitiveCase, vDescendingOrder); + } else if (vInsensitiveCase) { + const auto ret = stricmp(vA.c_str(), vB.c_str()); + return vDescendingOrder ? (ret > 0) : (ret < 0); + } else { + const auto ret = strcmp(vA.c_str(), vB.c_str()); + return vDescendingOrder ? (ret > 0) : (ret < 0); + } +} + +void IGFD::FileManager::m_SortFields(const FileDialogInternal& vFileDialogInternal, std::vector >& vFileInfosList, std::vector >& vFileInfosFilteredList) { + if (sortingField != SortingFieldEnum::FIELD_NONE) { + headerFileName = tableHeaderFileNameString; + headerFileType = tableHeaderFileTypeString; + headerFileSize = tableHeaderFileSizeString; + headerFileDate = tableHeaderFileDateString; +#ifdef USE_THUMBNAILS + headerFileThumbnails = tableHeaderFileThumbnailsString; +#endif // #ifdef USE_THUMBNAILS + } + if (sortingField == SortingFieldEnum::FIELD_FILENAME) { + if (sortingDirection[0]) { +#ifdef USE_CUSTOM_SORTING_ICON + headerFileName = tableHeaderAscendingIcon + headerFileName; +#endif // USE_CUSTOM_SORTING_ICON + std::sort(vFileInfosList.begin(), vFileInfosList.end(), // + [&vFileDialogInternal](const std::shared_ptr& a, const std::shared_ptr& b) -> bool { + if (!a.use_count() || !b.use_count()) return false; + if (a->fileType != b->fileType) return (a->fileType < b->fileType); // directories first + return M_SortStrings(vFileDialogInternal, true, false, a->fileNameExt, b->fileNameExt); // sort in insensitive case + }); + } else { +#ifdef USE_CUSTOM_SORTING_ICON + headerFileName = tableHeaderDescendingIcon + headerFileName; +#endif // USE_CUSTOM_SORTING_ICON + std::sort(vFileInfosList.begin(), vFileInfosList.end(), // + [&vFileDialogInternal](const std::shared_ptr& a, const std::shared_ptr& b) -> bool { + if (!a.use_count() || !b.use_count()) return false; + if (a->fileType != b->fileType) return (a->fileType > b->fileType); // directories last + return M_SortStrings(vFileDialogInternal, true, true, a->fileNameExt, b->fileNameExt); // sort in insensitive case + }); + } + } else if (sortingField == SortingFieldEnum::FIELD_TYPE) { + if (sortingDirection[1]) { +#ifdef USE_CUSTOM_SORTING_ICON + headerFileType = tableHeaderAscendingIcon + headerFileType; +#endif // USE_CUSTOM_SORTING_ICON + std::sort(vFileInfosList.begin(), vFileInfosList.end(), [&vFileDialogInternal](const std::shared_ptr& a, const std::shared_ptr& b) -> bool { + if (!a.use_count() || !b.use_count()) return false; + if (a->fileType != b->fileType) return (a->fileType < b->fileType); // directory in first + return M_SortStrings(vFileDialogInternal, true, false, a->fileExtLevels[0], b->fileExtLevels[0]); // sort in sensitive case + }); + } else { +#ifdef USE_CUSTOM_SORTING_ICON + headerFileType = tableHeaderDescendingIcon + headerFileType; +#endif // USE_CUSTOM_SORTING_ICON + std::sort(vFileInfosList.begin(), vFileInfosList.end(), [&vFileDialogInternal](const std::shared_ptr& a, const std::shared_ptr& b) -> bool { + if (!a.use_count() || !b.use_count()) return false; + if (a->fileType != b->fileType) return (a->fileType > b->fileType); // directory in last + return M_SortStrings(vFileDialogInternal, true, true, a->fileExtLevels[0], b->fileExtLevels[0]); // sort in sensitive case + }); + } + } else if (sortingField == SortingFieldEnum::FIELD_SIZE) { + if (sortingDirection[2]) { +#ifdef USE_CUSTOM_SORTING_ICON + headerFileSize = tableHeaderAscendingIcon + headerFileSize; +#endif // USE_CUSTOM_SORTING_ICON + std::sort(vFileInfosList.begin(), vFileInfosList.end(), [](const std::shared_ptr& a, const std::shared_ptr& b) -> bool { + if (!a.use_count() || !b.use_count()) return false; + if (a->fileType != b->fileType) return (a->fileType < b->fileType); // directory in first + return (a->fileSize < b->fileSize); // else + }); + } else { +#ifdef USE_CUSTOM_SORTING_ICON + headerFileSize = tableHeaderDescendingIcon + headerFileSize; +#endif // USE_CUSTOM_SORTING_ICON + std::sort(vFileInfosList.begin(), vFileInfosList.end(), [](const std::shared_ptr& a, const std::shared_ptr& b) -> bool { + if (!a.use_count() || !b.use_count()) return false; + if (a->fileType != b->fileType) return (a->fileType > b->fileType); // directory in last + return (a->fileSize > b->fileSize); // else + }); + } + } else if (sortingField == SortingFieldEnum::FIELD_DATE) { + if (sortingDirection[3]) { +#ifdef USE_CUSTOM_SORTING_ICON + headerFileDate = tableHeaderAscendingIcon + headerFileDate; +#endif // USE_CUSTOM_SORTING_ICON + std::sort(vFileInfosList.begin(), vFileInfosList.end(), [](const std::shared_ptr& a, const std::shared_ptr& b) -> bool { + if (!a.use_count() || !b.use_count()) return false; + if (a->fileType != b->fileType) return (a->fileType < b->fileType); // directory in first + return (a->fileModifDate < b->fileModifDate); // else + }); + } else { +#ifdef USE_CUSTOM_SORTING_ICON + headerFileDate = tableHeaderDescendingIcon + headerFileDate; +#endif // USE_CUSTOM_SORTING_ICON + std::sort(vFileInfosList.begin(), vFileInfosList.end(), [](const std::shared_ptr& a, const std::shared_ptr& b) -> bool { + if (!a.use_count() || !b.use_count()) return false; + if (a->fileType != b->fileType) return (a->fileType > b->fileType); // directory in last + return (a->fileModifDate > b->fileModifDate); // else + }); + } + } +#ifdef USE_THUMBNAILS + else if (sortingField == SortingFieldEnum::FIELD_THUMBNAILS) { + // we will compare thumbnails by : + // 1) width + // 2) height + + if (sortingDirection[4]) { +#ifdef USE_CUSTOM_SORTING_ICON + headerFileThumbnails = tableHeaderAscendingIcon + headerFileThumbnails; +#endif // USE_CUSTOM_SORTING_ICON + std::sort(vFileInfosList.begin(), vFileInfosList.end(), [](const std::shared_ptr& a, const std::shared_ptr& b) -> bool { + if (!a.use_count() || !b.use_count()) return false; + if (a->fileType != b->fileType) return (a->fileType.isDir()); // directory in first + if (a->thumbnailInfo.textureWidth == b->thumbnailInfo.textureWidth) return (a->thumbnailInfo.textureHeight < b->thumbnailInfo.textureHeight); + return (a->thumbnailInfo.textureWidth < b->thumbnailInfo.textureWidth); + }); + } + + else { +#ifdef USE_CUSTOM_SORTING_ICON + headerFileThumbnails = tableHeaderDescendingIcon + headerFileThumbnails; +#endif // USE_CUSTOM_SORTING_ICON + std::sort(vFileInfosList.begin(), vFileInfosList.end(), [](const std::shared_ptr& a, const std::shared_ptr& b) -> bool { + if (!a.use_count() || !b.use_count()) return false; + if (a->fileType != b->fileType) return (!a->fileType.isDir()); // directory in last + if (a->thumbnailInfo.textureWidth == b->thumbnailInfo.textureWidth) return (a->thumbnailInfo.textureHeight > b->thumbnailInfo.textureHeight); + return (a->thumbnailInfo.textureWidth > b->thumbnailInfo.textureWidth); + }); + } + } +#endif // USE_THUMBNAILS + + m_ApplyFilteringOnFileList(vFileDialogInternal, vFileInfosList, vFileInfosFilteredList); +} + +bool IGFD::FileManager::m_CompleteFileInfosWithUserFileAttirbutes(const FileDialogInternal& vFileDialogInternal, const std::shared_ptr& vInfos) { + if (vFileDialogInternal.getDialogConfig().userFileAttributes != nullptr) { + if (!vFileDialogInternal.getDialogConfig().userFileAttributes(vInfos.get(), vFileDialogInternal.getDialogConfig().userDatas)) { + return false; // the file will be ignored, so not added to the file list, so not displayed + } else { + if (!vInfos->fileType.isDir()) { + vInfos->formatedFileSize = IGFD::Utils::FormatFileSize(vInfos->fileSize); + } + } + } + return true; // file will be added to file list, so displayed +} + +void IGFD::FileManager::ClearFileLists() { + m_FilteredFileList.clear(); + m_FileList.clear(); + m_SelectedFileNames.clear(); +} + +void IGFD::FileManager::ClearPathLists() { + m_FilteredPathList.clear(); + m_PathList.clear(); + m_SelectedFileNames.clear(); +} + +void IGFD::FileManager::m_AddFile(const FileDialogInternal& vFileDialogInternal, const std::string& vPath, const std::string& vFileName, const FileType& vFileType) { + auto pInfos = FileInfos::create(); + + pInfos->filePath = vPath; + pInfos->fileNameExt = vFileName; + pInfos->fileNameExt_optimized = Utils::LowerCaseString(pInfos->fileNameExt); + pInfos->fileType = vFileType; + + if (pInfos->fileNameExt.empty() || (pInfos->fileNameExt == "." && !vFileDialogInternal.filterManager.dLGFilters.empty())) { // filename empty or filename is the current dir '.' //-V807 + return; + } + + if (pInfos->fileNameExt != ".." && (vFileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_DontShowHiddenFiles) && pInfos->fileNameExt[0] == '.') { // dont show hidden files + if (!vFileDialogInternal.filterManager.dLGFilters.empty() || (vFileDialogInternal.filterManager.dLGFilters.empty() && pInfos->fileNameExt != ".")) { // except "." if in directory mode //-V728 + return; + } + } + + if (pInfos->FinalizeFileTypeParsing(vFileDialogInternal.filterManager.GetSelectedFilter().count_dots)) { + if (!vFileDialogInternal.filterManager.IsCoveredByFilters(*pInfos.get(), // + (vFileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_CaseInsensitiveExtentionFiltering) != 0)) { + return; + } + } + + vFileDialogInternal.filterManager.FillFileStyle(pInfos); + + m_CompleteFileInfos(pInfos); + + if (m_CompleteFileInfosWithUserFileAttirbutes(vFileDialogInternal, pInfos)) { + m_FileList.push_back(pInfos); + } +} + +void IGFD::FileManager::m_AddPath(const FileDialogInternal& vFileDialogInternal, const std::string& vPath, const std::string& vFileName, const FileType& vFileType) { + if (!vFileType.isDir()) return; + + auto pInfos = FileInfos::create(); + + pInfos->filePath = vPath; + pInfos->fileNameExt = vFileName; + pInfos->fileNameExt_optimized = Utils::LowerCaseString(pInfos->fileNameExt); + pInfos->fileType = vFileType; + + if (pInfos->fileNameExt.empty() || (pInfos->fileNameExt == "." && !vFileDialogInternal.filterManager.dLGFilters.empty())) { // filename empty or filename is the current dir '.' //-V807 + return; + } + + if (pInfos->fileNameExt != ".." && (vFileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_DontShowHiddenFiles) && pInfos->fileNameExt[0] == '.') { // dont show hidden files + if (!vFileDialogInternal.filterManager.dLGFilters.empty() || (vFileDialogInternal.filterManager.dLGFilters.empty() && pInfos->fileNameExt != ".")) { // except "." if in directory mode //-V728 + return; + } + } + + vFileDialogInternal.filterManager.FillFileStyle(pInfos); + + m_CompleteFileInfos(pInfos); + + if (m_CompleteFileInfosWithUserFileAttirbutes(vFileDialogInternal, pInfos)) { + m_PathList.push_back(pInfos); + } +} + +void IGFD::FileManager::ScanDir(const FileDialogInternal& vFileDialogInternal, const std::string& vPath) { + std::string path = vPath; + + if (m_CurrentPathDecomposition.empty()) { + SetCurrentDir(path); + } + + if (!m_CurrentPathDecomposition.empty()) { +#ifdef _IGFD_WIN_ + if (path == fsRoot) { + path += IGFD::Utils::GetPathSeparator(); + } +#endif // _IGFD_WIN_ + + ClearFileLists(); + + const auto& files = m_FileSystemPtr->ScanDirectory(path); + for (const auto& file : files) { + m_AddFile(vFileDialogInternal, path, file.fileNameExt, file.fileType); + } + + m_SortFields(vFileDialogInternal, m_FileList, m_FilteredFileList); + } +} + +void IGFD::FileManager::m_ScanDirForPathSelection(const FileDialogInternal& vFileDialogInternal, const std::string& vPath) { + std::string path = vPath; + + if (!path.empty()) { +#ifdef _IGFD_WIN_ + if (path == fsRoot) path += IGFD::Utils::GetPathSeparator(); +#endif // _IGFD_WIN_ + + ClearPathLists(); + + const auto& files = m_FileSystemPtr->ScanDirectory(path); + for (const auto& file : files) { + if (file.fileType.isDir()) { + m_AddPath(vFileDialogInternal, path, file.fileNameExt, file.fileType); + } + } + + m_SortFields(vFileDialogInternal, m_PathList, m_FilteredPathList); + } +} + +void IGFD::FileManager::m_OpenPathPopup(const FileDialogInternal& vFileDialogInternal, std::vector::iterator vPathIter) { + const auto path = ComposeNewPath(vPathIter); + m_ScanDirForPathSelection(vFileDialogInternal, path); + m_PopupComposedPath = vPathIter; + ImGui::OpenPopup("IGFD_Path_Popup"); +} + +bool IGFD::FileManager::GetDevices() { + const auto devices = m_FileSystemPtr->GetDevicesList(); + if (!devices.empty()) { + m_CurrentPath.clear(); + m_CurrentPathDecomposition.clear(); + ClearFileLists(); + for (const auto& drive : devices) { + auto pInfo = FileInfos::create(); + pInfo->fileNameExt = drive.first; + pInfo->fileNameExt_optimized = Utils::LowerCaseString(drive.first); + pInfo->deviceInfos = drive.second; + pInfo->fileType.SetContent(FileType::ContentType::Directory); + if (!pInfo->fileNameExt.empty()) { + m_FileList.push_back(pInfo); + showDevices = true; + } + } + return true; + } + return false; +} + +bool IGFD::FileManager::IsComposerEmpty() const { + return m_CurrentPathDecomposition.empty(); +} + +size_t IGFD::FileManager::GetComposerSize() const { + return m_CurrentPathDecomposition.size(); +} + +bool IGFD::FileManager::IsFileListEmpty() const { + return m_FileList.empty(); +} + +bool IGFD::FileManager::IsPathListEmpty() const { + return m_PathList.empty(); +} + +size_t IGFD::FileManager::GetFullFileListSize() const { + return m_FileList.size(); +} + +std::shared_ptr IGFD::FileManager::GetFullFileAt(size_t vIdx) { + if (vIdx < m_FileList.size()) return m_FileList[vIdx]; + return nullptr; +} + +bool IGFD::FileManager::IsFilteredListEmpty() const { + return m_FilteredFileList.empty(); +} + +bool IGFD::FileManager::IsPathFilteredListEmpty() const { + return m_FilteredPathList.empty(); +} + +size_t IGFD::FileManager::GetFilteredListSize() const { + return m_FilteredFileList.size(); +} + +size_t IGFD::FileManager::GetPathFilteredListSize() const { + return m_FilteredPathList.size(); +} + +std::shared_ptr IGFD::FileManager::GetFilteredFileAt(size_t vIdx) { + if (vIdx < m_FilteredFileList.size()) return m_FilteredFileList[vIdx]; + return nullptr; +} + +std::shared_ptr IGFD::FileManager::GetFilteredPathAt(size_t vIdx) { + if (vIdx < m_FilteredPathList.size()) return m_FilteredPathList[vIdx]; + return nullptr; +} + +std::vector::iterator IGFD::FileManager::GetCurrentPopupComposedPath() const { + return m_PopupComposedPath; +} + +bool IGFD::FileManager::IsFileNameSelected(const std::string& vFileName) { + return m_SelectedFileNames.find(vFileName) != m_SelectedFileNames.end(); +} + +std::string IGFD::FileManager::GetBack() { + return m_CurrentPathDecomposition.back(); +} + +void IGFD::FileManager::ClearComposer() { + m_CurrentPathDecomposition.clear(); +} + +void IGFD::FileManager::ClearAll() { + ClearComposer(); + ClearFileLists(); + ClearPathLists(); +} +void IGFD::FileManager::ApplyFilteringOnFileList(const FileDialogInternal& vFileDialogInternal) { + m_ApplyFilteringOnFileList(vFileDialogInternal, m_FileList, m_FilteredFileList); +} + +void IGFD::FileManager::m_ApplyFilteringOnFileList(const FileDialogInternal& vFileDialogInternal, std::vector >& vFileInfosList, std::vector >& vFileInfosFilteredList) { + vFileInfosFilteredList.clear(); + for (const auto& file : vFileInfosList) { + if (!file.use_count()) continue; + bool show = true; + if (!file->SearchForTag(vFileDialogInternal.searchManager.searchTag)) // if search tag + show = false; + if (dLGDirectoryMode && !file->fileType.isDir()) show = false; + if (show) vFileInfosFilteredList.push_back(file); + } +} + +void IGFD::FileManager::m_CompleteFileInfos(const std::shared_ptr& vInfos) { + if (!vInfos.use_count()) return; + + if ((vInfos->fileNameExt == ".") || // current dir (special case, not really a dir or a file) + (vInfos->fileNameExt == "..")) { // last dir (special case, not really a dir or a file) + return; + } + + // _stat struct : + // dev_t st_dev; /* ID of device containing file */ + // ino_t st_ino; /* inode number */ + // mode_t st_mode; /* protection */ + // nlink_t st_nlink; /* number of hard links */ + // uid_t st_uid; /* user ID of owner */ + // gid_t st_gid; /* group ID of owner */ + // dev_t st_rdev; /* device ID (if special file) */ + // off_t st_size; /* total size, in bytes */ + // blksize_t st_blksize; /* blocksize for file system I/O */ + // blkcnt_t st_blocks; /* number of 512B blocks allocated */ + // time_t st_atime; /* time of last access - not sure out of ntfs */ + // time_t st_mtime; /* time of last modification - not sure out of ntfs */ + // time_t st_ctime; /* time of last status change - not sure out of ntfs */ + + std::string fpn; + + // FIXME: so the condition is always true? + if (vInfos->fileType.isFile() || vInfos->fileType.isLinkToUnknown() || vInfos->fileType.isDir()) { + fpn = vInfos->filePath + IGFD::Utils::GetPathSeparator() + vInfos->fileNameExt; + } + + m_FileSystemPtr->GetFileDateAndSize(fpn, vInfos->fileType, vInfos->fileModifDate, vInfos->fileSize); + + if (!vInfos->fileType.isDir()) { + vInfos->formatedFileSize = IGFD::Utils::FormatFileSize(vInfos->fileSize); + } +} + +void IGFD::FileManager::m_RemoveFileNameInSelection(const std::string& vFileName) { + m_SelectedFileNames.erase(vFileName); + + if (m_SelectedFileNames.size() == 1) { + snprintf(fileNameBuffer, MAX_FILE_DIALOG_NAME_BUFFER, "%s", vFileName.c_str()); + } else { + snprintf(fileNameBuffer, MAX_FILE_DIALOG_NAME_BUFFER, "%zu files Selected", m_SelectedFileNames.size()); + } +} + +void IGFD::FileManager::m_AddFileNameInSelection(const std::string& vFileName, bool vSetLastSelectionFileName) { + if (vFileName == "." || vFileName == "..") { + return; + } + m_SelectedFileNames.emplace(vFileName); + + if (m_SelectedFileNames.size() == 1) { + snprintf(fileNameBuffer, MAX_FILE_DIALOG_NAME_BUFFER, "%s", vFileName.c_str()); + } else { + snprintf(fileNameBuffer, MAX_FILE_DIALOG_NAME_BUFFER, "%zu files Selected", m_SelectedFileNames.size()); + } + + if (vSetLastSelectionFileName) { + m_LastSelectedFileName = vFileName; + } +} + +void IGFD::FileManager::SetCurrentDir(const std::string& vPath) { + std::string path = vPath; +#ifdef _IGFD_WIN_ + if (fsRoot == path) path += IGFD::Utils::GetPathSeparator(); +#endif // _IGFD_WIN_ + + bool dir_opened = m_FileSystemPtr->IsDirectory(path); + if (!dir_opened) { + path = "."; + dir_opened = m_FileSystemPtr->IsDirectory(path); + } + if (dir_opened) { +#ifdef _IGFD_WIN_ + DWORD numchar = 0; + std::wstring wpath = IGFD::Utils::UTF8Decode(path); + numchar = GetFullPathNameW(wpath.c_str(), 0, nullptr, nullptr); + std::wstring fpath(numchar, 0); + GetFullPathNameW(wpath.c_str(), numchar, (wchar_t*)fpath.data(), nullptr); + std::string real_path = IGFD::Utils::UTF8Encode(fpath); + while (real_path.back() == '\0') { // for fix issue we can have with std::string concatenation.. if there is a \0 at end + real_path = real_path.substr(0, real_path.size() - 1U); + } + if (!real_path.empty()) +#elif defined(_IGFD_UNIX_) // _IGFD_UNIX_ is _IGFD_WIN_ or APPLE + char real_path[PATH_MAX]; + char* numchar = realpath(path.c_str(), real_path); + if (numchar != nullptr) +#endif // _IGFD_WIN_ + { + m_CurrentPath = std::move(real_path); + if (m_CurrentPath.size() > 1 && m_CurrentPath[m_CurrentPath.size() - 1] == PATH_SEP) { + m_CurrentPath = m_CurrentPath.substr(0, m_CurrentPath.size() - 1); + } + IGFD::Utils::SetBuffer(inputPathBuffer, MAX_PATH_BUFFER_SIZE, m_CurrentPath); + m_CurrentPathDecomposition = IGFD::Utils::SplitStringToVector(m_CurrentPath, PATH_SEP, false); +#ifdef _IGFD_UNIX_ // _IGFD_UNIX_ is _IGFD_WIN_ or APPLE + m_CurrentPathDecomposition.insert(m_CurrentPathDecomposition.begin(), IGFD::Utils::GetPathSeparator()); +#endif // _IGFD_UNIX_ + if (!m_CurrentPathDecomposition.empty()) { +#ifdef _IGFD_WIN_ + fsRoot = m_CurrentPathDecomposition[0]; +#endif // _IGFD_WIN_ + } + } + } +} + +bool IGFD::FileManager::CreateDir(const std::string& vPath) { + if (!vPath.empty()) { + std::string path = m_CurrentPath + IGFD::Utils::GetPathSeparator() + vPath; + return m_FileSystemPtr->CreateDirectoryIfNotExist(path); + } + return false; +} + +std::string IGFD::FileManager::ComposeNewPath(std::vector::iterator vIter) { + std::string res; + + while (true) { + if (!res.empty()) { +#ifdef _IGFD_WIN_ + res = *vIter + IGFD::Utils::GetPathSeparator() + res; +#elif defined(_IGFD_UNIX_) // _IGFD_UNIX_ is _IGFD_WIN_ or APPLE + if (*vIter == fsRoot) + res = *vIter + res; + else + res = *vIter + PATH_SEP + res; +#endif // _IGFD_WIN_ + } else + res = *vIter; + + if (vIter == m_CurrentPathDecomposition.begin()) { +#ifdef _IGFD_UNIX_ // _IGFD_UNIX_ is _IGFD_WIN_ or APPLE + if (res[0] != PATH_SEP) res = PATH_SEP + res; +#else + if (res.back() != PATH_SEP) res.push_back(PATH_SEP); +#endif // defined(_IGFD_UNIX_) + break; + } + + --vIter; + } + + return res; +} + +bool IGFD::FileManager::SetPathOnParentDirectoryIfAny() { + if (m_CurrentPathDecomposition.size() > 1) { + m_CurrentPath = ComposeNewPath(m_CurrentPathDecomposition.end() - 2); + return true; + } + return false; +} + +std::string IGFD::FileManager::GetCurrentPath() { + if (m_CurrentPath.empty()) { + m_CurrentPath = "."; + } + return m_CurrentPath; +} + +void IGFD::FileManager::SetCurrentPath(const std::string& vCurrentPath) { + if (vCurrentPath.empty()) + m_CurrentPath = "."; + else + m_CurrentPath = vCurrentPath; +} + +void IGFD::FileManager::SetDefaultFileName(const std::string& vFileName) { + dLGDefaultFileName = vFileName; + IGFD::Utils::SetBuffer(fileNameBuffer, MAX_FILE_DIALOG_NAME_BUFFER, vFileName); +} + +bool IGFD::FileManager::SelectDirectory(const std::shared_ptr& vInfos) { + if (!vInfos.use_count()) return false; + + bool pathClick = false; + + if (vInfos->fileNameExt == "..") { + pathClick = SetPathOnParentDirectoryIfAny(); + } else { + std::string newPath; + + if (showDevices) { + newPath = vInfos->fileNameExt + IGFD::Utils::GetPathSeparator(); + } else { +#ifdef __linux__ + if (fsRoot == m_CurrentPath) + newPath = m_CurrentPath + vInfos->fileNameExt; + else +#endif // __linux__ + newPath = m_CurrentPath + IGFD::Utils::GetPathSeparator() + vInfos->fileNameExt; + } + + if (m_FileSystemPtr->IsDirectoryCanBeOpened(newPath)) { + if (showDevices) { + m_CurrentPath = vInfos->fileNameExt; + fsRoot = m_CurrentPath; + } else { + m_CurrentPath = newPath; //-V820 + } + pathClick = true; + } + } + + return pathClick; +} + +void IGFD::FileManager::SelectAllFileNames() { + m_SelectedFileNames.clear(); + for (const auto& pInfos : m_FilteredFileList) { + if (pInfos != nullptr) { + m_AddFileNameInSelection(pInfos->fileNameExt, true); + } + } +} + +void IGFD::FileManager::SelectFileName(const std::shared_ptr& vInfos) { + if (!vInfos.use_count()) { + return; + } + m_AddFileNameInSelection(vInfos->fileNameExt, true); +} + +void IGFD::FileManager::SelectOrDeselectFileName(const FileDialogInternal& vFileDialogInternal, const std::shared_ptr& vInfos) { + if (!vInfos.use_count()) { + return; + } + + if (ImGui::IsKeyDown(ImGuiMod_Ctrl)) { + if (dLGcountSelectionMax == 0) { // infinite selection + if (m_SelectedFileNames.find(vInfos->fileNameExt) == m_SelectedFileNames.end()) { // not found +> add + m_AddFileNameInSelection(vInfos->fileNameExt, true); + } else { // found +> remove + m_RemoveFileNameInSelection(vInfos->fileNameExt); + } + } else { // selection limited by size + if (m_SelectedFileNames.size() < dLGcountSelectionMax) { + if (m_SelectedFileNames.find(vInfos->fileNameExt) == m_SelectedFileNames.end()) { // not found +> add + m_AddFileNameInSelection(vInfos->fileNameExt, true); + } else { // found +> remove + m_RemoveFileNameInSelection(vInfos->fileNameExt); + } + } + } + } else if (ImGui::IsKeyDown(ImGuiMod_Shift)) { + if (dLGcountSelectionMax != 1) { + m_SelectedFileNames.clear(); + // we will iterate filelist and get the last selection after the start selection + bool startMultiSelection = false; + std::string fileNameToSelect = vInfos->fileNameExt; + std::string savedLastSelectedFileName; // for invert selection mode + for (const auto& file : m_FileList) { + if (!file.use_count()) { + continue; + } + bool canTake = true; + if (!file->SearchForTag(vFileDialogInternal.searchManager.searchTag)) canTake = false; + if (canTake) { // if not filtered, we will take files who are filtered by the dialog + if (file->fileNameExt == m_LastSelectedFileName) { + startMultiSelection = true; + m_AddFileNameInSelection(m_LastSelectedFileName, false); + } else if (startMultiSelection) { + if (dLGcountSelectionMax == 0) { // infinite selection + m_AddFileNameInSelection(file->fileNameExt, false); + } else { // selection limited by size + if (m_SelectedFileNames.size() < dLGcountSelectionMax) { + m_AddFileNameInSelection(file->fileNameExt, false); + } else { + startMultiSelection = false; + if (!savedLastSelectedFileName.empty()) m_LastSelectedFileName = savedLastSelectedFileName; + break; + } + } + } + + if (file->fileNameExt == fileNameToSelect) { + if (!startMultiSelection) { // we are before the last Selected FileName, so we must inverse + savedLastSelectedFileName = m_LastSelectedFileName; + m_LastSelectedFileName = fileNameToSelect; + fileNameToSelect = savedLastSelectedFileName; + startMultiSelection = true; + m_AddFileNameInSelection(m_LastSelectedFileName, false); + } else { + startMultiSelection = false; + if (!savedLastSelectedFileName.empty()) m_LastSelectedFileName = savedLastSelectedFileName; + break; + } + } + } + } + } + } else { + m_SelectedFileNames.clear(); + IGFD::Utils::ResetBuffer(fileNameBuffer); + m_AddFileNameInSelection(vInfos->fileNameExt, true); + } +} + +void IGFD::FileManager::DrawDirectoryCreation(const FileDialogInternal& vFileDialogInternal) { + if (vFileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_DisableCreateDirectoryButton) return; + + if (IMGUI_BUTTON(createDirButtonString)) { + if (!m_CreateDirectoryMode) { + m_CreateDirectoryMode = true; + IGFD::Utils::ResetBuffer(directoryNameBuffer); + } + } + if (ImGui::IsItemHovered()) ImGui::SetTooltip(buttonCreateDirString); + + if (m_CreateDirectoryMode) { + ImGui::SameLine(); + + ImGui::PushItemWidth(100.0f); + ImGui::InputText("##DirectoryFileName", directoryNameBuffer, MAX_FILE_DIALOG_NAME_BUFFER); + ImGui::PopItemWidth(); + + ImGui::SameLine(); + + if (IMGUI_BUTTON(okButtonString)) { + std::string newDir = std::string(directoryNameBuffer); + if (CreateDir(newDir)) { + SetCurrentPath(m_CurrentPath + IGFD::Utils::GetPathSeparator() + newDir); + OpenCurrentPath(vFileDialogInternal); + } + + m_CreateDirectoryMode = false; + } + + ImGui::SameLine(); + + if (IMGUI_BUTTON(cancelButtonString)) { + m_CreateDirectoryMode = false; + } + } + + ImGui::SameLine(); +} + +void IGFD::FileManager::DrawPathComposer(const FileDialogInternal& vFileDialogInternal) { + if (IMGUI_BUTTON(resetButtonString)) { + SetCurrentPath("."); + OpenCurrentPath(vFileDialogInternal); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(buttonResetPathString); + } + if (vFileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_ShowDevicesButton) { + ImGui::SameLine(); + if (IMGUI_BUTTON(devicesButtonString)) { + devicesClicked = true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(buttonDriveString); + } + } + + ImGui::SameLine(); + + if (IMGUI_BUTTON(editPathButtonString)) { + inputPathActivated = !inputPathActivated; + if (inputPathActivated) { + if (!m_CurrentPathDecomposition.empty()) { + auto endIt = m_CurrentPathDecomposition.end(); + m_CurrentPath = ComposeNewPath(--endIt); + IGFD::Utils::SetBuffer(inputPathBuffer, MAX_PATH_BUFFER_SIZE, m_CurrentPath); + } + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(buttonEditPathString); + } + + ImGui::SameLine(); + + ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); + + // show current path + if (!m_CurrentPathDecomposition.empty()) { + ImGui::SameLine(); + + if (inputPathActivated) { + ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::InputText("##pathedition", inputPathBuffer, MAX_PATH_BUFFER_SIZE); + ImGui::PopItemWidth(); + } else { + int _id = 0; + for (auto itPathDecomp = m_CurrentPathDecomposition.begin(); itPathDecomp != m_CurrentPathDecomposition.end(); ++itPathDecomp) { + if (itPathDecomp != m_CurrentPathDecomposition.begin()) { +#if defined(CUSTOM_PATH_SPACING) + ImGui::SameLine(0, CUSTOM_PATH_SPACING); +#else + ImGui::SameLine(); +#endif // USE_CUSTOM_PATH_SPACING + if (!(vFileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_DisableQuickPathSelection)) { +#if defined(_IGFD_WIN_) + const char* sep = "\\"; +#elif defined(_IGFD_UNIX_) + const char* sep = "/"; + if (itPathDecomp != m_CurrentPathDecomposition.begin() + 1) +#endif + { + ImGui::PushID(_id++); + bool click = IMGUI_PATH_BUTTON(sep); + ImGui::PopID(); + +#if defined(CUSTOM_PATH_SPACING) + ImGui::SameLine(0, CUSTOM_PATH_SPACING); +#else + ImGui::SameLine(); +#endif // USE_CUSTOM_PATH_SPACING + + if (click) { + m_OpenPathPopup(vFileDialogInternal, itPathDecomp - 1); + } else if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + m_SetCurrentPath(itPathDecomp - 1); + break; + } + } + } + } + + ImGui::PushID(_id++); + bool click = IMGUI_PATH_BUTTON((*itPathDecomp).c_str()); + ImGui::PopID(); + if (click) { + m_CurrentPath = ComposeNewPath(itPathDecomp); + pathClicked = true; + break; + } else if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { // activate input for path + m_SetCurrentPath(itPathDecomp); + break; + } + } + } + } +} + +void IGFD::FileManager::m_SetCurrentPath(std::vector::iterator vPathIter) { + m_CurrentPath = ComposeNewPath(vPathIter); + IGFD::Utils::SetBuffer(inputPathBuffer, MAX_PATH_BUFFER_SIZE, m_CurrentPath); + inputPathActivated = true; +} + +std::string IGFD::FileManager::GetResultingPath() { + if (dLGDirectoryMode && m_SelectedFileNames.size() == 1) { // if directory mode with selection 1 + std::string selectedDirectory = fileNameBuffer; + std::string path = m_CurrentPath; + if (!selectedDirectory.empty() && selectedDirectory != ".") { + path += IGFD::Utils::GetPathSeparator() + selectedDirectory; + } + return path; + } + return m_CurrentPath; // if file mode +} + +std::string IGFD::FileManager::GetResultingFileName(FileDialogInternal& vFileDialogInternal, IGFD_ResultMode vFlag) { + if (!dLGDirectoryMode) { // if not directory mode + const auto& filename = std::string(fileNameBuffer); + return vFileDialogInternal.filterManager.ReplaceExtentionWithCurrentFilterIfNeeded(filename, vFlag); + } + return ""; // directory mode +} + +std::string IGFD::FileManager::GetResultingFilePathName(FileDialogInternal& vFileDialogInternal, IGFD_ResultMode vFlag) { + if (!dLGDirectoryMode) { // if not directory mode + auto result = GetResultingPath(); + const auto& file_path_name = GetResultingFileName(vFileDialogInternal, vFlag); + if (!file_path_name.empty()) { + if (m_FileSystemPtr != nullptr && file_path_name.find(IGFD::Utils::GetPathSeparator()) != std::string::npos && // check if a path + m_FileSystemPtr->IsFileExist(file_path_name)) { // do that only if filename is a path, not only a file name + result = file_path_name; // #144, exist file, so absolute, so return it (maybe set by user in inputText) + } else { // #144, else concate path with current filename +#ifdef _IGFD_UNIX_ + if (fsRoot != result) +#endif // _IGFD_UNIX_ + { + result += IGFD::Utils::GetPathSeparator(); + } + result += file_path_name; + } + } + + return result; // file mode + } + return ""; // directory mode +} + +std::map IGFD::FileManager::GetResultingSelection(FileDialogInternal& vFileDialogInternal, IGFD_ResultMode vFlag) { + std::map res; + const auto& result_path = GetResultingPath(); + if (!m_SelectedFileNames.empty()) { + for (const auto& selectedFileName : m_SelectedFileNames) { + auto result = result_path; +#ifdef _IGFD_UNIX_ + if (fsRoot != result) +#endif // _IGFD_UNIX_ + { + result += IGFD::Utils::GetPathSeparator(); + } + result += vFileDialogInternal.filterManager.ReplaceExtentionWithCurrentFilterIfNeeded(selectedFileName, vFlag); + res[selectedFileName] = result; + } + } else { // opened directory with no selection + if (vFileDialogInternal.fileManager.dLGDirectoryMode) { // directory mode + res["."] = result_path; + } + } + return res; +} + +void IGFD::FileDialogInternal::NewFrame() { + canWeContinue = true; // reset flag for possibily validate the dialog + isOk = false; // reset dialog result + fileManager.devicesClicked = false; + fileManager.pathClicked = false; + + needToExitDialog = false; + +#ifdef USE_DIALOG_EXIT_WITH_KEY + if (ImGui::IsKeyPressed(IGFD_EXIT_KEY)) { + // we do that here with the data's defined at the last frame + // because escape key can quit input activation and at the end of the frame all flag will be false + // so we will detect nothing + if (!(fileManager.inputPathActivated || searchManager.searchInputIsActive || fileInputIsActive || fileListViewIsActive)) { + needToExitDialog = true; // need to quit dialog + } + } else +#endif + { + searchManager.searchInputIsActive = false; + fileInputIsActive = false; + fileListViewIsActive = false; + } +} + +void IGFD::FileDialogInternal::EndFrame() { + // directory change + if (fileManager.pathClicked) { + fileManager.OpenCurrentPath(*this); + } + + if (fileManager.devicesClicked) { + if (fileManager.GetDevices()) { + fileManager.ApplyFilteringOnFileList(*this); + } + } + + if (fileManager.inputPathActivated) { + auto gio = ImGui::GetIO(); + if (ImGui::IsKeyReleased(ImGuiKey_Enter)) { + fileManager.SetCurrentPath(std::string(fileManager.inputPathBuffer)); + fileManager.OpenCurrentPath(*this); + fileManager.inputPathActivated = false; + } + if (ImGui::IsKeyReleased(ImGuiKey_Escape)) { + fileManager.inputPathActivated = false; + } + } + + if (ImGui::IsKeyDown(ImGuiMod_Ctrl)) { + if (ImGui::IsKeyDown(SelectAllFilesKey)) { + fileManager.SelectAllFileNames(); + } + } +} + +void IGFD::FileDialogInternal::ResetForNewDialog() { +} + +void IGFD::FileDialogInternal::configureDialog(const std::string& vKey, const std::string& vTitle, const char* vFilters, const FileDialogConfig& vConfig) { + m_DialogConfig = vConfig; + ResetForNewDialog(); + dLGkey = vKey; + dLGtitle = vTitle; + + // treatment + if (m_DialogConfig.sidePane == nullptr) { + m_DialogConfig.sidePaneWidth = 0.0f; + } + + if (m_DialogConfig.filePathName.empty()) { + if (m_DialogConfig.path.empty()) { + fileManager.dLGpath = fileManager.GetCurrentPath(); + } else { + fileManager.dLGpath = m_DialogConfig.path; + } + fileManager.SetCurrentPath(m_DialogConfig.path); + fileManager.dLGcountSelectionMax = (size_t)m_DialogConfig.countSelectionMax; + fileManager.SetDefaultFileName(m_DialogConfig.fileName); + } else { + auto ps = fileManager.GetFileSystemInstance()->ParsePathFileName(m_DialogConfig.filePathName); + if (ps.isOk) { + fileManager.dLGpath = ps.path; + fileManager.SetDefaultFileName(ps.name); + filterManager.dLGdefaultExt = "." + ps.ext; + } else { + fileManager.dLGpath = fileManager.GetCurrentPath(); + fileManager.SetDefaultFileName(""); + filterManager.dLGdefaultExt.clear(); + } + } + + filterManager.dLGdefaultExt.clear(); + filterManager.ParseFilters(vFilters); + filterManager.SetSelectedFilterWithExt(filterManager.dLGdefaultExt); + fileManager.SetCurrentPath(fileManager.dLGpath); + fileManager.dLGDirectoryMode = (vFilters == nullptr); + fileManager.dLGcountSelectionMax = m_DialogConfig.countSelectionMax; //-V101 + fileManager.ClearAll(); + showDialog = true; +} + +const IGFD::FileDialogConfig& IGFD::FileDialogInternal::getDialogConfig() const { + return m_DialogConfig; +} + +IGFD::FileDialogConfig& IGFD::FileDialogInternal::getDialogConfigRef() { + return m_DialogConfig; +} + +IGFD::ThumbnailFeature::ThumbnailFeature() { +#ifdef USE_THUMBNAILS + m_DisplayMode = DisplayModeEnum::FILE_LIST; +#endif +} + +IGFD::ThumbnailFeature::~ThumbnailFeature() = default; + +void IGFD::ThumbnailFeature::m_NewThumbnailFrame(FileDialogInternal& /*vFileDialogInternal*/) { +#ifdef USE_THUMBNAILS + m_StartThumbnailFileDatasExtraction(); +#endif +} + +void IGFD::ThumbnailFeature::m_EndThumbnailFrame(FileDialogInternal& vFileDialogInternal) { +#ifdef USE_THUMBNAILS + m_ClearThumbnails(vFileDialogInternal); +#else + (void)vFileDialogInternal; +#endif +} + +void IGFD::ThumbnailFeature::m_QuitThumbnailFrame(FileDialogInternal& vFileDialogInternal) { +#ifdef USE_THUMBNAILS + m_StopThumbnailFileDatasExtraction(); + m_ClearThumbnails(vFileDialogInternal); +#else + (void)vFileDialogInternal; +#endif +} + +#ifdef USE_THUMBNAILS +void IGFD::ThumbnailFeature::m_StartThumbnailFileDatasExtraction() { + const bool res = m_ThumbnailGenerationThread.use_count() && m_ThumbnailGenerationThread->joinable(); + if (!res) { + m_IsWorking = true; + m_CountFiles = 0U; + m_ThumbnailGenerationThread = std::shared_ptr(new std::thread(&IGFD::ThumbnailFeature::m_ThreadThumbnailFileDatasExtractionFunc, this), [this](std::thread* obj_ptr) { + m_IsWorking = false; + if (obj_ptr != nullptr) { + m_ThumbnailFileDatasToGetCv.notify_all(); + obj_ptr->join(); + } + }); + } +} + +bool IGFD::ThumbnailFeature::m_StopThumbnailFileDatasExtraction() { + const bool res = m_ThumbnailGenerationThread.use_count() && m_ThumbnailGenerationThread->joinable(); + if (res) { + m_ThumbnailGenerationThread.reset(); + } + return res; +} + +void IGFD::ThumbnailFeature::m_ThreadThumbnailFileDatasExtractionFunc() { + m_CountFiles = 0U; + m_IsWorking = true; + // infinite loop while is thread working + while (m_IsWorking) { + std::unique_lock thumbnailFileDatasToGetLock(m_ThumbnailFileDatasToGetMutex); + m_ThumbnailFileDatasToGetCv.wait(thumbnailFileDatasToGetLock); + if (!m_ThumbnailFileDatasToGet.empty()) { + std::shared_ptr file = nullptr; + // get the first file in the list + file = (*m_ThumbnailFileDatasToGet.begin()); + m_ThumbnailFileDatasToGet.pop_front(); + thumbnailFileDatasToGetLock.unlock(); + // retrieve datas of the texture file if its an image file + if (file.use_count()) { + if (file->fileType.isFile()) { //-V522 + //|| file->fileExtLevels == ".hdr" => format float so in few times + if (file->SearchForExts(".png,.bmp,.tga,.jpg,.jpeg,.gif,.psd,.pic,.ppm,.pgm", true)) { + auto fpn = file->filePath + IGFD::Utils::GetPathSeparator() + file->fileNameExt; + int w = 0; + int h = 0; + int chans = 0; + uint8_t* datas = stbi_load(fpn.c_str(), &w, &h, &chans, STBI_rgb_alpha); + if (datas != nullptr) { + if (w != 0 && h != 0) { + // resize with respect to glyph ratio + const float ratioX = (float)w / (float)h; + const float newX = DisplayMode_ThumbailsList_ImageHeight * ratioX; + float newY = w / ratioX; + if (newX < w) { + newY = DisplayMode_ThumbailsList_ImageHeight; + } + const auto newWidth = (int)newX; + const auto newHeight = (int)newY; + const auto newBufSize = (size_t)(newWidth * newHeight * 4U); //-V112 //-V1028 + auto resizedData = new uint8_t[newBufSize]; + const auto* resizeSucceeded = stbir_resize_uint8_linear(datas, w, h, 0, resizedData, newWidth, newHeight, 0, stbir_pixel_layout::STBIR_RGBA); //-V112 + if (resizeSucceeded != nullptr) { + auto th = &file->thumbnailInfo; + th->textureFileDatas = resizedData; + th->textureWidth = newWidth; + th->textureHeight = newHeight; + th->textureChannels = 4; //-V112 + // we set that at least, because will launch the gpu creation of the texture in the + // main thread + th->isReadyToUpload = true; + // need gpu loading + m_AddThumbnailToCreate(file); + } else { + delete[] resizedData; + } + } else { + printf("image loading fail : w:%i h:%i c:%i\n", w, h, 4); //-V112 + } + stbi_image_free(datas); + } + } + } + } + } else { + thumbnailFileDatasToGetLock.unlock(); + } + } +} + +void IGFD::ThumbnailFeature::m_VariadicProgressBar(float fraction, const ImVec2& size_arg, const char* fmt, ...) { + va_list args; + va_start(args, fmt); + char TempBuffer[512]; + const int w = vsnprintf(TempBuffer, 511, fmt, args); + va_end(args); + if (w) { + ImGui::ProgressBar(fraction, size_arg, TempBuffer); + } +} + +void IGFD::ThumbnailFeature::m_DrawThumbnailGenerationProgress() { + if (m_ThumbnailGenerationThread.use_count() && m_ThumbnailGenerationThread->joinable()) { + m_ThumbnailFileDatasToGetMutex.lock(); + if (!m_ThumbnailFileDatasToGet.empty()) { + const auto p = (float)((double)m_CountFiles / (double)m_ThumbnailFileDatasToGet.size()); // read => no thread concurency issues + m_VariadicProgressBar(p, ImVec2(50, 0), "%u/%u", m_CountFiles, (uint32_t)m_ThumbnailFileDatasToGet.size()); // read => no thread concurency issues + ImGui::SameLine(); + } + m_ThumbnailFileDatasToGetMutex.unlock(); + m_ThumbnailFileDatasToGetCv.notify_all(); + } +} + +void IGFD::ThumbnailFeature::m_AddThumbnailToLoad(const std::shared_ptr& vFileInfos) { + if (vFileInfos.use_count()) { + if (vFileInfos->fileType.isFile()) { + //|| file->fileExtLevels == ".hdr" => format float so in few times + if (vFileInfos->SearchForExts(".png,.bmp,.tga,.jpg,.jpeg,.gif,.psd,.pic,.ppm,.pgm", true)) { + // write => thread concurency issues + m_ThumbnailFileDatasToGetMutex.lock(); + m_ThumbnailFileDatasToGet.push_back(vFileInfos); + vFileInfos->thumbnailInfo.isLoadingOrLoaded = true; + m_ThumbnailFileDatasToGetMutex.unlock(); + } + m_ThumbnailFileDatasToGetCv.notify_all(); + } + } +} + +void IGFD::ThumbnailFeature::m_AddThumbnailToCreate(const std::shared_ptr& vFileInfos) { + if (vFileInfos.use_count()) { + // write => thread concurency issues + m_ThumbnailToCreateMutex.lock(); + m_ThumbnailToCreate.push_back(vFileInfos); + m_ThumbnailToCreateMutex.unlock(); + } +} + +void IGFD::ThumbnailFeature::m_AddThumbnailToDestroy(const IGFD_Thumbnail_Info& vIGFD_Thumbnail_Info) { + // write => thread concurency issues + m_ThumbnailToDestroyMutex.lock(); + m_ThumbnailToDestroy.push_back(vIGFD_Thumbnail_Info); + m_ThumbnailToDestroyMutex.unlock(); +} + +void IGFD::ThumbnailFeature::m_DrawDisplayModeToolBar() { + if (IMGUI_RADIO_BUTTON(DisplayMode_FilesList_ButtonString, m_DisplayMode == DisplayModeEnum::FILE_LIST)) m_DisplayMode = DisplayModeEnum::FILE_LIST; + if (ImGui::IsItemHovered()) ImGui::SetTooltip(DisplayMode_FilesList_ButtonHelp); + ImGui::SameLine(); + if (IMGUI_RADIO_BUTTON(DisplayMode_ThumbailsList_ButtonString, m_DisplayMode == DisplayModeEnum::THUMBNAILS_LIST)) m_DisplayMode = DisplayModeEnum::THUMBNAILS_LIST; + if (ImGui::IsItemHovered()) ImGui::SetTooltip(DisplayMode_ThumbailsList_ButtonHelp); + ImGui::SameLine(); + /* todo + if (IMGUI_RADIO_BUTTON(DisplayMode_ThumbailsGrid_ButtonString, + m_DisplayMode == DisplayModeEnum::THUMBNAILS_GRID)) + m_DisplayMode = DisplayModeEnum::THUMBNAILS_GRID; + if (ImGui::IsItemHovered()) ImGui::SetTooltip(DisplayMode_ThumbailsGrid_ButtonHelp); + ImGui::SameLine(); + */ + m_DrawThumbnailGenerationProgress(); +} + +void IGFD::ThumbnailFeature::m_ClearThumbnails(FileDialogInternal& vFileDialogInternal) { + // directory wil be changed so the file list will be erased + if (vFileDialogInternal.fileManager.pathClicked) { + size_t count = vFileDialogInternal.fileManager.GetFullFileListSize(); + for (size_t idx = 0U; idx < count; idx++) { + auto file = vFileDialogInternal.fileManager.GetFullFileAt(idx); + if (file.use_count()) { + if (file->thumbnailInfo.isReadyToDisplay) //-V522 + { + m_AddThumbnailToDestroy(file->thumbnailInfo); + } + } + } + } +} + +void IGFD::ThumbnailFeature::SetCreateThumbnailCallback(const CreateThumbnailFun& vCreateThumbnailFun) { + m_CreateThumbnailFun = vCreateThumbnailFun; +} + +void IGFD::ThumbnailFeature::SetDestroyThumbnailCallback(const DestroyThumbnailFun& vCreateThumbnailFun) { + m_DestroyThumbnailFun = vCreateThumbnailFun; +} + +void IGFD::ThumbnailFeature::ManageGPUThumbnails() { + if (m_CreateThumbnailFun) { + m_ThumbnailToCreateMutex.lock(); + if (!m_ThumbnailToCreate.empty()) { + for (const auto& file : m_ThumbnailToCreate) { + if (file.use_count()) { + m_CreateThumbnailFun(&file->thumbnailInfo); + } + } + m_ThumbnailToCreate.clear(); + } + m_ThumbnailToCreateMutex.unlock(); + } else { + printf( + "No Callback found for create texture\nYou need to define the callback with a call to " + "SetCreateThumbnailCallback\n"); + } + + if (m_DestroyThumbnailFun) { + m_ThumbnailToDestroyMutex.lock(); + if (!m_ThumbnailToDestroy.empty()) { + for (auto thumbnail : m_ThumbnailToDestroy) { + m_DestroyThumbnailFun(&thumbnail); + } + m_ThumbnailToDestroy.clear(); + } + m_ThumbnailToDestroyMutex.unlock(); + } else { + printf( + "No Callback found for destroy texture\nYou need to define the callback with a call to " + "SetCreateThumbnailCallback\n"); + } +} + +#endif // USE_THUMBNAILS + +IGFD::PlacesFeature::PlacesFeature() { +#ifdef USE_PLACES_FEATURE + m_PlacesPaneWidth = defaultPlacePaneWith; + m_PlacesPaneShown = PLACES_PANE_DEFAULT_SHOWN; +#endif // USE_PLACES_FEATURE +} + +#ifdef USE_PLACES_FEATURE +void IGFD::PlacesFeature::m_InitPlaces(FileDialogInternal& vFileDialogInternal) { +#ifdef USE_PLACES_BOOKMARKS + (void)vFileDialogInternal; // for disable compiler warning about unused var + AddPlacesGroup(placesBookmarksGroupName, placesBookmarksDisplayOrder, true, PLACES_BOOKMARK_DEFAULT_OPEPEND); +#endif // USE_PLACES_BOOKMARK +#ifdef USE_PLACES_DEVICES + AddPlacesGroup(placesDevicesGroupName, placesDevicesDisplayOrder, false, PLACES_DEVICES_DEFAULT_OPEPEND); + auto devices_ptr = GetPlacesGroupPtr(placesDevicesGroupName); + if (devices_ptr != nullptr && vFileDialogInternal.fileManager.GetFileSystemInstance() != nullptr) { + const auto& devices = vFileDialogInternal.fileManager.GetFileSystemInstance()->GetDevicesList(); + for (const auto& device : devices) { + devices_ptr->AddPlace(device.first + " " + device.second, device.first + IGFD::Utils::GetPathSeparator(), false); + } + devices_ptr = nullptr; + } +#endif // USE_PLACES_DEVICES +} + +void IGFD::PlacesFeature::m_DrawPlacesButton() { + IMGUI_TOGGLE_BUTTON(placesButtonString, &m_PlacesPaneShown); + if (ImGui::IsItemHovered()) ImGui::SetTooltip(placesButtonHelpString); +} + +bool IGFD::PlacesFeature::m_DrawPlacesPane(FileDialogInternal& vFileDialogInternal, const ImVec2& vSize) { + bool res = false; + ImGui::BeginChild("##placespane", vSize); + for (const auto& group : m_OrderedGroups) { + auto group_ptr = group.second.lock(); + if (group_ptr != nullptr) { + if (ImGui::CollapsingHeader(group_ptr->name.c_str(), group_ptr->collapsingHeaderFlag)) { + ImGui::BeginChild(group_ptr->name.c_str(), ImVec2(0, 0), ImGuiChildFlags_AutoResizeY); + if (group_ptr->canBeEdited) { + ImGui::PushID(group_ptr.get()); + if (IMGUI_BUTTON(addPlaceButtonString "##ImGuiFileDialogAddPlace")) { + if (!vFileDialogInternal.fileManager.IsComposerEmpty()) { + group_ptr->AddPlace(vFileDialogInternal.fileManager.GetBack(), vFileDialogInternal.fileManager.GetCurrentPath(), true); + } + } + if (group_ptr->selectedPlaceForEdition >= 0 && group_ptr->selectedPlaceForEdition < (int)group_ptr->places.size()) { + ImGui::SameLine(); + if (IMGUI_BUTTON(removePlaceButtonString "##ImGuiFileDialogRemovePlace")) { + group_ptr->places.erase(group_ptr->places.begin() + group_ptr->selectedPlaceForEdition); + if (group_ptr->selectedPlaceForEdition == (int)group_ptr->places.size()) { + --group_ptr->selectedPlaceForEdition; + } + } + if (group_ptr->selectedPlaceForEdition >= 0 && group_ptr->selectedPlaceForEdition < (int)group_ptr->places.size()) { + ImGui::SameLine(); + if (IMGUI_BUTTON(validatePlaceButtonString "##ImGuiFileDialogOkPlace")) { + group_ptr->places[(size_t)group_ptr->selectedPlaceForEdition].name = std::string(group_ptr->editBuffer); + group_ptr->selectedPlaceForEdition = -1; + } + ImGui::SameLine(); + ImGui::PushItemWidth(vSize.x - ImGui::GetCursorPosX()); + if (ImGui::InputText("##ImGuiFileDialogPlaceEdit", group_ptr->editBuffer, MAX_FILE_DIALOG_NAME_BUFFER)) { + group_ptr->places[(size_t)group_ptr->selectedPlaceForEdition].name = std::string(group_ptr->editBuffer); + } + ImGui::PopItemWidth(); + } + } + ImGui::PopID(); + ImGui::Separator(); + } + if (!group_ptr->places.empty()) { + const auto& current_path = vFileDialogInternal.fileManager.GetCurrentPath(); + group_ptr->clipper.Begin((int)group_ptr->places.size(), ImGui::GetTextLineHeightWithSpacing()); + while (group_ptr->clipper.Step()) { + for (int i = group_ptr->clipper.DisplayStart; i < group_ptr->clipper.DisplayEnd; i++) { + if (i < 0) { + continue; + } + const PlaceStruct& place = group_ptr->places[(size_t)i]; + if (place.thickness > 0.0f) { + ImGui::SeparatorEx(ImGuiSeparatorFlags_Horizontal, place.thickness); + } else { + ImGui::PushID(i); + std::string place_name = place.name; + if (!place.style.icon.empty()) { + place_name = place.style.icon + " " + place_name; + } + if (group_ptr->canBeEdited) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + if (ImGui::SmallButton(editPlaceButtonString "##ImGuiFileDialogPlaceEditButton")) { + group_ptr->selectedPlaceForEdition = i; + IGFD::Utils::ResetBuffer(group_ptr->editBuffer); + IGFD::Utils::AppendToBuffer(group_ptr->editBuffer, MAX_FILE_DIALOG_NAME_BUFFER, place.name); + } + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + ImGui::SameLine(); + } + if (ImGui::Selectable(place_name.c_str(), current_path == place.path || group_ptr->selectedPlaceForEdition == i, ImGuiSelectableFlags_AllowDoubleClick)) { // select if path is current + if (ImGui::IsMouseDoubleClicked(0)) { + group_ptr->selectedPlaceForEdition = -1; // stop edition + // apply path + vFileDialogInternal.fileManager.SetCurrentPath(place.path); + vFileDialogInternal.fileManager.OpenCurrentPath(vFileDialogInternal); + res = true; + } + } + ImGui::PopID(); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", place.path.c_str()); + } + } + } + } + group_ptr->clipper.End(); + } + ImGui::EndChild(); + } + } + } + ImGui::EndChild(); + return res; +} + +std::string IGFD::PlacesFeature::SerializePlaces(const bool /*vForceSerialisationForAll*/) { + std::string res; + size_t idx = 0; + for (const auto& group : m_Groups) { + if (group.second->canBeSaved) { + // ## is used because reserved by imgui, so an input text cannot have ## + res += "###" + group.first + "###"; + for (const auto& place : group.second->places) { + if (place.canBeSaved) { + if (idx++ != 0) res += "##"; + res += place.name + "##" + place.path; + } + } + } + } + return res; +} + +void IGFD::PlacesFeature::DeserializePlaces(const std::string& vPlaces) { + if (!vPlaces.empty()) { + const auto& groups = IGFD::Utils::SplitStringToVector(vPlaces, "###", false); + if (groups.size() > 1) { + for (size_t i = 0; i < groups.size(); i += 2) { + auto group_ptr = GetPlacesGroupPtr(groups[i]); + if (group_ptr != nullptr) { + const auto& places = IGFD::Utils::SplitStringToVector(groups[i + 1], "##", false); + if (places.size() > 1) { + for (size_t j = 0; j < places.size(); j += 2) { + group_ptr->AddPlace(places[j], places[j + 1], true); // was saved so we set canBeSaved to true + } + } + } + } + } + } +} + +bool IGFD::PlacesFeature::AddPlacesGroup(const std::string& vGroupName, const size_t& vDisplayOrder, const bool vCanBeEdited, const bool vOpenedByDefault) { + if (vGroupName.empty()) { + return false; + } + auto group_ptr = std::make_shared(); + group_ptr->displayOrder = vDisplayOrder; + group_ptr->name = vGroupName; + group_ptr->defaultOpened = vOpenedByDefault; + if (group_ptr->defaultOpened) { + group_ptr->collapsingHeaderFlag = ImGuiTreeNodeFlags_DefaultOpen; + } + group_ptr->canBeSaved = group_ptr->canBeEdited = vCanBeEdited; // can be user edited mean can be saved + m_Groups[vGroupName] = group_ptr; + m_OrderedGroups[group_ptr->displayOrder] = group_ptr; // an exisitng display order will be overwrote for code simplicity + return true; +} + +bool IGFD::PlacesFeature::RemovePlacesGroup(const std::string& vGroupName) { + for (auto it = m_Groups.begin(); it != m_Groups.end(); ++it) { + if ((*it).second->name == vGroupName) { + m_Groups.erase(it); + return true; + } + } + return false; +} + +IGFD::PlacesFeature::GroupStruct* IGFD::PlacesFeature::GetPlacesGroupPtr(const std::string& vGroupName) { + if (m_Groups.find(vGroupName) != m_Groups.end()) { + return m_Groups.at(vGroupName).get(); + } + return nullptr; +} + +bool IGFD::PlacesFeature::GroupStruct::AddPlace(const std::string& vPlaceName, const std::string& vPlacePath, const bool vCanBeSaved, const FileStyle& vStyle) { + if (vPlaceName.empty() || vPlacePath.empty()) { + return false; + } + canBeSaved |= vCanBeSaved; // if one place must be saved so we mark the group to be saved + PlaceStruct place; + place.name = vPlaceName; + place.path = vPlacePath; + place.canBeSaved = vCanBeSaved; + place.style = vStyle; + places.push_back(place); + return true; +} + +void IGFD::PlacesFeature::GroupStruct::AddPlaceSeparator(const float& vThickness) { + PlaceStruct place; + place.thickness = vThickness; + places.push_back(place); +} + +bool IGFD::PlacesFeature::GroupStruct::RemovePlace(const std::string& vPlaceName) { + if (vPlaceName.empty()) { + return false; + } + for (auto places_it = places.begin(); places_it != places.end(); ++places_it) { + if ((*places_it).name == vPlaceName) { + places.erase(places_it); + return true; + } + } + return false; +} +#endif // USE_PLACES_FEATURE + +IGFD::KeyExplorerFeature::KeyExplorerFeature() = default; + +#ifdef USE_EXPLORATION_BY_KEYS +bool IGFD::KeyExplorerFeature::m_LocateItem_Loop(FileDialogInternal& vFileDialogInternal, ImWchar vC) { + bool found = false; + + auto& fdi = vFileDialogInternal.fileManager; + if (!fdi.IsFilteredListEmpty()) { + auto countFiles = fdi.GetFilteredListSize(); + for (size_t i = m_LocateFileByInputChar_lastFileIdx; i < countFiles; i++) { + auto nfo = fdi.GetFilteredFileAt(i); + if (nfo.use_count()) { + if (nfo->fileNameExt_optimized[0] == vC || // lower case search //-V522 + nfo->fileNameExt[0] == vC) // maybe upper case search + { + // float p = ((float)i) * ImGui::GetTextLineHeightWithSpacing(); + float p = (float)((double)i / (double)countFiles) * ImGui::GetScrollMaxY(); + ImGui::SetScrollY(p); + m_LocateFileByInputChar_lastFound = true; + m_LocateFileByInputChar_lastFileIdx = i; + m_StartFlashItem(m_LocateFileByInputChar_lastFileIdx); + + auto pInfos = fdi.GetFilteredFileAt(m_LocateFileByInputChar_lastFileIdx); + if (pInfos.use_count()) { + if (pInfos->fileType.isDir()) //-V522 + { + if (fdi.dLGDirectoryMode) // directory chooser + { + fdi.SelectFileName(pInfos); + } + } else { + fdi.SelectFileName(pInfos); + } + + found = true; + break; + } + } + } + } + } + + return found; +} + +void IGFD::KeyExplorerFeature::m_LocateByInputKey(FileDialogInternal& vFileDialogInternal) { + ImGuiContext& g = *GImGui; + auto& fdi = vFileDialogInternal.fileManager; + if (!g.ActiveId && !fdi.IsFilteredListEmpty()) { + auto& queueChar = ImGui::GetIO().InputQueueCharacters; + auto countFiles = fdi.GetFilteredListSize(); + + // point by char + if (!queueChar.empty()) { + ImWchar c = queueChar.back(); + if (m_LocateFileByInputChar_InputQueueCharactersSize != queueChar.size()) { + if (c == m_LocateFileByInputChar_lastChar) // next file starting with same char until + { + if (m_LocateFileByInputChar_lastFileIdx < countFiles - 1U) + m_LocateFileByInputChar_lastFileIdx++; + else + m_LocateFileByInputChar_lastFileIdx = 0; + } + + if (!m_LocateItem_Loop(vFileDialogInternal, c)) { + // not found, loop again from 0 this time + m_LocateFileByInputChar_lastFileIdx = 0; + m_LocateItem_Loop(vFileDialogInternal, c); + } + + m_LocateFileByInputChar_lastChar = c; + } + } + + m_LocateFileByInputChar_InputQueueCharactersSize = queueChar.size(); + } +} + +void IGFD::KeyExplorerFeature::m_ExploreWithkeys(FileDialogInternal& vFileDialogInternal, ImGuiID vListViewID) { + auto& fdi = vFileDialogInternal.fileManager; + if (!fdi.IsFilteredListEmpty()) { + bool canWeExplore = false; + bool hasNav = (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard); + + ImGuiContext& g = *GImGui; + if (!hasNav && !g.ActiveId) // no nav and no activated inputs + canWeExplore = true; + + if (g.NavId && g.NavId == vListViewID) { + if (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter) || ImGui::IsKeyPressed(ImGuiKey_Space)) { + ImGui::ActivateItemByID(vListViewID); + ImGui::SetActiveID(vListViewID, g.CurrentWindow); + } + } + + if (vListViewID == g.LastActiveId - 1) // if listview id is the last acticated nav id (ImGui::ActivateItemByID(vListViewID);) + canWeExplore = true; + + if (canWeExplore && ImGui::IsWindowFocused()) { + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { + ImGui::ClearActiveID(); + g.LastActiveId = 0; + } + + auto countFiles = fdi.GetFilteredListSize(); + + // explore + bool exploreByKey = false; + bool enterInDirectory = false; + bool exitDirectory = false; + + if ((hasNav && ImGui::IsKeyPressed(ImGuiKey_UpArrow)) || (!hasNav && ImGui::IsKeyPressed(ImGuiKey_UpArrow))) { + exploreByKey = true; + if (m_LocateFileByInputChar_lastFileIdx > 0) + m_LocateFileByInputChar_lastFileIdx--; + else + m_LocateFileByInputChar_lastFileIdx = countFiles - 1U; + } else if ((hasNav && ImGui::IsKeyPressed(ImGuiKey_DownArrow)) || (!hasNav && ImGui::IsKeyPressed(ImGuiKey_DownArrow))) { + exploreByKey = true; + if (m_LocateFileByInputChar_lastFileIdx < countFiles - 1U) + m_LocateFileByInputChar_lastFileIdx++; + else + m_LocateFileByInputChar_lastFileIdx = 0U; + } else if (ImGui::IsKeyReleased(ImGuiKey_Enter)) { + exploreByKey = true; + enterInDirectory = true; + } else if (ImGui::IsKeyReleased(ImGuiKey_Backspace)) { + exploreByKey = true; + exitDirectory = true; + } + + if (exploreByKey) { + // float totalHeight = m_FilteredFileList.size() * ImGui::GetTextLineHeightWithSpacing(); + float p = (float)((double)m_LocateFileByInputChar_lastFileIdx / (double)(countFiles - 1U)) * ImGui::GetScrollMaxY(); // seems not udpated in tables version outside tables + // float p = ((float)locateFileByInputChar_lastFileIdx) * ImGui::GetTextLineHeightWithSpacing(); + ImGui::SetScrollY(p); + m_StartFlashItem(m_LocateFileByInputChar_lastFileIdx); + + auto pInfos = fdi.GetFilteredFileAt(m_LocateFileByInputChar_lastFileIdx); + if (pInfos.use_count()) { + if (pInfos->fileType.isDir()) //-V522 + { + if (!fdi.dLGDirectoryMode || enterInDirectory) { + if (enterInDirectory) { + if (fdi.SelectDirectory(pInfos)) { + // changement de repertoire + vFileDialogInternal.fileManager.OpenCurrentPath(vFileDialogInternal); + if (m_LocateFileByInputChar_lastFileIdx > countFiles - 1U) { + m_LocateFileByInputChar_lastFileIdx = 0; + } + } + } + } else // directory chooser + { + fdi.SelectFileName(pInfos); + } + } else { + fdi.SelectFileName(pInfos); + + if (enterInDirectory) { + vFileDialogInternal.isOk = true; + } + } + + if (exitDirectory) { + auto nfo_ptr = FileInfos::create(); + nfo_ptr->fileNameExt = ".."; + + if (fdi.SelectDirectory(nfo_ptr)) { + // changement de repertoire + vFileDialogInternal.fileManager.OpenCurrentPath(vFileDialogInternal); + if (m_LocateFileByInputChar_lastFileIdx > countFiles - 1U) { + m_LocateFileByInputChar_lastFileIdx = 0; + } + } +#ifdef _IGFD_WIN_ + else { + if (fdi.GetComposerSize() == 1U) { + if (fdi.GetDevices()) { + fdi.ApplyFilteringOnFileList(vFileDialogInternal); + } + } + } +#endif // _IGFD_WIN_ + } + } + } + } + } +} + +bool IGFD::KeyExplorerFeature::m_FlashableSelectable(const char* label, bool selected, ImGuiSelectableFlags flags, bool vFlashing, const ImVec2& size_arg) { + using namespace ImGui; + + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + + // Submit label or explicit size to ItemSize(), whereas ItemAdd() will submit a larger/spanning rectangle. + ImGuiID id = window->GetID(label); + ImVec2 label_size = CalcTextSize(label, NULL, true); + ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y); + ImVec2 pos = window->DC.CursorPos; + pos.y += window->DC.CurrLineTextBaseOffset; + ItemSize(size, 0.0f); + + // Fill horizontal space + // We don't support (size < 0.0f) in Selectable() because the ItemSpacing extension would make explicitly right-aligned sizes not visibly match other widgets. + const bool span_all_columns = (flags & ImGuiSelectableFlags_SpanAllColumns) != 0; + const float min_x = span_all_columns ? window->ParentWorkRect.Min.x : pos.x; + const float max_x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x; + if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_SpanAvailWidth)) size.x = ImMax(label_size.x, max_x - min_x); + + // Text stays at the submission position, but bounding box may be extended on both sides + const ImVec2 text_min = pos; + const ImVec2 text_max(min_x + size.x, pos.y + size.y); + + // Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable. + // FIXME: Not part of layout so not included in clipper calculation, but ItemSize currenty doesn't allow offsetting CursorPos. + ImRect bb(min_x, pos.y, text_max.x, text_max.y); + if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0) { + const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x; + const float spacing_y = style.ItemSpacing.y; + const float spacing_L = IM_TRUNC(spacing_x * 0.50f); + const float spacing_U = IM_TRUNC(spacing_y * 0.50f); + bb.Min.x -= spacing_L; + bb.Min.y -= spacing_U; + bb.Max.x += (spacing_x - spacing_L); + bb.Max.y += (spacing_y - spacing_U); + } + // if (g.IO.KeyCtrl) { GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(0, 255, 0, 255)); } + + // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable.. + const float backup_clip_rect_min_x = window->ClipRect.Min.x; + const float backup_clip_rect_max_x = window->ClipRect.Max.x; + if (span_all_columns) { + window->ClipRect.Min.x = window->ParentWorkRect.Min.x; + window->ClipRect.Max.x = window->ParentWorkRect.Max.x; + } + + const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0; + const bool is_visible = ItemAdd(bb, id, NULL, disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None); + + if (span_all_columns) { + window->ClipRect.Min.x = backup_clip_rect_min_x; + window->ClipRect.Max.x = backup_clip_rect_max_x; + } + + const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0; + if (!is_visible) + if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(bb)) // Extra layer of "no logic clip" for box-select support (would be more overhead to add to ItemAdd) + return false; + + const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0; + if (disabled_item && !disabled_global) // Only testing this as an optimization + BeginDisabled(); + + // FIXME: We can standardize the behavior of those two, we could also keep the fast path of override ClipRect + full push on render only, + // which would be advantageous since most selectable are not selected. + if (span_all_columns) { + if (g.CurrentTable) + TablePushBackgroundChannel(); + else if (window->DC.CurrentColumns) + PushColumnsBackground(); + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect; + g.LastItemData.ClipRect = window->ClipRect; + } + + // We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries + ImGuiButtonFlags button_flags = 0; + if (flags & ImGuiSelectableFlags_NoHoldingActiveID) { + button_flags |= ImGuiButtonFlags_NoHoldingActiveId; + } + if (flags & ImGuiSelectableFlags_NoSetKeyOwner) { + button_flags |= ImGuiButtonFlags_NoSetKeyOwner; + } + if (flags & ImGuiSelectableFlags_SelectOnClick) { + button_flags |= ImGuiButtonFlags_PressedOnClick; + } + if (flags & ImGuiSelectableFlags_SelectOnRelease) { + button_flags |= ImGuiButtonFlags_PressedOnRelease; + } + if (flags & ImGuiSelectableFlags_AllowDoubleClick) { + button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; + } + if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap)) { + button_flags |= ImGuiButtonFlags_AllowOverlap; + } + + // Multi-selection support (header) + const bool was_selected = selected; + if (is_multi_select) { + // Handle multi-select + alter button flags for it + MultiSelectItemHeader(id, &selected, &button_flags); + } + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags); + + // Multi-selection support (footer) + if (is_multi_select) { + MultiSelectItemFooter(id, &selected, &pressed); + } else { + // Auto-select when moved into + // - This will be more fully fleshed in the range-select branch + // - This is not exposed as it won't nicely work with some user side handling of shift/control + // - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons + // - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope()) + // - (2) usage will fail with clipped items + // The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API. + if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId) + if (g.NavJustMovedToId == id) selected = pressed = true; + } + + ////////////////////////////////////////////////////////////////// + // this function copy ImGui::Selectable just for this line.... + hovered |= vFlashing; + ////////////////////////////////////////////////////////////////// + + // Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with gamepad/keyboard + if (pressed || (hovered && (flags & ImGuiSelectableFlags_SetNavIdOnHover))) { + if (!g.NavHighlightItemUnderNav && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent) { + SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, WindowRectAbsToRel(window, bb)); // (bb == NavRect) + g.NavCursorVisible = false; + } + } + if (pressed) MarkItemEdited(id); + + if (selected != was_selected) g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection; + + // Render + if (is_visible) { + if (hovered || selected) { + // FIXME-MULTISELECT: Styling: Color for 'selected' elements? ImGuiCol_HeaderSelected + ImU32 col; + if (selected && !hovered) + col = GetColorU32(ImLerp(GetStyleColorVec4(ImGuiCol_Header), GetStyleColorVec4(ImGuiCol_HeaderHovered), 0.5f)); + else + col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + RenderFrame(bb.Min, bb.Max, col, false, 0.0f); + } + if (g.NavId == id) { + ImGuiNavRenderCursorFlags flags = ImGuiNavRenderCursorFlags_Compact | ImGuiNavRenderCursorFlags_NoRounding; + if (is_multi_select) flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle + RenderNavCursor(bb, id, flags); + } + } + + if (span_all_columns) { + if (g.CurrentTable) + TablePopBackgroundChannel(); + else if (window->DC.CurrentColumns) + PopColumnsBackground(); + } + + if (is_visible) RenderTextClipped(text_min, text_max, label, NULL, &label_size, style.SelectableTextAlign, &bb); + + // Automatically close popups + if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.ItemFlags & ImGuiItemFlags_AutoClosePopups)) CloseCurrentPopup(); + + if (disabled_item && !disabled_global) EndDisabled(); + + // Selectable() always returns a pressed state! + // Users of BeginMultiSelect()/EndMultiSelect() scope: you may call ImGui::IsItemToggledSelection() to retrieve + // selection toggle, only useful if you need that state updated (e.g. for rendering purpose) before reaching EndMultiSelect(). + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + return pressed; //-V1020 +} + +void IGFD::KeyExplorerFeature::m_StartFlashItem(size_t vIdx) { + m_FlashAlpha = 1.0f; + m_FlashedItem = vIdx; +} + +bool IGFD::KeyExplorerFeature::m_BeginFlashItem(size_t vIdx) { + bool res = false; + + if (m_FlashedItem == vIdx && std::abs(m_FlashAlpha - 0.0f) > 0.00001f) { + m_FlashAlpha -= m_FlashAlphaAttenInSecs * ImGui::GetIO().DeltaTime; + if (m_FlashAlpha < 0.0f) m_FlashAlpha = 0.0f; + + ImVec4 hov = ImGui::GetStyleColorVec4(ImGuiCol_HeaderHovered); + hov.w = m_FlashAlpha; + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, hov); + res = true; + } + + return res; +} + +void IGFD::KeyExplorerFeature::m_EndFlashItem() { + ImGui::PopStyleColor(); +} + +void IGFD::KeyExplorerFeature::SetFlashingAttenuationInSeconds(float vAttenValue) { + m_FlashAlphaAttenInSecs = 1.0f / ImMax(vAttenValue, 0.01f); +} +#endif // USE_EXPLORATION_BY_KEYS + +IGFD::FileDialog::FileDialog() : PlacesFeature(), KeyExplorerFeature(), ThumbnailFeature() { +} +IGFD::FileDialog::~FileDialog() = default; + +////////////////////////////////////////////////////////////////////////////////////////////////// +///// FILE DIALOG STANDARD DIALOG //////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////////////////////////// + +// path and fileNameExt can be specified +void IGFD::FileDialog::OpenDialog(const std::string& vKey, const std::string& vTitle, const char* vFilters, const FileDialogConfig& vConfig) { + if (m_FileDialogInternal.showDialog) // if already opened, quit + return; + m_FileDialogInternal.configureDialog(vKey, vTitle, vFilters, vConfig); +#ifdef USE_PLACES_FEATURE + m_InitPlaces(m_FileDialogInternal); +#endif +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +///// FILE DIALOG DISPLAY FUNCTION /////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////////////////////////// + +bool IGFD::FileDialog::Display(const std::string& vKey, ImGuiWindowFlags vFlags, ImVec2 vMinSize, ImVec2 vMaxSize) { + bool res = false; + + if (m_FileDialogInternal.showDialog && m_FileDialogInternal.dLGkey == vKey) { + if (m_FileDialogInternal.puUseCustomLocale) setlocale(m_FileDialogInternal.localeCategory, m_FileDialogInternal.localeBegin.c_str()); + + auto& fdFile = m_FileDialogInternal.fileManager; + auto& fdFilter = m_FileDialogInternal.filterManager; + + // to be sure than only one dialog is displayed per frame + ImGuiContext& g = *GImGui; + if (g.FrameCount == m_FileDialogInternal.lastImGuiFrameCount) { // one instance was displayed this frame before + return res; // for this key +> quit + } + m_FileDialogInternal.lastImGuiFrameCount = g.FrameCount; // mark this instance as used this frame + + m_CurrentDisplayedFlags = vFlags; + std::string name = m_FileDialogInternal.dLGtitle + "##" + m_FileDialogInternal.dLGkey; + if (m_FileDialogInternal.name != name) { + fdFile.ClearComposer(); + fdFile.ClearFileLists(); + } + + m_NewFrame(); + +#ifdef IMGUI_HAS_VIEWPORT + if (!ImGui::GetIO().ConfigViewportsNoDecoration) { + // https://github.com/ocornut/imgui/issues/4534 + ImGuiWindowClass window_class; + window_class.ViewportFlagsOverrideClear = ImGuiViewportFlags_NoDecoration; + ImGui::SetNextWindowClass(&window_class); + } +#endif // IMGUI_HAS_VIEWPORT + + bool beg = false; + ImVec2 frameSize = ImVec2(0, 0); + if (m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_NoDialog) { // disable our own dialog system (standard or modal) + frameSize = vMinSize; + beg = true; + } else { + ImGui::SetNextWindowSizeConstraints(vMinSize, vMaxSize); + if (m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_Modal && // disable modal because the confirm dialog for overwrite is + !m_FileDialogInternal.okResultToConfirm) { // a new modal + ImGui::OpenPopup(name.c_str()); + beg = ImGui::BeginPopupModal(name.c_str(), (bool*)nullptr, m_CurrentDisplayedFlags | ImGuiWindowFlags_NoScrollbar); + } else { + beg = ImGui::Begin(name.c_str(), (bool*)nullptr, m_CurrentDisplayedFlags | ImGuiWindowFlags_NoScrollbar); + } + } + if (beg) { +#ifdef IMGUI_HAS_VIEWPORT + // if decoration is enabled we disable the resizing feature of imgui for avoid crash with SDL2 and GLFW3 + if (ImGui::GetIO().ConfigViewportsNoDecoration) { + m_CurrentDisplayedFlags = vFlags; + } else { + auto win = ImGui::GetCurrentWindowRead(); + if (win->Viewport->Idx != 0) + m_CurrentDisplayedFlags |= ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar; + else + m_CurrentDisplayedFlags = vFlags; + } +#endif // IMGUI_HAS_VIEWPORT + + if (ImGui::BeginChild("childContent", frameSize, false, m_CurrentDisplayedFlags | ImGuiWindowFlags_NoScrollbar)) { + m_FileDialogInternal.name = name; //-V820 + if (fdFile.dLGpath.empty()) { + fdFile.dLGpath = "."; // defaut path is '.' + } + fdFilter.SetDefaultFilterIfNotDefined(); + + // init list of files + if (fdFile.IsFileListEmpty() && !fdFile.showDevices) { + if (fdFile.dLGpath != ".") // Removes extension seperator in filename if we don't check + IGFD::Utils::ReplaceString(fdFile.dLGDefaultFileName, fdFile.dLGpath, ""); // local path + + if (!fdFile.dLGDefaultFileName.empty()) { + fdFile.SetDefaultFileName(fdFile.dLGDefaultFileName); + fdFilter.SetSelectedFilterWithExt(fdFilter.dLGdefaultExt); + } else if (fdFile.dLGDirectoryMode) // directory mode + fdFile.SetDefaultFileName("."); + fdFile.ScanDir(m_FileDialogInternal, fdFile.dLGpath); + } + + // draw dialog parts + m_DrawHeader(); // place, directory, path + m_DrawContent(); // place, files view, side pane + res = m_DrawFooter(); // file field, filter combobox, ok/cancel buttons + + m_EndFrame(); + } + ImGui::EndChild(); + + // for display in dialog center, the confirm to overwrite dlg + m_FileDialogInternal.dialogCenterPos = ImGui::GetCurrentWindowRead()->ContentRegionRect.GetCenter(); + + // when the confirm to overwrite dialog will appear we need to + // disable the modal mode of the main file dialog + // see prOkResultToConfirm under + if (m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_Modal && !m_FileDialogInternal.okResultToConfirm) { + ImGui::EndPopup(); + } + } + + if (m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_NoDialog) { // disable our own dialog system (standard or modal) + } else { + // same things here regarding prOkResultToConfirm + if (!(m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_Modal) || m_FileDialogInternal.okResultToConfirm) { + ImGui::End(); + } + } + // confirm the result and show the confirm to overwrite dialog if needed + res = m_Confirm_Or_OpenOverWriteFileDialog_IfNeeded(res, vFlags); + + if (m_FileDialogInternal.puUseCustomLocale) setlocale(m_FileDialogInternal.localeCategory, m_FileDialogInternal.localeEnd.c_str()); + } + + return res; +} + +void IGFD::FileDialog::m_NewFrame() { + m_FileDialogInternal.NewFrame(); + m_NewThumbnailFrame(m_FileDialogInternal); +} + +void IGFD::FileDialog::m_EndFrame() { + m_EndThumbnailFrame(m_FileDialogInternal); + m_FileDialogInternal.EndFrame(); +} +void IGFD::FileDialog::m_QuitFrame() { + m_QuitThumbnailFrame(m_FileDialogInternal); +} + +void IGFD::FileDialog::m_DrawHeader() { +#ifdef USE_PLACES_FEATURE + if (!(m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_DisablePlaceMode)) { + m_DrawPlacesButton(); + ImGui::SameLine(); + } + +#endif // USE_PLACES_FEATURE + + m_FileDialogInternal.fileManager.DrawDirectoryCreation(m_FileDialogInternal); + + if ( +#ifdef USE_PLACES_FEATURE + !(m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_DisablePlaceMode) || +#endif // USE_PLACES_FEATURE + !(m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_DisableCreateDirectoryButton)) { + ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); + ImGui::SameLine(); + } + m_FileDialogInternal.fileManager.DrawPathComposer(m_FileDialogInternal); + +#ifdef USE_THUMBNAILS + if (!(m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_DisableThumbnailMode)) { + m_DrawDisplayModeToolBar(); + ImGui::SameLine(); + ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); + ImGui::SameLine(); + } +#endif // USE_THUMBNAILS + + m_FileDialogInternal.searchManager.DrawSearchBar(m_FileDialogInternal); +} + +void IGFD::FileDialog::m_DrawContent() { + ImVec2 size = ImGui::GetContentRegionAvail() - ImVec2(0.0f, m_FileDialogInternal.footerHeight); + +#ifdef USE_PLACES_FEATURE + if (!(m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_DisablePlaceMode)) { + if (m_PlacesPaneShown) { + float otherWidth = size.x - m_PlacesPaneWidth; + ImGui::PushID("splitterplaces"); + IGFD::Utils::ImSplitter(true, 4.0f, &m_PlacesPaneWidth, &otherWidth, 10.0f, 10.0f + m_FileDialogInternal.getDialogConfig().sidePaneWidth, size.y); + ImGui::PopID(); + size.x -= otherWidth; + m_DrawPlacesPane(m_FileDialogInternal, size); + ImGui::SameLine(); + } + } +#endif // USE_PLACES_FEATURE + + size.x = ImGui::GetContentRegionAvail().x - m_FileDialogInternal.getDialogConfig().sidePaneWidth; + + if (m_FileDialogInternal.getDialogConfig().sidePane) { + ImGui::PushID("splittersidepane"); + IGFD::Utils::ImSplitter(true, 4.0f, &size.x, &m_FileDialogInternal.getDialogConfigRef().sidePaneWidth, 10.0f, 10.0f, size.y); + ImGui::PopID(); + } + +#ifdef USE_THUMBNAILS + if (m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_DisableThumbnailMode) { + m_DrawFileListView(size); + } else { + switch (m_DisplayMode) { + case DisplayModeEnum::FILE_LIST: m_DrawFileListView(size); break; + case DisplayModeEnum::THUMBNAILS_LIST: m_DrawThumbnailsListView(size); break; + case DisplayModeEnum::THUMBNAILS_GRID: m_DrawThumbnailsGridView(size); + } + } +#else // USE_THUMBNAILS + m_DrawFileListView(size); +#endif // USE_THUMBNAILS + + if (m_FileDialogInternal.getDialogConfig().sidePane) { + m_DrawSidePane(size.y); + } + + if (!(m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_DisableQuickPathSelection)) { + m_DisplayPathPopup(size); + } +} + +void IGFD::FileDialog::m_DisplayPathPopup(ImVec2 vSize) { + ImVec2 size = ImVec2(vSize.x * 0.5f, vSize.y * 0.5f); + if (ImGui::BeginPopup("IGFD_Path_Popup")) { + auto& fdi = m_FileDialogInternal.fileManager; + + ImGui::PushID(this); + + static ImGuiTableFlags flags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg | ImGuiTableFlags_Hideable | ImGuiTableFlags_ScrollY | ImGuiTableFlags_NoHostExtendY; + auto listViewID = ImGui::GetID("##FileDialog_pathTable"); + if (ImGui::BeginTableEx("##FileDialog_pathTable", listViewID, 1, flags, size, 0.0f)) //-V112 + { + ImGui::TableSetupScrollFreeze(0, 1); // Make header always visible + ImGui::TableSetupColumn(tableHeaderFileNameString, ImGuiTableColumnFlags_WidthStretch | (defaultSortOrderFilename ? ImGuiTableColumnFlags_PreferSortAscending : ImGuiTableColumnFlags_PreferSortDescending), -1, 0); + + ImGui::TableHeadersRow(); + + if (!fdi.IsPathFilteredListEmpty()) { + std::string _str; + ImFont* _font = nullptr; + bool _showColor = false; + + m_PathListClipper.Begin((int)fdi.GetPathFilteredListSize(), ImGui::GetTextLineHeightWithSpacing()); + while (m_PathListClipper.Step()) { + for (int i = m_PathListClipper.DisplayStart; i < m_PathListClipper.DisplayEnd; i++) { + if (i < 0) continue; + + auto pInfos = fdi.GetFilteredPathAt((size_t)i); + if (!pInfos.use_count()) continue; + + m_BeginFileColorIconStyle(pInfos, _showColor, _str, &_font); + + bool selected = fdi.IsFileNameSelected(pInfos->fileNameExt); // found + + ImGui::TableNextRow(); + + if (ImGui::TableNextColumn()) // file name + { + if (ImGui::Selectable(pInfos->fileNameExt.c_str(), &selected, static_cast(ImGuiSelectableFlags_SpanAllColumns) | static_cast(ImGuiSelectableFlags_SpanAvailWidth))) { + fdi.SetCurrentPath(fdi.ComposeNewPath(fdi.GetCurrentPopupComposedPath())); + fdi.pathClicked = fdi.SelectDirectory(pInfos); + ImGui::CloseCurrentPopup(); + } + } + + m_EndFileColorIconStyle(_showColor, _font); + } + } + m_PathListClipper.End(); + } + + ImGui::EndTable(); + } + + ImGui::PopID(); + + ImGui::EndPopup(); + } +} + +bool IGFD::FileDialog::m_DrawOkButton() { + auto& fdFile = m_FileDialogInternal.fileManager; + if ((m_FileDialogInternal.canWeContinue && strlen(fdFile.fileNameBuffer)) || // + (m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_OptionalFileName)) { // optional + if (IMGUI_BUTTON(okButtonString "##validationdialog", ImVec2(okButtonWidth, 0.0f)) || m_FileDialogInternal.isOk) { + m_FileDialogInternal.isOk = true; + return true; + } + +#if !invertOkAndCancelButtons + ImGui::SameLine(); +#endif + } + + return false; +} + +bool IGFD::FileDialog::m_DrawCancelButton() { + if (IMGUI_BUTTON(cancelButtonString "##validationdialog", ImVec2(cancelButtonWidth, 0.0f)) || m_FileDialogInternal.needToExitDialog) // dialog exit asked + { + m_FileDialogInternal.isOk = false; + return true; + } + +#if invertOkAndCancelButtons + ImGui::SameLine(); +#endif + + return false; +} + +bool IGFD::FileDialog::m_DrawValidationButtons() { + bool res = false; + + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (ImGui::GetContentRegionAvail().x - prOkCancelButtonWidth) * okCancelButtonAlignement); + + ImGui::BeginGroup(); + + if (invertOkAndCancelButtons) { + res |= m_DrawCancelButton(); + res |= m_DrawOkButton(); + } else { + res |= m_DrawOkButton(); + res |= m_DrawCancelButton(); + } + + ImGui::EndGroup(); + + prOkCancelButtonWidth = ImGui::GetItemRectSize().x; + + return res; +} + +bool IGFD::FileDialog::m_DrawFooter() { + auto& fdFile = m_FileDialogInternal.fileManager; + + float posY = ImGui::GetCursorPos().y; // height of last bar calc + ImGui::AlignTextToFramePadding(); + if (!fdFile.dLGDirectoryMode) + ImGui::Text(fileNameString); + else // directory chooser + ImGui::Text(dirNameString); + ImGui::SameLine(); + + // Input file fields + float width = ImGui::GetContentRegionAvail().x; + if (!fdFile.dLGDirectoryMode) { + ImGuiContext& g = *GImGui; + width -= m_FileDialogInternal.filterManager.GetFilterComboBoxWidth() + g.Style.ItemSpacing.x; + } + + ImGui::PushItemWidth(width); + ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue; + if (m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_ReadOnlyFileNameField) { + flags |= ImGuiInputTextFlags_ReadOnly; + } + if (ImGui::InputText("##FileName", fdFile.fileNameBuffer, MAX_FILE_DIALOG_NAME_BUFFER, flags)) { + m_FileDialogInternal.isOk = true; + } + if (ImGui::GetItemID() == ImGui::GetActiveID()) m_FileDialogInternal.fileInputIsActive = true; + ImGui::PopItemWidth(); + + // combobox of filters + m_FileDialogInternal.filterManager.DrawFilterComboBox(m_FileDialogInternal); + + bool res = m_DrawValidationButtons(); + m_FileDialogInternal.footerHeight = ImGui::GetCursorPosY() - posY; + return res; +} + +bool IGFD::FileDialog::m_Selectable(int vRowIdx, const char* vLabel, bool vSelected, ImGuiSelectableFlags vFlags, const ImVec2& vSizeArg) { + bool res = false; +#ifdef USE_EXPLORATION_BY_KEYS + bool flashed = m_BeginFlashItem((size_t)vRowIdx); + res = m_FlashableSelectable(vLabel, vSelected, vFlags, flashed, vSizeArg); + if (flashed) { + m_EndFlashItem(); + } +#else // USE_EXPLORATION_BY_KEYS + (void)vRowIdx; // remove a warnings for unused var + res = ImGui::Selectable(vLabel, vSelected, vFlags, vSizeArg); +#endif // USE_EXPLORATION_BY_KEYS + return res; +} + +void IGFD::FileDialog::m_SelectableItem(int vRowIdx, std::shared_ptr vInfos, bool vSelected, const char* vFmt, ...) { + if (!vInfos.use_count()) return; + + auto& fdi = m_FileDialogInternal.fileManager; + + static ImGuiSelectableFlags selectableFlags = ImGuiSelectableFlags_AllowDoubleClick | ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_SpanAvailWidth; + + va_list args; + va_start(args, vFmt); + vsnprintf(fdi.variadicBuffer, MAX_FILE_DIALOG_NAME_BUFFER, vFmt, args); + va_end(args); + + float h = 0.0f; +#ifdef USE_THUMBNAILS + if (m_DisplayMode == DisplayModeEnum::THUMBNAILS_LIST && !(m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_DisableThumbnailMode)) { + h = DisplayMode_ThumbailsList_ImageHeight; + } +#endif // USE_THUMBNAILS + if (m_Selectable(vRowIdx, fdi.variadicBuffer, vSelected, selectableFlags, ImVec2(-1.0f, h))) { + if (vInfos->fileType.isDir()) { + // nav system, selectable cause open directory or select directory + if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) { + // little fix for get back the mouse behavior in nav system + if (ImGui::IsMouseDoubleClicked(0)) { // 0 -> left mouse button double click + fdi.pathClicked = fdi.SelectDirectory(vInfos); + } else if (fdi.dLGDirectoryMode) { // directory chooser + fdi.SelectOrDeselectFileName(m_FileDialogInternal, vInfos); + } else { + fdi.pathClicked = fdi.SelectDirectory(vInfos); + } + } else { // no nav system => classic behavior + if (ImGui::IsMouseDoubleClicked(0)) { // 0 -> left mouse button double click + fdi.pathClicked = fdi.SelectDirectory(vInfos); + } else if (fdi.dLGDirectoryMode) { // directory chooser + fdi.SelectOrDeselectFileName(m_FileDialogInternal, vInfos); + } + } + } else { + fdi.SelectOrDeselectFileName(m_FileDialogInternal, vInfos); + if (ImGui::IsMouseDoubleClicked(0)) { + m_FileDialogInternal.isOk = true; + } + } + } +} + +void IGFD::FileDialog::m_DisplayFileInfosTooltip(const int32_t& vRowIdx, const int32_t& vColumnIdx, std::shared_ptr vFileInfos) { + // IsItemHovered is not sufficient since file size have two calls to Text + if ((ImGui::TableGetHoveredColumn() == vColumnIdx) && // column hovered + (ImGui::TableGetHoveredRow() == (vRowIdx + 1)) && // row hovered + (vFileInfos != nullptr) && // fileinfo not null + (vFileInfos->tooltipColumn == vColumnIdx) && // good tooltip column + (!vFileInfos->tooltipMessage.empty())) { // tooltip not empty + ImGui::SetTooltip("%s", vFileInfos->tooltipMessage.c_str()); + } +} + +void IGFD::FileDialog::m_BeginFileColorIconStyle(std::shared_ptr vFileInfos, bool& vOutShowColor, std::string& vOutStr, ImFont** vOutFont) { + vOutStr.clear(); + vOutShowColor = false; + + if (vFileInfos->fileStyle != nullptr) { + vOutShowColor = true; + *vOutFont = vFileInfos->fileStyle->font; + } + + if (vOutShowColor && !vFileInfos->fileStyle->icon.empty()) { + vOutStr = vFileInfos->fileStyle->icon; + } else if (vFileInfos->fileType.isDir()) { + vOutStr = dirEntryString; + } else if (vFileInfos->fileType.isLinkToUnknown()) { + vOutStr = linkEntryString; + } else if (vFileInfos->fileType.isFile()) { + vOutStr = fileEntryString; + } + + vOutStr += " " + vFileInfos->fileNameExt; + + if (vOutShowColor) { + ImGui::PushStyleColor(ImGuiCol_Text, vFileInfos->fileStyle->color); + } + if (*vOutFont) { +#if IMGUI_VERSION_NUM < 19201 + ImGui::PushFont(*vOutFont); +#else + ImGui::PushFont(*vOutFont, 0.0f); +#endif + } +} + +void IGFD::FileDialog::m_EndFileColorIconStyle(const bool vShowColor, ImFont* vFont) { + if (vFont) { + ImGui::PopFont(); + } + if (vShowColor) { + ImGui::PopStyleColor(); + } +} + +void IGFD::FileDialog::m_drawColumnText(int /*vColIdx*/, const char* vFmt, const char* vLabel, bool /*vSelected*/, bool /*vHovered*/) { + ImGui::Text(vFmt, vLabel); +} + +void IGFD::FileDialog::m_rightAlignText(const char* text, const char* maxWidthText) { + const auto maxWidth = ImGui::CalcTextSize(maxWidthText).x; + const auto actualWidth = ImGui::CalcTextSize(text).x; + const auto spacing = maxWidth - actualWidth; + if (spacing > 0.0f) { + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + spacing); + } + ImGui::TextUnformatted(text); +} + +void IGFD::FileDialog::m_DrawFileListView(ImVec2 vSize) { + auto& fdi = m_FileDialogInternal.fileManager; + + ImGui::PushID(this); + + static ImGuiTableFlags flags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg | ImGuiTableFlags_Hideable | ImGuiTableFlags_ScrollY | ImGuiTableFlags_NoHostExtendY +#ifndef USE_CUSTOM_SORTING_ICON + | ImGuiTableFlags_Sortable +#endif // USE_CUSTOM_SORTING_ICON + ; + const auto listViewID = ImGui::GetID("FileTable"); + if (ImGui::BeginTableEx("FileTable", listViewID, 4, flags, vSize, 0.0f)) { + ImGui::TableSetupScrollFreeze(0, 1); // Make header always visible + ImGui::TableSetupColumn(fdi.headerFileName.c_str(), ImGuiTableColumnFlags_WidthStretch | (defaultSortOrderFilename ? ImGuiTableColumnFlags_PreferSortAscending : ImGuiTableColumnFlags_PreferSortDescending), -1, 0); + ImGui::TableSetupColumn(fdi.headerFileType.c_str(), + ImGuiTableColumnFlags_WidthFixed | (defaultSortOrderType ? ImGuiTableColumnFlags_PreferSortAscending : ImGuiTableColumnFlags_PreferSortDescending) | + ((m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_HideColumnType) ? ImGuiTableColumnFlags_DefaultHide : 0), + -1, 1); + ImGui::TableSetupColumn(fdi.headerFileSize.c_str(), + ImGuiTableColumnFlags_WidthFixed | (defaultSortOrderSize ? ImGuiTableColumnFlags_PreferSortAscending : ImGuiTableColumnFlags_PreferSortDescending) | + ((m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_HideColumnSize) ? ImGuiTableColumnFlags_DefaultHide : 0), + -1, 2); + ImGui::TableSetupColumn(fdi.headerFileDate.c_str(), + ImGuiTableColumnFlags_WidthFixed | (defaultSortOrderDate ? ImGuiTableColumnFlags_PreferSortAscending : ImGuiTableColumnFlags_PreferSortDescending) | + ((m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_HideColumnDate) ? ImGuiTableColumnFlags_DefaultHide : 0), + -1, 3); + +#ifndef USE_CUSTOM_SORTING_ICON + // Sort our data if sort specs have been changed! + if (ImGuiTableSortSpecs* sorts_specs = ImGui::TableGetSortSpecs()) { + if (sorts_specs->SpecsDirty && !fdi.IsFileListEmpty()) { + bool direction = sorts_specs->Specs->SortDirection == ImGuiSortDirection_Ascending; + + if (sorts_specs->Specs->ColumnUserID == 0) { + fdi.sortingField = IGFD::FileManager::SortingFieldEnum::FIELD_FILENAME; + fdi.sortingDirection[0] = direction; + fdi.SortFields(m_FileDialogInternal); + } else if (sorts_specs->Specs->ColumnUserID == 1) { + fdi.sortingField = IGFD::FileManager::SortingFieldEnum::FIELD_TYPE; + fdi.sortingDirection[1] = direction; + fdi.SortFields(m_FileDialogInternal); + } else if (sorts_specs->Specs->ColumnUserID == 2) { + fdi.sortingField = IGFD::FileManager::SortingFieldEnum::FIELD_SIZE; + fdi.sortingDirection[2] = direction; + fdi.SortFields(m_FileDialogInternal); + } else // if (sorts_specs->Specs->ColumnUserID == 3) => alwayd true for the moment, to uncomment if we + // add a fourth column + { + fdi.sortingField = IGFD::FileManager::SortingFieldEnum::FIELD_DATE; + fdi.sortingDirection[3] = direction; + fdi.SortFields(m_FileDialogInternal); + } + + sorts_specs->SpecsDirty = false; + } + } + + ImGui::TableHeadersRow(); +#else // USE_CUSTOM_SORTING_ICON + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + for (int column = 0; column < 4; column++) //-V112 + { + ImGui::TableSetColumnIndex(column); + const char* column_name = ImGui::TableGetColumnName(column); // Retrieve name passed to TableSetupColumn() + ImGui::PushID(column); + ImGui::TableHeader(column_name); + ImGui::PopID(); + if (ImGui::IsItemClicked()) { + if (column == 0) { + if (fdi.sortingField == IGFD::FileManager::SortingFieldEnum::FIELD_FILENAME) + fdi.sortingDirection[0] = !fdi.sortingDirection[0]; + else + fdi.sortingField = IGFD::FileManager::SortingFieldEnum::FIELD_FILENAME; + + fdi.SortFields(m_FileDialogInternal); + } else if (column == 1) { + if (fdi.sortingField == IGFD::FileManager::SortingFieldEnum::FIELD_TYPE) + fdi.sortingDirection[1] = !fdi.sortingDirection[1]; + else + fdi.sortingField = IGFD::FileManager::SortingFieldEnum::FIELD_TYPE; + + fdi.SortFields(m_FileDialogInternal); + } else if (column == 2) { + if (fdi.sortingField == IGFD::FileManager::SortingFieldEnum::FIELD_SIZE) + fdi.sortingDirection[2] = !fdi.sortingDirection[2]; + else + fdi.sortingField = IGFD::FileManager::SortingFieldEnum::FIELD_SIZE; + + fdi.SortFields(m_FileDialogInternal); + } else // if (column == 3) => alwayd true for the moment, to uncomment if we add a fourth column + { + if (fdi.sortingField == IGFD::FileManager::SortingFieldEnum::FIELD_DATE) + fdi.sortingDirection[3] = !fdi.sortingDirection[3]; + else + fdi.sortingField = IGFD::FileManager::SortingFieldEnum::FIELD_DATE; + + fdi.SortFields(m_FileDialogInternal); + } + } + } +#endif // USE_CUSTOM_SORTING_ICON + if (!fdi.IsFilteredListEmpty()) { + std::string _str; + ImFont* _font = nullptr; + bool _showColor = false; + + int column_id = 0; + bool _rowHovered = false; + m_FileListClipper.Begin((int)fdi.GetFilteredListSize(), ImGui::GetTextLineHeightWithSpacing()); + while (m_FileListClipper.Step()) { + for (int i = m_FileListClipper.DisplayStart; i < m_FileListClipper.DisplayEnd; i++) { + if (i < 0) { + continue; + } + + auto pInfos = fdi.GetFilteredFileAt((size_t)i); + if (pInfos == nullptr) { + continue; + } + + m_BeginFileColorIconStyle(pInfos, _showColor, _str, &_font); + + const bool selected = fdi.IsFileNameSelected(pInfos->fileNameExt); // found + + ImGui::TableNextRow(); + + column_id = 0; + _rowHovered = false; + if (ImGui::TableNextColumn()) { // file name + if (!pInfos->deviceInfos.empty()) { + _str += " " + pInfos->deviceInfos; + } + m_SelectableItem(i, pInfos, selected, _str.c_str()); + _rowHovered = ImGui::IsItemHovered(); + m_DisplayFileInfosTooltip(i, column_id, pInfos); + } + column_id++; // some columns can be hidden, but the id must keep good + if (ImGui::TableNextColumn()) { // file type + m_drawColumnText(column_id, "%s", pInfos->fileExtLevels[0].c_str(), selected, _rowHovered); + m_DisplayFileInfosTooltip(i, column_id, pInfos); + } + column_id++; // some columns can be hidden, but the id must keep good + if (ImGui::TableNextColumn()) { // file size + if (!pInfos->fileType.isDir()) { + m_rightAlignText(pInfos->formatedFileSize.first.c_str(), "9999.99"); + ImGui::SameLine(0.0f, 0.0f); + m_drawColumnText(column_id, " %s ", pInfos->formatedFileSize.second.c_str(), selected, _rowHovered); + } else { + ImGui::TextUnformatted(""); + } + m_DisplayFileInfosTooltip(i, column_id, pInfos); + } + column_id++; // some columns can be hidden, but the id must keep good + if (ImGui::TableNextColumn()) { // file date + time + m_drawColumnText(column_id, "%s", pInfos->fileModifDate.c_str(), selected, _rowHovered); + m_DisplayFileInfosTooltip(i, column_id, pInfos); + } + m_EndFileColorIconStyle(_showColor, _font); + } + } + m_FileListClipper.End(); + } + +#ifdef USE_EXPLORATION_BY_KEYS + if (!fdi.inputPathActivated) { + m_LocateByInputKey(m_FileDialogInternal); + m_ExploreWithkeys(m_FileDialogInternal, listViewID); + } +#endif // USE_EXPLORATION_BY_KEYS + + ImGuiContext& g = *GImGui; + if (g.LastActiveId - 1 == listViewID || g.LastActiveId == listViewID) { + m_FileDialogInternal.fileListViewIsActive = true; + } + + ImGui::EndTable(); + } + + ImGui::PopID(); +} + +#ifdef USE_THUMBNAILS +void IGFD::FileDialog::m_DrawThumbnailsListView(ImVec2 vSize) { + auto& fdi = m_FileDialogInternal.fileManager; + + ImGui::PushID(this); + + static ImGuiTableFlags flags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg | ImGuiTableFlags_Hideable | ImGuiTableFlags_ScrollY | ImGuiTableFlags_NoHostExtendY +#ifndef USE_CUSTOM_SORTING_ICON + | ImGuiTableFlags_Sortable +#endif // USE_CUSTOM_SORTING_ICON + ; + auto listViewID = ImGui::GetID("##FileDialog_fileTable"); + if (ImGui::BeginTableEx("##FileDialog_fileTable", listViewID, 5, flags, vSize, 0.0f)) { + ImGui::TableSetupScrollFreeze(0, 1); // Make header always visible + ImGui::TableSetupColumn(fdi.headerFileName.c_str(), ImGuiTableColumnFlags_WidthStretch | (defaultSortOrderFilename ? ImGuiTableColumnFlags_PreferSortAscending : ImGuiTableColumnFlags_PreferSortDescending), -1, 0); + ImGui::TableSetupColumn(fdi.headerFileType.c_str(), + ImGuiTableColumnFlags_WidthFixed | (defaultSortOrderType ? ImGuiTableColumnFlags_PreferSortAscending : ImGuiTableColumnFlags_PreferSortDescending) | + ((m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_HideColumnType) ? ImGuiTableColumnFlags_DefaultHide : 0), + -1, 1); + ImGui::TableSetupColumn(fdi.headerFileSize.c_str(), + ImGuiTableColumnFlags_WidthFixed | (defaultSortOrderSize ? ImGuiTableColumnFlags_PreferSortAscending : ImGuiTableColumnFlags_PreferSortDescending) | + ((m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_HideColumnSize) ? ImGuiTableColumnFlags_DefaultHide : 0), + -1, 2); + ImGui::TableSetupColumn(fdi.headerFileDate.c_str(), + ImGuiTableColumnFlags_WidthFixed | (defaultSortOrderDate ? ImGuiTableColumnFlags_PreferSortAscending : ImGuiTableColumnFlags_PreferSortDescending) | + ((m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_HideColumnDate) ? ImGuiTableColumnFlags_DefaultHide : 0), + -1, 3); + // not needed to have an option for hide the thumbnails since this is why this view is used + ImGui::TableSetupColumn(fdi.headerFileThumbnails.c_str(), ImGuiTableColumnFlags_WidthFixed | (defaultSortOrderThumbnails ? ImGuiTableColumnFlags_PreferSortAscending : ImGuiTableColumnFlags_PreferSortDescending), -1, 4); //-V112 + +#ifndef USE_CUSTOM_SORTING_ICON + // Sort our data if sort specs have been changed! + if (ImGuiTableSortSpecs* sorts_specs = ImGui::TableGetSortSpecs()) { + if (sorts_specs->SpecsDirty && !fdi.IsFileListEmpty()) { + bool direction = sorts_specs->Specs->SortDirection == ImGuiSortDirection_Ascending; + + if (sorts_specs->Specs->ColumnUserID == 0) { + fdi.sortingField = IGFD::FileManager::SortingFieldEnum::FIELD_FILENAME; + fdi.sortingDirection[0] = direction; + fdi.SortFields(m_FileDialogInternal); + } else if (sorts_specs->Specs->ColumnUserID == 1) { + fdi.sortingField = IGFD::FileManager::SortingFieldEnum::FIELD_TYPE; + fdi.sortingDirection[1] = direction; + fdi.SortFields(m_FileDialogInternal); + } else if (sorts_specs->Specs->ColumnUserID == 2) { + fdi.sortingField = IGFD::FileManager::SortingFieldEnum::FIELD_SIZE; + fdi.sortingDirection[2] = direction; + fdi.SortFields(m_FileDialogInternal); + } else if (sorts_specs->Specs->ColumnUserID == 3) { + fdi.sortingField = IGFD::FileManager::SortingFieldEnum::FIELD_DATE; + fdi.sortingDirection[3] = direction; + fdi.SortFields(m_FileDialogInternal); + } else // if (sorts_specs->Specs->ColumnUserID == 4) = > always true for the moment, to uncomment if we + // add another column + { + fdi.sortingField = IGFD::FileManager::SortingFieldEnum::FIELD_THUMBNAILS; + fdi.sortingDirection[4] = direction; + fdi.SortFields(m_FileDialogInternal); + } + + sorts_specs->SpecsDirty = false; + } + } + + ImGui::TableHeadersRow(); +#else // USE_CUSTOM_SORTING_ICON + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + for (int column = 0; column < 5; column++) { + ImGui::TableSetColumnIndex(column); + const char* column_name = ImGui::TableGetColumnName(column); // Retrieve name passed to TableSetupColumn() + ImGui::PushID(column); + ImGui::TableHeader(column_name); + ImGui::PopID(); + if (ImGui::IsItemClicked()) { + if (column == 0) { + if (fdi.sortingField == IGFD::FileManager::SortingFieldEnum::FIELD_FILENAME) + fdi.sortingDirection[0] = !fdi.sortingDirection[0]; + else + fdi.sortingField = IGFD::FileManager::SortingFieldEnum::FIELD_FILENAME; + + fdi.SortFields(m_FileDialogInternal); + } else if (column == 1) { + if (fdi.sortingField == IGFD::FileManager::SortingFieldEnum::FIELD_TYPE) + fdi.sortingDirection[1] = !fdi.sortingDirection[1]; + else + fdi.sortingField = IGFD::FileManager::SortingFieldEnum::FIELD_TYPE; + + fdi.SortFields(m_FileDialogInternal); + } else if (column == 2) { + if (fdi.sortingField == IGFD::FileManager::SortingFieldEnum::FIELD_SIZE) + fdi.sortingDirection[2] = !fdi.sortingDirection[2]; + else + fdi.sortingField = IGFD::FileManager::SortingFieldEnum::FIELD_SIZE; + + fdi.SortFields(m_FileDialogInternal); + } else if (column == 3) { + if (fdi.sortingField == IGFD::FileManager::SortingFieldEnum::FIELD_DATE) + fdi.sortingDirection[3] = !fdi.sortingDirection[3]; + else + fdi.sortingField = IGFD::FileManager::SortingFieldEnum::FIELD_DATE; + + fdi.SortFields(m_FileDialogInternal); + } else // if (sorts_specs->Specs->ColumnUserID == 4) = > always true for the moment, to uncomment if we + // add another column + { + if (fdi.sortingField == IGFD::FileManager::SortingFieldEnum::FIELD_THUMBNAILS) + fdi.sortingDirection[4] = !fdi.sortingDirection[4]; + else + fdi.sortingField = IGFD::FileManager::SortingFieldEnum::FIELD_THUMBNAILS; + + fdi.SortFields(m_FileDialogInternal); + } + } + } +#endif // USE_CUSTOM_SORTING_ICON + if (!fdi.IsFilteredListEmpty()) { + std::string _str; + ImFont* _font = nullptr; + bool _showColor = false; + + ImGuiContext& g = *GImGui; + const float itemHeight = ImMax(g.FontSize, DisplayMode_ThumbailsList_ImageHeight) + g.Style.ItemSpacing.y; + + int column_id = 0; + m_FileListClipper.Begin((int)fdi.GetFilteredListSize(), itemHeight); + while (m_FileListClipper.Step()) { + for (int i = m_FileListClipper.DisplayStart; i < m_FileListClipper.DisplayEnd; i++) { + if (i < 0) continue; + + auto pInfos = fdi.GetFilteredFileAt((size_t)i); + if (!pInfos.use_count()) continue; + + m_BeginFileColorIconStyle(pInfos, _showColor, _str, &_font); + + bool selected = fdi.IsFileNameSelected(pInfos->fileNameExt); // found + + ImGui::TableNextRow(); + + column_id = 0; + if (ImGui::TableNextColumn()) { // file name + if (!pInfos->deviceInfos.empty()) { + _str += " " + pInfos->deviceInfos; + } + m_SelectableItem(i, pInfos, selected, _str.c_str()); + m_DisplayFileInfosTooltip(i, column_id, pInfos); + } + column_id++; // some columns can be hidden, but the id must keep good + if (ImGui::TableNextColumn()) { // file type + ImGui::Text("%s", pInfos->fileExtLevels[0].c_str()); + m_DisplayFileInfosTooltip(i, column_id, pInfos); + } + column_id++; // some columns can be hidden, but the id must keep good + if (ImGui::TableNextColumn()) { // file size + if (!pInfos->fileType.isDir()) { + m_rightAlignText(pInfos->formatedFileSize.first.c_str(), "9999.99"); + ImGui::SameLine(0.0f, 0.0f); + ImGui::Text(" %s ", pInfos->formatedFileSize.second.c_str()); + } else { + ImGui::TextUnformatted(""); + } + m_DisplayFileInfosTooltip(i, column_id, pInfos); + } + column_id++; // some columns can be hidden, but the id must keep good + if (ImGui::TableNextColumn()) { // file date + time + ImGui::Text("%s", pInfos->fileModifDate.c_str()); + m_DisplayFileInfosTooltip(i, column_id, pInfos); + } + column_id++; // some columns can be hidden, but the id must keep good + if (ImGui::TableNextColumn()) { // file thumbnails + auto th = &pInfos->thumbnailInfo; + + if (!th->isLoadingOrLoaded) { + m_AddThumbnailToLoad(pInfos); + } + if (th->isReadyToDisplay && th->textureID) { + ImGui::Image((ImTextureID)th->textureID, ImVec2((float)th->textureWidth, (float)th->textureHeight)); + } + m_DisplayFileInfosTooltip(i, column_id, pInfos); + } + + m_EndFileColorIconStyle(_showColor, _font); + } + } + m_FileListClipper.End(); + } + +#ifdef USE_EXPLORATION_BY_KEYS + if (!fdi.inputPathActivated) { + m_LocateByInputKey(m_FileDialogInternal); + m_ExploreWithkeys(m_FileDialogInternal, listViewID); + } +#endif // USE_EXPLORATION_BY_KEYS + + ImGuiContext& g = *GImGui; + if (g.LastActiveId - 1 == listViewID || g.LastActiveId == listViewID) { + m_FileDialogInternal.fileListViewIsActive = true; + } + + ImGui::EndTable(); + } + + ImGui::PopID(); +} + +void IGFD::FileDialog::m_DrawThumbnailsGridView(ImVec2 vSize) { + if (ImGui::BeginChild("##thumbnailsGridsFiles", vSize)) { + // todo + } + + ImGui::EndChild(); +} + +#endif + +void IGFD::FileDialog::m_DrawSidePane(float vHeight) { + ImGui::SameLine(); + + ImGui::BeginChild("##FileTypes", ImVec2(0, vHeight)); + + m_FileDialogInternal.getDialogConfig().sidePane(m_FileDialogInternal.filterManager.GetSelectedFilter().getFirstFilter().c_str(), m_FileDialogInternal.getDialogConfigRef().userDatas, &m_FileDialogInternal.canWeContinue); + ImGui::EndChild(); +} + +void IGFD::FileDialog::Close() { + m_FileDialogInternal.dLGkey.clear(); + m_FileDialogInternal.showDialog = false; +} + +bool IGFD::FileDialog::WasOpenedThisFrame(const std::string& vKey) const { + bool res = m_FileDialogInternal.showDialog && m_FileDialogInternal.dLGkey == vKey; + if (res) { + res &= m_FileDialogInternal.lastImGuiFrameCount == GImGui->FrameCount; // return true if a dialog was displayed in this frame + } + return res; +} + +bool IGFD::FileDialog::WasOpenedThisFrame() const { + bool res = m_FileDialogInternal.showDialog; + if (res) { + res &= m_FileDialogInternal.lastImGuiFrameCount == GImGui->FrameCount; // return true if a dialog was displayed in this frame + } + return res; +} + +bool IGFD::FileDialog::IsOpened(const std::string& vKey) const { + return (m_FileDialogInternal.showDialog && m_FileDialogInternal.dLGkey == vKey); +} + +bool IGFD::FileDialog::IsOpened() const { + return m_FileDialogInternal.showDialog; +} + +std::string IGFD::FileDialog::GetOpenedKey() const { + if (m_FileDialogInternal.showDialog) { + return m_FileDialogInternal.dLGkey; + } + return ""; +} + +std::string IGFD::FileDialog::GetFilePathName(IGFD_ResultMode vFlag) { + return m_FileDialogInternal.fileManager.GetResultingFilePathName(m_FileDialogInternal, vFlag); +} + +std::string IGFD::FileDialog::GetCurrentPath() { + return m_FileDialogInternal.fileManager.GetResultingPath(); +} + +std::string IGFD::FileDialog::GetCurrentFileName(IGFD_ResultMode vFlag) { + return m_FileDialogInternal.fileManager.GetResultingFileName(m_FileDialogInternal, vFlag); +} + +std::string IGFD::FileDialog::GetCurrentFilter() { + return m_FileDialogInternal.filterManager.GetSelectedFilter().title; +} + +std::map IGFD::FileDialog::GetSelection(IGFD_ResultMode vFlag) { + return m_FileDialogInternal.fileManager.GetResultingSelection(m_FileDialogInternal, vFlag); +} + +IGFD::UserDatas IGFD::FileDialog::GetUserDatas() const { + return m_FileDialogInternal.getDialogConfig().userDatas; +} + +bool IGFD::FileDialog::IsOk() const { + return m_FileDialogInternal.isOk; +} + +void IGFD::FileDialog::SetFileStyle(const IGFD_FileStyleFlags& vFlags, const char* vCriteria, const FileStyle& vInfos) { + m_FileDialogInternal.filterManager.SetFileStyle(vFlags, vCriteria, vInfos); +} + +void IGFD::FileDialog::SetFileStyle(const IGFD_FileStyleFlags& vFlags, const char* vCriteria, const ImVec4& vColor, const std::string& vIcon, ImFont* vFont) { + m_FileDialogInternal.filterManager.SetFileStyle(vFlags, vCriteria, vColor, vIcon, vFont); +} + +void IGFD::FileDialog::SetFileStyle(FileStyle::FileStyleFunctor vFunctor) { + m_FileDialogInternal.filterManager.SetFileStyle(vFunctor); +} + +bool IGFD::FileDialog::GetFileStyle(const IGFD_FileStyleFlags& vFlags, const std::string& vCriteria, ImVec4* vOutColor, std::string* vOutIcon, ImFont** vOutFont) { + return m_FileDialogInternal.filterManager.GetFileStyle(vFlags, vCriteria, vOutColor, vOutIcon, vOutFont); +} + +void IGFD::FileDialog::ClearFilesStyle() { + m_FileDialogInternal.filterManager.ClearFilesStyle(); +} + +void IGFD::FileDialog::SetLocales(const int& /*vLocaleCategory*/, const std::string& vLocaleBegin, const std::string& vLocaleEnd) { + m_FileDialogInternal.puUseCustomLocale = true; + m_FileDialogInternal.localeBegin = vLocaleBegin; + m_FileDialogInternal.localeEnd = vLocaleEnd; +} + +////////////////////////////////////////////////////////////////////////////// +//// OVERWRITE DIALOG //////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////// + +bool IGFD::FileDialog::m_Confirm_Or_OpenOverWriteFileDialog_IfNeeded(bool vLastAction, ImGuiWindowFlags vFlags) { + // if confirmation => return true for confirm the overwrite et quit the dialog + // if cancel => return false && set IsOk to false for keep inside the dialog + + // if IsOk == false => return false for quit the dialog + if (!m_FileDialogInternal.isOk && vLastAction) { + m_QuitFrame(); + return true; + } + + // if IsOk == true && no check of overwrite => return true for confirm the dialog + if (m_FileDialogInternal.isOk && vLastAction && !(m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_ConfirmOverwrite)) { + m_QuitFrame(); + return true; + } + + // if IsOk == true && check of overwrite => return false and show confirm to overwrite dialog + if ((m_FileDialogInternal.okResultToConfirm || (m_FileDialogInternal.isOk && vLastAction)) && (m_FileDialogInternal.getDialogConfig().flags & ImGuiFileDialogFlags_ConfirmOverwrite)) { + if (m_FileDialogInternal.isOk) // catched only one time + { + if (!m_FileDialogInternal.fileManager.GetFileSystemInstance()->IsFileExist(GetFilePathName())) // not existing => quit dialog + { + m_QuitFrame(); + return true; + } else // existing => confirm dialog to open + { + m_FileDialogInternal.isOk = false; + m_FileDialogInternal.okResultToConfirm = true; + } + } + + std::string name = OverWriteDialogTitleString "##" + m_FileDialogInternal.dLGtitle + m_FileDialogInternal.dLGkey + "OverWriteDialog"; + + bool res = false; + + ImGui::OpenPopup(name.c_str()); + if (ImGui::BeginPopupModal(name.c_str(), (bool*)0, vFlags | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) { + ImGui::SetWindowPos(m_FileDialogInternal.dialogCenterPos - ImGui::GetWindowSize() * 0.5f); // next frame needed for GetWindowSize to work + + ImGui::Text("%s", OverWriteDialogMessageString); + + if (IMGUI_BUTTON(OverWriteDialogConfirmButtonString)) { + m_FileDialogInternal.okResultToConfirm = false; + m_FileDialogInternal.isOk = true; + res = true; + ImGui::CloseCurrentPopup(); + } + + ImGui::SameLine(); + + if (IMGUI_BUTTON(OverWriteDialogCancelButtonString)) { + m_FileDialogInternal.okResultToConfirm = false; + m_FileDialogInternal.isOk = false; + res = false; + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } + + if (res) { + m_QuitFrame(); + } + return res; + } + + return false; +} + +#endif // __cplusplus + +// return an initialized IGFD_FileDialog_Config +IGFD_C_API IGFD_FileDialog_Config IGFD_FileDialog_Config_Get() { + IGFD_FileDialog_Config res = {}; + res.path = ""; + res.fileName = ""; + res.filePathName = ""; + res.countSelectionMax = 1; + res.userDatas = nullptr; + res.sidePane = nullptr; + res.sidePaneWidth = 250.0f; + res.flags = ImGuiFileDialogFlags_Default; + return res; +} + +// Return an initialized IGFD_Selection_Pair +IGFD_C_API IGFD_Selection_Pair IGFD_Selection_Pair_Get(void) { + IGFD_Selection_Pair res = {}; + res.fileName = nullptr; + res.filePathName = nullptr; + return res; +} + +// destroy only the content of vSelection_Pair +IGFD_C_API void IGFD_Selection_Pair_DestroyContent(IGFD_Selection_Pair* vSelection_Pair) { + if (vSelection_Pair) { + delete[] vSelection_Pair->fileName; + delete[] vSelection_Pair->filePathName; + } +} + +// Return an initialized IGFD_Selection +IGFD_C_API IGFD_Selection IGFD_Selection_Get(void) { + return {nullptr, 0U}; +} + +// destroy only the content of vSelection +IGFD_C_API void IGFD_Selection_DestroyContent(IGFD_Selection* vSelection) { + if (vSelection) { + if (vSelection->table) { + for (size_t i = 0U; i < vSelection->count; i++) { + IGFD_Selection_Pair_DestroyContent(&vSelection->table[i]); + } + delete[] vSelection->table; + } + vSelection->count = 0U; + } +} + +// create an instance of ImGuiFileDialog +IGFD_C_API ImGuiFileDialog* IGFD_Create(void) { + return new ImGuiFileDialog(); +} + +// destroy the instance of ImGuiFileDialog +IGFD_C_API void IGFD_Destroy(ImGuiFileDialog* vContextPtr) { + if (vContextPtr != nullptr) { + delete vContextPtr; + vContextPtr = nullptr; + } +} + +IGFD_C_API void IGFD_OpenDialog( // open a standard dialog + ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + const char* vKey, // key dialog + const char* vTitle, // title + const char* vFilters, // filters/filter collections. set it to null for directory mode + const IGFD_FileDialog_Config vConfig) { // path + if (vContextPtr != nullptr) { + IGFD::FileDialogConfig config; + config.path = vConfig.path; + config.fileName = vConfig.fileName; + config.filePathName = vConfig.filePathName; + config.countSelectionMax = vConfig.countSelectionMax; + config.userDatas = vConfig.userDatas; + config.flags = vConfig.flags; + config.sidePane = vConfig.sidePane; + config.sidePaneWidth = vConfig.sidePaneWidth; + vContextPtr->OpenDialog(vKey, vTitle, vFilters, config); + } +} + +IGFD_C_API bool IGFD_DisplayDialog(ImGuiFileDialog* vContextPtr, const char* vKey, ImGuiWindowFlags vFlags, ImVec2 vMinSize, ImVec2 vMaxSize) { + if (vContextPtr != nullptr) { + return vContextPtr->Display(vKey, vFlags, vMinSize, vMaxSize); + } + return false; +} + +IGFD_C_API void IGFD_CloseDialog(ImGuiFileDialog* vContextPtr) { + if (vContextPtr != nullptr) { + vContextPtr->Close(); + } +} + +IGFD_C_API bool IGFD_IsOk(ImGuiFileDialog* vContextPtr) { + if (vContextPtr != nullptr) { + return vContextPtr->IsOk(); + } + return false; +} + +IGFD_C_API bool IGFD_WasKeyOpenedThisFrame(ImGuiFileDialog* vContextPtr, const char* vKey) { + if (vContextPtr != nullptr) { + return vContextPtr->WasOpenedThisFrame(vKey); + } + return false; +} + +IGFD_C_API bool IGFD_WasOpenedThisFrame(ImGuiFileDialog* vContextPtr) { + if (vContextPtr != nullptr) { + return vContextPtr->WasOpenedThisFrame(); + } + + return false; +} + +IGFD_C_API bool IGFD_IsKeyOpened(ImGuiFileDialog* vContextPtr, const char* vCurrentOpenedKey) { + if (vContextPtr != nullptr) { + return vContextPtr->IsOpened(vCurrentOpenedKey); + } + + return false; +} + +IGFD_C_API bool IGFD_IsOpened(ImGuiFileDialog* vContextPtr) { + if (vContextPtr != nullptr) { + return vContextPtr->IsOpened(); + } + + return false; +} + +IGFD_C_API IGFD_Selection IGFD_GetSelection(ImGuiFileDialog* vContextPtr, IGFD_ResultMode vMode) { + IGFD_Selection res = IGFD_Selection_Get(); + if (vContextPtr != nullptr) { + auto sel = vContextPtr->GetSelection(vMode); + if (!sel.empty()) { + res.count = sel.size(); + res.table = new IGFD_Selection_Pair[res.count]; + + size_t idx = 0U; + for (const auto& s : sel) { + IGFD_Selection_Pair* pair = res.table + idx++; + + // fileNameExt + if (!s.first.empty()) { + size_t siz = s.first.size() + 1U; + pair->fileName = new char[siz]; +#ifndef _MSC_VER + strncpy(pair->fileName, s.first.c_str(), siz); +#else // _MSC_VER + strncpy_s(pair->fileName, siz, s.first.c_str(), siz); +#endif // _MSC_VER + pair->fileName[siz - 1U] = '\0'; + } + + // filePathName + if (!s.second.empty()) { + size_t siz = s.second.size() + 1U; + pair->filePathName = new char[siz]; +#ifndef _MSC_VER + strncpy(pair->filePathName, s.second.c_str(), siz); +#else // _MSC_VER + strncpy_s(pair->filePathName, siz, s.second.c_str(), siz); +#endif // _MSC_VER + pair->filePathName[siz - 1U] = '\0'; + } + } + + return res; + } + } + + return res; +} + +IGFD_C_API char* IGFD_GetFilePathName(ImGuiFileDialog* vContextPtr, IGFD_ResultMode vMode) { + char* res = nullptr; + + if (vContextPtr != nullptr) { + auto s = vContextPtr->GetFilePathName(vMode); + if (!s.empty()) { + size_t siz = s.size() + 1U; + res = (char*)malloc(siz); + if (res) { +#ifndef _MSC_VER + strncpy(res, s.c_str(), siz); +#else // _MSC_VER + strncpy_s(res, siz, s.c_str(), siz); +#endif // _MSC_VER + res[siz - 1U] = '\0'; + } + } + } + + return res; +} + +IGFD_C_API char* IGFD_GetCurrentFileName(ImGuiFileDialog* vContextPtr, IGFD_ResultMode vMode) { + char* res = nullptr; + + if (vContextPtr != nullptr) { + auto s = vContextPtr->GetCurrentFileName(vMode); + if (!s.empty()) { + size_t siz = s.size() + 1U; + res = (char*)malloc(siz); + if (res) { +#ifndef _MSC_VER + strncpy(res, s.c_str(), siz); +#else // _MSC_VER + strncpy_s(res, siz, s.c_str(), siz); +#endif // _MSC_VER + res[siz - 1U] = '\0'; + } + } + } + + return res; +} + +IGFD_C_API char* IGFD_GetCurrentPath(ImGuiFileDialog* vContextPtr) { + char* res = nullptr; + + if (vContextPtr != nullptr) { + auto s = vContextPtr->GetCurrentPath(); + if (!s.empty()) { + size_t siz = s.size() + 1U; + res = (char*)malloc(siz); + if (res) { +#ifndef _MSC_VER + strncpy(res, s.c_str(), siz); +#else // _MSC_VER + strncpy_s(res, siz, s.c_str(), siz); +#endif // _MSC_VER + res[siz - 1U] = '\0'; + } + } + } + + return res; +} + +IGFD_C_API char* IGFD_GetCurrentFilter(ImGuiFileDialog* vContextPtr) { + char* res = nullptr; + + if (vContextPtr != nullptr) { + auto s = vContextPtr->GetCurrentFilter(); + if (!s.empty()) { + size_t siz = s.size() + 1U; + res = (char*)malloc(siz); + if (res) { +#ifndef _MSC_VER + strncpy(res, s.c_str(), siz); +#else // _MSC_VER + strncpy_s(res, siz, s.c_str(), siz); +#endif // _MSC_VER + res[siz - 1U] = '\0'; + } + } + } + + return res; +} + +IGFD_C_API void* IGFD_GetUserDatas(ImGuiFileDialog* vContextPtr) { + if (vContextPtr != nullptr) { + return vContextPtr->GetUserDatas(); + } + + return nullptr; +} + +IGFD_C_API void IGFD_SetFileStyle(ImGuiFileDialog* vContextPtr, IGFD_FileStyleFlags vFlags, const char* vCriteria, ImVec4 vColor, const char* vIcon, + ImFont* vFont) //-V813 +{ + if (vContextPtr != nullptr) { + vContextPtr->SetFileStyle(vFlags, vCriteria, vColor, vIcon, vFont); + } +} + +IGFD_C_API void IGFD_SetFileStyle2(ImGuiFileDialog* vContextPtr, IGFD_FileStyleFlags vFlags, const char* vCriteria, float vR, float vG, float vB, float vA, const char* vIcon, ImFont* vFont) { + if (vContextPtr != nullptr) { + vContextPtr->SetFileStyle(vFlags, vCriteria, ImVec4(vR, vG, vB, vA), vIcon, vFont); + } +} + +IGFD_C_API bool IGFD_GetFileStyle(ImGuiFileDialog* vContextPtr, IGFD_FileStyleFlags vFlags, const char* vCriteria, ImVec4* vOutColor, char** vOutIconText, ImFont** vOutFont) { + if (vContextPtr != nullptr) { + std::string icon; + bool res = vContextPtr->GetFileStyle(vFlags, vCriteria, vOutColor, &icon, vOutFont); + if (!icon.empty() && vOutIconText) { + size_t siz = icon.size() + 1U; + *vOutIconText = (char*)malloc(siz); + if (*vOutIconText) { +#ifndef _MSC_VER + strncpy(*vOutIconText, icon.c_str(), siz); +#else // _MSC_VER + strncpy_s(*vOutIconText, siz, icon.c_str(), siz); +#endif // _MSC_VER + (*vOutIconText)[siz - 1U] = '\0'; + } + } + return res; + } + + return false; +} + +IGFD_C_API void IGFD_ClearFilesStyle(ImGuiFileDialog* vContextPtr) { + if (vContextPtr != nullptr) { + vContextPtr->ClearFilesStyle(); + } +} + +IGFD_C_API void SetLocales(ImGuiFileDialog* vContextPtr, const int vCategory, const char* vBeginLocale, const char* vEndLocale) { + if (vContextPtr != nullptr) { + vContextPtr->SetLocales(vCategory, (vBeginLocale ? vBeginLocale : ""), (vEndLocale ? vEndLocale : "")); + } +} + +#ifdef USE_EXPLORATION_BY_KEYS +IGFD_C_API void IGFD_SetFlashingAttenuationInSeconds(ImGuiFileDialog* vContextPtr, float vAttenValue) { + if (vContextPtr != nullptr) { + vContextPtr->SetFlashingAttenuationInSeconds(vAttenValue); + } +} +#endif + +#ifdef USE_PLACES_FEATURE +IGFD_C_API char* IGFD_SerializePlaces(ImGuiFileDialog* vContextPtr, bool vDontSerializeCodeBasedPlaces) { + char* res = nullptr; + + if (vContextPtr != nullptr) { + auto s = vContextPtr->SerializePlaces(vDontSerializeCodeBasedPlaces); + if (!s.empty()) { + size_t siz = s.size() + 1U; + res = (char*)malloc(siz); + if (res) { +#ifndef _MSC_VER + strncpy(res, s.c_str(), siz); +#else // _MSC_VER + strncpy_s(res, siz, s.c_str(), siz); +#endif // _MSC_VER + res[siz - 1U] = '\0'; + } + } + } + + return res; +} + +IGFD_C_API void IGFD_DeserializePlaces(ImGuiFileDialog* vContextPtr, const char* vPlaces) { + if (vContextPtr != nullptr) { + vContextPtr->DeserializePlaces(vPlaces); + } +} + +IGFD_C_API bool IGFD_AddPlacesGroup(ImGuiFileDialog* vContextPtr, const char* vGroupName, size_t vDisplayOrder, bool vCanBeEdited) { + if (vContextPtr != nullptr) { + return vContextPtr->AddPlacesGroup(vGroupName, vDisplayOrder, vCanBeEdited); + } + return false; +} + +IGFD_C_API bool IGFD_RemovePlacesGroup(ImGuiFileDialog* vContextPtr, const char* vGroupName) { + if (vContextPtr != nullptr) { + return vContextPtr->RemovePlacesGroup(vGroupName); + } + return false; +} + +IGFD_C_API bool IGFD_AddPlace(ImGuiFileDialog* vContextPtr, const char* vGroupName, const char* vPlaceName, const char* vPlacePath, bool vCanBeSaved, const char* vIconText) { + if (vContextPtr != nullptr) { + auto group_ptr = vContextPtr->GetPlacesGroupPtr(vGroupName); + if (group_ptr != nullptr) { + IGFD::FileStyle style; + style.icon = vIconText; + return group_ptr->AddPlace(vPlaceName, vPlacePath, vCanBeSaved, style); + } + } + return false; +} + +IGFD_C_API bool IGFD_RemovePlace(ImGuiFileDialog* vContextPtr, const char* vGroupName, const char* vPlaceName) { + if (vContextPtr != nullptr) { + auto group_ptr = vContextPtr->GetPlacesGroupPtr(vGroupName); + if (group_ptr != nullptr) { + return group_ptr->RemovePlace(vPlaceName); + } + } + return false; +} + +#endif + +#ifdef USE_THUMBNAILS +IGFD_C_API void SetCreateThumbnailCallback(ImGuiFileDialog* vContextPtr, const IGFD_CreateThumbnailFun vCreateThumbnailFun) { + if (vContextPtr != nullptr) { + vContextPtr->SetCreateThumbnailCallback(vCreateThumbnailFun); + } +} + +IGFD_C_API void SetDestroyThumbnailCallback(ImGuiFileDialog* vContextPtr, const IGFD_DestroyThumbnailFun vDestroyThumbnailFun) { + if (vContextPtr != nullptr) { + vContextPtr->SetDestroyThumbnailCallback(vDestroyThumbnailFun); + } +} + +IGFD_C_API void ManageGPUThumbnails(ImGuiFileDialog* vContextPtr) { + if (vContextPtr != nullptr) { + vContextPtr->ManageGPUThumbnails(); + } +} +#endif // USE_THUMBNAILS + +#pragma endregion \ No newline at end of file diff --git a/src/lib/ui/ImGuiFileDialog.h b/src/lib/ui/ImGuiFileDialog.h new file mode 100644 index 00000000..9ece0296 --- /dev/null +++ b/src/lib/ui/ImGuiFileDialog.h @@ -0,0 +1,1236 @@ +/* + _____ _____ _ ______ _ _ _____ _ _ + |_ _| / ____| (_)| ____|(_)| | | __ \ (_) | | + | | _ __ ___ | | __ _ _ _ | |__ _ | | ___ | | | | _ __ _ | | ___ __ _ + | | | '_ ` _ \ | | |_ || | | || || __| | || | / _ \| | | || | / _` || | / _ \ / _` | + _| |_ | | | | | || |__| || |_| || || | | || || __/| |__| || || (_| || || (_) || (_| | + |_____||_| |_| |_| \_____| \__,_||_||_| |_||_| \___||_____/ |_| \__,_||_| \___/ \__, | + __/ | + |___/ + ___ __ ___ + / _ \ / / / _ \ + __ __| | | | / /_ | (_) | + \ \ / /| | | | | '_ \ \__, | + \ V / | |_| |_| (_) |_ / / + \_/ \___/(_)\___/(_)/_/ + + +GITHUB REPOT : https://github.com/aiekick/ImGuiFileDialog +DOCUMENTATION : see the attached Documentation.md + +generated with "Text to ASCII Art Generator (TAAG)" +https://patorjk.com/software/taag/#p=display&h=1&v=0&f=Big&t=ImGuiFileDialog%0Av0.6.9 + +MIT License + +Copyright (c) 2018-2025 Stephane Cuillerdier (aka aiekick) + +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. +*/ + +#pragma once + +#define IGFD_VERSION "v0.6.9 WIP" +#define IGFD_IMGUI_SUPPORTED_VERSION "1.92.3" + +// Config file +#ifndef CUSTOM_IMGUIFILEDIALOG_CONFIG +#include "ImGuiFileDialogConfig.h" +#else // CUSTOM_IMGUIFILEDIALOG_CONFIG +#include CUSTOM_IMGUIFILEDIALOG_CONFIG +#endif // CUSTOM_IMGUIFILEDIALOG_CONFIG + +// Define attributes of all API symbols declarations (e.g. for DLL under Windows) +// Using ImGuiFileDialog via a shared library is not recommended, because we don't guarantee +// backward nor forward ABI compatibility and also function call overhead. If you +// do use ImGuiFileDialog as a DLL, be sure to call ImGui::SetImGuiContext (see ImGui doc Miscellanous section). + +#ifndef IGFD_API +#define IGFD_API +#endif // IGFD_API + +/////////////////////////////////////////////////////////// +/////////////// FLAGS ///////////////////////////////////// +/////////////////////////////////////////////////////////// + +// file style enum for file display (color, icon, font) +typedef int IGFD_FileStyleFlags; // -> enum IGFD_FileStyleFlags_ +enum IGFD_FileStyleFlags_ // by evaluation / priority order +{ + IGFD_FileStyle_None = 0, // define none style + IGFD_FileStyleByTypeFile = (1 << 0), // define style for all files + IGFD_FileStyleByTypeDir = (1 << 1), // define style for all dir + IGFD_FileStyleByTypeLink = (1 << 2), // define style for all link + IGFD_FileStyleByExtention = (1 << 3), // define style by extention, for files or links + IGFD_FileStyleByFullName = (1 << 4), // define style for particular file/dir/link full name (filename + extention) + IGFD_FileStyleByContainedInFullName = (1 << 5), // define style for file/dir/link when criteria is contained in full name +}; + +typedef int ImGuiFileDialogFlags; // -> enum ImGuiFileDialogFlags_ +enum ImGuiFileDialogFlags_ { + ImGuiFileDialogFlags_None = 0, // define none default flag + ImGuiFileDialogFlags_ConfirmOverwrite = (1 << 0), // show confirm to overwrite dialog + ImGuiFileDialogFlags_DontShowHiddenFiles = (1 << 1), // dont show hidden file (file starting with a .) + ImGuiFileDialogFlags_DisableCreateDirectoryButton = (1 << 2), // disable the create directory button + ImGuiFileDialogFlags_HideColumnType = (1 << 3), // hide column file type + ImGuiFileDialogFlags_HideColumnSize = (1 << 4), // hide column file size + ImGuiFileDialogFlags_HideColumnDate = (1 << 5), // hide column file date + ImGuiFileDialogFlags_NoDialog = (1 << 6), // let the dialog embedded in your own imgui begin / end scope + ImGuiFileDialogFlags_ReadOnlyFileNameField = (1 << 7), // don't let user type in filename field for file open style dialogs + ImGuiFileDialogFlags_CaseInsensitiveExtentionFiltering = (1 << 8), // the file extentions filtering will not take into account the case + ImGuiFileDialogFlags_Modal = (1 << 9), // modal + ImGuiFileDialogFlags_DisableThumbnailMode = (1 << 10), // disable the thumbnail mode + ImGuiFileDialogFlags_DisablePlaceMode = (1 << 11), // disable the place mode + ImGuiFileDialogFlags_DisableQuickPathSelection = (1 << 12), // disable the quick path selection + ImGuiFileDialogFlags_ShowDevicesButton = (1 << 13), // show the devices selection button + ImGuiFileDialogFlags_NaturalSorting = (1 << 14), // enable the antural sorting for filenames and extentions, slower than standard sorting + ImGuiFileDialogFlags_OptionalFileName = (1 << 15), // the input filename is optional, so the dialog can be validated even if the filebname input is empty + + // default behavior when no flags is defined. seems to be the more common cases + ImGuiFileDialogFlags_Default = ImGuiFileDialogFlags_ConfirmOverwrite | // + ImGuiFileDialogFlags_Modal | // + ImGuiFileDialogFlags_HideColumnType +}; + +// flags used for GetFilePathName(flag) or GetSelection(flag) +typedef int IGFD_ResultMode; // -> enum IGFD_ResultMode_ +enum IGFD_ResultMode_ { + // IGFD_ResultMode_AddIfNoFileExt + // add the file ext only if there is no file ext + // filter {.cpp,.h} with file : + // toto.h => toto.h + // toto.a.h => toto.a.h + // toto.a. => toto.a.cpp + // toto. => toto.cpp + // toto => toto.cpp + // filter {.z,.a.b} with file : + // toto.a.h => toto.a.h + // toto. => toto.z + // toto => toto.z + // filter {.g.z,.a} with file : + // toto.a.h => toto.a.h + // toto. => toto.g.z + // toto => toto.g.z + IGFD_ResultMode_AddIfNoFileExt = 0, // default + + // IGFD_ResultMode_OverwriteFileExt + // Overwrite the file extention by the current filter : + // filter {.cpp,.h} with file : + // toto.h => toto.cpp + // toto.a.h => toto.a.cpp + // toto.a. => toto.a.cpp + // toto.a.h.t => toto.a.h.cpp + // toto. => toto.cpp + // toto => toto.cpp + // filter {.z,.a.b} with file : + // toto.a.h => toto.z + // toto.a.h.t => toto.a.z + // toto. => toto.z + // toto => toto.z + // filter {.g.z,.a} with file : + // toto.a.h => toto.g.z + // toto.a.h.y => toto.a.g.z + // toto.a. => toto.g.z + // toto. => toto.g.z + // toto => toto.g.z + IGFD_ResultMode_OverwriteFileExt = 1, // behavior pre IGFD v0.6.6 + + // IGFD_ResultMode_KeepInputFile + // keep the input file => no modification : + // filter {.cpp,.h} with file : + // toto.h => toto.h + // toto. => toto. + // toto => toto + // filter {.z,.a.b} with file : + // toto.a.h => toto.a.h + // toto. => toto. + // toto => toto + // filter {.g.z,.a} with file : + // toto.a.h => toto.a.h + // toto. => toto. + // toto => toto + IGFD_ResultMode_KeepInputFile = 2 +}; + +/////////////////////////////////////////////////////////// +/////////////// STRUCTS /////////////////////////////////// +/////////////////////////////////////////////////////////// + +#ifdef USE_THUMBNAILS +struct IGFD_Thumbnail_Info { + int isReadyToDisplay = 0; // ready to be rendered, so texture created + int isReadyToUpload = 0; // ready to upload to gpu + int isLoadingOrLoaded = 0; // was sent to laoding or loaded + int textureWidth = 0; // width of the texture to upload + int textureHeight = 0; // height of the texture to upload + int textureChannels = 0; // count channels of the texture to upload + unsigned char* textureFileDatas = 0; // file texture datas, will be rested to null after gpu upload + void* textureID = 0; // 2d texture id (void* is like ImtextureID type) (GL, DX, VK, Etc..) + void* userDatas = 0; // user datas +}; +#endif // USE_THUMBNAILS + +// stdint is used for cpp and c apî (cstdint is only for cpp) +#include + +#ifdef __cplusplus + +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif // IMGUI_DEFINE_MATH_OPERATORS + +#ifdef IMGUI_INCLUDE +#include IMGUI_INCLUDE +#else // IMGUI_INCLUDE +#include +#endif // IMGUI_INCLUDE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef defaultSortField +#define defaultSortField FIELD_FILENAME +#endif // defaultSortField + +#ifndef defaultSortOrderFilename +#define defaultSortOrderFilename true +#endif // defaultSortOrderFilename +#ifndef defaultSortOrderType +#define defaultSortOrderType true +#endif // defaultSortOrderType +#ifndef defaultSortOrderSize +#define defaultSortOrderSize true +#endif // defaultSortOrderSize +#ifndef defaultSortOrderDate +#define defaultSortOrderDate true +#endif // defaultSortOrderDate +#ifndef defaultSortOrderThumbnails +#define defaultSortOrderThumbnails true +#endif // defaultSortOrderThumbnails + +#ifndef MAX_FILE_DIALOG_NAME_BUFFER +#define MAX_FILE_DIALOG_NAME_BUFFER 1024 +#endif // MAX_FILE_DIALOG_NAME_BUFFER + +#ifndef MAX_PATH_BUFFER_SIZE +#define MAX_PATH_BUFFER_SIZE 1024 +#endif // MAX_PATH_BUFFER_SIZE + +#ifndef EXT_MAX_LEVEL +#define EXT_MAX_LEVEL 10U +#endif // EXT_MAX_LEVEL + +namespace IGFD { + +template +class SearchableVector { +private: + std::unordered_map m_Dico; + std::vector m_Array; + +public: + void clear() { + m_Dico.clear(); + m_Array.clear(); + } + bool empty() const { + return m_Array.empty(); + } + size_t size() const { + return m_Array.size(); + } + T& operator[](const size_t& vIdx) { + return m_Array[vIdx]; + } + T& at(const size_t& vIdx) { + return m_Array.at(vIdx); + } + typename std::vector::iterator begin() { + return m_Array.begin(); + } + typename std::vector::const_iterator begin() const { + return m_Array.begin(); + } + typename std::vector::iterator end() { + return m_Array.end(); + } + typename std::vector::const_iterator end() const { + return m_Array.end(); + } + + bool try_add(T vKey) { + if (!exist(vKey)) { + m_Dico[vKey] = m_Array.size(); + m_Array.push_back(vKey); + return true; + } + return false; + } + + bool try_set_existing(T vKey) { + if (exist(vKey)) { + auto row = m_Dico.at(vKey); + m_Array[row] = vKey; + return true; + } + return false; + } + + bool exist(const std::string& vKey) const { + return (m_Dico.find(vKey) != m_Dico.end()); + } +}; + +class IGFD_API FileInfos; +class IGFD_API FileDialogInternal; + +class IGFD_API Utils { + friend class TestUtils; + +public: + struct PathStruct { + std::string path; + std::string name; + std::string ext; + bool isOk = false; + }; + +public: + static bool ImSplitter(bool split_vertically, float thickness, float* size1, float* size2, float min_size1, float min_size2, float splitter_long_axis_size = -1.0f); + static bool ReplaceString(std::string& str, const std::string& oldStr, const std::string& newStr, const size_t& vMaxRecursion = 10U); + static void AppendToBuffer(char* vBuffer, size_t vBufferLen, const std::string& vStr); + static void ResetBuffer(char* vBuffer); + static void SetBuffer(char* vBuffer, size_t vBufferLen, const std::string& vStr); + static std::string UTF8Encode(const std::wstring& wstr); + static std::wstring UTF8Decode(const std::string& str); + static std::vector SplitStringToVector(const std::string& vText, const std::string& vDelimiterPattern, const bool vPushEmpty); + static std::vector SplitStringToVector(const std::string& vText, const char& vDelimiter, const bool vPushEmpty); + static std::string LowerCaseString(const std::string& vString); // turn all text in lower case for search facilitie + static size_t GetCharCountInString(const std::string& vString, const char& vChar); + static size_t GetLastCharPosWithMinCharCount(const std::string& vString, const char& vChar, const size_t& vMinCharCount); + static std::string GetPathSeparator(); // return the slash for any OS ( \\ win, / unix) + static std::string RoundNumber(double vvalue, int n); // custom rounding number + static std::pair FormatFileSize(size_t vByteSize); // format file size field. return pair(number, unit) + static bool NaturalCompare(const std::string& vA, const std::string& vB, bool vInsensitiveCase, bool vDescending); // natural sorting + +private: + static bool M_IsAValidCharExt(const char& c); + static bool M_IsAValidCharSuffix(const char& c); + static bool M_ExtractNumFromStringAtPos(const std::string& str, size_t& pos, double& vOutNum); +}; + +class IGFD_API FileStyle { +public: + typedef std::function FileStyleFunctor; + +public: + ImVec4 color = ImVec4(0, 0, 0, 0); + std::string icon; + ImFont* font = nullptr; + IGFD_FileStyleFlags flags = 0; + +public: + FileStyle(); + FileStyle(const FileStyle& vStyle); + FileStyle(const ImVec4& vColor, const std::string& vIcon = "", ImFont* vFont = nullptr); +}; + +class IGFD_API SearchManager { +public: + std::string searchTag; + char searchBuffer[MAX_FILE_DIALOG_NAME_BUFFER] = ""; + bool searchInputIsActive = false; + +public: + void Clear(); // clear datas + void DrawSearchBar(FileDialogInternal& vFileDialogInternal); // draw the search bar +}; + +class IGFD_API FilterInfos { +private: + // just for return a default const std::string& in getFirstFilter. + // cannot be const, because FilterInfos must be affected to an another FilterInfos + // must stay empty all time + std::string empty_string; + +public: + std::string title; // displayed filter.can be different than rela filter + SearchableVector filters; // filters + SearchableVector filters_optimized; // optimized filters for case insensitive search + std::vector filters_regex; // collection of regex filter type + size_t count_dots = 0U; // the max count dot the max per filter of all filters + +public: + void clear(); // clear the datas + bool empty() const; // is filter empty + const std::string& getFirstFilter() const; // get the first filter + bool regexExist(const std::string& vFilter) const; // is regex filter exist + bool exist(const FileInfos& vFileInfos, bool vIsCaseInsensitive) const; // is filter exist + void setCollectionTitle(const std::string& vTitle); // set the collection title + void addFilter(const std::string& vFilter, const bool vIsRegex); // add a filter + void addCollectionFilter(const std::string& vFilter, const bool vIsRegex); // add a filter in collection + static std::string transformAsteriskBasedFilterToRegex(const std::string& vFilter); // will transform a filter who contain * to a regex +}; + +class IGFD_API FilterManager { + friend class TestFilterManager; +private: + std::vector m_ParsedFilters; + std::unordered_map > > m_FilesStyle; // file infos for file + // extention only + std::vector m_FilesStyleFunctors; // file style via lambda function + FilterInfos m_SelectedFilter; + +public: + std::string dLGFilters; + std::string dLGdefaultExt; + +public: + const FilterInfos& GetSelectedFilter() const; + void ParseFilters(const char* vFilters); // Parse filter syntax, detect and parse filter collection + void SetSelectedFilterWithExt(const std::string& vFilter); // Select filter + bool FillFileStyle(std::shared_ptr vFileInfos) const; // fill with the good style + void SetFileStyle(const IGFD_FileStyleFlags& vFlags, const char* vCriteria, const FileStyle& vInfos); // Set FileStyle + void SetFileStyle(const IGFD_FileStyleFlags& vFlags, const char* vCriteria, const ImVec4& vColor, const std::string& vIcon, + ImFont* vFont); // link file style to Color and Icon and Font + void SetFileStyle(FileStyle::FileStyleFunctor vFunctor); // lambda functor for set file style. + bool GetFileStyle(const IGFD_FileStyleFlags& vFlags, const std::string& vCriteria, ImVec4* vOutColor, std::string* vOutIcon, + ImFont** vOutFont); // Get Color and Icon for Filter + void ClearFilesStyle(); // clear m_FileStyle + bool IsCoveredByFilters(const FileInfos& vFileInfos, + bool vIsCaseInsensitive) const; // check if current file extention (vExt) is covered by current filter, or by regex (vNameExt) + float GetFilterComboBoxWidth() const; // will return the current combo box widget width + bool DrawFilterComboBox(FileDialogInternal& vFileDialogInternal); // draw the filter combobox // get the current selected filter + std::string ReplaceExtentionWithCurrentFilterIfNeeded(const std::string& vFileName, + IGFD_ResultMode vFlag) const; // replace the extention of the current file by the selected filter + void SetDefaultFilterIfNotDefined(); // define the first filter if no filter is selected +}; + +class IGFD_API FileType { +public: + enum class ContentType { + // The ordering will be used during sort. + Invalid = -1, + Directory = 0, + File = 1, + LinkToUnknown = 2, // link to something that is not a regular file or directory. + }; + +private: + ContentType m_Content = ContentType::Invalid; + bool m_Symlink = false; + +public: + FileType(); + FileType(const ContentType& vContentType, const bool vIsSymlink); + + void SetContent(const ContentType& vContentType); + void SetSymLink(const bool vIsSymlink); + + bool isValid() const; + bool isDir() const; + bool isFile() const; + bool isLinkToUnknown() const; + bool isSymLink() const; + + // Comparisons only care about the content type, ignoring whether it's a symlink or not. + bool operator==(const FileType& rhs) const; + bool operator!=(const FileType& rhs) const; + bool operator<(const FileType& rhs) const; + bool operator>(const FileType& rhs) const; +}; + +class IGFD_API FileInfos { +public: + static std::shared_ptr create(); + +public: + // extention of the file, the array is the levels of ext, by ex : .a.b.c, will be save in {.a.b.c, .b.c, .c} + // 10 level max are sufficient i guess. the others levels will be checked if countExtDot > 1 + std::array fileExtLevels; + std::array fileExtLevels_optimized; // optimized for search => insensitivecase + // same for file name, can be sued in userFileAttributesFun + std::array fileNameLevels; + std::array fileNameLevels_optimized; // optimized for search => insensitivecase + size_t countExtDot = 0U; // count dots in file extention. this count will give the levels in fileExtLevels + FileType fileType; // fileType + std::string filePath; // path of the file + std::string fileName; // file name only + std::string fileNameExt; // filename of the file (file name + extention) (but no path) + std::string fileNameExt_optimized; // optimized for search => insensitivecase + std::string deviceInfos; // quick infos to display after name for devices + std::string tooltipMessage; // message to display on the tooltip, is not empty + int32_t tooltipColumn = -1; // the tooltip will appears only when the mouse is over the tooltipColumn if > -1 + size_t fileSize = 0U; // for sorting operations + std::pair formatedFileSize; // file size formated (10 o, 10 ko, 10 mo, 10 go) + std::string fileModifDate; // file user defined format of the date (data + time by default) + std::shared_ptr fileStyle = nullptr; // style of the file +#ifdef USE_THUMBNAILS + IGFD_Thumbnail_Info thumbnailInfo; // structre for the display for image file tetxure +#endif // USE_THUMBNAILS + +public: + bool SearchForTag(const std::string& vTag) const; // will search a tag in fileNameExt and fileNameExt_optimized + bool SearchForExt(const std::string& vExt, const bool vIsCaseInsensitive, + const size_t& vMaxLevel = EXT_MAX_LEVEL) const; // will check the fileExtLevels levels for vExt, until vMaxLevel + bool SearchForExts(const std::string& vComaSepExts, const bool vIsCaseInsensitive, + const size_t& vMaxLevel = EXT_MAX_LEVEL) const; // will check the fileExtLevels levels for vExts (ext are coma separated), until vMaxLevel + bool FinalizeFileTypeParsing(const size_t& vMaxDotToExtract); // finalize the parsing the file (only a file or link to file. no dir) +}; + +typedef std::pair PathDisplayedName; + +class IFileSystem { +public: + virtual ~IFileSystem() = default; + // say if a directory can be openened or for any reason locked + virtual bool IsDirectoryCanBeOpened(const std::string& vName) = 0; + // say if a directory exist + virtual bool IsDirectoryExist(const std::string& vName) = 0; + // say if a file exist + virtual bool IsFileExist(const std::string& vName) = 0; + // say if a directory was created, return false if vName is invalid or alreayd exist + virtual bool CreateDirectoryIfNotExist(const std::string& vName) = 0; + // extract the component of a file path name, like path, name, ext + virtual IGFD::Utils::PathStruct ParsePathFileName(const std::string& vPathFileName) = 0; + // will return a list of files inside a path + virtual std::vector ScanDirectory(const std::string& vPath) = 0; + // say if the path is well a directory + virtual bool IsDirectory(const std::string& vFilePathName) = 0; + // return a device list () on windows, but can be used on other platforms for give to the user a list of devices paths. + virtual std::vector GetDevicesList() = 0; + // return via argument the date and the size of a file (for solve issue regarding apis and widechars) + virtual void GetFileDateAndSize(const std::string& vFilePathName, const IGFD::FileType& vFileType, std::string& voDate, size_t& voSize) = 0; +}; + +class IGFD_API FileManager { + friend class TestFileManager; +public: // types + enum class SortingFieldEnum { // sorting for filetering of the file lsit + FIELD_NONE = 0, // no sorting reference, result indetermined haha.. + FIELD_FILENAME, // sorted by filename + FIELD_TYPE, // sorted by filetype + FIELD_SIZE, // sorted by filesize (formated file size) + FIELD_DATE, // sorted by filedate + FIELD_THUMBNAILS, // sorted by thumbnails (comparaison by width then by height) + }; + +private: + std::string m_CurrentPath; // current path (to be decomposed in m_CurrentPathDecomposition + std::vector m_CurrentPathDecomposition; // part words + std::vector > m_FileList; // base container + std::vector > m_FilteredFileList; // filtered container (search, sorting, etc..) + std::vector > m_PathList; // base container for path selection + std::vector > m_FilteredPathList; // filtered container for path selection (search, sorting, etc..) + std::vector::iterator m_PopupComposedPath; // iterator on m_CurrentPathDecomposition for Current Path popup + std::string m_LastSelectedFileName; // for shift multi selection + std::set m_SelectedFileNames; // the user selection of FilePathNames + bool m_CreateDirectoryMode = false; // for create directory widget + std::string m_FileSystemName; + std::unique_ptr m_FileSystemPtr = nullptr; + +public: + bool inputPathActivated = false; // show input for path edition + bool devicesClicked = false; // event when a drive button is clicked + bool pathClicked = false; // event when a path button was clicked + char inputPathBuffer[MAX_PATH_BUFFER_SIZE] = ""; // input path buffer for imgui widget input text (displayed in palce of composer) + char variadicBuffer[MAX_FILE_DIALOG_NAME_BUFFER] = ""; // called by m_SelectableItem + char fileNameBuffer[MAX_FILE_DIALOG_NAME_BUFFER] = ""; // file name buffer in footer for imgui widget input text + char directoryNameBuffer[MAX_FILE_DIALOG_NAME_BUFFER] = ""; // directory name buffer (when in directory mode) + std::string headerFileName; // detail view name of column file + std::string headerFileType; // detail view name of column type + std::string headerFileSize; // detail view name of column size + std::string headerFileDate; // detail view name of column date + time +#ifdef USE_THUMBNAILS + std::string headerFileThumbnails; // detail view name of column thumbnails + bool sortingDirection[5] = { // true => Ascending, false => Descending + defaultSortOrderFilename, defaultSortOrderType, defaultSortOrderSize, defaultSortOrderDate, defaultSortOrderThumbnails}; +#else + bool sortingDirection[4] = { // true => Ascending, false => Descending + defaultSortOrderFilename, defaultSortOrderType, defaultSortOrderSize, defaultSortOrderDate}; +#endif + SortingFieldEnum sortingField = SortingFieldEnum::FIELD_FILENAME; // detail view sorting column + bool showDevices = false; // devices are shown (only on os windows) + + std::string dLGpath; // base path set by user when OpenDialog was called + std::string dLGDefaultFileName; // base default file path name set by user when OpenDialog was called + size_t dLGcountSelectionMax = 1U; // 0 for infinite // base max selection count set by user when OpenDialog was called + bool dLGDirectoryMode = false; // is directory mode (defiend like : dLGDirectoryMode = (filters.empty())) + + std::string fsRoot; + +private: + void m_CompleteFileInfos(const std::shared_ptr& vInfos); // set time and date infos of a file (detail view mode) + void m_RemoveFileNameInSelection(const std::string& vFileName); // selection : remove a file name + void m_AddFileNameInSelection(const std::string& vFileName, bool vSetLastSelectionFileName); // selection : add a file name + void m_AddFile(const FileDialogInternal& vFileDialogInternal, const std::string& vPath, const std::string& vFileName, + const FileType& vFileType); // add file called by scandir + void m_AddPath(const FileDialogInternal& vFileDialogInternal, const std::string& vPath, const std::string& vFileName, + const FileType& vFileType); // add file called by scandir + void m_ScanDirForPathSelection(const FileDialogInternal& vFileDialogInternal, + const std::string& vPath); // scan the directory for retrieve the path list + void m_OpenPathPopup(const FileDialogInternal& vFileDialogInternal, + std::vector::iterator vPathIter); // open the popup list of paths + void m_SetCurrentPath(std::vector::iterator vPathIter); // set the current path, update the path bar + void m_ApplyFilteringOnFileList(const FileDialogInternal& vFileDialogInternal, std::vector >& vFileInfosList, std::vector >& vFileInfosFilteredList); + static bool M_SortStrings(const FileDialogInternal& vFileDialogInternal, // + const bool vInsensitiveCase, const bool vDescendingOrder, // + const std::string& vA, const std::string& vB); + void m_SortFields(const FileDialogInternal& vFileDialogInternal, std::vector >& vFileInfosList, + std::vector >& vFileInfosFilteredList); // will sort a column + bool m_CompleteFileInfosWithUserFileAttirbutes(const FileDialogInternal& vFileDialogInternal, const std::shared_ptr& vInfos); + +public: + FileManager(); + bool IsComposerEmpty() const; + size_t GetComposerSize() const; + bool IsFileListEmpty() const; + bool IsPathListEmpty() const; + bool IsFilteredListEmpty() const; + bool IsPathFilteredListEmpty() const; + size_t GetFullFileListSize() const; + std::shared_ptr GetFullFileAt(size_t vIdx); + size_t GetFilteredListSize() const; + size_t GetPathFilteredListSize() const; + std::shared_ptr GetFilteredFileAt(size_t vIdx); + std::shared_ptr GetFilteredPathAt(size_t vIdx); + std::vector::iterator GetCurrentPopupComposedPath() const; + bool IsFileNameSelected(const std::string& vFileName); + std::string GetBack(); + void ClearComposer(); + void ClearFileLists(); // clear file list, will destroy thumbnail textures + void ClearPathLists(); // clear path list, will destroy thumbnail textures + void ClearAll(); + void ApplyFilteringOnFileList(const FileDialogInternal& vFileDialogInternal); + void SortFields(const FileDialogInternal& vFileDialogInternal); // will sort a column + void OpenCurrentPath(const FileDialogInternal& vFileDialogInternal); // set the path of the dialog, will launch the scandir for populate the file listview + bool GetDevices(); // list devices + bool CreateDir(const std::string& vPath); // create a directory on the file system + std::string ComposeNewPath(std::vector::iterator vIter); // compose a path from the compose path widget + bool SetPathOnParentDirectoryIfAny(); // compose paht on parent directory + std::string GetCurrentPath(); // get the current path + void SetCurrentPath(const std::string& vCurrentPath); // set the current path + void SetDefaultFileName(const std::string& vFileName); + bool SelectDirectory(const std::shared_ptr& vInfos); // enter directory + void SelectAllFileNames(); + void SelectFileName(const std::shared_ptr& vInfos); // add a filename in selection + void SelectOrDeselectFileName(const FileDialogInternal& vFileDialogInternal, const std::shared_ptr& vInfos); // add/remove a filename in selection + void SetCurrentDir(const std::string& vPath); // define current directory for scan + void ScanDir(const FileDialogInternal& vFileDialogInternal, + const std::string& vPath); // scan the directory for retrieve the file list + std::string GetResultingPath(); + std::string GetResultingFileName(FileDialogInternal& vFileDialogInternal, IGFD_ResultMode vFlag); + std::string GetResultingFilePathName(FileDialogInternal& vFileDialogInternal, IGFD_ResultMode vFlag); + std::map GetResultingSelection(FileDialogInternal& vFileDialogInternal, IGFD_ResultMode vFlag); + + void DrawDirectoryCreation(const FileDialogInternal& vFileDialogInternal); // draw directory creation widget + void DrawPathComposer(const FileDialogInternal& vFileDialogInternal); + + IFileSystem* GetFileSystemInstance() const { + return m_FileSystemPtr.get(); + } + const std::string& GetFileSystemName() const { + return m_FileSystemName; + } +}; + +typedef void* UserDatas; +typedef std::function PaneFun; // side pane function binding +typedef std::function UserFileAttributesFun; // custom file Attributes call back, reject file if false + +struct IGFD_API FileDialogConfig { + std::string path; // path + std::string fileName; // defaut file name + std::string filePathName; // if not empty, the filename and the path will be obtained from filePathName + int32_t countSelectionMax = 1; // count selection max, 0 for infinite + UserDatas userDatas = nullptr; // user datas (can be retrieved in pane) + ImGuiFileDialogFlags flags = ImGuiFileDialogFlags_None; // ImGuiFileDialogFlags + PaneFun sidePane; // side pane callback + float sidePaneWidth = 250.0f; // side pane width + UserFileAttributesFun userFileAttributes; // user file Attibutes callback +}; + +class IGFD_API FileDialogInternal { +public: + FileManager fileManager; // the file manager + FilterManager filterManager; // the filter manager + SearchManager searchManager; // the search manager + +public: + std::string name; // the internal dialog name (title + ##word) + bool showDialog = false; // the dialog is shown + ImVec2 dialogCenterPos = ImVec2(0, 0); // center pos for display the confirm overwrite dialog + int lastImGuiFrameCount = 0; // to be sure than only one dialog displayed per frame + float footerHeight = 0.0f; // footer height + bool canWeContinue = true; // events + bool okResultToConfirm = false; // to confim if ok for OverWrite + bool isOk = false; // is dialog ok button click + bool fileInputIsActive = false; // when input text for file or directory is active + bool fileListViewIsActive = false; // when list view is active + std::string dLGkey; // the dialog key + std::string dLGtitle; // the dialog title + bool needToExitDialog = false; // we need to exit the dialog + bool puUseCustomLocale = false; // custom user locale + int localeCategory = LC_ALL; // locale category to use + std::string localeBegin; // the locale who will be applied at start of the display dialog + std::string localeEnd; // the locale who will be applaied at end of the display dialog + +private: + FileDialogConfig m_DialogConfig; + +public: + void NewFrame(); // new frame, so maybe neded to do somethings, like reset events + void EndFrame(); // end frame, so maybe neded to do somethings fater all + void ResetForNewDialog(); // reset what is needed to reset for the openging of a new dialog + + void configureDialog( // open simple dialog + const std::string& vKey, // key dialog + const std::string& vTitle, // title + const char* vFilters, // filters, if null, will display only directories + const FileDialogConfig& vConfig); // FileDialogConfig + const FileDialogConfig& getDialogConfig() const; + FileDialogConfig& getDialogConfigRef(); +}; + +class IGFD_API ThumbnailFeature { +protected: + ThumbnailFeature(); + ~ThumbnailFeature(); + + void m_NewThumbnailFrame(FileDialogInternal& vFileDialogInternal); + void m_EndThumbnailFrame(FileDialogInternal& vFileDialogInternal); + void m_QuitThumbnailFrame(FileDialogInternal& vFileDialogInternal); + +#ifdef USE_THUMBNAILS +public: + typedef std::function CreateThumbnailFun; // texture 2d creation function binding + typedef std::function DestroyThumbnailFun; // texture 2d destroy function binding + +protected: + enum class DisplayModeEnum { FILE_LIST = 0, THUMBNAILS_LIST, THUMBNAILS_GRID }; + +private: + uint32_t m_CountFiles = 0U; + bool m_IsWorking = false; + std::shared_ptr m_ThumbnailGenerationThread = nullptr; + std::list > m_ThumbnailFileDatasToGet; // base container + std::mutex m_ThumbnailFileDatasToGetMutex; + std::condition_variable m_ThumbnailFileDatasToGetCv; + std::list > m_ThumbnailToCreate; // base container + std::mutex m_ThumbnailToCreateMutex; + std::list m_ThumbnailToDestroy; // base container + std::mutex m_ThumbnailToDestroyMutex; + + CreateThumbnailFun m_CreateThumbnailFun = nullptr; + DestroyThumbnailFun m_DestroyThumbnailFun = nullptr; + +protected: + DisplayModeEnum m_DisplayMode = DisplayModeEnum::FILE_LIST; + +private: + void m_VariadicProgressBar(float fraction, const ImVec2& size_arg, const char* fmt, ...); + +protected: + // will be call in cpu zone (imgui computations, will call a texture file retrieval thread) + void m_StartThumbnailFileDatasExtraction(); // start the thread who will get byte buffer from image files + bool m_StopThumbnailFileDatasExtraction(); // stop the thread who will get byte buffer from image files + void m_ThreadThumbnailFileDatasExtractionFunc(); // the thread who will get byte buffer from image files + void m_DrawThumbnailGenerationProgress(); // a little progressbar who will display the texture gen status + void m_AddThumbnailToLoad(const std::shared_ptr& vFileInfos); // add texture to load in the thread + void m_AddThumbnailToCreate(const std::shared_ptr& vFileInfos); + void m_AddThumbnailToDestroy(const IGFD_Thumbnail_Info& vIGFD_Thumbnail_Info); + void m_DrawDisplayModeToolBar(); // draw display mode toolbar (file list, thumbnails list, small thumbnails grid, big thumbnails grid) + void m_ClearThumbnails(FileDialogInternal& vFileDialogInternal); + +public: + void SetCreateThumbnailCallback(const CreateThumbnailFun& vCreateThumbnailFun); + void SetDestroyThumbnailCallback(const DestroyThumbnailFun& vCreateThumbnailFun); + + // must be call in gpu zone (rendering, possibly one rendering thread) + void ManageGPUThumbnails(); // in gpu rendering zone, whill create or destroy texture +#endif +}; + +class IGFD_API PlacesFeature { +protected: + PlacesFeature(); + +#ifdef USE_PLACES_FEATURE +private: + struct PlaceStruct { + std::string name; // name of the place + // todo: the path could be relative, better if the app is moved but place path can be outside of the app + std::string path; // absolute path of the place + bool canBeSaved = true; // defined by code, can be used for prevent serialization / deserialization + FileStyle style; + float thickness = 0.0f; // when more than 0.0f, is a separator + }; + + struct GroupStruct { + bool canBeSaved = false; // defined by code, can be used for prevent serialization / deserialization + size_t displayOrder = 0U; // the display order will be usedf first, then alphanumeric + bool defaultOpened = false; // the group is opened by default + bool canBeEdited = false; // will show +/- button for add/remove place in the group + char editBuffer[MAX_FILE_DIALOG_NAME_BUFFER] = ""; // temp buffer for name edition + int32_t selectedPlaceForEdition = -1; + ImGuiTreeNodeFlags collapsingHeaderFlag = ImGuiTreeNodeFlags_None; + ImGuiListClipper clipper; // the list clipper of the grou + std::string name; // the group name, will be displayed + std::vector places; // the places (name + path) + bool AddPlace( // add a place by code + const std::string& vPlaceName, // place name + const std::string& vPlacePath, // place path + const bool vCanBeSaved, // prevent serialization + const FileStyle& vStyle = {}); // style + void AddPlaceSeparator(const float& vThickness = 1.0f); + bool RemovePlace( // remove a place by code, return true if succeed + const std::string& vPlaceName); // place name to remove + }; + +private: + std::unordered_map > m_Groups; + std::map > m_OrderedGroups; + +protected: + float m_PlacesPaneWidth = 200.0f; + bool m_PlacesPaneShown = false; + +protected: + void m_InitPlaces(FileDialogInternal& vFileDialogInternal); + void m_DrawPlacesButton(); // draw place button + bool m_DrawPlacesPane(FileDialogInternal& vFileDialogInternal, const ImVec2& vSize); // draw place Pane + +public: + std::string SerializePlaces( // serialize place : return place buffer to save in a file + const bool vForceSerialisationForAll = true); // for avoid serialization of places with flag 'canBeSaved to false' + void DeserializePlaces( // deserialize place : load place buffer to load in the dialog (saved from + const std::string& vPlaces); // previous use with SerializePlaces()) place buffer to load + bool AddPlacesGroup( // add a group + const std::string& vGroupName, // the group name + const size_t& vDisplayOrder, // the display roder of the group + const bool vCanBeEdited = false, // let the user add/remove place in the group + const bool vOpenedByDefault = true); // hte group is opened by default + bool RemovePlacesGroup(const std::string& vGroupName); // remove the group + GroupStruct* GetPlacesGroupPtr(const std::string& vGroupName); // get the group, if not existed, will be created +#endif // USE_PLACES_FEATURE +}; + +// file localization by input chat // widget flashing +class IGFD_API KeyExplorerFeature { +protected: + KeyExplorerFeature(); + +#ifdef USE_EXPLORATION_BY_KEYS +private: + bool m_LocateFileByInputChar_lastFound = false; + ImWchar m_LocateFileByInputChar_lastChar = 0; + float m_FlashAlpha = 0.0f; // flash when select by char + float m_FlashAlphaAttenInSecs = 1.0f; // fps display dependant + int m_LocateFileByInputChar_InputQueueCharactersSize = 0; + size_t m_FlashedItem = 0; // flash when select by char + size_t m_LocateFileByInputChar_lastFileIdx = 0; + +protected: + void m_LocateByInputKey(FileDialogInternal& vFileDialogInternal); // select a file line in listview according to char key + bool m_LocateItem_Loop(FileDialogInternal& vFileDialogInternal, + ImWchar vC); // restrat for start of list view if not found a corresponding file + void m_ExploreWithkeys(FileDialogInternal& vFileDialogInternal, + ImGuiID vListViewID); // select file/directory line in listview accroding to up/down enter/backspace keys + void m_StartFlashItem(size_t vIdx); // define than an item must be flashed + bool m_BeginFlashItem(size_t vIdx); // start the flashing of a line in lsit view + static void m_EndFlashItem(); // end the fleshing accrdoin to var m_FlashAlphaAttenInSecs + static bool m_FlashableSelectable(const char* label, bool selected = false, ImGuiSelectableFlags flags = 0, bool vFlashing = false, + const ImVec2& size = ImVec2(0, 0)); // custom flashing selectable widgets, for flash the selected line in a short time + +public: + void SetFlashingAttenuationInSeconds( // set the flashing time of the line in file list when use exploration keys + float vAttenValue); // set the attenuation (from flashed to not flashed) in seconds +#endif // USE_EXPLORATION_BY_KEYS +}; + +class IGFD_API FileDialog : public PlacesFeature, public KeyExplorerFeature, public ThumbnailFeature { +protected: + FileDialogInternal m_FileDialogInternal; + ImGuiListClipper m_FileListClipper; + ImGuiListClipper m_PathListClipper; + float prOkCancelButtonWidth = 0.0f; + ImGuiWindowFlags m_CurrentDisplayedFlags; + +public: +#ifndef NEW_SINGLETON + // Singleton for easier accces form anywhere but only one dialog at a time + // vCopy or vForce can be used for share a memory pointer in a new memory space like a dll module + static FileDialog* Instance(FileDialog* vCopy = nullptr, bool vForce = false) { + static FileDialog _instance; + static FileDialog* _instance_copy = nullptr; + if (vCopy || vForce) { + _instance_copy = vCopy; + } + if (_instance_copy) { + return _instance_copy; + } + return &_instance; + } +#else // NEW_SINGLETON + static std::unique_ptr& initSingleton(FileDialog* vCopy = nullptr, bool vForce = false) { + static auto mp_instance = std::unique_ptr(new FileDialog()); + static std::unique_ptr mp_instance_copy; + if (vCopy != nullptr || vForce) { + if (vCopy != nullptr) { + mp_instance_copy = std::unique_ptr(vCopy); + } else { + mp_instance_copy.release(); // frees the internal borrowed pointer without deletion + } + } + if (mp_instance_copy != nullptr) { + return mp_instance_copy; + } + return mp_instance; + } + static FileDialog& ref() { return *initSingleton().get(); } + static void unitSingleton() { + initSingleton(nullptr, true); // frees the borrowed pointer in case + initSingleton().reset(); // else the reset with destroy the borrowed pointer + } +#endif // NEW_SINGLETON + +public: + FileDialog(); // ImGuiFileDialog Constructor. can be used for have many dialog at same time (not possible with singleton) + virtual ~FileDialog(); // ImGuiFileDialog Destructor + + // standard dialog + virtual void OpenDialog( // open simple dialog + const std::string& vKey, // key dialog + const std::string& vTitle, // title + const char* vFilters, // filters, if null, will display only directories + const FileDialogConfig& vConfig = {}); // FileDialogConfig + + // Display / Close dialog form + bool Display( // Display the dialog. return true if a result was obtained (Ok or not) + const std::string& vKey, // key dialog to display (if not the same key as defined by OpenDialog => no opening) + ImGuiWindowFlags vFlags = ImGuiWindowFlags_NoCollapse, // ImGuiWindowFlags + ImVec2 vMinSize = ImVec2(0, 0), // mininmal size contraint for the ImGuiWindow + ImVec2 vMaxSize = ImVec2(FLT_MAX, FLT_MAX)); // maximal size contraint for the ImGuiWindow + + void Close(); // close dialog + + // queries + bool WasOpenedThisFrame(const std::string& vKey) const; // say if the dialog key was already opened this frame + bool WasOpenedThisFrame() const; // say if the dialog was already opened this frame + bool IsOpened(const std::string& vKey) const; // say if the key is opened + bool IsOpened() const; // say if the dialog is opened somewhere + std::string GetOpenedKey() const; // return the dialog key who is opened, return nothing if not opened + + // get result + bool IsOk() const; // true => Dialog Closed with Ok result / false : Dialog closed with cancel result + std::map GetSelection(IGFD_ResultMode vFlag = IGFD_ResultMode_KeepInputFile); // Open File behavior : will return selection via a + // map + std::string GetFilePathName(IGFD_ResultMode vFlag = IGFD_ResultMode_AddIfNoFileExt); // Save File behavior : will return the current file path name + std::string GetCurrentFileName(IGFD_ResultMode vFlag = IGFD_ResultMode_AddIfNoFileExt); // Save File behavior : will return the content file name + std::string GetCurrentPath(); // will return current file path + std::string GetCurrentFilter(); // will return current filter + UserDatas GetUserDatas() const; // will return user datas send with Open Dialog + + // file style by extentions + void SetFileStyle( // SetExtention datas for have custom display of particular file type + const IGFD_FileStyleFlags& vFlags, // file style + const char* vCriteria, // extention filter to tune + const FileStyle& vInfos); // Filter Extention Struct who contain Color and Icon/Text for the display of the + // file with extention filter + void SetFileStyle( // SetExtention datas for have custom display of particular file type + const IGFD_FileStyleFlags& vFlags, // file style + const char* vCriteria, // extention filter to tune + const ImVec4& vColor, // wanted color for the display of the file with extention filter + const std::string& vIcon = "", // wanted text or icon of the file with extention filter + ImFont* vFont = nullptr); // wanted font + void SetFileStyle(FileStyle::FileStyleFunctor vFunctor); // set file style via lambda function + bool GetFileStyle( // GetExtention datas. return true is extention exist + const IGFD_FileStyleFlags& vFlags, // file style + const std::string& vCriteria, // extention filter (same as used in SetExtentionInfos) + ImVec4* vOutColor, // color to retrieve + std::string* vOutIcon = nullptr, // icon or text to retrieve + ImFont** vOutFont = nullptr); // font to retreive + void ClearFilesStyle(); // clear extentions setttings + + void SetLocales( // set locales to use before and after the dialog display + const int& vLocaleCategory, // set local category + const std::string& vLocaleBegin, // locale to use at begining of the dialog display + const std::string& vLocaleEnd); // locale to use at the end of the dialog display + +protected: + void m_NewFrame(); // new frame just at begining of display + void m_EndFrame(); // end frame just at end of display + void m_QuitFrame(); // quit frame when qui quit the dialog + + // others + bool m_Confirm_Or_OpenOverWriteFileDialog_IfNeeded(bool vLastAction, ImGuiWindowFlags vFlags); // treatment of the result, start the confirm to overwrite dialog + // if needed (if defined with flag) + + // dialog parts + virtual void m_DrawHeader(); // draw header part of the dialog (place btn, dir creation, path composer, search bar) + virtual void m_DrawContent(); // draw content part of the dialog (place pane, file list, side pane) + virtual bool m_DrawFooter(); // draw footer part of the dialog (file field, fitler combobox, ok/cancel btn's) + + // widgets components + virtual void m_DisplayPathPopup(ImVec2 vSize); // draw path popup when click on a \ or / + virtual bool m_DrawValidationButtons(); // draw validations btns, ok, cancel buttons + virtual bool m_DrawOkButton(); // draw ok button + virtual bool m_DrawCancelButton(); // draw cancel button + virtual void m_DrawSidePane(float vHeight); // draw side pane + virtual bool m_Selectable(int vRowIdx, const char* vLabel, bool vSelected, ImGuiSelectableFlags vFlags, const ImVec2& vSizeArg); + virtual void m_SelectableItem(int vRowIdx, std::shared_ptr vInfos, bool vSelected, const char* vFmt, ...); // draw a custom selectable behavior item + virtual void m_drawColumnText(int vColIdx, const char* vFmt, const char* vLabel, bool vSelected, bool vHovered); + virtual void m_rightAlignText(const char* text, const char* maxWidthText); // align a text on right + virtual void m_DrawFileListView(ImVec2 vSize); // draw file list view (default mode) + +#ifdef USE_THUMBNAILS + virtual void m_DrawThumbnailsListView(ImVec2 vSize); // draw file list view with small thumbnails on the same line + virtual void m_DrawThumbnailsGridView(ImVec2 vSize); // draw a grid of small thumbnails +#endif + + // to be called only by these function and theirs overrides + // - m_DrawFileListView + // - m_DrawThumbnailsListView + // - m_DrawThumbnailsGridView + void m_BeginFileColorIconStyle(std::shared_ptr vFileInfos, bool& vOutShowColor, std::string& vOutStr, + ImFont** vOutFont); // begin style apply of filter with color an icon if any + void m_EndFileColorIconStyle(const bool vShowColor, ImFont* vFont); // end style apply of filter + + void m_DisplayFileInfosTooltip(const int32_t& vRowIdx, const int32_t& vColumnIdx, std::shared_ptr vFileInfos); +}; + +} // namespace IGFD + +#endif // __cplusplus + +///////////////////////////////////////////////// +////// C LANG API /////////////////////////////// +///////////////////////////////////////////////// + +#ifdef __cplusplus +#define IGFD_C_API extern "C" IGFD_API +typedef IGFD::UserDatas IGFDUserDatas; +typedef IGFD::PaneFun IGFDPaneFun; +typedef IGFD::FileDialog ImGuiFileDialog; +#else // __cplusplus +#define IGFD_C_API +typedef struct ImGuiFileDialog ImGuiFileDialog; +typedef struct IGFD_Selection_Pair IGFD_Selection_Pair; +typedef struct IGFD_Selection IGFD_Selection; +#endif // __cplusplus + +typedef void (*IGFD_PaneFun)(const char*, void*, bool*); // callback fucntion for display the pane + +struct IGFD_FileDialog_Config { + const char* path; // path + const char* fileName; // defaut file name + const char* filePathName; // if not empty, the filename and the path will be obtained from filePathName + int32_t countSelectionMax; // count selection max + void* userDatas; // user datas (can be retrieved in pane) + IGFD_PaneFun sidePane; // side pane callback + float sidePaneWidth; // side pane width}; + ImGuiFileDialogFlags flags; // ImGuiFileDialogFlags +}; +IGFD_C_API struct IGFD_FileDialog_Config IGFD_FileDialog_Config_Get(); // return an initialized IGFD_FileDialog_Config + +struct IGFD_Selection_Pair { + char* fileName; + char* filePathName; +}; + +IGFD_C_API IGFD_Selection_Pair IGFD_Selection_Pair_Get(); // return an initialized IGFD_Selection_Pair +IGFD_C_API void IGFD_Selection_Pair_DestroyContent(IGFD_Selection_Pair* vSelection_Pair); // destroy the content of a IGFD_Selection_Pair + +struct IGFD_Selection { + IGFD_Selection_Pair* table; // 0 + size_t count; // 0U +}; + +IGFD_C_API IGFD_Selection IGFD_Selection_Get(); // return an initialized IGFD_Selection +IGFD_C_API void IGFD_Selection_DestroyContent(IGFD_Selection* vSelection); // destroy the content of a IGFD_Selection + +// constructor / destructor +IGFD_C_API ImGuiFileDialog* IGFD_Create(void); // create the filedialog context +IGFD_C_API void IGFD_Destroy(ImGuiFileDialog* vContextPtr); // destroy the filedialog context + +#ifdef USE_THUMBNAILS +typedef void (*IGFD_CreateThumbnailFun)(IGFD_Thumbnail_Info*); // callback function for create thumbnail texture +typedef void (*IGFD_DestroyThumbnailFun)(IGFD_Thumbnail_Info*); // callback fucntion for destroy thumbnail texture +#endif // USE_THUMBNAILS + +IGFD_C_API void IGFD_OpenDialog( // open a standard dialog + ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + const char* vKey, // key dialog + const char* vTitle, // title + const char* vFilters, // filters/filter collections. set it to null for directory mode + const struct IGFD_FileDialog_Config vConfig); // config + +IGFD_C_API bool IGFD_DisplayDialog( // Display the dialog + ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + const char* vKey, // key dialog to display (if not the same key as defined by OpenDialog => no opening) + ImGuiWindowFlags vFlags, // ImGuiWindowFlags + ImVec2 vMinSize, // mininmal size contraint for the ImGuiWindow + ImVec2 vMaxSize); // maximal size contraint for the ImGuiWindow + +IGFD_C_API void IGFD_CloseDialog( // Close the dialog + ImGuiFileDialog* vContextPtr); // ImGuiFileDialog context + +IGFD_C_API bool IGFD_IsOk( // true => Dialog Closed with Ok result / false : Dialog closed with cancel result + ImGuiFileDialog* vContextPtr); // ImGuiFileDialog context + +IGFD_C_API bool IGFD_WasKeyOpenedThisFrame( // say if the dialog key was already opened this frame + ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + const char* vKey); + +IGFD_C_API bool IGFD_WasOpenedThisFrame( // say if the dialog was already opened this frame + ImGuiFileDialog* vContextPtr); // ImGuiFileDialog context + +IGFD_C_API bool IGFD_IsKeyOpened( // say if the dialog key is opened + ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + const char* vCurrentOpenedKey); // the dialog key + +IGFD_C_API bool IGFD_IsOpened( // say if the dialog is opened somewhere + ImGuiFileDialog* vContextPtr); // ImGuiFileDialog context + +IGFD_C_API IGFD_Selection IGFD_GetSelection( // Open File behavior : will return selection via a map + ImGuiFileDialog* vContextPtr, // user datas (can be retrieved in pane) + IGFD_ResultMode vMode); // Result Mode + +IGFD_C_API char* IGFD_GetFilePathName( // Save File behavior : will always return the content of the field with current + // filter extention and current path, WARNINGS you are responsible to free it + ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + IGFD_ResultMode vMode); // Result Mode + +IGFD_C_API char* IGFD_GetCurrentFileName( // Save File behavior : will always return the content of the field with + // current filter extention, WARNINGS you are responsible to free it + ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + IGFD_ResultMode vMode); // Result Mode + +IGFD_C_API char* IGFD_GetCurrentPath( // will return current path, WARNINGS you are responsible to free it + ImGuiFileDialog* vContextPtr); // ImGuiFileDialog context + +IGFD_C_API char* IGFD_GetCurrentFilter( // will return selected filter, WARNINGS you are responsible to free it + ImGuiFileDialog* vContextPtr); // ImGuiFileDialog context + +IGFD_C_API void* IGFD_GetUserDatas( // will return user datas send with Open Dialog + ImGuiFileDialog* vContextPtr); // ImGuiFileDialog context + +IGFD_C_API void IGFD_SetFileStyle( // SetExtention datas for have custom display of particular file type + ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + IGFD_FileStyleFlags vFileStyleFlags, // file style type + const char* vFilter, // extention filter to tune + ImVec4 vColor, // wanted color for the display of the file with extention filter + const char* vIconText, // wanted text or icon of the file with extention filter (can be sued with font icon) + ImFont* vFont); // wanted font pointer + +IGFD_C_API void IGFD_SetFileStyle2( // SetExtention datas for have custom display of particular file type + ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + IGFD_FileStyleFlags vFileStyleFlags, // file style type + const char* vFilter, // extention filter to tune + float vR, float vG, float vB, + float vA, // wanted color channels RGBA for the display of the file with extention filter + const char* vIconText, // wanted text or icon of the file with extention filter (can be sued with font icon) + ImFont* vFont); // wanted font pointer + +IGFD_C_API bool IGFD_GetFileStyle(ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + IGFD_FileStyleFlags vFileStyleFlags, // file style type + const char* vFilter, // extention filter (same as used in SetExtentionInfos) + ImVec4* vOutColor, // color to retrieve + char** vOutIconText, // icon or text to retrieve, WARNINGS you are responsible to free it + ImFont** vOutFont); // font pointer to retrived + +IGFD_C_API void IGFD_ClearFilesStyle( // clear extentions setttings + ImGuiFileDialog* vContextPtr); // ImGuiFileDialog context + +IGFD_C_API void SetLocales( // set locales to use before and after display + ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + const int vCategory, // set local category + const char* vBeginLocale, // locale to use at begining of the dialog display + const char* vEndLocale); // locale to set at end of the dialog display + +#ifdef USE_EXPLORATION_BY_KEYS +IGFD_C_API void IGFD_SetFlashingAttenuationInSeconds( // set the flashing time of the line in file list when use + // exploration keys + ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + float vAttenValue); // set the attenuation (from flashed to not flashed) in seconds +#endif + +#ifdef USE_PLACES_FEATURE +IGFD_C_API char* IGFD_SerializePlaces( // serialize place : return place buffer to save in a file, WARNINGS + // you are responsible to free it + ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + bool vDontSerializeCodeBasedPlaces); // for avoid serialization of place added by code + +IGFD_C_API void IGFD_DeserializePlaces( // deserialize place : load bookmar buffer to load in the dialog (saved + // from previous use with SerializePlaces()) + ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + const char* vPlaces); // place buffer to load + +IGFD_C_API bool IGFD_AddPlacesGroup( // add a places group by code + ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + const char* vGroupName, // the group name + size_t vDisplayOrder, // the display roder of the group + bool vCanBeEdited); // let the user add/remove place in the group + +IGFD_C_API bool IGFD_RemovePlacesGroup( // remove a place group by code, return true if succeed + ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + const char* vGroupName); // place name to remove + +IGFD_C_API bool IGFD_AddPlace( // add a place by code + ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + const char* vGroupName, // the group name + const char* vPlaceName, // place name + const char* vPlacePath, // place path + bool vCanBeSaved, // place can be saved + const char* vIconText); // wanted text or icon of the file with extention filter (can be used with font icon) + +IGFD_C_API bool IGFD_RemovePlace( // remove a place by code, return true if succeed + ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + const char* vGroupName, // the group name + const char* vPlaceName); // place name to remove + +#endif + +#ifdef USE_THUMBNAILS +IGFD_C_API void SetCreateThumbnailCallback( // define the callback for create the thumbnails texture + ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + IGFD_CreateThumbnailFun vCreateThumbnailFun); // the callback for create the thumbnails texture + +IGFD_C_API void SetDestroyThumbnailCallback( // define the callback for destroy the thumbnails texture + ImGuiFileDialog* vContextPtr, // ImGuiFileDialog context + IGFD_DestroyThumbnailFun vDestroyThumbnailFun); // the callback for destroy the thumbnails texture + +IGFD_C_API void ManageGPUThumbnails( // must be call in gpu zone, possibly a thread, will call the callback for create + // / destroy the textures + ImGuiFileDialog* vContextPtr); // ImGuiFileDialog context +#endif // USE_THUMBNAILS \ No newline at end of file diff --git a/src/lib/ui/ImGuiFileDialogConfig.h b/src/lib/ui/ImGuiFileDialogConfig.h new file mode 100644 index 00000000..40282772 --- /dev/null +++ b/src/lib/ui/ImGuiFileDialogConfig.h @@ -0,0 +1,204 @@ +#pragma once + +// uncomment and modify defines under for customize ImGuiFileDialog + +///////////////////////////////// +//// STL FILE SYSTEM //////////// +///////////////////////////////// + +// uncomment if you need to use your FileSystem Interface +// if commented, you have two defualt interface, std::filesystem or dirent +// #define USE_CUSTOM_FILESYSTEM +// this options need c++17 +// #define USE_STD_FILESYSTEM + +///////////////////////////////// +//// MISC /////////////////////// +///////////////////////////////// + +// the spacing between button path's can be customized. +// if disabled the spacing is defined by the imgui theme +// define the space between path buttons +// #define CUSTOM_PATH_SPACING 2 + +// #define MAX_FILE_DIALOG_NAME_BUFFER 1024 +// #define MAX_PATH_BUFFER_SIZE 1024 + +///////////////////////////////// +//// QUICK PATH ///////////////// +///////////////////////////////// + +// the slash's buttons in path cna be used for quick select parallles directories +// #define USE_QUICK_PATH_SELECT + +///////////////////////////////// +//// THUMBNAILS ///////////////// +///////////////////////////////// + +// #define USE_THUMBNAILS +// the thumbnail generation use the stb_image and stb_resize lib who need to define the implementation +// btw if you already use them in your app, you can have compiler error due to "implemntation found in double" +// so uncomment these line for prevent the creation of implementation of these libs again +// #define DONT_DEFINE_AGAIN__STB_IMAGE_IMPLEMENTATION +// #define DONT_DEFINE_AGAIN__STB_IMAGE_RESIZE_IMPLEMENTATION +// #define IMGUI_RADIO_BUTTON RadioButton +// #define DisplayMode_ThumbailsList_ImageHeight 32.0f +// #define tableHeaderFileThumbnailsString "Thumbnails" +// #define DisplayMode_FilesList_ButtonString "FL" +// #define DisplayMode_FilesList_ButtonHelp "File List" +// #define DisplayMode_ThumbailsList_ButtonString "TL" +// #define DisplayMode_ThumbailsList_ButtonHelp "Thumbnails List" +// todo +// #define DisplayMode_ThumbailsGrid_ButtonString "TG" +// #define DisplayMode_ThumbailsGrid_ButtonHelp "Thumbnails Grid" + +///////////////////////////////// +//// EXPLORATION BY KEYS //////// +///////////////////////////////// + +// #define USE_EXPLORATION_BY_KEYS +// this mapping by default is for GLFW but you can use another +// #include +// Up key for explore to the top +// #define IGFD_KEY_UP ImGuiKey_UpArrow +// Down key for explore to the bottom +// #define IGFD_KEY_DOWN ImGuiKey_DownArrow +// Enter key for open directory +// #define IGFD_KEY_ENTER ImGuiKey_Enter +// BackSpace for comming back to the last directory +// #define IGFD_KEY_BACKSPACE ImGuiKey_Backspace + +///////////////////////////////// +//// SHORTCUTS => ctrl + KEY //// +///////////////////////////////// + +// #define SelectAllFilesKey ImGuiKey_A + +///////////////////////////////// +//// DIALOG EXIT //////////////// +///////////////////////////////// + +// by ex you can quit the dialog by pressing the key excape +// #define USE_DIALOG_EXIT_WITH_KEY +// #define IGFD_EXIT_KEY ImGuiKey_Escape + +///////////////////////////////// +//// WIDGETS //////////////////// +///////////////////////////////// + +// widget +// begin combo widget +// #define IMGUI_BEGIN_COMBO ImGui::BeginCombo +// when auto resized, FILTER_COMBO_MIN_WIDTH will be considered has minimum width +// FILTER_COMBO_AUTO_SIZE is enabled by default now to 1 +// uncomment if you want disable +// #define FILTER_COMBO_AUTO_SIZE 0 +// filter combobox width +// #define FILTER_COMBO_MIN_WIDTH 120.0f +// button widget use for compose path +// #define IMGUI_PATH_BUTTON ImGui::Button +// standard button +// #define IMGUI_BUTTON ImGui::Button + +///////////////////////////////// +//// STRING'S /////////////////// +///////////////////////////////// + +// locales string +// #define createDirButtonString "+" +// #define resetButtonString "R" +// #define devicesButtonString "Devices" +// #define editPathButtonString "E" +// #define searchString "Search" +// #define dirEntryString "[DIR] " +// #define linkEntryString "[LINK] " +// #define fileEntryString "[FILE] " +// #define fileNameString "File Name : " +// #define dirNameString "Directory Path :" +// #define buttonResetSearchString "Reset search" +// #define buttonDriveString "Devices" +// #define buttonEditPathString "Edit path\nYou can also right click on path buttons" +// #define buttonResetPathString "Reset to current directory" +// #define buttonCreateDirString "Create Directory" +// #define OverWriteDialogTitleString "The file Already Exist !" +// #define OverWriteDialogMessageString "Would you like to OverWrite it ?" +// #define OverWriteDialogConfirmButtonString "Confirm" +// #define OverWriteDialogCancelButtonString "Cancel" + +// Validation buttons +// #define okButtonString " OK" +// #define okButtonWidth 0.0f +// #define cancelButtonString " Cancel" +// #define cancelButtonWidth 0.0f +// alignement [0:1], 0.0 is left, 0.5 middle, 1.0 right, and other ratios +// #define okCancelButtonAlignement 0.0f +// #define invertOkAndCancelButtons 0 + +// DateTimeFormat +// see strftime functionin for customize +// "%Y/%m/%d %H:%M" give 2021:01:22 11:47 +// "%Y/%m/%d %i:%M%p" give 2021:01:22 11:45PM +// #define DateTimeFormat "%Y/%m/%d %i:%M%p" + +///////////////////////////////// +//// SORTING ICONS ////////////// +///////////////////////////////// + +// theses icons will appear in table headers +// #define USE_CUSTOM_SORTING_ICON +// #define tableHeaderAscendingIcon "A|" +// #define tableHeaderDescendingIcon "D|" +// #define tableHeaderFileNameString " File name" +// #define tableHeaderFileTypeString " Type" +// #define tableHeaderFileSizeString " Size" +// #define tableHeaderFileDateTimeString " Date" +// #define fileSizeBytes "o" +// #define fileSizeKiloBytes "Ko" +// #define fileSizeMegaBytes "Mo" +// #define fileSizeGigaBytes "Go" + +// default table sort field (must be FIELD_FILENAME, FIELD_TYPE, FIELD_SIZE, FIELD_DATE or FIELD_THUMBNAILS) +// #define defaultSortField FIELD_FILENAME + +// default table sort order for each field (true => Descending, false => Ascending) +// #define defaultSortOrderFilename true +// #define defaultSortOrderType true +// #define defaultSortOrderSize true +// #define defaultSortOrderDate true +// #define defaultSortOrderThumbnails true + +///////////////////////////////// +//// PLACES FEATURES //////////// +///////////////////////////////// + +// #define USE_PLACES_FEATURE +// #define PLACES_PANE_DEFAULT_SHOWN false +// #define placesPaneWith 150.0f +// #define IMGUI_TOGGLE_BUTTON ToggleButton +// #define placesButtonString "Place" +// #define placesButtonHelpString "Places" +// #define addPlaceButtonString "+" +// #define removePlaceButtonString "-" +// #define validatePlaceButtonString "ok" +// #define editPlaceButtonString "E" + +////////////////////////////////////// +//// PLACES FEATURES : BOOKMARKS ///// +////////////////////////////////////// + +// a group for bookmarks will be added by default, but you can also create it yourself and many more +// #define USE_PLACES_BOOKMARKS +// #define PLACES_BOOKMARK_DEFAULT_OPEPEND true +// #define placesBookmarksGroupName "Bookmarks" +// #define placesBookmarksDisplayOrder 0 // to the first + +////////////////////////////////////// +//// PLACES FEATURES : DEVICES /////// +////////////////////////////////////// + +// a group for system devices (returned by IFileSystem), but you can also add yours +// by ex if you would like to display a specific icon for some devices +// #define USE_PLACES_DEVICES +// #define PLACES_DEVICES_DEFAULT_OPEPEND true +// #define placesDevicesGroupName "Devices" +// #define placesDevicesDisplayOrder 10 // to the end \ No newline at end of file diff --git a/src/lib/web.cpp b/src/lib/web.cpp index eed9c26c..8381fba9 100644 --- a/src/lib/web.cpp +++ b/src/lib/web.cpp @@ -1,19 +1,56 @@ #ifdef __EMSCRIPTEN__ #include "Companion.h" - +#include +#include #include using namespace emscripten; Companion* Companion::Instance; +static emscripten::val gSinkCallback = emscripten::val::null(); + +class EMSink : public spdlog::sinks::base_sink { +protected: + void sink_it_(const spdlog::details::log_msg& msg) override { + if (gSinkCallback.isNull() || gSinkCallback.isUndefined()) { + return; + } + + // Format the message using the sink's formatter_ + spdlog::memory_buf_t buf; + formatter_->format(msg, buf); + + // Send to JS callback + gSinkCallback(std::string(buf.data(), buf.size())); + } + + void flush_() override { + // No-op for JS sink + } +}; void Bind(Companion* instance) { Companion::Instance = instance; } +void RegisterLogSink(emscripten::val sink) { + if (sink.isUndefined() || sink.isNull()) { + // Unregister the current sink + spdlog::drop_all(); + gSinkCallback = emscripten::val::null(); + return; + } + + gSinkCallback = sink; + auto logger = std::make_shared("emscripten_logger", std::make_shared()); + spdlog::register_logger(logger); + spdlog::set_default_logger(logger); +} + EMSCRIPTEN_BINDINGS(Torch) { register_vector("VectorUInt8"); function("Bind", &Bind, allow_raw_pointers()); + function("RegisterLogSink", &RegisterLogSink); enum_("ExportType") .value("Header", ExportType::Header) @@ -34,6 +71,8 @@ EMSCRIPTEN_BINDINGS(Torch) { .value("Unknown", N64::CountryCode::Unknown); class_("Cartridge") + .constructor>() + .function("Initialize", &N64::Cartridge::Initialize) .function("GetGameTitle", &N64::Cartridge::GetGameTitle) .function("GetCountryCode", &N64::Cartridge::GetCountryCode) .function("GetCountry", &N64::Cartridge::GetCountry) @@ -46,8 +85,10 @@ EMSCRIPTEN_BINDINGS(Torch) { .constructor, ArchiveType, bool, bool, std::string>() .constructor, ArchiveType, bool, bool>() .function("Init", &Companion::Init) - .function("GetCartridge", &Companion::GetCartridge, allow_raw_pointers()) .function("Process", &Companion::Process) + .function("Finalize", &Companion::Finalize) + .function("GetOutputPath", &Companion::GetOutputPath) + .function("GetCartridge", &Companion::GetCartridge, allow_raw_pointers()) .function("GetRomData", &Companion::GetRomData, allow_raw_pointers()); } #endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index b5ea9fc9..590e5b2c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,9 +3,120 @@ #include "Companion.h" #if defined(STANDALONE) && !defined(__EMSCRIPTEN__) - Companion* Companion::Instance; +#ifdef BUILD_UI +#include "raylib.h" +#include "rlImGui.h" +#include "imgui.h" + +#define RAYGUI_IMPLEMENTATION +#include "ui/View.h" +#include "ui/list/Main.h" + +void ApplyRayguiTheme() +{ + ImGuiStyle& style = ImGui::GetStyle(); + + // Set all rounding to 0.0f for sharp corners + style.WindowRounding = 0.0f; + style.ChildRounding = 0.0f; + style.FrameRounding = 0.0f; + style.GrabRounding = 0.0f; + style.PopupRounding = 0.0f; + style.ScrollbarRounding = 0.0f; + style.TabRounding = 0.0f; + + // Set all border sizes to 1.0f for a consistent outline + style.WindowBorderSize = 1.0f; + style.ChildBorderSize = 1.0f; + style.FrameBorderSize = 1.0f; + style.PopupBorderSize = 1.0f; + style.TabBorderSize = 1.0f; + + // Tweak spacing to be compact like raygui + style.WindowPadding = ImVec2(8.0f, 8.0f); + style.FramePadding = ImVec2(6.0f, 4.0f); + style.ItemSpacing = ImVec2(8.0f, 4.0f); + style.ItemInnerSpacing = ImVec2(4.0f, 4.0f); + style.ScrollbarSize = 12.0f; + + // Main Colors + ImVec4* colors = style.Colors; + colors[ImGuiCol_Text] = ImVec4(0.25f, 0.25f, 0.25f, 1.00f); // Dark Gray + colors[ImGuiCol_TextDisabled] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.96f, 0.96f, 0.96f, 1.00f); // Almost White + colors[ImGuiCol_ChildBg] = ImVec4(0.92f, 0.92f, 0.92f, 1.00f); + colors[ImGuiCol_PopupBg] = ImVec4(0.98f, 0.98f, 0.98f, 1.00f); + colors[ImGuiCol_Border] = ImVec4(0.56f, 0.56f, 0.56f, 1.00f); // Mid Gray + colors[ImGuiCol_BorderShadow] = ImVec4(1.00f, 1.00f, 1.00f, 0.20f); // Transparent white for a subtle inner highlight + + // Frame/Control Colors + colors[ImGuiCol_FrameBg] = ImVec4(0.78f, 0.78f, 0.78f, 1.00f); // Lighter Gray + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.78f, 0.82f, 0.90f, 1.00f); // Light Blue + colors[ImGuiCol_FrameBgActive] = ImVec4(0.61f, 0.67f, 0.78f, 1.00f); // Darker Blue + + // Title Bar + colors[ImGuiCol_TitleBg] = ImVec4(0.85f, 0.85f, 0.85f, 1.00f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.78f, 0.82f, 0.90f, 1.00f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.95f, 0.95f, 0.95f, 1.00f); + + // Buttons + colors[ImGuiCol_Button] = colors[ImGuiCol_FrameBg]; + colors[ImGuiCol_ButtonHovered] = colors[ImGuiCol_FrameBgHovered]; + colors[ImGuiCol_ButtonActive] = colors[ImGuiCol_FrameBgActive]; + + // Headers (Selectable, CollapsingHeader, etc.) + colors[ImGuiCol_Header] = ImVec4(0.85f, 0.85f, 0.85f, 1.00f); + colors[ImGuiCol_HeaderHovered] = colors[ImGuiCol_FrameBgHovered]; + colors[ImGuiCol_HeaderActive] = colors[ImGuiCol_FrameBgActive]; + + // Tabs + colors[ImGuiCol_Tab] = colors[ImGuiCol_Header]; + colors[ImGuiCol_TabHovered] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_TabActive] = colors[ImGuiCol_HeaderActive]; + colors[ImGuiCol_TabUnfocused] = colors[ImGuiCol_TitleBg]; + colors[ImGuiCol_TabUnfocusedActive] = colors[ImGuiCol_TitleBgActive]; + + // Checkbox and Sliders + colors[ImGuiCol_CheckMark] = ImVec4(0.25f, 0.25f, 0.25f, 1.00f); + colors[ImGuiCol_SliderGrab] = ImVec4(0.56f, 0.56f, 0.56f, 1.00f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.40f, 0.40f, 0.40f, 1.00f); + + // Separators and Resize Grips + colors[ImGuiCol_Separator] = colors[ImGuiCol_Border]; + colors[ImGuiCol_ResizeGrip] = ImVec4(0.85f, 0.85f, 0.85f, 0.50f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.78f, 0.82f, 0.90f, 0.78f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.61f, 0.67f, 0.78f, 1.00f); +} + +void launchUI() { + const int screenWidth = 800; + const int screenHeight = 600; + std::shared_ptr view = std::make_shared(); + + InitWindow(screenWidth, screenHeight, "Torch GUI"); + SetTargetFPS(60); + + view->SetView(std::make_shared()); + + rlImGuiSetup(false); + + ApplyRayguiTheme(); + + while (!WindowShouldClose()) { + BeginDrawing(); + ClearBackground(RAYWHITE); + rlImGuiBegin(); + view->Render(); + rlImGuiEnd(); + EndDrawing(); + } + + CloseWindow(); +} +#endif + int main(int argc, char *argv[]) { CLI::App app{"Torch - [T]orch is [O]ur [R]esource [C]onversion [H]elper\n\ * It extracts from a baserom and generates code or an otr.\n\ @@ -74,6 +185,7 @@ int main(int argc, char *argv[]) { binary->parse_complete_callback([&] { const auto instance = Companion::Instance = new Companion(filename, ArchiveType::None, debug, srcdir, destdir); instance->Init(ExportType::Binary); + instance->Process(); }); /* Generate headers */ @@ -167,6 +279,11 @@ int main(int argc, char *argv[]) { } }); +#ifdef BUILD_UI + const auto ui = app.add_subcommand("ui", "UI - Launch the GUI (if built with UI support)"); + ui->parse_complete_callback(launchUI); +#endif + try { app.parse(argc, argv); } catch (const CLI::ParseError &e) { diff --git a/src/n64/Cartridge.h b/src/n64/Cartridge.h index 4477a597..8fad764f 100644 --- a/src/n64/Cartridge.h +++ b/src/n64/Cartridge.h @@ -17,7 +17,7 @@ class Cartridge { explicit Cartridge(const std::vector& romData) : gRomData(romData), gCountryCode(CountryCode::Unknown), gVersion(0), gGameTitle("Unknown"), gRomCRC(0) { } - void Initialize(); + void Initialize(); const std::string& GetGameTitle(); std::string GetCountryCode(); CountryCode GetCountry(); diff --git a/src/ui/View.h b/src/ui/View.h new file mode 100644 index 00000000..4e8cdec0 --- /dev/null +++ b/src/ui/View.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include "ui/ImGuiFileDialog.h" +#include "extras/IconsFontAwesome6.h" + +class ViewManager; + +class View { +public: + virtual ~View() = default; + + virtual void Init() {} + virtual void Update() {} + virtual void Render() = 0; + + void InternalInit(std::shared_ptr manager) { + this->manager = manager; + this->Init(); + } +private: + std::shared_ptr manager; +}; + +class ViewManager : public std::enable_shared_from_this { +public: + void SetView(std::shared_ptr view) { + if (current) { + current->~View(); + } + current = view; + current->InternalInit(shared_from_this()); + } + + void Render() { + if (current) { + current->Update(); + current->Render(); + } + } +private: + std::shared_ptr current = nullptr; +}; \ No newline at end of file diff --git a/src/ui/list/Main.h b/src/ui/list/Main.h new file mode 100644 index 00000000..0ede20e5 --- /dev/null +++ b/src/ui/list/Main.h @@ -0,0 +1,179 @@ +#pragma once + +#include +#include +#include + +#include "ui/View.h" +#include "utils/TorchUtils.h" + +namespace fs = std::filesystem; + +class MainView : public View { +public: + std::optional configFilePath = std::nullopt; + std::optional romFilePath = std::nullopt; + std::optional selectedFile = std::nullopt; + std::vector files; + + std::vector item_heights; + std::vector item_y_positions; + float total_height = 0.0f; + + void Update() override { + ImGuiIO& io = ImGui::GetIO(); + float width = io.DisplaySize.x; + float height = io.DisplaySize.y; + + ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImVec2((width / 2) - 300, (height / 2) - 200), ImGuiCond_Always); + if (ImGuiFileDialog::Instance()->Display("ChooseRom")) { + if (ImGuiFileDialog::Instance()->IsOk()) { + romFilePath = fs::path(ImGuiFileDialog::Instance()->GetFilePathName()); + printf("Selected ROM: %s\n", romFilePath->string().c_str()); + } + + // close + ImGuiFileDialog::Instance()->Close(); + } + ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImVec2((width / 2) - 300, (height / 2) - 200), ImGuiCond_Always); + if (ImGuiFileDialog::Instance()->Display("ChooseConfigYML")) { + if (ImGuiFileDialog::Instance()->IsOk()) { + fs::path file = fs::path(ImGuiFileDialog::Instance()->GetFilePathName()); + if (file.filename() == "config.yml") { + configFilePath = file; + } + printf("Selected Config: %s\n", configFilePath->string().c_str()); + } + + // close + ImGuiFileDialog::Instance()->Close(); + } + } + + void Render() override { + ImGuiIO& io = ImGui::GetIO(); + float width = io.DisplaySize.x; + float height = io.DisplaySize.y; + + ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(width, height), ImGuiCond_Always); + ImGui::Begin("Torch GUI", NULL, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar); + DrawToolbar(); + ImGui::BeginChild("FilesPanel", ImVec2(236, height - 45), true, ImGuiWindowFlags_HorizontalScrollbar); + { + ImGui::Text("Files"); + ImGui::Separator(); + for (int i = 0; i < files.size(); i++) { + if (ImGui::Selectable(files[i].filename().c_str(), selectedFile == files[i])) { + selectedFile = files[i]; + RecalculateLayout(); + } + } + } + ImGui::EndChild(); + ImGui::SameLine(); + ImGui::BeginChild("AssetsPanel", ImVec2(0, height - 45), true); + { + ImGui::Text("Assets"); + ImGui::Separator(); + if(selectedFile.has_value()) { + auto parseData = Companion::Instance->GetParseResults(); + auto items = parseData[selectedFile.value().string()]; + ImGuiListClipper clipper; + + float cursorY = ImGui::GetCursorPosY(); + clipper.Begin(total_height, 1.0f); + while (clipper.Step()) { + auto it_start = std::lower_bound(item_y_positions.begin(), item_y_positions.end(), clipper.DisplayStart); + int start_index = std::distance(item_y_positions.begin(), it_start); + if (it_start != item_y_positions.begin()) { + start_index--; + } + + for (int i = start_index; i < items.size(); ++i) { + const float item_top = item_y_positions[i]; + const float item_bottom = item_top + item_heights[i]; + + if (item_top >= clipper.DisplayEnd) { + break; + } + + if (item_bottom >= clipper.DisplayStart) { + ImGui::SetCursorPosY(cursorY + item_top); + auto& asset = items[i]; + auto ui = Companion::Instance->GetUIFactory(asset.type); + if(!ui.has_value() || !asset.data.has_value()) { + ImGui::Text("No UI available for asset type: %s", asset.type.c_str()); + } else { + ui.value()->DrawUI(asset); + } + ImGui::Separator(); + } + } + } + clipper.End(); + } + } + ImGui::EndChild(); + ImGui::End(); + } + + void DrawToolbar() { + const ImVec2 button = ImVec2(25, 25); + if(ImGui::Button(ICON_FA_FILE, button)) { + IGFD::FileDialogConfig config; + config.path = "."; + ImGuiFileDialog::Instance()->OpenDialog("ChooseConfigYML", "Choose Config File", ".yml", config); + } + ImGui::SameLine(); + if(ImGui::Button(ICON_FA_FILE_IMPORT, button)) { + IGFD::FileDialogConfig config; + config.path = "."; + ImGuiFileDialog::Instance()->OpenDialog("ChooseRom", "Choose ROM File", ".z64", config); + } + ImGui::SameLine(); + if(ImGui::Button(ICON_FA_PLAY, button)) { + if (configFilePath.has_value() && romFilePath.has_value()) { + // Launch Torch + const auto instance = Companion::Instance = new Companion(romFilePath->string(), ArchiveType::O2R, false, configFilePath->parent_path().string(), ""); + instance->Init(ExportType::Binary, false); + instance->Process(); + ReloadScrollFiles(); + } else { + printf("Please select both a config.yml and a ROM file before running Torch.\n"); + } + } + } + + void ReloadScrollFiles() { + files.clear(); + const auto entries = Companion::Instance->GetAddrMap(); + for (const auto & entry : entries){ + files.push_back(fs::path(entry.first)); + } + } + + void RecalculateLayout() { + item_heights.clear(); + item_y_positions.clear(); + total_height = 0.0f; + auto parseData = Companion::Instance->GetParseResults(); + auto items = parseData[selectedFile.value().string()]; + + for (const auto& asset : items) { + auto ui = Companion::Instance->GetUIFactory(asset.type); + float y = !ui.has_value() || !asset.data.has_value() ? ImGui::GetTextLineHeightWithSpacing() : ui.value()->GetItemHeight(asset); + y += ImGui::GetStyle().ItemSpacing.y * 2; + item_heights.push_back(y); + } + + float current_y = 0.0f; + for (float height : item_heights) { + item_y_positions.push_back(current_y); + current_y += height; + } + total_height = current_y; + } +}; \ No newline at end of file