GCC Code Coverage Report


Directory: ./
File: lib/geogram_gfx/imgui_ext/imgui_ext.cpp
Date: 2026-09-07 02:28:19
Exec Total Coverage
Lines: 0 441 0.0%
Functions: 0 31 0.0%
Branches: 0 542 0.0%

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