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*)(&current_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