ImGuiFileBrowser.cpp (47137B)
1#include "ImGuiFileBrowser.h" 2#ifndef IMGUI_DEFINE_MATH_OPERATORS 3#define IMGUI_DEFINE_MATH_OPERATORS 4#endif 5#include "../imgui/imgui_internal.h" 6 7#include <iostream> 8#include <functional> 9#include <climits> 10#include <string.h> 11#include <sstream> 12#include <cwchar> 13#include <cctype> 14#include <algorithm> 15#include <cmath> 16#if defined (WIN32) || defined (_WIN32) || defined (__WIN32) 17#define OSWIN 18#ifndef NOMINMAX 19 #define NOMINMAX 20#endif 21#include "Dirent/dirent.h" 22#include <windows.h> 23#else 24#include <dirent.h> 25#endif // defined (WIN32) || defined (_WIN32) 26 27namespace imgui_addons 28{ 29 ImGuiFileBrowser::ImGuiFileBrowser() 30 { 31 filter_mode = FilterMode_Files | FilterMode_Dirs; 32 33 show_inputbar_combobox = false; 34 validate_file = false; 35 show_hidden = false; 36 is_dir = false; 37 filter_dirty = true; 38 is_appearing = true; 39 40 col_items_limit = 12; 41 selected_idx = -1; 42 selected_ext_idx = 0; 43 ext_box_width = -1.0f; 44 col_width = 280.0f; 45 min_size = ImVec2(500,300); 46 47 invfile_modal_id = "Invalid File!"; 48 repfile_modal_id = "Replace File?"; 49 selected_fn = ""; 50 selected_path = ""; 51 52 #ifdef OSWIN 53 current_path = "./"; 54 #else 55 initCurrentPath(); 56 #endif 57 } 58 59 ImGuiFileBrowser::~ImGuiFileBrowser() 60 { 61 62 } 63 64 void ImGuiFileBrowser::clearFileList() 65 { 66 //Clear pointer references to subdirs and subfiles 67 filtered_dirs.clear(); 68 filtered_files.clear(); 69 inputcb_filter_files.clear(); 70 71 //Now clear subdirs and subfiles 72 subdirs.clear(); 73 subfiles.clear(); 74 filter_dirty = true; 75 selected_idx = -1; 76 } 77 78 void ImGuiFileBrowser::closeDialog() 79 { 80 if (is_open_) 81 *is_open_ = false; 82 valid_types = ""; 83 valid_exts.clear(); 84 selected_ext_idx = 0; 85 selected_idx = -1; 86 87 input_fn[0] = '\0'; //Hide any text in Input bar for the next time save dialog is opened. 88 filter.Clear(); //Clear Filter for the next time open dialog is called. 89 90 show_inputbar_combobox = false; 91 validate_file = false; 92 show_hidden = false; 93 is_dir = false; 94 filter_dirty = true; 95 is_appearing = true; 96 97 //Clear pointer references to subdirs and subfiles 98 filtered_dirs.clear(); 99 filtered_files.clear(); 100 inputcb_filter_files.clear(); 101 102 //Now clear subdirs and subfiles 103 subdirs.clear(); 104 subfiles.clear(); 105 106 ImGui::CloseCurrentPopup(); 107 } 108 109 bool ImGuiFileBrowser::showFileDialog(const std::string& label, const DialogMode mode, const ImVec2& sz_xy, const std::string& valid_types, bool* is_open) 110 { 111 is_open_ = is_open; 112 dialog_mode = mode; 113 ImGuiIO& io = ImGui::GetIO(); 114 max_size.x = io.DisplaySize.x; 115 max_size.y = io.DisplaySize.y; 116 ImGui::SetNextWindowSizeConstraints(min_size, max_size); 117 ImGui::SetNextWindowPos(io.DisplaySize * 0.5f, ImGuiCond_Appearing, ImVec2(0.5f,0.5f)); 118 ImGui::SetNextWindowSize(ImVec2(std::max(sz_xy.x, min_size.x), std::max(sz_xy.y, min_size.y)), ImGuiCond_Appearing); 119 120 //Set Proper Filter Mode. 121 if(mode == DialogMode::SELECT) 122 filter_mode = FilterMode_Dirs; 123 else 124 filter_mode = FilterMode_Files | FilterMode_Dirs; 125 126 if (ImGui::BeginPopupModal(label.c_str(), nullptr, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) 127 { 128 bool show_error = false; 129 if (is_open_) 130 *is_open_ = true; 131 132 // If this is the initial run, read current directory and load data once. 133 if(is_appearing) 134 { 135 selected_fn.clear(); 136 selected_path.clear(); 137 if(mode != DialogMode::SELECT) 138 { 139 this->valid_types = valid_types; 140 setValidExtTypes(valid_types); 141 } 142 143 /* If current path is empty (can happen on Windows if user closes dialog while inside MyComputer. 144 * Since this is a virtual folder, path would be empty) load the drives on Windows else initialize the current path on Unix. 145 */ 146 if(current_path.empty()) 147 { 148 #ifdef OSWIN 149 show_error |= !(loadWindowsDrives()); 150 #else 151 initCurrentPath(); 152 show_error |= !(readDIR(current_path)); 153 #endif // OSWIN 154 } 155 else 156 show_error |= !(readDIR(current_path)); 157 is_appearing = false; 158 } 159 160 show_error |= renderNavAndSearchBarRegion(); 161 show_error |= renderFileListRegion(); 162 show_error |= renderButtonsAndCheckboxRegion(); 163 164 if (*is_open) 165 show_error |= renderInputTextAndExtRegion(); 166 167 if(validate_file) 168 { 169 validate_file = false; 170 bool check = validateFile(); 171 172 if(!check && dialog_mode == DialogMode::OPEN) 173 { 174 ImGui::OpenPopup(invfile_modal_id.c_str()); 175 selected_fn.clear(); 176 selected_path.clear(); 177 } 178 179 else if(!check && dialog_mode == DialogMode::SAVE) 180 ImGui::OpenPopup(repfile_modal_id.c_str()); 181 182 else if(!check && dialog_mode == DialogMode::SELECT) 183 { 184 selected_fn.clear(); 185 selected_path.clear(); 186 show_error = true; 187 error_title = "Invalid Directory!"; 188 error_msg = "Invalid Directory Selected. Please make sure the directory exists."; 189 } 190 191 //If selected file passes through validation check, set path to the file and close file dialog 192 if(check) 193 { 194 selected_path = current_path + selected_fn; 195 196 //Add a trailing "/" to emphasize its a directory not a file. If you want just the dir name it's accessible through "selected_fn" 197 if(dialog_mode == DialogMode::SELECT) 198 selected_path += "/"; 199 closeDialog(); 200 } 201 } 202 203 // We don't need to check as the modals will only be shown if OpenPopup is called 204 showInvalidFileModal(); 205 if(showReplaceFileModal()) 206 closeDialog(); 207 208 //Show Error Modal if there was an error opening any directory 209 if(show_error) 210 ImGui::OpenPopup(error_title.c_str()); 211 showErrorModal(); 212 213 ImGui::EndPopup(); 214 return (!selected_fn.empty() && !selected_path.empty()); 215 } 216 else 217 return false; 218 } 219 220 bool ImGuiFileBrowser::renderNavAndSearchBarRegion() 221 { 222 ImGuiStyle& style = ImGui::GetStyle(); 223 bool show_error = false; 224 float frame_height = ImGui::GetFrameHeight(); 225 float list_item_height = GImGui->FontSize + style.ItemSpacing.y; 226 227 ImVec2 pw_content_size = ImGui::GetWindowSize() - style.WindowPadding * 2.0; 228 ImVec2 sw_size = ImVec2(ImGui::CalcTextSize("Random").x + 140, style.WindowPadding.y * 2.0f + frame_height); 229 ImVec2 sw_content_size = sw_size - style.WindowPadding * 2.0; 230 ImVec2 nw_size = ImVec2(pw_content_size.x - style.ItemSpacing.x - sw_size.x, sw_size.y); 231 232 233 ImGui::BeginChild("##NavigationWindow", nw_size, true, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar); 234 235 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.882f, 0.745f, 0.078f,1.0f)); 236 for(std::size_t i = 0; i < current_dirlist.size(); i++) 237 { 238 if( ImGui::Button(current_dirlist[i].c_str()) ) 239 { 240 //If last button clicked, nothing happens 241 if(i != current_dirlist.size() - 1) 242 show_error |= !(onNavigationButtonClick((int)i)); 243 } 244 245 //Draw Arrow Buttons 246 if(i != current_dirlist.size() - 1) 247 { 248 ImGui::SameLine(0,0); 249 float next_label_width = ImGui::CalcTextSize(current_dirlist[i+1].c_str()).x; 250 251 if(i+1 < current_dirlist.size() - 1) 252 next_label_width += frame_height + ImGui::CalcTextSize(">>").x; 253 254 if(ImGui::GetCursorPosX() + next_label_width >= (nw_size.x - style.WindowPadding.x * 3.0)) 255 { 256 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 1.0f, 1.0f, 0.01f)); 257 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f,1.0f)); 258 259 //Render a drop down of navigation items on button press 260 if(ImGui::Button(">>")) 261 ImGui::OpenPopup("##NavBarDropboxPopup"); 262 if(ImGui::BeginPopup("##NavBarDropboxPopup")) 263 { 264 ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.125f, 0.125f, 0.125f, 1.0f)); 265 if(ImGui::ListBoxHeader("##NavBarDropBox", ImVec2(0, list_item_height* 5))) 266 { 267 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.882f, 0.745f, 0.078f,1.0f)); 268 for(std::size_t j = i+1; j < current_dirlist.size(); j++) 269 { 270 if(ImGui::Selectable(current_dirlist[j].c_str(), false) && j != current_dirlist.size() - 1) 271 { 272 show_error |= !(onNavigationButtonClick((int)j)); 273 ImGui::CloseCurrentPopup(); 274 } 275 } 276 ImGui::PopStyleColor(); 277 ImGui::ListBoxFooter(); 278 } 279 ImGui::PopStyleColor(); 280 ImGui::EndPopup(); 281 } 282 ImGui::PopStyleColor(2); 283 break; 284 } 285 else 286 { 287 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 1.0f, 1.0f, 0.01f)); 288 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f,1.0f)); 289 ImGui::ArrowButtonEx("##Right", ImGuiDir_Right, ImVec2(frame_height, frame_height), ImGuiButtonFlags_Disabled); 290 ImGui::SameLine(0,0); 291 ImGui::PopStyleColor(2); 292 } 293 } 294 } 295 ImGui::PopStyleColor(); 296 ImGui::EndChild(); 297 298 ImGui::SameLine(); 299 ImGui::BeginChild("##SearchWindow", sw_size, true, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar); 300 301 //Render Search/Filter bar 302 float marker_width = ImGui::CalcTextSize("(?)").x + style.ItemSpacing.x; 303 if(filter.Draw("##SearchBar", sw_content_size.x - marker_width) || filter_dirty ) 304 filterFiles(filter_mode); 305 306 //If filter bar was focused clear selection 307 if(ImGui::GetFocusID() == ImGui::GetID("##SearchBar")) 308 selected_idx = -1; 309 310 ImGui::SameLine(); 311 showHelpMarker("Filter (inc, -exc)"); 312 313 ImGui::EndChild(); 314 return show_error; 315 } 316 317 bool ImGuiFileBrowser::renderFileListRegion() 318 { 319 ImGuiStyle& style = ImGui::GetStyle(); 320 ImVec2 pw_size = ImGui::GetWindowSize(); 321 bool show_error = false; 322 float list_item_height = ImGui::CalcTextSize("").y + style.ItemSpacing.y; 323 float input_bar_ypos = pw_size.y - ImGui::GetFrameHeightWithSpacing() * 2.5f - style.WindowPadding.y; 324 float window_height = input_bar_ypos - ImGui::GetCursorPosY() - style.ItemSpacing.y; 325 float window_content_height = window_height - style.WindowPadding.y * 2.0f; 326 float min_content_size = pw_size.x - style.WindowPadding.x * 4.0f; 327 328 if(window_content_height <= 0.0f) 329 return show_error; 330 331 //Reinitialize the limit on number of selectables in one column based on height 332 col_items_limit = (int)std::max(1.0f, window_content_height/list_item_height); 333 int num_cols = (int)std::max(1.0f, std::ceil(static_cast<float>(filtered_dirs.size() + filtered_files.size()) / col_items_limit)); 334 335 //Limitation by ImGUI in 1.75. If columns are greater than 64 readjust the limit on items per column and recalculate number of columns 336 if(num_cols > 64) 337 { 338 int exceed_items_amount = (num_cols - 64) * col_items_limit; 339 col_items_limit += (int)std::ceil(exceed_items_amount/64.0); 340 num_cols = (int)std::max(1.0f, std::ceil(static_cast<float>(filtered_dirs.size() + filtered_files.size()) / col_items_limit)); 341 } 342 343 float content_width = num_cols * col_width; 344 if(content_width < min_content_size) 345 content_width = 0; 346 347 ImGui::SetNextWindowContentSize(ImVec2(content_width, 0)); 348 ImGui::BeginChild("##ScrollingRegion", ImVec2(0, window_height), true, ImGuiWindowFlags_HorizontalScrollbar); 349 ImGui::Columns(num_cols); 350 351 //Output directories in yellow 352 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.882f, 0.745f, 0.078f,1.0f)); 353 int items = 0; 354 for (std::size_t i = 0; i < filtered_dirs.size(); i++) 355 { 356 if(!filtered_dirs[i].is_hidden || show_hidden) 357 { 358 items++; 359 if(ImGui::Selectable(filtered_dirs[i].name.c_str(), selected_idx == (int)i && is_dir, ImGuiSelectableFlags_AllowDoubleClick)) 360 { 361 selected_idx = (int)i; 362 is_dir = true; 363 364 // If dialog mode is SELECT then copy the selected dir name to the input text bar 365 if(dialog_mode == DialogMode::SELECT) 366 strcpy(input_fn, filtered_dirs[i].name.c_str()); 367 368 if(ImGui::IsMouseDoubleClicked(0)) 369 { 370 show_error |= !(onDirClick((int)i)); 371 break; 372 } 373 } 374 if( (items) % col_items_limit == 0) 375 ImGui::NextColumn(); 376 } 377 } 378 ImGui::PopStyleColor(1); 379 380 //Output files 381 for (std::size_t i = 0; i < filtered_files.size(); i++) 382 { 383 if(!filtered_files[i].is_hidden || show_hidden) 384 { 385 items++; 386 if(ImGui::Selectable(filtered_files[i].name.c_str(), selected_idx == (int)i && !is_dir, ImGuiSelectableFlags_AllowDoubleClick)) 387 { 388 // unused: int len = filtered_files[i]->name.length(); 389 selected_idx = (int)i; 390 is_dir = false; 391 392 // If dialog mode is OPEN/SAVE then copy the selected file name to the input text bar 393 strcpy(input_fn, filtered_files[i].name.c_str()); 394 395 if(ImGui::IsMouseDoubleClicked(0)) 396 { 397 selected_fn = filtered_files[i].name; 398 validate_file = true; 399 } 400 } 401 if( (items) % col_items_limit == 0) 402 ImGui::NextColumn(); 403 } 404 } 405 ImGui::Columns(1); 406 ImGui::EndChild(); 407 408 return show_error; 409 } 410 411 bool ImGuiFileBrowser::renderInputTextAndExtRegion() 412 { 413 std::string label = (dialog_mode == DialogMode::SAVE) ? "Save As:" : "Open:"; 414 ImGuiStyle& style = ImGui::GetStyle(); 415 // unused: ImGuiIO& io = ImGui::GetIO(); 416 417 ImVec2 pw_pos = ImGui::GetWindowPos(); 418 ImVec2 pw_content_sz = ImGui::GetWindowSize() - style.WindowPadding * 2.0; 419 ImVec2 cursor_pos = ImGui::GetCursorPos(); 420 421 if(ext_box_width < 0.0) 422 ext_box_width = ImGui::CalcTextSize(".abc").x + 100; 423 float label_width = ImGui::CalcTextSize(label.c_str()).x + style.ItemSpacing.x; 424 float frame_height_spacing = ImGui::GetFrameHeightWithSpacing(); 425 float input_bar_width = pw_content_sz.x - label_width; 426 if(dialog_mode != DialogMode::SELECT) 427 input_bar_width -= (ext_box_width + style.ItemSpacing.x); 428 429 bool show_error = false; 430 ImGui::SetCursorPosY(pw_content_sz.y - frame_height_spacing * 2.0f); 431 432 //Render Input Text Bar label 433 ImGui::Text("%s", label.c_str()); 434 ImGui::SameLine(); 435 436 //Render Input Text Bar 437 input_combobox_pos = ImVec2(pw_pos + ImGui::GetCursorPos()); 438 input_combobox_sz = ImVec2(input_bar_width, 0); 439 ImGui::PushItemWidth(input_bar_width); 440 if(ImGui::InputTextWithHint("##FileNameInput", "Type a name...", &input_fn[0], 256, ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) 441 { 442 if(strlen(input_fn) > 0) 443 { 444 selected_fn = std::string(input_fn); 445 validate_file = true; 446 } 447 } 448 ImGui::PopItemWidth(); 449 450 //If input bar was focused clear selection 451 if(ImGui::IsItemEdited()) 452 selected_idx = -1; 453 454 // If Input Bar is edited show a list of files or dirs matching the input text. 455 if(ImGui::IsItemEdited() || ImGui::IsItemActivated()) 456 { 457 //If dialog_mode is OPEN/SAVE then filter from list of files.. 458 if(dialog_mode == DialogMode::OPEN || dialog_mode == DialogMode::SAVE) 459 { 460 inputcb_filter_files.clear(); 461 for(std::size_t i = 0; i < subfiles.size(); i++) 462 { 463 if(ImStristr(subfiles[i].name.c_str(), nullptr, input_fn, nullptr) != nullptr) 464 inputcb_filter_files.push_back(std::ref(subfiles[i].name)); 465 } 466 } 467 468 //If dialog_mode == SELECT then filter from list of directories 469 else if(dialog_mode == DialogMode::SELECT) 470 { 471 inputcb_filter_files.clear(); 472 for(std::size_t i = 0; i < subdirs.size(); i++) 473 { 474 if(ImStristr(subdirs[i].name.c_str(), nullptr, input_fn, nullptr) != nullptr) 475 inputcb_filter_files.push_back(std::ref(subdirs[i].name)); 476 } 477 } 478 479 //If filtered list has any items show dropdown 480 if(inputcb_filter_files.size() > 0) 481 show_inputbar_combobox = true; 482 else 483 show_inputbar_combobox = false; 484 } 485 486 //Render Extensions and File Types DropDown 487 if(dialog_mode != DialogMode::SELECT) 488 { 489 ImGui::SameLine(); 490 renderExtBox(); 491 } 492 493 //Render a Drop Down of files/dirs (depending on mode) that have matching characters as the input text only. 494 show_error |= renderInputComboBox(); 495 496 ImGui::SetCursorPos(cursor_pos); 497 return show_error; 498 } 499 500 bool ImGuiFileBrowser::renderButtonsAndCheckboxRegion() 501 { 502 ImVec2 pw_size = ImGui::GetWindowSize(); 503 ImGuiStyle& style = ImGui::GetStyle(); 504 bool show_error = false; 505 float frame_height = ImGui::GetFrameHeight(); 506 float frame_height_spacing = ImGui::GetFrameHeightWithSpacing(); 507 float opensave_btn_width = getButtonSize("Open").x; // Since both Open/Save are 4 characters long, width gonna be same. 508 float selcan_btn_width = getButtonSize("Cancel").x; // Since both Cacnel/Select have same number of characters, so same width. 509 float buttons_xpos; 510 511 if (dialog_mode == DialogMode::SELECT) 512 buttons_xpos = pw_size.x - opensave_btn_width - (2.0f * selcan_btn_width) - ( 2.0f * style.ItemSpacing.x) - style.WindowPadding.x; 513 else 514 buttons_xpos = pw_size.x - opensave_btn_width - selcan_btn_width - style.ItemSpacing.x - style.WindowPadding.x; 515 516 ImGui::SetCursorPosY(pw_size.y - frame_height_spacing - style.WindowPadding.y); 517 518 //Render Checkbox 519 float label_width = ImGui::CalcTextSize("Show Hidden Files and Folders").x + ImGui::GetCursorPosX() + frame_height; 520 bool show_marker = (label_width >= buttons_xpos); 521 ImGui::Checkbox( (show_marker) ? "##showHiddenFiles" : "Show Hidden Files and Folders", &show_hidden); 522 if(show_marker) 523 { 524 ImGui::SameLine(); 525 showHelpMarker("Show Hidden Files and Folders"); 526 } 527 528 //Render an Open Button (in OPEN/SELECT dialog_mode) or Open/Save depending on what's selected in SAVE dialog_mode 529 ImGui::SameLine(); 530 ImGui::SetCursorPosX(buttons_xpos); 531 if(dialog_mode == DialogMode::SAVE) 532 { 533 // If directory selected and Input Text Bar doesn't have focus, render Open Button 534 if(selected_idx != -1 && is_dir && ImGui::GetFocusID() != ImGui::GetID("##FileNameInput")) 535 { 536 if (ImGui::Button("Open")) 537 show_error |= !(onDirClick(selected_idx)); 538 } 539 else if (ImGui::Button("Save") && strlen(input_fn) > 0) 540 { 541 selected_fn = std::string(input_fn); 542 validate_file = true; 543 } 544 } 545 else 546 { 547 if (ImGui::Button("Open")) 548 { 549 //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. 550 //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. 551 if(selected_idx >= 0 && is_dir) 552 show_error |= !(onDirClick(selected_idx)); 553 else if(strlen(input_fn) > 0) 554 { 555 selected_fn = std::string(input_fn); 556 validate_file = true; 557 } 558 } 559 560 //Render Select Button if in SELECT Mode 561 if(dialog_mode == DialogMode::SELECT) 562 { 563 //Render Select Button 564 ImGui::SameLine(); 565 if (ImGui::Button("Select")) 566 { 567 if(strlen(input_fn) > 0) 568 { 569 selected_fn = std::string(input_fn); 570 validate_file = true; 571 } 572 } 573 } 574 } 575 576 //Render Cancel Button 577 ImGui::SameLine(); 578 if (ImGui::Button("Cancel")) 579 closeDialog(); 580 581 return show_error; 582 } 583 584 bool ImGuiFileBrowser::renderInputComboBox() 585 { 586 bool show_error = false; 587 ImGuiStyle& style = ImGui::GetStyle(); 588 ImGuiID input_id = ImGui::GetID("##FileNameInput"); 589 ImGuiID focus_scope_id = ImGui::GetID("##InputBarComboBoxListScope"); 590 float frame_height = ImGui::GetFrameHeight(); 591 592 input_combobox_sz.y = std::min((inputcb_filter_files.size() + 1) * frame_height + style.WindowPadding.y * 2.0f, 593 8 * ImGui::GetFrameHeight() + style.WindowPadding.y * 2.0f); 594 595 if(show_inputbar_combobox && ( ImGui::GetFocusScopeID() == focus_scope_id || ImGui::GetCurrentContext()->ActiveIdIsAlive == input_id )) 596 { 597 ImGuiWindowFlags popupFlags = ImGuiWindowFlags_NoTitleBar | 598 ImGuiWindowFlags_NoResize | 599 ImGuiWindowFlags_NoMove | 600 ImGuiWindowFlags_NoFocusOnAppearing | 601 ImGuiWindowFlags_NoScrollbar | 602 ImGuiWindowFlags_NoSavedSettings; 603 604 605 ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.1f, 0.1f, 0.1f, 1.0f)); 606 ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.125f, 0.125f, 0.125f, 1.0f)); 607 ImGui::SetNextWindowBgAlpha(1.0); 608 ImGui::SetNextWindowPos(input_combobox_pos + ImVec2(0, ImGui::GetFrameHeightWithSpacing())); 609 ImGui::PushClipRect(ImVec2(0,0), ImGui::GetIO().DisplaySize, false); 610 611 ImGui::BeginChild("##InputBarComboBox", input_combobox_sz, true, popupFlags); 612 613 ImVec2 listbox_size = input_combobox_sz - ImGui::GetStyle().WindowPadding * 2.0f; 614 if(ImGui::ListBoxHeader("##InputBarComboBoxList", listbox_size)) 615 { 616 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f,1.0f)); 617 ImGui::PushFocusScope(focus_scope_id); 618 for(auto& element : inputcb_filter_files) 619 { 620 if(ImGui::Selectable(element.get().c_str(), false, ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_SelectOnClick)) 621 { 622 if(element.get().size() > 256) 623 { 624 error_title = "Error!"; 625 error_msg = "Selected File Name is longer than 256 characters."; 626 show_error = true; 627 } 628 else 629 { 630 strcpy(input_fn, element.get().c_str()); 631 show_inputbar_combobox = false; 632 } 633 } 634 } 635 ImGui::PopFocusScope(); 636 ImGui::PopStyleColor(1); 637 ImGui::ListBoxFooter(); 638 } 639 ImGui::EndChild(); 640 ImGui::PopStyleColor(2); 641 ImGui::PopClipRect(); 642 } 643 return show_error; 644 } 645 646 void ImGuiFileBrowser::renderExtBox() 647 { 648 ImGui::PushItemWidth(ext_box_width); 649 if(ImGui::BeginCombo("##FileTypes", valid_exts[selected_ext_idx].c_str())) 650 { 651 for(std::size_t i = 0; i < valid_exts.size(); i++) 652 { 653 if(ImGui::Selectable(valid_exts[i].c_str(), selected_ext_idx == (int)i)) 654 { 655 selected_ext_idx = (int)i; 656 if(dialog_mode == DialogMode::SAVE) 657 { 658 std::string name(input_fn); 659 size_t idx = name.find_last_of("."); 660 if(idx == std::string::npos) 661 idx = strlen(input_fn); 662 for(std::size_t j = 0; j < valid_exts[selected_ext_idx].size(); j++) 663 input_fn[idx++] = valid_exts[selected_ext_idx][j]; 664 input_fn[idx++] = '\0'; 665 } 666 else 667 filterFiles(FilterMode_Files); 668 } 669 } 670 ImGui::EndCombo(); 671 } 672 ext = valid_exts[selected_ext_idx]; 673 ImGui::PopItemWidth(); 674 } 675 676 bool ImGuiFileBrowser::onNavigationButtonClick(int idx) 677 { 678 std::string new_path(current_path); 679 680 //First Button corresponds to virtual folder Computer which lists all logical drives (hard disks and removables) and "/" on Unix 681 if(idx == 0) 682 { 683 #ifdef OSWIN 684 if(!loadWindowsDrives()) 685 return false; 686 current_path.clear(); 687 current_dirlist.clear(); 688 current_dirlist.push_back("Computer"); 689 return true; 690 #else 691 new_path = "/"; 692 #endif // OSWIN 693 } 694 else 695 { 696 #ifdef OSWIN 697 //Clicked on a drive letter? 698 if(idx == 1) 699 new_path = current_path.substr(0, 3); 700 else 701 new_path = current_path.substr(0, current_path.find("/" + current_dirlist[idx]) + current_dirlist[idx].length() + 2 ); 702 #else 703 //find returns 0 based indices and substr takes length of chars thus + 1. Another +1 to include trailing "/" 704 new_path = current_path.substr(0, current_path.find("/" + current_dirlist[idx]) + current_dirlist[idx].length() + 2 ); 705 #endif 706 } 707 708 if(readDIR(new_path)) 709 { 710 current_dirlist.erase(current_dirlist.begin()+idx+1, current_dirlist.end()); 711 current_path = new_path; 712 return true; 713 } 714 else 715 return false; 716 } 717 718 bool ImGuiFileBrowser::onDirClick(int idx) 719 { 720 std::string name; 721 std::string new_path(current_path); 722 bool drives_shown = false; 723 724 #ifdef OSWIN 725 drives_shown = (current_dirlist.size() == 1 && current_dirlist.back() == "Computer"); 726 #endif // OSWIN 727 728 name = filtered_dirs[idx].name; 729 730 if(name == "..") 731 { 732 new_path.pop_back(); // Remove trailing '/' 733 new_path = new_path.substr(0, new_path.find_last_of('/') + 1); // Also include a trailing '/' 734 } 735 else 736 { 737 //Remember we displayed drives on Windows as *Local/Removable Disk: X* hence we need last char only 738 if(drives_shown) 739 name = std::string(1, name.back()) + ":"; 740 new_path += name + "/"; 741 } 742 743 if(readDIR(new_path)) 744 { 745 if(name == "..") 746 current_dirlist.pop_back(); 747 else 748 current_dirlist.push_back(name); 749 750 current_path = new_path; 751 return true; 752 } 753 else 754 return false; 755 } 756 757 bool ImGuiFileBrowser::readDIR(std::string pathdir) 758 { 759 DIR* dir; 760 struct dirent *ent; 761 762 /* 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. 763 * 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. 764 */ 765 dir = opendir(pathdir.c_str()); 766 if(dir == nullptr && is_appearing) 767 { 768 current_dirlist.clear(); 769 #ifdef OSWIN 770 current_path = pathdir = "./"; 771 #else 772 initCurrentPath(); 773 pathdir = current_path; 774 #endif // OSWIN 775 776 dir = opendir(pathdir.c_str()); 777 } 778 779 if (dir != nullptr) 780 { 781 #ifdef OSWIN 782 // If we are on Windows and current path is relative then get absolute path from dirent structure 783 if(current_dirlist.empty() && pathdir == "./") 784 { 785 const wchar_t* absolute_path = dir->wdirp->patt; 786 std::string current_directory = wStringToString(absolute_path); 787 std::replace(current_directory.begin(), current_directory.end(), '\\', '/'); 788 789 //Remove trailing "*" returned by ** dir->wdirp->patt ** 790 current_directory.pop_back(); 791 current_path = current_directory; 792 793 //Create a vector of each directory in the file path for the filepath bar. Not Necessary for linux as starting directory is "/" 794 parsePathTabs(current_path); 795 } 796 #endif // OSWIN 797 798 // store all the files and directories within directory and clear previous entries 799 clearFileList(); 800 while ((ent = readdir (dir)) != nullptr) 801 { 802 bool is_hidden = false; 803 std::string name(ent->d_name); 804 805 //Ignore current directory 806 if(name == ".") 807 continue; 808 809 //Somehow there is a '..' present in root directory in linux. 810 #ifndef OSWIN 811 if(name == ".." && pathdir == "/") 812 continue; 813 #endif // OSWIN 814 815 if(name != "..") 816 { 817 #ifdef OSWIN 818 std::string dir = pathdir + std::string(ent->d_name); 819 // IF system file skip it... 820 if (FILE_ATTRIBUTE_SYSTEM & GetFileAttributesA(dir.c_str())) 821 continue; 822 if (FILE_ATTRIBUTE_HIDDEN & GetFileAttributesA(dir.c_str())) 823 is_hidden = true; 824 #else 825 if(name[0] == '.') 826 is_hidden = true; 827 #endif // OSWIN 828 } 829 //Store directories and files in separate vectors 830 if(ent->d_type == DT_DIR) 831 subdirs.push_back(Info(name, is_hidden)); 832 else if(ent->d_type == DT_REG && dialog_mode != DialogMode::SELECT) 833 subfiles.push_back(Info(name, is_hidden)); 834 835 #ifndef OSWIN 836 else if(ent->d_type == DT_LNK) 837 { 838 subdirs.push_back(Info(name, is_hidden)); 839 } 840 #endif // OSWIN 841 } 842 closedir (dir); 843 std::sort(subdirs.begin(), subdirs.end(), alphaSortComparator); 844 std::sort(subfiles.begin(), subfiles.end(), alphaSortComparator); 845 846 //Initialize Filtered dirs and files 847 filterFiles(filter_mode); 848 } 849 else 850 { 851 error_title = "Error!"; 852 error_msg = "Error opening directory! Make sure the directory exists and you have the proper rights to access the directory."; 853 return false; 854 } 855 return true; 856 } 857 858 void ImGuiFileBrowser::filterFiles(int filter_mode) 859 { 860 filter_dirty = false; 861 if((filter_mode & FilterMode_Dirs) != 0) 862 { 863 filtered_dirs.clear(); 864 for (size_t i = 0; i < subdirs.size(); ++i) 865 { 866 if(filter.PassFilter(subdirs[i].name.c_str())) 867 filtered_dirs.push_back(subdirs[i]); 868 } 869 } 870 if((filter_mode & FilterMode_Files) != 0) 871 { 872 filtered_files.clear(); 873 for (size_t i = 0; i < subfiles.size(); ++i) 874 { 875 if(valid_exts[selected_ext_idx] == "*.*") 876 { 877 if(filter.PassFilter(subfiles[i].name.c_str())) 878 filtered_files.push_back(subfiles[i]); 879 } 880 else 881 { 882 //if(filter.PassFilter(subfiles[i].name.c_str()) && (ImStristr(subfiles[i].name.c_str(), nullptr, valid_exts[selected_ext_idx].c_str(), nullptr)) != nullptr) 883 884 int last = (int)subfiles[i].name.find_last_of("."); 885 886 if (last >= 0 ) 887 { 888 std::string extension = subfiles[i].name.substr(last); 889 std::transform(extension.begin(), extension.end(), extension.begin(), 890 [](unsigned char c){ return std::tolower(c); }); 891 892 if(filter.PassFilter(subfiles[i].name.c_str()) && (extension == valid_exts[selected_ext_idx])) 893 filtered_files.push_back(subfiles[i]); 894 } 895 } 896 } 897 } 898 } 899 900 void ImGuiFileBrowser::showHelpMarker(std::string desc) 901 { 902 ImGui::TextDisabled("(?)"); 903 if (ImGui::IsItemHovered()) 904 { 905 ImGui::BeginTooltip(); 906 ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); 907 ImGui::TextUnformatted(desc.c_str()); 908 ImGui::PopTextWrapPos(); 909 ImGui::EndTooltip(); 910 } 911 } 912 913 void ImGuiFileBrowser::showErrorModal() 914 { 915 ImVec2 window_size(260, 0); 916 ImGui::SetNextWindowSize(window_size); 917 918 if (ImGui::BeginPopupModal(error_title.c_str(), nullptr, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoResize)) 919 { 920 ImGui::TextWrapped("%s", error_msg.c_str()); 921 922 ImGui::Separator(); 923 ImGui::SetCursorPosX(window_size.x/2.0f - getButtonSize("OK").x/2.0f); 924 if (ImGui::Button("OK", getButtonSize("OK"))) 925 ImGui::CloseCurrentPopup(); 926 ImGui::EndPopup(); 927 } 928 } 929 930 bool ImGuiFileBrowser::showReplaceFileModal() 931 { 932 ImVec2 window_size(250, 0); 933 ImGui::SetNextWindowSize(window_size); 934 bool ret_val = false; 935 if (ImGui::BeginPopupModal(repfile_modal_id.c_str(), nullptr, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoResize)) 936 { 937 // unused: float frame_height = ImGui::GetFrameHeightWithSpacing(); 938 939 std::string text = "A file with the following filename already exists. Are you sure you want to replace the existing file?"; 940 ImGui::TextWrapped("%s", text.c_str()); 941 942 ImGui::Separator(); 943 944 float buttons_width = getButtonSize("Yes").x + getButtonSize("No").x + ImGui::GetStyle().ItemSpacing.x; 945 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetWindowWidth()/2.0f - buttons_width/2.0f - ImGui::GetStyle().WindowPadding.x); 946 947 if (ImGui::Button("Yes", getButtonSize("Yes"))) 948 { 949 selected_path = current_path + selected_fn; 950 ImGui::CloseCurrentPopup(); 951 ret_val = true; 952 } 953 954 ImGui::SameLine(); 955 if (ImGui::Button("No", getButtonSize("No"))) 956 { 957 selected_fn.clear(); 958 selected_path.clear(); 959 ImGui::CloseCurrentPopup(); 960 ret_val = false; 961 } 962 ImGui::EndPopup(); 963 } 964 return ret_val; 965 } 966 967 void ImGuiFileBrowser::showInvalidFileModal() 968 { 969 // unused: ImGuiStyle& style = ImGui::GetStyle(); 970 std::string text = "Selected file either doesn't exist or is not supported. Please select a file with the following extensions..."; 971 // unused: ImVec2 text_size = ImGui::CalcTextSize(text.c_str(), nullptr, true, 350 - style.WindowPadding.x * 2.0); 972 ImVec2 button_size = getButtonSize("OK"); 973 974 float frame_height = ImGui::GetFrameHeightWithSpacing(); 975 float cw_content_height = valid_exts.size() * frame_height; 976 float cw_height = std::min(4.0f * frame_height, cw_content_height); 977 ImVec2 window_size(350, 0); 978 ImGui::SetNextWindowSize(window_size); 979 980 if (ImGui::BeginPopupModal(invfile_modal_id.c_str(), nullptr, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoResize)) 981 { 982 983 ImGui::TextWrapped("%s", text.c_str()); 984 ImGui::BeginChild("##SupportedExts", ImVec2(0, cw_height), true); 985 for(std::size_t i = 0; i < valid_exts.size(); i++) 986 ImGui::BulletText("%s", valid_exts[i].c_str()); 987 ImGui::EndChild(); 988 989 ImGui::SetCursorPosX(window_size.x/2.0f - button_size.x/2.0f); 990 if (ImGui::Button("OK", button_size)) 991 ImGui::CloseCurrentPopup(); 992 ImGui::EndPopup(); 993 } 994 } 995 996 void ImGuiFileBrowser::setValidExtTypes(const std::string& valid_types_string) 997 { 998 /* Initialize a list of files extensions that are valid. 999 * If the user chooses a file that doesn't match the extensions in the 1000 * list, we will show an error modal... 1001 */ 1002 std::string max_str = ""; 1003 valid_exts.clear(); 1004 std::string extension = ""; 1005 std::istringstream iss(valid_types_string); 1006 while(std::getline(iss, extension, ',')) 1007 { 1008 if(!extension.empty()) 1009 { 1010 if(max_str.size() < extension.size()) 1011 max_str = extension; 1012 std::transform(extension.begin(), extension.end(), extension.begin(), 1013 [](unsigned char c){ return std::tolower(c); }); 1014 valid_exts.push_back(extension); 1015 } 1016 } 1017 float min_width = ImGui::CalcTextSize(".abc").x + 100; 1018 ext_box_width = std::max(min_width, ImGui::CalcTextSize(max_str.c_str()).x); 1019 } 1020 1021 bool ImGuiFileBrowser::validateFile() 1022 { 1023 bool match = false; 1024 1025 //If there is an item selected, check if the selected file name (the input filename, in other words) matches the selection. 1026 if(selected_idx >= 0) 1027 { 1028 if(dialog_mode == DialogMode::SELECT) 1029 match = (filtered_dirs[selected_idx].name == selected_fn); 1030 else 1031 match = (filtered_files[selected_idx].name == selected_fn); 1032 } 1033 1034 //If the input filename doesn't match we need to explicitly find the input filename.. 1035 if(!match) 1036 { 1037 if(dialog_mode == DialogMode::SELECT) 1038 { 1039 for(std::size_t i = 0; i < subdirs.size(); i++) 1040 { 1041 if(subdirs[i].name == selected_fn) 1042 { 1043 match = true; 1044 break; 1045 } 1046 } 1047 1048 } 1049 else 1050 { 1051 for(std::size_t i = 0; i < subfiles.size(); i++) 1052 { 1053 if(subfiles[i].name == selected_fn) 1054 { 1055 match = true; 1056 break; 1057 } 1058 } 1059 } 1060 } 1061 1062 // 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) 1063 if(!match) 1064 return (dialog_mode == DialogMode::SAVE); 1065 1066 // If file matches, return false on SAVE, we need to show a replace file modal 1067 if(dialog_mode == DialogMode::SAVE) 1068 return false; 1069 // Return true on SELECT, no need to validate extensions 1070 else if(dialog_mode == DialogMode::SELECT) 1071 return true; 1072 else 1073 { 1074 // If list of extensions has all types, no need to validate. 1075 for(auto ext : valid_exts) 1076 { 1077 if(ext == "*.*") 1078 return true; 1079 } 1080 int idx = (int)selected_fn.find_last_of('.'); 1081 std::string file_ext = idx == (int)std::string::npos ? "" : selected_fn.substr(idx, selected_fn.length() - idx); 1082 std::transform(file_ext.begin(), file_ext.end(), file_ext.begin(), 1083 [](unsigned char c){ return std::tolower(c); }); 1084 return (std::find(valid_exts.begin(), valid_exts.end(), file_ext) != valid_exts.end()); 1085 } 1086 } 1087 1088 ImVec2 ImGuiFileBrowser::getButtonSize(std::string button_text) 1089 { 1090 return (ImGui::CalcTextSize(button_text.c_str()) + ImGui::GetStyle().FramePadding * 2.0); 1091 } 1092 1093 void ImGuiFileBrowser::parsePathTabs(std::string path) 1094 { 1095 std::string path_element = ""; 1096 std::string root = ""; 1097 1098 #ifdef OSWIN 1099 current_dirlist.push_back("Computer"); 1100 #else 1101 if(path[0] == '/') 1102 current_dirlist.push_back("/"); 1103 #endif //OSWIN 1104 1105 std::istringstream iss(path); 1106 while(std::getline(iss, path_element, '/')) 1107 { 1108 if(!path_element.empty()) 1109 current_dirlist.push_back(path_element); 1110 } 1111 } 1112 1113 std::string ImGuiFileBrowser::wStringToString(const wchar_t* wchar_arr) 1114 { 1115 std::mbstate_t state = std::mbstate_t(); 1116 1117 //MinGW bug (patched in mingw-w64), wcsrtombs doesn't ignore length parameter when dest = nullptr. Hence the large number. 1118 size_t len = 1 + std::wcsrtombs(nullptr, &(wchar_arr), 600000, &state); 1119 1120 char* char_arr = new char[len]; 1121 std::wcsrtombs(char_arr, &wchar_arr, len, &state); 1122 1123 std::string ret_val(char_arr); 1124 1125 delete[] char_arr; 1126 return ret_val; 1127 } 1128 1129 bool ImGuiFileBrowser::alphaSortComparator(const Info& a, const Info& b) 1130 { 1131 return a.name < b.name; 1132 } 1133 1134 //Windows Exclusive function 1135 #ifdef OSWIN 1136 bool ImGuiFileBrowser::loadWindowsDrives() 1137 { 1138 DWORD len = GetLogicalDriveStringsA(0,nullptr); 1139 char* drives = new char[len]; 1140 if(!GetLogicalDriveStringsA(len,drives)) 1141 { 1142 delete[] drives; 1143 return false; 1144 } 1145 1146 clearFileList(); 1147 char* temp = drives; 1148 for(char *drv = nullptr; *temp != '\0'; temp++) 1149 { 1150 drv = temp; 1151 if(DRIVE_REMOVABLE == GetDriveTypeA(drv)) 1152 subdirs.push_back({"Removable Disk: " + std::string(1,drv[0]), false}); 1153 else if(DRIVE_FIXED == GetDriveTypeA(drv)) 1154 subdirs.push_back({"Local Disk: " + std::string(1,drv[0]), false}); 1155 //Go to nullptr character 1156 while(*(++temp)); 1157 } 1158 delete[] drives; 1159 return true; 1160 } 1161 #endif 1162 1163 //Unix only 1164 #ifndef OSWIN 1165 void ImGuiFileBrowser::initCurrentPath() 1166 { 1167 bool path_max_def = false; 1168 1169 #ifdef PATH_MAX 1170 path_max_def = true; 1171 #endif // PATH_MAX 1172 1173 char* buffer = nullptr; 1174 1175 //If PATH_MAX is defined deal with memory using new/delete. Else fallback to malloc'ed memory from `realpath()` 1176 if(path_max_def) 1177 buffer = new char[PATH_MAX]; 1178 1179 char* real_path = realpath("./", buffer); 1180 if (real_path == nullptr) 1181 { 1182 current_path = "/"; 1183 current_dirlist.push_back("/"); 1184 } 1185 else 1186 { 1187 current_path = std::string(real_path); 1188 current_path += "/"; 1189 parsePathTabs(current_path); 1190 } 1191 1192 if(path_max_def) 1193 delete[] buffer; 1194 else 1195 free(real_path); 1196 } 1197 #endif // OSWIN 1198}