1
0
mirror of synced 2024-12-11 23:46:00 +01:00
ImHex/external/ImGui/source/ImGuiFileBrowser.cpp

1215 lines
45 KiB
C++
Raw Normal View History

#include "ImGuiFileBrowser.h"
#ifndef IMGUI_DEFINE_MATH_OPERATORS
#define IMGUI_DEFINE_MATH_OPERATORS
#endif
#include "imgui_internal.h"
#include <iostream>
#include <functional>
#include <climits>
#include <string.h>
#include <sstream>
#include <cwchar>
#include <cctype>
#include <algorithm>
#include <cmath>
#if defined (WIN32) || defined (_WIN32) || defined (__WIN32)
#define OSWIN
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include "Dirent/dirent.h"
#include <windows.h>
#else
#include <dirent.h>
#endif // defined (WIN32) || defined (_WIN32)
namespace imgui_addons
{
ImGuiFileBrowser::ImGuiFileBrowser()
{
filter_mode = FilterMode_Files | FilterMode_Dirs;
show_inputbar_combobox = false;
validate_file = false;
show_hidden = false;
is_dir = false;
filter_dirty = true;
is_appearing = true;
path_input_enabled = false;
col_items_limit = 12;
selected_idx = -1;
selected_ext_idx = 0;
ext_box_width = -1.0f;
col_width = 280.0f;
invfile_modal_id = "Invalid File!";
repfile_modal_id = "Replace File?";
selected_fn = "";
selected_path = "";
input_fn[0] = '\0';
temp_dir_input[0] = '\0';
#ifdef OSWIN
current_path = "./";
#else
initCurrentPath();
#endif
}
ImGuiFileBrowser::~ImGuiFileBrowser()
{
}
void ImGuiFileBrowser::clearFileList()
{
//Clear pointer references to subdirs and subfiles
filtered_dirs.clear();
filtered_files.clear();
inputcb_filter_files.clear();
//Now clear subdirs and subfiles
subdirs.clear();
subfiles.clear();
filter_dirty = true;
selected_idx = -1;
}
void ImGuiFileBrowser::closeDialog()
{
valid_types = "";
valid_exts.clear();
selected_ext_idx = 0;
selected_idx = -1;
input_fn[0] = '\0'; //Hide any text in Input bar for the next time save dialog is opened.
filter.Clear(); //Clear Filter for the next time open dialog is called.
show_inputbar_combobox = false;
validate_file = false;
show_hidden = false;
is_dir = false;
filter_dirty = true;
is_appearing = true;
//Clear pointer references to subdirs and subfiles
filtered_dirs.clear();
filtered_files.clear();
inputcb_filter_files.clear();
//Now clear subdirs and subfiles
subdirs.clear();
subfiles.clear();
ImGui::CloseCurrentPopup();
}
bool ImGuiFileBrowser::showFileDialog(const std::string& label, const DialogMode mode, const ImVec2& sz_xy, const std::string& valid_types)
{
dialog_mode = mode;
ImGuiIO& io = ImGui::GetIO();
Proper DPI scaling and basic custom font (#85) * add glm to arch deps After running got `None of the required 'glm' found`. This fixes that * dist/fedora: Include file magic headers Due to differences in package names between Deb based systems, Arch Linux, and RPM based systems the package containing the development headers for file were missing from the Fedora dependencies script. This includes the package `file-devel`, which is the package which resolves the issue. In Fedora, one can identify the package providing a specific file using the verb "whatprovides" with the command dnf, e.g.: [~]$ dnf whatprovides /usr/include/magic.h Last metadata expiration check: 4 days, 0:23:05 ago on Fri 04 Dec 2020 09:06:53 AM PST. file-devel-5.39-3.fc33.i686 : Libraries and header files for file development Repo : fedora Matched from: Filename : /usr/include/magic.h file-devel-5.39-3.fc33.x86_64 : Libraries and header files for file development Repo : @System Matched from: Filename : /usr/include/magic.h file-devel-5.39-3.fc33.x86_64 : Libraries and header files for file development Repo : fedora Matched from: Filename : /usr/include/magic.h If one is unsure of the specific path, globbing may be used (but must be quoted): dnf whatprovides "*/magic.h" Resolves #48 * dist: Prevent already installed packages in ArchLinux and MSYS2. Use --needed option with pacman to prevent it. * Add script to install dependencies on Debian/Ubuntu. Tested with Xubuntu 20.04 and Debian testing (in today's Docker image bitnami/minideb). Update README.md. * ci: rework (#31) * Support non standard LLVM library names (#86) This fix openSUSE and Gentoo issue mentioned in https://github.com/WerWolv/ImHex/issues/37#issuecomment-739503138. (tested on openSUSE tumbleweed via Docker) I also took the liberty of renaming llvm_lib to llvm_demangle_lib to be more specific in the ``CMakeLists.txt``. * Implement proper DPI handling * Implement basic custom font support * Fix building on windows * Hopefully fix fonts on Windows * Fix several scaling issues * Replace font renderer with freetype * Updated CI and dependency scripts * Rebuild default font atlas * Correct platform detection macro for mingw * Fixed PKGBUILD Co-authored-by: brockelmore <31553173+brockelmore@users.noreply.github.com> Co-authored-by: Brian 'Redbeard' Harrington <redbeard@dead-city.org> Co-authored-by: Biswapriyo Nath <nathbappai@gmail.com> Co-authored-by: Stéphane Gourichon <stephane.gourichon@fidergo.fr> Co-authored-by: umarcor <38422348+umarcor@users.noreply.github.com> Co-authored-by: Mary <me@thog.eu> Co-authored-by: WerWolv <werwolv98@gmail.com>
2020-12-11 14:24:42 +01:00
max_size = ImVec2( io.DisplaySize.x, io.DisplaySize.y);
min_size = ImVec2(0.5f * io.DisplaySize.x, 0.5f * io.DisplaySize.y);
ImGui::SetNextWindowSizeConstraints(min_size, max_size);
ImGui::SetNextWindowPos(io.DisplaySize * 0.5f, ImGuiCond_Appearing, ImVec2(0.5f,0.5f));
ImGui::SetNextWindowSize(ImVec2(std::max(sz_xy.x, min_size.x), std::max(sz_xy.y, min_size.y)), ImGuiCond_Appearing);
//Set Proper Filter Mode.
if(mode == DialogMode::SELECT)
filter_mode = FilterMode_Dirs;
else
filter_mode = FilterMode_Files | FilterMode_Dirs;
if (ImGui::BeginPopupModal(label.c_str(), nullptr, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse))
{
bool show_error = false;
// If this is the initial run, read current directory and load data once.
if(is_appearing)
{
selected_fn.clear();
selected_path.clear();
if(mode != DialogMode::SELECT)
{
this->valid_types = valid_types;
setValidExtTypes(valid_types);
}
/* If current path is empty (can happen on Windows if user closes dialog while inside MyComputer.
* Since this is a virtual folder, path would be empty) load the drives on Windows else initialize the current path on Unix.
*/
if(current_path.empty())
{
#ifdef OSWIN
show_error |= !(loadWindowsDrives());
#else
initCurrentPath();
show_error |= !(readDIR(current_path));
#endif // OSWIN
}
else
show_error |= !(readDIR(current_path));
is_appearing = false;
}
show_error |= renderNavAndSearchBarRegion();
show_error |= renderFileListRegion();
show_error |= renderInputTextAndExtRegion();
show_error |= renderButtonsAndCheckboxRegion();
if(validate_file)
{
validate_file = false;
bool check = validateFile();
if(!check && dialog_mode == DialogMode::OPEN)
{
ImGui::OpenPopup(invfile_modal_id.c_str());
selected_fn.clear();
selected_path.clear();
}
else if(!check && dialog_mode == DialogMode::SAVE)
ImGui::OpenPopup(repfile_modal_id.c_str());
else if(!check && dialog_mode == DialogMode::SELECT)
{
selected_fn.clear();
selected_path.clear();
show_error = true;
error_title = "Invalid Directory!";
error_msg = "Invalid Directory Selected. Please make sure the directory exists.";
}
//If selected file passes through validation check, set path to the file and close file dialog
if(check)
{
selected_path = current_path + selected_fn;
//Add a trailing "/" to emphasize its a directory not a file. If you want just the dir name it's accessible through "selected_fn"
if(dialog_mode == DialogMode::SELECT)
selected_path += "/";
closeDialog();
}
}
// We don't need to check as the modals will only be shown if OpenPopup is called
showInvalidFileModal();
if(showReplaceFileModal())
closeDialog();
//Show Error Modal if there was an error opening any directory
if(show_error)
ImGui::OpenPopup(error_title.c_str());
showErrorModal();
ImGui::EndPopup();
return (!selected_fn.empty() && !selected_path.empty());
}
else
return false;
}
bool ImGuiFileBrowser::renderNavAndSearchBarRegion()
{
ImGuiStyle& style = ImGui::GetStyle();
bool show_error = false;
float frame_height = ImGui::GetFrameHeight();
float list_item_height = GImGui->FontSize + style.ItemSpacing.y;
ImVec2 pw_content_size = ImGui::GetWindowSize() - style.WindowPadding * 2.0;
ImVec2 sw_size = ImVec2(ImGui::CalcTextSize("Random").x + 140, style.WindowPadding.y * 2.0f + frame_height);
ImVec2 sw_content_size = sw_size - style.WindowPadding * 2.0;
ImVec2 nw_size = ImVec2(pw_content_size.x - style.ItemSpacing.x - sw_size.x, sw_size.y);
ImGui::BeginChild("##NavigationWindow", nw_size, true, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar);
if (!this->path_input_enabled) {
if (ImGui::Button("D")) {
memcpy(temp_dir_input, this->current_path.c_str(), this->current_path.length());
this->path_input_enabled = true;
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.882f, 0.745f, 0.078f, 1.0f));
for (int i = 0; i < current_dirlist.size(); i++) {
if (ImGui::Button(current_dirlist[i].c_str())) {
//If last button clicked, nothing happens
if (i != current_dirlist.size() - 1)
show_error |= !(onNavigationButtonClick(i));
}
//Draw Arrow Buttons
if (i != current_dirlist.size() - 1) {
ImGui::SameLine(0, 0);
float next_label_width = ImGui::CalcTextSize(current_dirlist[i + 1].c_str()).x;
if (i + 1 < current_dirlist.size() - 1)
next_label_width += frame_height + ImGui::CalcTextSize(">>").x;
if (ImGui::GetCursorPosX() + next_label_width >= (nw_size.x - style.WindowPadding.x * 3.0)) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 1.0f, 1.0f, 0.01f));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
//Render a drop down of navigation items on button press
if (ImGui::Button(">>"))
ImGui::OpenPopup("##NavBarDropboxPopup");
if (ImGui::BeginPopup("##NavBarDropboxPopup")) {
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.125f, 0.125f, 0.125f, 1.0f));
if (ImGui::ListBoxHeader("##NavBarDropBox", ImVec2(0, list_item_height * 5))) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.882f, 0.745f, 0.078f, 1.0f));
for (int j = i + 1; j < current_dirlist.size(); j++) {
if (ImGui::Selectable(current_dirlist[j].c_str(), false) &&
j != current_dirlist.size() - 1) {
show_error |= !(onNavigationButtonClick(j));
ImGui::CloseCurrentPopup();
}
}
ImGui::PopStyleColor();
ImGui::ListBoxFooter();
}
ImGui::PopStyleColor();
ImGui::EndPopup();
}
ImGui::PopStyleColor(2);
break;
} else {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 1.0f, 1.0f, 0.01f));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::ArrowButtonEx("##Right", ImGuiDir_Right, ImVec2(frame_height, frame_height),
ImGuiButtonFlags_Disabled);
ImGui::SameLine(0, 0);
ImGui::PopStyleColor(2);
}
}
}
ImGui::PopStyleColor();
} else {
ImGui::PushItemWidth(nw_size.x - 15);
if (ImGui::InputText("##nolabel", temp_dir_input, 256, ImGuiInputTextFlags_EnterReturnsTrue)) {
if(readDIR(std::string(temp_dir_input) + "/")) {
parsePathTabs(std::string(temp_dir_input) + "/");
current_path = std::string(temp_dir_input);
if (current_path.back() != '/')
current_path.push_back('/');
this->path_input_enabled = false;
memset(this->temp_dir_input, 0x00, 256);
}
}
ImGui::PopItemWidth();
}
ImGui::EndChild();
ImGui::SameLine();
ImGui::BeginChild("##SearchWindow", sw_size, true, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar);
//Render Search/Filter bar
float marker_width = ImGui::CalcTextSize("(?)").x + style.ItemSpacing.x;
if(filter.Draw("##SearchBar", sw_content_size.x - marker_width) || filter_dirty )
filterFiles(filter_mode);
//If filter bar was focused clear selection
if(ImGui::GetFocusID() == ImGui::GetID("##SearchBar"))
selected_idx = -1;
ImGui::SameLine();
showHelpMarker("Filter (inc, -exc)");
ImGui::EndChild();
return show_error;
}
bool ImGuiFileBrowser::renderFileListRegion()
{
ImGuiStyle& style = ImGui::GetStyle();
ImVec2 pw_size = ImGui::GetWindowSize();
bool show_error = false;
float list_item_height = ImGui::CalcTextSize("").y + style.ItemSpacing.y;
float input_bar_ypos = pw_size.y - ImGui::GetFrameHeightWithSpacing() * 2.5f - style.WindowPadding.y;
float window_height = input_bar_ypos - ImGui::GetCursorPosY() - style.ItemSpacing.y;
float window_content_height = window_height - style.WindowPadding.y * 2.0f;
float min_content_size = pw_size.x - style.WindowPadding.x * 4.0f;
if(window_content_height <= 0.0f)
return show_error;
//Reinitialize the limit on number of selectables in one column based on height
col_items_limit = static_cast<int>(std::max(1.0f, window_content_height/list_item_height));
int num_cols = static_cast<int>(std::max(1.0f, std::ceil(static_cast<float>(filtered_dirs.size() + filtered_files.size()) / col_items_limit)));
//Limitation by ImGUI in 1.75. If columns are greater than 64 readjust the limit on items per column and recalculate number of columns
if(num_cols > 64)
{
int exceed_items_amount = (num_cols - 64) * col_items_limit;
col_items_limit += static_cast<int>(std::ceil(exceed_items_amount/64.0));
num_cols = static_cast<int>(std::max(1.0f, std::ceil(static_cast<float>(filtered_dirs.size() + filtered_files.size()) / col_items_limit)));
}
float content_width = num_cols * col_width;
if(content_width < min_content_size)
content_width = 0;
ImGui::SetNextWindowContentSize(ImVec2(content_width, 0));
ImGui::BeginChild("##ScrollingRegion", ImVec2(0, window_height), true, ImGuiWindowFlags_HorizontalScrollbar);
ImGui::Columns(num_cols);
//Output directories in yellow
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.882f, 0.745f, 0.078f,1.0f));
int items = 0;
for (int i = 0; i < filtered_dirs.size(); i++)
{
if(!filtered_dirs[i]->is_hidden || show_hidden)
{
items++;
if(ImGui::Selectable(filtered_dirs[i]->name.c_str(), selected_idx == i && is_dir, ImGuiSelectableFlags_AllowDoubleClick))
{
selected_idx = i;
is_dir = true;
// If dialog mode is SELECT then copy the selected dir name to the input text bar
if(dialog_mode == DialogMode::SELECT)
strcpy(input_fn, filtered_dirs[i]->name.c_str());
if (ImGui::IsMouseClicked(0))
path_input_enabled = false;
if(ImGui::IsMouseDoubleClicked(0))
{
show_error |= !(onDirClick(i));
break;
}
}
if( (items) % col_items_limit == 0)
ImGui::NextColumn();
}
}
ImGui::PopStyleColor(1);
//Output files
for (int i = 0; i < filtered_files.size(); i++)
{
if(!filtered_files[i]->is_hidden || show_hidden)
{
items++;
if(ImGui::Selectable(filtered_files[i]->name.c_str(), selected_idx == i && !is_dir, ImGuiSelectableFlags_AllowDoubleClick))
{
//int len = filtered_files[i]->name.length();
selected_idx = i;
is_dir = false;
// If dialog mode is OPEN/SAVE then copy the selected file name to the input text bar
strcpy(input_fn, filtered_files[i]->name.c_str());
if(ImGui::IsMouseDoubleClicked(0))
{
selected_fn = filtered_files[i]->name;
validate_file = true;
}
}
if( (items) % col_items_limit == 0)
ImGui::NextColumn();
}
}
ImGui::Columns(1);
ImGui::EndChild();
return show_error;
}
bool ImGuiFileBrowser::renderInputTextAndExtRegion()
{
std::string label = (dialog_mode == DialogMode::SAVE) ? "Save As:" : "Open:";
ImGuiStyle& style = ImGui::GetStyle();
ImGuiIO& io = ImGui::GetIO();
ImVec2 pw_pos = ImGui::GetWindowPos();
ImVec2 pw_content_sz = ImGui::GetWindowSize() - style.WindowPadding * 2.0;
ImVec2 cursor_pos = ImGui::GetCursorPos();
if(ext_box_width < 0.0)
ext_box_width = ImGui::CalcTextSize(".abc").x + 100;
float label_width = ImGui::CalcTextSize(label.c_str()).x + style.ItemSpacing.x;
float frame_height_spacing = ImGui::GetFrameHeightWithSpacing();
float input_bar_width = pw_content_sz.x - label_width;
if(dialog_mode != DialogMode::SELECT)
input_bar_width -= (ext_box_width + style.ItemSpacing.x);
bool show_error = false;
ImGui::SetCursorPosY(pw_content_sz.y - frame_height_spacing * 2.0f);
//Render Input Text Bar label
ImGui::Text("%s", label.c_str());
ImGui::SameLine();
//Render Input Text Bar
input_combobox_pos = ImVec2(pw_pos + ImGui::GetCursorPos());
input_combobox_sz = ImVec2(input_bar_width, 0);
ImGui::PushItemWidth(input_bar_width);
if(ImGui::InputTextWithHint("##FileNameInput", "Type a name...", &input_fn[0], 256, ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll))
{
if(strlen(input_fn) > 0)
{
selected_fn = std::string(input_fn);
validate_file = true;
}
}
ImGui::PopItemWidth();
//If input bar was focused clear selection
if(ImGui::IsItemEdited())
selected_idx = -1;
// If Input Bar is edited show a list of files or dirs matching the input text.
if(ImGui::IsItemEdited() || ImGui::IsItemActivated())
{
//If dialog_mode is OPEN/SAVE then filter from list of files..
if(dialog_mode == DialogMode::OPEN || dialog_mode == DialogMode::SAVE)
{
inputcb_filter_files.clear();
for(int i = 0; i < subfiles.size(); i++)
{
if(ImStristr(subfiles[i].name.c_str(), nullptr, input_fn, nullptr) != nullptr)
inputcb_filter_files.push_back(std::ref(subfiles[i].name));
}
}
//If dialog_mode == SELECT then filter from list of directories
else if(dialog_mode == DialogMode::SELECT)
{
inputcb_filter_files.clear();
for(int i = 0; i < subdirs.size(); i++)
{
if(ImStristr(subdirs[i].name.c_str(), nullptr, input_fn, nullptr) != nullptr)
inputcb_filter_files.push_back(std::ref(subdirs[i].name));
}
}
//If filtered list has any items show dropdown
if(inputcb_filter_files.size() > 0)
show_inputbar_combobox = true;
else
show_inputbar_combobox = false;
}
//Render Extensions and File Types DropDown
if(dialog_mode != DialogMode::SELECT)
{
ImGui::SameLine();
renderExtBox();
}
//Render a Drop Down of files/dirs (depending on mode) that have matching characters as the input text only.
show_error |= renderInputComboBox();
ImGui::SetCursorPos(cursor_pos);
return show_error;
}
bool ImGuiFileBrowser::renderButtonsAndCheckboxRegion()
{
ImVec2 pw_size = ImGui::GetWindowSize();
ImGuiStyle& style = ImGui::GetStyle();
bool show_error = false;
float frame_height = ImGui::GetFrameHeight();
float frame_height_spacing = ImGui::GetFrameHeightWithSpacing();
float opensave_btn_width = getButtonSize("Open").x; // Since both Open/Save are 4 characters long, width gonna be same.
float selcan_btn_width = getButtonSize("Cancel").x; // Since both Cacnel/Select have same number of characters, so same width.
float buttons_xpos;
if (dialog_mode == DialogMode::SELECT)
buttons_xpos = pw_size.x - opensave_btn_width - (2.0f * selcan_btn_width) - ( 2.0f * style.ItemSpacing.x) - style.WindowPadding.x;
else
buttons_xpos = pw_size.x - opensave_btn_width - selcan_btn_width - style.ItemSpacing.x - style.WindowPadding.x;
ImGui::SetCursorPosY(pw_size.y - frame_height_spacing - style.WindowPadding.y);
//Render Checkbox
float label_width = ImGui::CalcTextSize("Show Hidden Files and Folders").x + ImGui::GetCursorPosX() + frame_height;
bool show_marker = (label_width >= buttons_xpos);
ImGui::Checkbox( (show_marker) ? "##showHiddenFiles" : "Show Hidden Files and Folders", &show_hidden);
if(show_marker)
{
ImGui::SameLine();
showHelpMarker("Show Hidden Files and Folders");
}
//Render an Open Button (in OPEN/SELECT dialog_mode) or Open/Save depending on what's selected in SAVE dialog_mode
ImGui::SameLine();
ImGui::SetCursorPosX(buttons_xpos);
if(dialog_mode == DialogMode::SAVE)
{
// If directory selected and Input Text Bar doesn't have focus, render Open Button
if(selected_idx != -1 && is_dir && ImGui::GetFocusID() != ImGui::GetID("##FileNameInput"))
{
if (ImGui::Button("Open"))
show_error |= !(onDirClick(selected_idx));
}
else if (ImGui::Button("Save") && strlen(input_fn) > 0)
{
selected_fn = std::string(input_fn);
validate_file = true;
}
}
else
{
if (ImGui::Button("Open"))
{
//It's possible for both to be true at once (user selected directory but input bar has some text. In this case we chose to open the directory instead of opening the file.
//Also note that we don't need to access the selected file through "selected_idx" since the if a file is selected, input bar will get populated with that name.
if(selected_idx >= 0 && is_dir)
show_error |= !(onDirClick(selected_idx));
else if(strlen(input_fn) > 0)
{
selected_fn = std::string(input_fn);
validate_file = true;
}
}
//Render Select Button if in SELECT Mode
if(dialog_mode == DialogMode::SELECT)
{
//Render Select Button
ImGui::SameLine();
if (ImGui::Button("Select"))
{
if(strlen(input_fn) > 0)
{
selected_fn = std::string(input_fn);
validate_file = true;
}
}
}
}
//Render Cancel Button
ImGui::SameLine();
if (ImGui::Button("Cancel"))
closeDialog();
return show_error;
}
bool ImGuiFileBrowser::renderInputComboBox()
{
bool show_error = false;
ImGuiStyle& style = ImGui::GetStyle();
ImGuiID input_id = ImGui::GetID("##FileNameInput");
ImGuiID focus_scope_id = ImGui::GetID("##InputBarComboBoxListScope");
float frame_height = ImGui::GetFrameHeight();
input_combobox_sz.y = std::min((inputcb_filter_files.size() + 1) * frame_height + style.WindowPadding.y * 2.0f,
8 * ImGui::GetFrameHeight() + style.WindowPadding.y * 2.0f);
if(show_inputbar_combobox && ( ImGui::GetFocusedFocusScope() == focus_scope_id || ImGui::GetCurrentContext()->ActiveIdIsAlive == input_id ))
{
ImGuiWindowFlags popupFlags = ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoFocusOnAppearing |
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoSavedSettings;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.1f, 0.1f, 0.1f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.125f, 0.125f, 0.125f, 1.0f));
ImGui::SetNextWindowBgAlpha(1.0);
ImGui::SetNextWindowPos(input_combobox_pos + ImVec2(0, ImGui::GetFrameHeightWithSpacing()));
ImGui::PushClipRect(ImVec2(0,0), ImGui::GetIO().DisplaySize, false);
ImGui::BeginChild("##InputBarComboBox", input_combobox_sz, true, popupFlags);
ImVec2 listbox_size = input_combobox_sz - ImGui::GetStyle().WindowPadding * 2.0f;
if(ImGui::ListBoxHeader("##InputBarComboBoxList", listbox_size))
{
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f,1.0f));
ImGui::PushFocusScope(focus_scope_id);
for(auto& element : inputcb_filter_files)
{
if(ImGui::Selectable(element.get().c_str(), false, ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_SelectOnClick))
{
if(element.get().size() > 256)
{
error_title = "Error!";
error_msg = "Selected File Name is longer than 256 characters.";
show_error = true;
}
else
{
strcpy(input_fn, element.get().c_str());
show_inputbar_combobox = false;
}
}
}
ImGui::PopFocusScope();
ImGui::PopStyleColor(1);
ImGui::ListBoxFooter();
}
ImGui::EndChild();
ImGui::PopStyleColor(2);
ImGui::PopClipRect();
}
return show_error;
}
void ImGuiFileBrowser::renderExtBox()
{
ImGui::PushItemWidth(ext_box_width);
if(ImGui::BeginCombo("##FileTypes", valid_exts[selected_ext_idx].c_str()))
{
for(int i = 0; i < valid_exts.size(); i++)
{
if(ImGui::Selectable(valid_exts[i].c_str(), selected_ext_idx == i))
{
selected_ext_idx = i;
if(dialog_mode == DialogMode::SAVE)
{
std::string name(input_fn);
size_t idx = name.find_last_of(".");
if(idx == std::string::npos)
idx = strlen(input_fn);
for(int j = 0; j < valid_exts[selected_ext_idx].size(); j++)
input_fn[idx++] = valid_exts[selected_ext_idx][j];
input_fn[idx++] = '\0';
}
filterFiles(FilterMode_Files);
}
}
ImGui::EndCombo();
}
ext = valid_exts[selected_ext_idx];
ImGui::PopItemWidth();
}
bool ImGuiFileBrowser::onNavigationButtonClick(int idx)
{
std::string new_path = "";
//First Button corresponds to virtual folder Computer which lists all logical drives (hard disks and removables) and "/" on Unix
if(idx == 0)
{
#ifdef OSWIN
if(!loadWindowsDrives())
return false;
current_path.clear();
current_dirlist.clear();
current_dirlist.push_back("Computer");
return true;
#else
new_path = "/";
#endif // OSWIN
}
else
{
#ifdef OSWIN
//Clicked on a drive letter?
if(idx == 1)
new_path = current_path.substr(0, 3);
else
{
//Start from i=1 since at 0 lies "MyComputer" which is only virtual and shouldn't be read by readDIR
for (int i = 1; i <= idx; i++)
new_path += current_dirlist[i] + "/";
}
#else
//Since UNIX absolute paths start at "/", we handle this separately to avoid adding a double slash at the beginning
new_path += current_dirlist[0];
for (int i = 1; i <= idx; i++)
new_path += current_dirlist[i] + "/";
#endif
}
if(readDIR(new_path))
{
current_dirlist.erase(current_dirlist.begin()+idx+1, current_dirlist.end());
current_path = new_path;
return true;
}
else
return false;
}
bool ImGuiFileBrowser::onDirClick(int idx)
{
std::string name;
std::string new_path(current_path);
bool drives_shown = false;
#ifdef OSWIN
drives_shown = (current_dirlist.size() == 1 && current_dirlist.back() == "Computer");
#endif // OSWIN
name = filtered_dirs[idx]->name;
if(name == "..")
{
new_path.pop_back(); // Remove trailing '/'
new_path = new_path.substr(0, new_path.find_last_of('/') + 1); // Also include a trailing '/'
}
else
{
//Remember we displayed drives on Windows as *Local/Removable Disk: X* hence we need last char only
if(drives_shown)
name = std::string(1, name.back()) + ":";
new_path += name + "/";
}
if(readDIR(new_path))
{
if(name == "..")
current_dirlist.pop_back();
else
current_dirlist.push_back(name);
current_path = new_path;
return true;
}
else
return false;
}
bool ImGuiFileBrowser::readDIR(std::string pathdir)
{
DIR* dir;
struct dirent *ent;
/* If the current directory doesn't exist, and we are opening the dialog for the first time, reset to defaults to avoid looping of showing error modal.
* An example case is when user closes the dialog in a folder. Then deletes the folder outside. On reopening the dialog the current path (previous) would be invalid.
*/
dir = opendir(pathdir.c_str());
if(dir == nullptr && is_appearing)
{
current_dirlist.clear();
#ifdef OSWIN
current_path = pathdir = "./";
#else
initCurrentPath();
pathdir = current_path;
#endif // OSWIN
dir = opendir(pathdir.c_str());
}
if (dir != nullptr)
{
#ifdef OSWIN
// If we are on Windows and current path is relative then get absolute path from dirent structure
if(current_dirlist.empty() && pathdir == "./")
{
const wchar_t* absolute_path = dir->wdirp->patt;
std::string current_directory = wStringToString(absolute_path);
std::replace(current_directory.begin(), current_directory.end(), '\\', '/');
//Remove trailing "*" returned by ** dir->wdirp->patt **
current_directory.pop_back();
current_path = current_directory;
//Create a vector of each directory in the file path for the filepath bar. Not Necessary for linux as starting directory is "/"
parsePathTabs(current_path);
}
#endif // OSWIN
// store all the files and directories within directory and clear previous entries
clearFileList();
while ((ent = readdir (dir)) != nullptr)
{
bool is_hidden = false;
std::string name(ent->d_name);
//Ignore current directory
if(name == ".")
continue;
//Somehow there is a '..' present in root directory in linux.
#ifndef OSWIN
if(name == ".." && pathdir == "/")
continue;
#endif // OSWIN
if(name != "..")
{
#ifdef OSWIN
std::string dir = pathdir + std::string(ent->d_name);
// IF system file skip it...
if (FILE_ATTRIBUTE_SYSTEM & GetFileAttributesA(dir.c_str()))
continue;
if (FILE_ATTRIBUTE_HIDDEN & GetFileAttributesA(dir.c_str()))
is_hidden = true;
#else
if(name[0] == '.')
is_hidden = true;
#endif // OSWIN
}
//Store directories and files in separate vectors
if(ent->d_type == DT_DIR)
subdirs.push_back(Info(name, is_hidden));
else if(ent->d_type == DT_REG && dialog_mode != DialogMode::SELECT)
subfiles.push_back(Info(name, is_hidden));
}
closedir (dir);
std::sort(subdirs.begin(), subdirs.end(), alphaSortComparator);
std::sort(subfiles.begin(), subfiles.end(), alphaSortComparator);
//Initialize Filtered dirs and files
filterFiles(filter_mode);
}
else
{
error_title = "Error!";
error_msg = "Error opening directory! Make sure the directory exists and you have the proper rights to access the directory.";
return false;
}
return true;
}
void ImGuiFileBrowser::filterFiles(int filter_mode)
{
filter_dirty = false;
if(filter_mode | FilterMode_Dirs)
{
filtered_dirs.clear();
for (size_t i = 0; i < subdirs.size(); ++i)
{
if(filter.PassFilter(subdirs[i].name.c_str()))
filtered_dirs.push_back(&subdirs[i]);
}
}
if(filter_mode | FilterMode_Files)
{
filtered_files.clear();
for (size_t i = 0; i < subfiles.size(); ++i)
{
if(valid_exts[selected_ext_idx] == "*.*")
{
if(filter.PassFilter(subfiles[i].name.c_str()))
filtered_files.push_back(&subfiles[i]);
}
else
{
if(filter.PassFilter(subfiles[i].name.c_str()) && (ImStristr(subfiles[i].name.c_str(), nullptr, valid_exts[selected_ext_idx].c_str(), nullptr)) != nullptr)
filtered_files.push_back(&subfiles[i]);
}
}
}
}
void ImGuiFileBrowser::showHelpMarker(std::string desc)
{
ImGui::TextDisabled("(?)");
if (ImGui::IsItemHovered())
{
ImGui::BeginTooltip();
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
ImGui::TextUnformatted(desc.c_str());
ImGui::PopTextWrapPos();
ImGui::EndTooltip();
}
}
void ImGuiFileBrowser::showErrorModal()
{
ImVec2 window_size(260, 0);
ImGui::SetNextWindowSize(window_size);
if (ImGui::BeginPopupModal(error_title.c_str(), nullptr, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoResize))
{
ImGui::TextWrapped("%s", error_msg.c_str());
ImGui::Separator();
ImGui::SetCursorPosX(window_size.x/2.0f - getButtonSize("OK").x/2.0f);
if (ImGui::Button("OK", getButtonSize("OK")))
ImGui::CloseCurrentPopup();
ImGui::EndPopup();
}
}
bool ImGuiFileBrowser::showReplaceFileModal()
{
ImVec2 window_size(250, 0);
ImGui::SetNextWindowSize(window_size);
bool ret_val = false;
if (ImGui::BeginPopupModal(repfile_modal_id.c_str(), nullptr, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoResize))
{
float frame_height = ImGui::GetFrameHeightWithSpacing();
std::string text = "A file with the following filename already exists. Are you sure you want to replace the existing file?";
ImGui::TextWrapped("%s", text.c_str());
ImGui::Separator();
float buttons_width = getButtonSize("Yes").x + getButtonSize("No").x + ImGui::GetStyle().ItemSpacing.x;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetWindowWidth()/2.0f - buttons_width/2.0f - ImGui::GetStyle().WindowPadding.x);
if (ImGui::Button("Yes", getButtonSize("Yes")))
{
selected_path = current_path + selected_fn;
ImGui::CloseCurrentPopup();
ret_val = true;
}
ImGui::SameLine();
if (ImGui::Button("No", getButtonSize("No")))
{
selected_fn.clear();
selected_path.clear();
ImGui::CloseCurrentPopup();
ret_val = false;
}
ImGui::EndPopup();
}
return ret_val;
}
void ImGuiFileBrowser::showInvalidFileModal()
{
ImGuiStyle& style = ImGui::GetStyle();
std::string text = "Selected file either doesn't exist or is not supported. Please select a file with the following extensions...";
ImVec2 text_size = ImGui::CalcTextSize(text.c_str(), nullptr, true, 350 - style.WindowPadding.x * 2.0f);
ImVec2 button_size = getButtonSize("OK");
float frame_height = ImGui::GetFrameHeightWithSpacing();
float cw_content_height = valid_exts.size() * frame_height;
float cw_height = std::min(4.0f * frame_height, cw_content_height);
ImVec2 window_size(350, 0);
ImGui::SetNextWindowSize(window_size);
if (ImGui::BeginPopupModal(invfile_modal_id.c_str(), nullptr, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoResize))
{
ImGui::TextWrapped("%s", text.c_str());
ImGui::BeginChild("##SupportedExts", ImVec2(0, cw_height), true);
for(int i = 0; i < valid_exts.size(); i++)
ImGui::BulletText("%s", valid_exts[i].c_str());
ImGui::EndChild();
ImGui::SetCursorPosX(window_size.x/2.0f - button_size.x/2.0f);
if (ImGui::Button("OK", button_size))
ImGui::CloseCurrentPopup();
ImGui::EndPopup();
}
}
void ImGuiFileBrowser::setValidExtTypes(const std::string& valid_types_string)
{
/* Initialize a list of files extensions that are valid.
* If the user chooses a file that doesn't match the extensions in the
* list, we will show an error modal...
*/
std::string max_str = "";
valid_exts.clear();
std::string extension = "";
std::istringstream iss(valid_types_string);
while(std::getline(iss, extension, ','))
{
if(!extension.empty())
{
if(max_str.size() < extension.size())
max_str = extension;
valid_exts.push_back(extension);
}
}
float min_width = ImGui::CalcTextSize(".abc").x + 100;
ext_box_width = std::max(min_width, ImGui::CalcTextSize(max_str.c_str()).x);
}
bool ImGuiFileBrowser::validateFile()
{
bool match = false;
//If there is an item selected, check if the selected file name (the input filename, in other words) matches the selection.
if(selected_idx >= 0)
{
if(dialog_mode == DialogMode::SELECT)
match = (filtered_dirs[selected_idx]->name == selected_fn);
else
match = (filtered_files[selected_idx]->name == selected_fn);
}
//If the input filename doesn't match we need to explicitly find the input filename..
if(!match)
{
if(dialog_mode == DialogMode::SELECT)
{
for(int i = 0; i < subdirs.size(); i++)
{
if(subdirs[i].name == selected_fn)
{
match = true;
break;
}
}
}
else
{
for(int i = 0; i < subfiles.size(); i++)
{
if(subfiles[i].name == selected_fn)
{
match = true;
break;
}
}
}
}
// If file doesn't match, return true on SAVE mode (since file doesn't exist, hence can be saved directly) and return false on other modes (since file doesn't exist so cant open/select)
if(!match)
return (dialog_mode == DialogMode::SAVE);
// If file matches, return false on SAVE, we need to show a replace file modal
if(dialog_mode == DialogMode::SAVE)
return false;
// Return true on SELECT, no need to validate extensions
else if(dialog_mode == DialogMode::SELECT)
return true;
else
{
// If list of extensions has all types, no need to validate.
for(auto ext : valid_exts)
{
if(ext == "*.*")
return true;
}
size_t idx = selected_fn.find_last_of('.');
std::string file_ext = idx == std::string::npos ? "" : selected_fn.substr(idx, selected_fn.length() - idx);
return (std::find(valid_exts.begin(), valid_exts.end(), file_ext) != valid_exts.end());
}
}
ImVec2 ImGuiFileBrowser::getButtonSize(std::string button_text)
{
return (ImGui::CalcTextSize(button_text.c_str()) + ImGui::GetStyle().FramePadding * 2.0);
}
void ImGuiFileBrowser::parsePathTabs(std::string path)
{
std::string path_element = "";
std::string root = "";
current_dirlist.clear();
#ifdef OSWIN
current_dirlist.push_back("Computer");
#else
if(path[0] == '/')
current_dirlist.push_back("/");
#endif //OSWIN
std::istringstream iss(path);
while(std::getline(iss, path_element, '/'))
{
if(!path_element.empty())
current_dirlist.push_back(path_element);
}
}
std::string ImGuiFileBrowser::wStringToString(const wchar_t* wchar_arr)
{
std::mbstate_t state = std::mbstate_t();
//MinGW bug (patched in mingw-w64), wcsrtombs doesn't ignore length parameter when dest = nullptr. Hence the large number.
size_t len = 1 + std::wcsrtombs(nullptr, &(wchar_arr), 600000, &state);
char* char_arr = new char[len];
std::wcsrtombs(char_arr, &wchar_arr, len, &state);
std::string ret_val(char_arr);
delete[] char_arr;
return ret_val;
}
bool ImGuiFileBrowser::alphaSortComparator(const Info& a, const Info& b)
{
const char* str1 = a.name.c_str();
const char* str2 = b.name.c_str();
int ca, cb;
do
{
ca = (unsigned char) *str1++;
cb = (unsigned char) *str2++;
ca = std::tolower(std::toupper(ca));
cb = std::tolower(std::toupper(cb));
}
while (ca == cb && ca != '\0');
if(ca < cb)
return true;
else
return false;
}
//Windows Exclusive function
#ifdef OSWIN
bool ImGuiFileBrowser::loadWindowsDrives()
{
DWORD len = GetLogicalDriveStringsA(0,nullptr);
char* drives = new char[len];
if(!GetLogicalDriveStringsA(len,drives))
{
delete[] drives;
return false;
}
clearFileList();
char* temp = drives;
for(char *drv = nullptr; *temp != '\0'; temp++)
{
drv = temp;
if(DRIVE_REMOVABLE == GetDriveTypeA(drv))
subdirs.push_back({"Removable Disk: " + std::string(1,drv[0]), false});
else if(DRIVE_FIXED == GetDriveTypeA(drv))
subdirs.push_back({"Local Disk: " + std::string(1,drv[0]), false});
//Go to nullptr character
while(*(++temp));
}
delete[] drives;
return true;
}
#endif
//Unix only
#ifndef OSWIN
void ImGuiFileBrowser::initCurrentPath()
{
bool path_max_def = false;
#ifdef PATH_MAX
path_max_def = true;
#endif // PATH_MAX
char* buffer = nullptr;
//If PATH_MAX is defined deal with memory using new/delete. Else fallback to malloc'ed memory from `realpath()`
if(path_max_def)
buffer = new char[PATH_MAX];
char* real_path = realpath("./", buffer);
if (real_path == nullptr)
{
current_path = "/";
current_dirlist.push_back("/");
}
else
{
current_path = std::string(real_path);
current_path += "/";
parsePathTabs(current_path);
}
if(path_max_def)
delete[] buffer;
else
free(real_path);
}
#endif // OSWIN
}