1 /* 2 * Copyright (c) 2012-2016, Bruno Levy 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are met: 7 * 8 * * Redistributions of source code must retain the above copyright notice, 9 * this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above copyright notice, 11 * this list of conditions and the following disclaimer in the documentation 12 * and/or other materials provided with the distribution. 13 * * Neither the name of the ALICE Project-Team nor the names of its 14 * contributors may be used to endorse or promote products derived from this 15 * software without specific prior written permission. 16 * 17 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 * POSSIBILITY OF SUCH DAMAGE. 28 * 29 * If you modify this software, you should include a notice giving the 30 * name of the person performing the modification, the date of modification, 31 * and the reason for such modification. 32 * 33 * Contact: Bruno Levy 34 * 35 * Bruno.Levy@inria.fr 36 * http://www.loria.fr/~levy 37 * 38 * ALICE Project 39 * LORIA, INRIA Lorraine, 40 * Campus Scientifique, BP 239 41 * 54506 VANDOEUVRE LES NANCY CEDEX 42 * FRANCE 43 * 44 */ 45 46 #include <geogram_gfx/ImGui_ext/imgui_ext.h> 47 #include <geogram_gfx/ImGui_ext/icon_font.h> 48 #include <geogram_gfx/third_party/ImGui/imgui.h> 49 #include <geogram_gfx/third_party/ImGui/imgui_internal.h> 50 #include <geogram/basic/string.h> 51 #include <geogram/basic/logger.h> 52 #include <geogram/basic/file_system.h> 53 #include <geogram/basic/command_line.h> 54 #include <map> 55 56 namespace { 57 using namespace GEO; 58 59 bool initialized = false; 60 bool tooltips_enabled = true; 61 62 /** 63 * \brief Manages the GUI of a color editor. 64 * \details This creates a custom dialog with the color editor and 65 * a default palette, as in ImGUI example. 66 * \param[in] label the label of the widget, passed to ImGUI 67 * \param[in,out] color_in a pointer to an array of 3 floats if 68 * with_alpha is false or 4 floats if with_alpha is true 69 * \param[in] with_alpha true if transparency is edited, false otherwise 70 * \retval true if the color was changed 71 * \retval false otherwise 72 */ ColorEdit3or4WithPalette(const char * label,float * color_in,bool with_alpha)73 bool ColorEdit3or4WithPalette( 74 const char* label, float* color_in, bool with_alpha 75 ) { 76 bool result = false; 77 static bool saved_palette_initialized = false; 78 static ImVec4 saved_palette[40]; 79 static ImVec4 backup_color; 80 ImGui::PushID(label); 81 int flags = 82 ImGuiColorEditFlags_PickerHueWheel | 83 ImGuiColorEditFlags_Float; 84 85 if(!with_alpha) { 86 flags |= ImGuiColorEditFlags_NoAlpha ; 87 } 88 89 ImVec4& color = *(ImVec4*)color_in; 90 91 if (!saved_palette_initialized) { 92 93 for (int n = 0; n < 8; n++) { 94 saved_palette[n].x = 0.0f; 95 saved_palette[n].y = 0.0f; 96 saved_palette[n].z = 0.0f; 97 } 98 99 saved_palette[0] = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); 100 saved_palette[1] = ImVec4(0.5f, 0.5f, 0.5f, 1.0f); 101 saved_palette[2] = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); 102 saved_palette[3] = ImVec4(1.0f, 0.0f, 0.0f, 1.0f); 103 saved_palette[4] = ImVec4(1.0f, 1.0f, 0.0f, 1.0f); 104 saved_palette[5] = ImVec4(0.0f, 0.0f, 1.0f, 1.0f); 105 saved_palette[6] = ImVec4(0.0f, 1.0f, 0.0f, 1.0f); 106 saved_palette[7] = ImVec4(0.0f, 1.0f, 1.0f, 1.0f); 107 108 for (int n = 0; n < 32; n++) { 109 ImGui::ColorConvertHSVtoRGB( 110 float(n) / 31.0f, 0.8f, 0.8f, 111 saved_palette[n+8].x, 112 saved_palette[n+8].y, 113 saved_palette[n+8].z 114 ); 115 } 116 saved_palette_initialized = true; 117 } 118 119 bool open_popup = ImGui::ColorButton(label, color, flags); 120 121 if(label[0] != '#') { 122 ImGui::SameLine(); 123 ImGui::Text("%s",label); 124 } 125 if (open_popup) { 126 ImGui::OpenPopup("##PickerPopup"); 127 backup_color = color; 128 } 129 if (ImGui::BeginPopup("##PickerPopup")) { 130 if(label[0] != '#') { 131 ImGui::Text("%s",label); 132 ImGui::Separator(); 133 } 134 if(ImGui::ColorPicker4( 135 "##picker", (float*)&color, 136 flags | ImGuiColorEditFlags_NoSidePreview 137 | ImGuiColorEditFlags_NoSmallPreview 138 ) 139 ) { 140 result = true; 141 } 142 ImGui::SameLine(); 143 ImGui::BeginGroup(); 144 ImGui::Text("Current"); 145 ImGui::ColorButton( 146 "##current", color, 147 ImGuiColorEditFlags_NoPicker | 148 ImGuiColorEditFlags_AlphaPreviewHalf, 149 ImVec2(60,40) 150 ); 151 ImGui::Text("Previous"); 152 if (ImGui::ColorButton( 153 "##previous", backup_color, 154 ImGuiColorEditFlags_NoPicker | 155 ImGuiColorEditFlags_AlphaPreviewHalf, 156 ImVec2(60,40)) 157 ) { 158 color = backup_color; 159 result = true; 160 } 161 ImGui::Separator(); 162 ImGui::Text("Palette"); 163 164 #ifdef GEO_OS_ANDROID 165 int nb_btn_per_row = 4; 166 float btn_size = 35.0; 167 #else 168 int nb_btn_per_row = 8; 169 float btn_size = 20.0; 170 #endif 171 for (int n = 0; n < 40; n++) { 172 ImGui::PushID(n); 173 if ( (n % nb_btn_per_row) != 0 ) { 174 ImGui::SameLine(0.0f, ImGui::GetStyle().ItemSpacing.y); 175 } 176 if (ImGui::ColorButton( 177 "##palette", 178 saved_palette[n], 179 ImGuiColorEditFlags_NoPicker | 180 ImGuiColorEditFlags_NoTooltip, 181 ImVec2(btn_size,btn_size)) 182 ) { 183 color = ImVec4( 184 saved_palette[n].x, 185 saved_palette[n].y, 186 saved_palette[n].z, 187 color.w 188 ); // Preserve alpha! 189 result = true; 190 } 191 ImGui::PopID(); 192 } 193 ImGui::Separator(); 194 if(ImGui::Button( 195 "OK", ImVec2(-1,-1) 196 ) 197 ) { 198 ImGui::CloseCurrentPopup(); 199 } 200 ImGui::EndGroup(); 201 ImGui::EndPopup(); 202 } 203 ImGui::PopID(); 204 return result; 205 } 206 207 /**************************************************************************/ 208 209 /** 210 * \brief Safer version of strncpy() 211 * \param[in] dest a pointer to the destination string 212 * \param[in] source a pointer to the source string 213 * \param[in] max_dest_size number of characters available in 214 * destination string 215 * \return the length of the destination string after copy. If 216 * the source string + null terminator was greater than max_dest_size, 217 * then it is cropped. On exit, dest is always null-terminated (in 218 * contrast with strncpy()). 219 */ safe_strncpy(char * dest,const char * source,size_t max_dest_size)220 size_t safe_strncpy( 221 char* dest, const char* source, size_t max_dest_size 222 ) { 223 strncpy(dest, source, max_dest_size-1); 224 dest[max_dest_size-1] = '\0'; 225 return strlen(dest); 226 } 227 228 /** 229 * \brief Converts a complete path to a file to a label 230 * displayed in the file browser. 231 * \details Strips viewer_path from the input path. 232 * \param[in] path the complete path, can be either a directory or 233 * a file 234 * \return the label to be displayed in the menu 235 */ path_to_label(const std::string & viewer_path,const std::string & path)236 std::string path_to_label( 237 const std::string& viewer_path, const std::string& path 238 ) { 239 std::string result = path; 240 if(GEO::String::string_starts_with(result, viewer_path)) { 241 result = result.substr( 242 viewer_path.length(), result.length()-viewer_path.length() 243 ); 244 } 245 return result; 246 } 247 248 249 /** 250 * \brief Converts an icon symbolic name and a label to a string. 251 * \param[in] icon_sym the symbolic name of the icon. 252 * \param[in] label the label to be displayed. 253 * \param[in] no_label if true, do not display the label 254 * \return a UTF8 string with the icon and label, or just the label 255 * if the icon font is not initialized. 256 */ icon_label(const char * icon_sym,const char * label=nullptr,bool no_label=false)257 std::string icon_label( 258 const char* icon_sym, 259 const char* label = nullptr, 260 bool no_label = false 261 ) { 262 wchar_t str[2]; 263 str[0] = icon_wchar(icon_sym); 264 if(str[0] == '\0') { 265 return std::string(label); 266 } 267 str[1] = '\0'; 268 if(label == nullptr) { 269 return GEO::String::wchar_to_UTF8(str); 270 } 271 if(no_label) { 272 return GEO::String::wchar_to_UTF8(str) + "##" + label; 273 } 274 if(label[0] == '#') { 275 return GEO::String::wchar_to_UTF8(str) + label; 276 } 277 return GEO::String::wchar_to_UTF8(str) + " " + label; 278 } 279 280 /**************************************************************************/ 281 282 /** 283 * \brief The state for OpenFileDialog() and FileDialog() 284 */ 285 class FileDialog { 286 public: 287 288 /** 289 * \brief FileDialog constructor. 290 * \param[in] save_mode if true, FileDialog is used to create files 291 * \param[in] default_filename the default file name used if save_mode 292 * is set 293 */ FileDialog(bool save_mode=false,const std::string & default_filename="",FileSystem::Node * root=nullptr)294 FileDialog( 295 bool save_mode=false, 296 const std::string& default_filename="", 297 FileSystem::Node* root = nullptr 298 ) : visible_(false), 299 root_(nullptr), 300 current_write_extension_index_(0), 301 pinned_(false), 302 show_hidden_(false), 303 scroll_to_file_(false), 304 save_mode_(save_mode), 305 are_you_sure_(false) 306 { 307 set_root(root); 308 set_default_filename(default_filename); 309 current_file_index_ = 0; 310 current_directory_index_ = 0; 311 current_write_extension_index_ = 0; 312 no_docking_ = 313 !CmdLine::get_arg_bool("gui:expert") && 314 !CmdLine::get_arg_bool("gui:phone_screen"); 315 } 316 317 318 /** 319 * \brief Sets the root node to be used with the FileDialog. 320 * \param[in] root a pointer to the root node. 321 */ set_root(FileSystem::Node * root)322 void set_root(FileSystem::Node* root) { 323 FileSystem::Node* prev_root = root_; 324 root_ = root; 325 if(root_ == nullptr) { 326 FileSystem::get_root(root_); 327 } 328 if(prev_root != root_) { 329 #if defined(GEO_OS_WINDOWS) || defined(GEO_OS_ANDROID) 330 directory_ = root_->documents_directory(); 331 #else 332 directory_ = root_->get_current_working_directory(); 333 #endif 334 if( 335 directory_ == "" || 336 directory_[directory_.length()-1] != '/' 337 ) { 338 directory_ += "/"; 339 } 340 } 341 } 342 343 /** 344 * \brief Sets the default file. 345 * \details Only valid if save_mode is set. 346 * \param[in] default_filename the default file name. 347 */ set_default_filename(const std::string & default_filename)348 void set_default_filename(const std::string& default_filename) { 349 safe_strncpy( 350 current_file_, default_filename.c_str(), sizeof(current_file_) 351 ); 352 } 353 354 /** 355 * \brief Makes this FileDialog visible. 356 */ show()357 void show() { 358 update_files(); 359 visible_ = true; 360 } 361 362 /** 363 * \brief Makes this FileDialog invisibile. 364 */ hide()365 void hide() { 366 visible_ = false; 367 } 368 369 /** 370 * \brief Draws the console and handles the gui. 371 */ draw()372 void draw() { 373 if(!visible_) { 374 return; 375 } 376 377 bool phone_screen = CmdLine::get_arg_bool("gui:phone_screen"); 378 379 if(!phone_screen) { 380 ImGui::SetNextWindowSize( 381 ImVec2(ImGui::scaling()*400.0f, ImGui::scaling()*415.0f), 382 ImGuiCond_Once 383 ); 384 } 385 386 std::string label = std::string( 387 save_mode_ ? "Save as...##" : "Load...##" 388 ); 389 390 if(!phone_screen) { 391 label += String::to_string(this); 392 } 393 394 ImGui::Begin( 395 label.c_str(), 396 &visible_, 397 ImGuiWindowFlags_NoCollapse | ( 398 (no_docking_ && !pinned_) ? ImGuiWindowFlags_NoDocking : 0 399 ) 400 ); 401 402 float s = ImGui::CalcTextSize(icon_UTF8("thumbtack").c_str()).x; 403 float spacing = 0.15f*s; 404 405 bool compact = (ImGui::GetContentRegionAvail().x < s*10.0f); 406 407 if(phone_screen) { 408 if(ImGui::SimpleButton(icon_label( 409 "window-close","##file_dialog_close", compact 410 ))) { 411 visible_ = false; 412 } 413 ImGui::SameLine(); 414 ImGui::Dummy(ImVec2(spacing,1.0f)); 415 ImGui::SameLine(); 416 } 417 418 if(ImGui::SimpleButton(icon_label( 419 "arrow-circle-up","parent", compact 420 ))) { 421 set_directory("../"); 422 } 423 ImGui::SameLine(); 424 ImGui::Dummy(ImVec2(spacing,1.0f)); 425 ImGui::SameLine(); 426 if(ImGui::SimpleButton(icon_label("home","home",compact))) { 427 set_directory(root_->documents_directory()); 428 update_files(); 429 } 430 ImGui::SameLine(); 431 ImGui::Dummy(ImVec2(spacing,1.0f)); 432 ImGui::SameLine(); 433 if(ImGui::SimpleButton(icon_label("sync-alt","refresh",compact))) { 434 update_files(); 435 } 436 437 if(!save_mode_ && !phone_screen) { 438 ImGui::SameLine(); 439 ImGui::Dummy( 440 ImVec2( 441 ImGui::GetContentRegionAvail().x - s*1.1f,1.0f 442 ) 443 ); 444 ImGui::SameLine(); 445 if(pinned_) { 446 if(ImGui::SimpleButton( 447 icon_UTF8("dot-circle") + "##pin" 448 )) { 449 pinned_ = !pinned_; 450 } 451 } else { 452 if(ImGui::SimpleButton( 453 icon_UTF8("thumbtack") + "##pin" 454 )) { 455 pinned_ = !pinned_; 456 } 457 } 458 if(ImGui::IsItemHovered()) { 459 ImGui::SetTooltip("Keeps this dialog open."); 460 } 461 } 462 463 draw_disk_drives(); 464 ImGui::Separator(); 465 466 { 467 std::vector<std::string> path; 468 String::split_string(directory_, '/', path); 469 for(index_t i=0; i<path.size(); ++i) { 470 if(i != 0) { 471 ImGui::SameLine(); 472 if( 473 ImGui::GetContentRegionAvail().x < 474 ImGui::CalcTextSize(path[i].c_str()).x + 475 10.0f*ImGui::scaling() 476 ) { 477 ImGui::NewLine(); 478 } 479 } 480 // We need to generate a unique id, else there is an id 481 // clash with the "home" button right before !! 482 if(ImGui::SimpleButton( 483 (path[i] + "##path" + String::to_string(i)) 484 )) { 485 std::string new_dir; 486 if(path[0].length() >= 2 && path[0][1] == ':') { 487 new_dir = path[0]; 488 } else { 489 new_dir += "/"; 490 new_dir += path[0]; 491 } 492 for(index_t j=1; j<=i; ++j) { 493 new_dir += "/"; 494 new_dir += path[j]; 495 } 496 set_directory(new_dir); 497 } 498 ImGui::SameLine(); 499 ImGui::Text("/"); 500 } 501 } 502 503 const float footer_size = 504 phone_screen ? 0.0f : 35.0f*ImGui::scaling(); 505 506 if(phone_screen) { 507 draw_footer(); 508 } 509 { 510 ImGui::BeginChild( 511 "##directories", 512 ImVec2( 513 ImGui::GetWindowWidth()*0.5f-10.0f*ImGui::scaling(), 514 -footer_size 515 ), 516 true 517 ); 518 for(index_t i=0; i<directories_.size(); ++i) { 519 if(ImGui::Selectable( 520 directories_[i].c_str(), 521 (i == current_directory_index_) 522 )) { 523 current_directory_index_ = i; 524 set_directory(directories_[current_directory_index_]); 525 } 526 } 527 ImGui::EndChild(); 528 } 529 ImGui::SameLine(); 530 { 531 ImGui::BeginChild( 532 "##files", 533 ImVec2( 534 ImGui::GetWindowWidth()*0.5f-10.0f*ImGui::scaling(), 535 -footer_size 536 ), 537 true 538 ); 539 for(index_t i=0; i<files_.size(); ++i) { 540 if(ImGui::Selectable( 541 files_[i].c_str(), 542 (i == current_file_index_) 543 )) { 544 safe_strncpy( 545 current_file_,files_[i].c_str(), 546 sizeof(current_file_) 547 ); 548 if(current_file_index_ == i) { 549 file_selected(); 550 } else { 551 current_file_index_ = i; 552 } 553 } 554 if(scroll_to_file_ && i == current_file_index_) { 555 ImGui::SetScrollHereY(); 556 scroll_to_file_ = false; 557 } 558 } 559 ImGui::EndChild(); 560 } 561 if(!phone_screen) { 562 draw_footer(); 563 } 564 ImGui::End(); 565 draw_are_you_sure(); 566 } 567 568 /** 569 * \brief Sets whether this file dialog is for 570 * saving file. 571 * \details If this file dialog is for saving file, 572 * then the user can enter the name of a non-existing 573 * file, else he can only select existing files. 574 * \param[in] x true if this file dialog is for 575 * saving file. 576 */ set_save_mode(bool x)577 void set_save_mode(bool x) { 578 save_mode_ = x; 579 } 580 581 /** 582 * \brief Gets the selected file if any and resets it 583 * to the empty string. 584 * \return the selected file if there is any or the 585 * empty string otherwise. 586 */ get_and_reset_selected_file()587 std::string get_and_reset_selected_file() { 588 std::string result; 589 std::swap(result,selected_file_); 590 return result; 591 } 592 593 /** 594 * \brief Defines the file extensions managed by this 595 * FileDialog. 596 * \param[in] extensions a ';'-separated list of extensions 597 */ set_extensions(const std::string & extensions)598 void set_extensions(const std::string& extensions) { 599 extensions_.clear(); 600 GEO::String::split_string(extensions, ';', extensions_); 601 } 602 603 protected: 604 draw_footer()605 void draw_footer() { 606 if(ImGui::Button( 607 save_mode_ ? 608 icon_label("save","Save as").c_str() : 609 icon_label("folder-open","Load").c_str() 610 )) { 611 file_selected(); 612 } 613 ImGui::SameLine(); 614 ImGui::PushItemWidth( 615 save_mode_ ? 616 -80.0f*ImGui::scaling() : -5.0f*ImGui::scaling() 617 ); 618 if(ImGui::InputText( 619 "##filename", 620 current_file_, geo_imgui_string_length, 621 ImGuiInputTextFlags_EnterReturnsTrue | 622 ImGuiInputTextFlags_CallbackHistory | 623 ImGuiInputTextFlags_CallbackCompletion , 624 text_input_callback, 625 this 626 ) 627 ) { 628 scroll_to_file_ = true; 629 std::string file = current_file_; 630 for(index_t i=0; i<files_.size(); ++i) { 631 if(files_[i] == file) { 632 current_file_index_ = i; 633 } 634 } 635 file_selected(); 636 } 637 ImGui::PopItemWidth(); 638 // Keep auto focus on the input box 639 if (ImGui::IsItemHovered()) { 640 // Auto focus previous widget 641 ImGui::SetKeyboardFocusHere(-1); 642 } 643 644 if(save_mode_) { 645 ImGui::SameLine(); 646 ImGui::PushItemWidth(-5.0f*ImGui::scaling()); 647 648 std::vector<const char*> write_extensions; 649 for(index_t i=0; i<extensions_.size(); ++i) { 650 write_extensions.push_back(&extensions_[i][0]); 651 } 652 if(ImGui::Combo( 653 "##extension", 654 (int*)(¤t_write_extension_index_), 655 &write_extensions[0], 656 int(write_extensions.size()) 657 ) 658 ) { 659 std::string file = current_file_; 660 file = root_->base_name(file) + "." + 661 extensions_[current_write_extension_index_]; 662 safe_strncpy( 663 current_file_, file.c_str(), 664 sizeof(current_file_) 665 ); 666 } 667 ImGui::PopItemWidth(); 668 } 669 } 670 671 /** 672 * \brief Tests whether a file can be read. 673 * \param[in] filename the file name to be tested. 674 * \retval true if this file can be read. 675 * \retval false otherwise. 676 */ can_load(const std::string & filename)677 bool can_load(const std::string& filename) { 678 if(!root_->is_file(filename)) { 679 return false; 680 } 681 std::string ext = root_->extension(filename); 682 for(size_t i=0; i<extensions_.size(); ++i) { 683 if(extensions_[i] == ext || extensions_[i] == "*") { 684 return true; 685 } 686 } 687 return false; 688 } 689 690 /** 691 * \brief Updates the list of files and directories 692 * displayed by this FileDialog. 693 */ update_files()694 void update_files() { 695 directories_.clear(); 696 files_.clear(); 697 698 directories_.push_back("../"); 699 700 std::vector<std::string> entries; 701 root_->get_directory_entries(directory_, entries); 702 std::sort(entries.begin(), entries.end()); 703 for(index_t i=0; i<entries.size(); ++i) { 704 if(can_load(entries[i])) { 705 files_.push_back(path_to_label(directory_,entries[i])); 706 } else if(root_->is_directory(entries[i])) { 707 std::string subdir = 708 path_to_label(directory_,entries[i]) + "/"; 709 if(show_hidden_ || subdir[0] != '.') { 710 directories_.push_back(subdir); 711 } 712 } 713 } 714 if(current_directory_index_ >= directories_.size()) { 715 current_directory_index_ = 0; 716 } 717 if(current_file_index_ >= files_.size()) { 718 current_file_index_ = 0; 719 } 720 if(!save_mode_) { 721 if(current_file_index_ >= files_.size()) { 722 current_file_[0] = '\0'; 723 } else { 724 safe_strncpy( 725 current_file_, 726 files_[current_file_index_].c_str(), 727 sizeof(current_file_) 728 ); 729 } 730 } 731 } 732 733 /** 734 * \brief Changes the current directory. 735 * \param[in] directory either the path relative to the 736 * current directory or an absolute path 737 */ set_directory(const std::string & directory)738 void set_directory(const std::string& directory) { 739 current_directory_index_ = 0; 740 current_file_index_ = 0; 741 if(directory[0] == '/' || directory[1] == ':') { 742 directory_ = directory; 743 } else { 744 directory_ = root_->normalized_path( 745 directory_ + "/" + 746 directory 747 ); 748 } 749 if(directory_[directory_.length()-1] != '/') { 750 directory_ += "/"; 751 } 752 update_files(); 753 } 754 755 /** 756 * \brief The callback for handling the text input. 757 * \param[in,out] data a pointer to the callback data 758 */ text_input_callback(ImGuiInputTextCallbackData * data)759 static int text_input_callback(ImGuiInputTextCallbackData* data) { 760 FileDialog* dlg = static_cast<FileDialog*>(data->UserData); 761 if( 762 (data->EventFlag & 763 ImGuiInputTextFlags_CallbackCompletion) != 0 764 ) { 765 dlg->tab_callback(data); 766 } else if( 767 (data->EventFlag & ImGuiInputTextFlags_CallbackHistory) != 0 768 ) { 769 if(data->EventKey == ImGuiKey_UpArrow) { 770 dlg->updown_callback(data,-1); 771 } else if(data->EventKey == ImGuiKey_DownArrow) { 772 dlg->updown_callback(data,1); 773 } 774 } 775 return 0; 776 } 777 778 /** 779 * \brief Called whenever the up or down arrows are pressed. 780 * \param[in,out] data a pointer to the callback data 781 * \param[in] direction -1 if the up arrow was pressed, 1 if the 782 * down arrow was pressed 783 */ updown_callback(ImGuiInputTextCallbackData * data,int direction)784 void updown_callback(ImGuiInputTextCallbackData* data, int direction) { 785 int next = int(current_file_index_) + direction; 786 if(next < 0) { 787 if(files_.size() == 0) { 788 current_file_index_ = 0; 789 } else { 790 current_file_index_ = index_t(files_.size()-1); 791 } 792 } else if(next >= int(files_.size())) { 793 current_file_index_ = 0; 794 } else { 795 current_file_index_ = index_t(next); 796 } 797 798 if(files_.size() == 0) { 799 current_file_[0] = '\0'; 800 } else { 801 safe_strncpy( 802 current_file_, 803 files_[current_file_index_].c_str(), 804 sizeof(current_file_) 805 ); 806 } 807 update_text_edit_callback_data(data); 808 scroll_to_file_ = true; 809 } 810 811 /** 812 * \brief Called whenever the tab key is pressed. 813 * \param[in,out] data a pointer to the callback data 814 */ tab_callback(ImGuiInputTextCallbackData * data)815 void tab_callback(ImGuiInputTextCallbackData* data) { 816 std::string file(current_file_); 817 bool found = false; 818 for(index_t i=0; i<files_.size(); ++i) { 819 if(String::string_starts_with(files_[i],file)) { 820 current_file_index_ = i; 821 found = true; 822 break; 823 } 824 } 825 if(found) { 826 safe_strncpy( 827 current_file_, 828 files_[current_file_index_].c_str(), 829 sizeof(current_file_) 830 ); 831 update_text_edit_callback_data(data); 832 scroll_to_file_ = true; 833 } 834 } 835 836 /** 837 * \brief Copies the currently selected file into the 838 * string currently manipulated by InputText. 839 * \param[out] data a pointer to the callback data 840 */ update_text_edit_callback_data(ImGuiInputTextCallbackData * data)841 void update_text_edit_callback_data( 842 ImGuiInputTextCallbackData* data 843 ) { 844 data->BufTextLen = int( 845 safe_strncpy( 846 data->Buf, current_file_, (size_t)data->BufSize 847 ) 848 ); 849 data->CursorPos = data->BufTextLen; 850 data->SelectionStart = data->BufTextLen; 851 data->SelectionEnd = data->BufTextLen; 852 data->BufDirty = true; 853 } 854 855 /** 856 * \brief Called whenever a file is selected. 857 * \param[in] force in save_mode, if set, 858 * overwrites the file even if it already 859 * exists. 860 */ file_selected(bool force=false)861 void file_selected(bool force=false) { 862 std::string file = 863 root_->normalized_path(directory_+"/"+current_file_); 864 865 if(save_mode_) { 866 if(!force && root_->is_file(file)) { 867 are_you_sure_ = true; 868 return; 869 } else { 870 selected_file_ = file; 871 } 872 } else { 873 selected_file_ = file; 874 } 875 876 if(!pinned_) { 877 hide(); 878 } 879 } 880 881 /** 882 * \brief Handles the "are you sure ?" dialog 883 * when a file is about to be overwritten. 884 */ draw_are_you_sure()885 void draw_are_you_sure() { 886 if(are_you_sure_) { 887 ImGui::OpenPopup("File exists"); 888 } 889 if( 890 ImGui::BeginPopupModal( 891 "File exists", nullptr, ImGuiWindowFlags_AlwaysAutoResize 892 ) 893 ) { 894 ImGui::Text( 895 "%s", 896 (std::string("File ") + current_file_ + 897 " already exists\nDo you want to overwrite it ?" 898 ).c_str() 899 ); 900 ImGui::Separator(); 901 if (ImGui::Button( 902 "Overwrite", 903 ImVec2(-ImGui::GetContentRegionAvail().x/2.0f,0.0f)) 904 ) { 905 are_you_sure_ = false; 906 ImGui::CloseCurrentPopup(); 907 file_selected(true); 908 } 909 ImGui::SameLine(); 910 if (ImGui::Button("Cancel", ImVec2(-1.0f, 0.0f))) { 911 are_you_sure_ = false; 912 ImGui::CloseCurrentPopup(); 913 } 914 ImGui::EndPopup(); 915 } 916 } 917 918 /** 919 * \brief Under Windows, add buttons to change 920 * disk drive. 921 */ draw_disk_drives()922 void draw_disk_drives() { 923 #ifdef GEO_OS_WINDOWS 924 DWORD drives = GetLogicalDrives(); 925 for(DWORD b=0; b<16; ++b) { 926 if((drives & (1u << b)) != 0) { 927 std::string drive; 928 drive += char('A' + char(b)); 929 drive += ":"; 930 if(ImGui::Button(drive)) { 931 set_directory(drive); 932 } 933 ImGui::SameLine(); 934 if( 935 ImGui::GetContentRegionAvail().x < 936 ImGui::CalcTextSize("X:").x + 10.0f*ImGui::scaling() 937 ) { 938 ImGui::NewLine(); 939 } 940 } 941 } 942 #endif 943 } 944 945 private: 946 bool visible_; 947 FileSystem::Node* root_; 948 std::string directory_; 949 index_t current_directory_index_; 950 index_t current_file_index_; 951 std::vector<std::string> directories_; 952 std::vector<std::string> files_; 953 std::vector<std::string> extensions_; 954 index_t current_write_extension_index_; 955 char current_file_[geo_imgui_string_length]; 956 bool pinned_; 957 bool show_hidden_; 958 bool scroll_to_file_; 959 bool save_mode_; 960 bool are_you_sure_; 961 std::string selected_file_; 962 bool no_docking_; 963 }; 964 965 std::map<std::string, FileDialog*> file_dialogs; 966 terminate_imgui_ext()967 void terminate_imgui_ext() { 968 for(auto& it : file_dialogs) { 969 delete it.second; 970 } 971 } 972 initialize_imgui_ext()973 void initialize_imgui_ext() { 974 if(!initialized) { 975 initialized = true; 976 atexit(terminate_imgui_ext); 977 } 978 } 979 } 980 981 namespace ImGui { 982 scaling()983 float scaling() { 984 ImGuiContext& g = *GImGui; 985 float s = 1.0; 986 if(g.Font->FontSize > 40.0f) { 987 s = g.Font->FontSize / 30.0f; 988 } else { 989 s = g.Font->FontSize / 20.0f; 990 } 991 return s * ImGui::GetIO().FontGlobalScale; 992 } 993 set_scaling(float x)994 void set_scaling(float x) { 995 ImGui::GetIO().FontGlobalScale = x; 996 } 997 998 /*******************************************************************/ 999 ColorEdit3WithPalette(const char * label,float * color_in)1000 bool ColorEdit3WithPalette(const char* label, float* color_in) { 1001 return ColorEdit3or4WithPalette(label, color_in, false); 1002 } 1003 ColorEdit4WithPalette(const char * label,float * color_in)1004 bool ColorEdit4WithPalette(const char* label, float* color_in) { 1005 return ColorEdit3or4WithPalette(label, color_in, true); 1006 } 1007 1008 /*******************************************************************/ 1009 OpenFileDialog(const char * label,const char * extensions,const char * filename,ImGuiExtFileDialogFlags flags,FileSystem::Node * root)1010 void OpenFileDialog( 1011 const char* label, 1012 const char* extensions, 1013 const char* filename, 1014 ImGuiExtFileDialogFlags flags, 1015 FileSystem::Node* root 1016 ) { 1017 initialize_imgui_ext(); 1018 ::FileDialog* dlg = nullptr; 1019 if(file_dialogs.find(label) == file_dialogs.end()) { 1020 file_dialogs[label] = new ::FileDialog(); 1021 } 1022 dlg = file_dialogs[label]; 1023 dlg->set_extensions(extensions); 1024 if(flags == ImGuiExtFileDialogFlags_Save) { 1025 dlg->set_save_mode(true); 1026 dlg->set_default_filename(filename); 1027 } else { 1028 dlg->set_save_mode(false); 1029 } 1030 dlg->set_root(root); 1031 dlg->show(); 1032 } 1033 FileDialog(const char * label,char * filename,size_t filename_buff_len)1034 bool FileDialog( 1035 const char* label, char* filename, size_t filename_buff_len 1036 ) { 1037 if(file_dialogs.find(label) == file_dialogs.end()) { 1038 filename[0] = '\0'; 1039 return false; 1040 } 1041 ::FileDialog* dlg = file_dialogs[label]; 1042 dlg->draw(); 1043 1044 std::string result = dlg->get_and_reset_selected_file(); 1045 if(result != "") { 1046 if(result.length() + 1 >= filename_buff_len) { 1047 GEO::Logger::err("ImGui_ext") << "filename_buff_len exceeded" 1048 << std::endl; 1049 return false; 1050 } else { 1051 strcpy(filename, result.c_str()); 1052 return true; 1053 } 1054 } else { 1055 return false; 1056 } 1057 } 1058 1059 /****************************************************************/ 1060 Tooltip(const char * str)1061 void Tooltip(const char* str) { 1062 if( 1063 tooltips_enabled && (str != nullptr) && (*str != '\0') && 1064 IsItemHovered() 1065 ) { 1066 SetTooltip("%s",str); 1067 } 1068 } 1069 EnableTooltips()1070 void EnableTooltips() { 1071 tooltips_enabled = true; 1072 } 1073 DisableTooltips()1074 void DisableTooltips() { 1075 tooltips_enabled = false; 1076 } 1077 1078 /****************************************************************/ 1079 SimpleButton(const char * label)1080 bool SimpleButton(const char* label) { 1081 std::string str(label); 1082 size_t off = str.find("##"); 1083 if(off != std::string::npos) { 1084 str = str.substr(0, off); 1085 } 1086 ImVec2 label_size = ImGui::CalcTextSize(str.c_str(), NULL, true); 1087 return ImGui::Selectable(label, false, 0, label_size); 1088 } 1089 CenteredText(const char * text)1090 void CenteredText(const char* text) { 1091 ImVec2 text_size = ImGui::CalcTextSize(text, NULL, true); 1092 ImVec2 avail_size = ImGui::GetContentRegionAvail(); 1093 float w = 0.95f*(avail_size.x - text_size.x) / 2.0f; 1094 if(w > 0.0f) { 1095 ImGui::Dummy(ImVec2(w, text_size.y)); 1096 } 1097 ImGui::SameLine(); 1098 ImGui::Text("%s",text); 1099 } 1100 } 1101 1102 1103