diff --git a/fluid/CMakeLists.txt b/fluid/CMakeLists.txt index 32bae731b..d04dd122f 100644 --- a/fluid/CMakeLists.txt +++ b/fluid/CMakeLists.txt @@ -119,6 +119,7 @@ set(CPPFILES app/shell_command.cxx app/templates.cxx io/Code_Writer.cxx + io/file_chooser.cxx io/Project_Writer.cxx io/Project_Reader.cxx io/String_Writer.cxx @@ -175,6 +176,7 @@ set(HEADERFILES app/shell_command.h app/templates.h io/Code_Writer.h + io/file_chooser.h io/Project_Writer.h io/Project_Reader.h io/String_Writer.h diff --git a/fluid/Fluid.cxx b/fluid/Fluid.cxx index 5578471ee..4ab143c0b 100644 --- a/fluid/Fluid.cxx +++ b/fluid/Fluid.cxx @@ -23,6 +23,7 @@ #include "proj/undo.h" #include "io/Project_Reader.h" #include "io/Project_Writer.h" +#include "io/file_chooser.h" #include "io/Code_Writer.h" #include "nodes/Node.h" #include "nodes/Function_Node.h" @@ -502,7 +503,12 @@ bool Application::open_project_file(const std::string &filename_arg) { // ask for a filename if none was given std::string new_filename = filename_arg; if (new_filename.empty()) { - new_filename = open_project_filechooser("Open Project File"); + new_filename = fluid::io::load_project_filechooser( + "Open Project File", + history.latest_project_path(), + launch_path(), + "FLUID Project Files\t*.f[ld]\n" + ); if (new_filename.empty()) { return false; } @@ -533,7 +539,12 @@ bool Application::merge_project_file(const std::string &filename_arg) { // ask for a filename if none was given std::string new_filename = filename_arg; if (new_filename.empty()) { - new_filename = open_project_filechooser(title); + new_filename = fluid::io::load_project_filechooser( + title, + history.latest_project_path(), + launch_path(), + "FLUID Project Files\t*.f[ld]\n" + ); if (new_filename.empty()) { return false; } @@ -566,6 +577,7 @@ bool Application::merge_project_file(const std::string &filename_arg) { proj.set_modflag(0, 0); proj.undo.clear(); } + proj.update_settings_dialog(); if (oldfilename) free((void *)oldfilename); return true; } @@ -1207,32 +1219,6 @@ void Application::make_main_window() { } } - -/** - Open a native file chooser to allow choosing a project file for reading. - - Path and filename are preset with the current project filename, if there - is one. - - \param title a text describing the action after selecting a file (load, merge, ...) - \return the file path and name, or an empty string if the operation was canceled - */ -std::string Application::open_project_filechooser(const std::string &title) { - Fl_Native_File_Chooser dialog; - dialog.title(title.c_str()); - dialog.type(Fl_Native_File_Chooser::BROWSE_FILE); - dialog.filter("FLUID Files\t*.f[ld]\n"); - if (proj.proj_filename) { - std::string current_project_file = proj.proj_filename; - dialog.directory(fl_filename_path_str(current_project_file).c_str()); - dialog.preset_file(fl_filename_name_str(current_project_file).c_str()); - } - if (dialog.show() != 0) - return std::string(); - return std::string(dialog.filename()); -} - - /** Give the user the opportunity to save a project before clearing it. diff --git a/fluid/Fluid.h b/fluid/Fluid.h index d0d6f6f37..537f488f3 100644 --- a/fluid/Fluid.h +++ b/fluid/Fluid.h @@ -193,8 +193,6 @@ public: // Methods // Build the main app window and create a few other dialogs. void make_main_window(); - // Open a native file chooser to allow choosing a project file for reading. - std::string open_project_filechooser(const std::string &title); // Give the user the opportunity to save a project before clearing it. bool confirm_project_clear(); // Ensure that text widgets in the widget panel propagates apply current changes. diff --git a/fluid/app/history.cxx b/fluid/app/history.cxx index 2e4ea28a9..8a795efe3 100644 --- a/fluid/app/history.cxx +++ b/fluid/app/history.cxx @@ -48,6 +48,7 @@ void History::load() { else Fluid.history_item[i].flags = 0; } else break; } + Fluid.preferences.get("latest_project_path", latest_project_path_, ""); for (; i < 10; i ++) { if (i) Fluid.history_item[i-1].flags |= FL_MENU_DIVIDER; @@ -79,6 +80,12 @@ void History::update(std::string project_file) { if (!strcmp(absolute.c_str(), abspath[i])) break; #endif // _WIN32 || __APPLE__ + std::string path = fl_filename_path_str(absolute); + if (path != latest_project_path_) { + latest_project_path_ = path; + Fluid.preferences.set("latest_project_path", latest_project_path_); + } + // Does the first entry match? if (i == 0) return; diff --git a/fluid/app/history.h b/fluid/app/history.h index 83d543195..15049a318 100644 --- a/fluid/app/history.h +++ b/fluid/app/history.h @@ -24,7 +24,11 @@ namespace fluid { namespace app { -class History { +class History +{ +private: + std::string latest_project_path_ { }; + public: /// Stores the absolute filename of the last 10 project files, saved in app preferences. char abspath[10][FL_PATH_MAX] { }; @@ -36,6 +40,8 @@ public: void load(); // Add a new file to the project history using absolute paths. void update(std::string project_file); + // Return the path to the last opened project file, or an empty string if none. + std::string latest_project_path() const { return latest_project_path_; } }; } // namespace app diff --git a/fluid/io/file_chooser.cxx b/fluid/io/file_chooser.cxx new file mode 100644 index 000000000..ab44abcc7 --- /dev/null +++ b/fluid/io/file_chooser.cxx @@ -0,0 +1,198 @@ +// +// Fluid C++ File Chooser code for the Fast Light Tool Kit (FLTK). +// +// Copyright 2026 by Bill Spitzak and others. +// +// This library is free software. Distribution and use rights are outlined in +// the file "COPYING" which should have been included with this file. If this +// file is missing or damaged, see the license at: +// +// https://www.fltk.org/COPYING.php +// +// Please see the following page on how to report bugs and issues: +// +// https://www.fltk.org/bugs.php +// + +#include "io/file_chooser.h" + +#include "Fluid.h" + +#include +#include + + +static constexpr int load_type = Fl_Native_File_Chooser::BROWSE_FILE; +static constexpr int save_type = Fl_Native_File_Chooser::BROWSE_SAVE_FILE; +static constexpr int load_options = Fl_Native_File_Chooser::PREVIEW; +#ifdef __APPLE__ +static constexpr int save_options = Fl_Native_File_Chooser::NEW_FOLDER|Fl_Native_File_Chooser::SAVEAS_CONFIRM; +#else +static constexpr int save_options = Fl_Native_File_Chooser::NEW_FOLDER|Fl_Native_File_Chooser::SAVEAS_CONFIRM; +#endif + +static void set_paths( + Fl_Native_File_Chooser& fnfc, + const std::string& preset_path, + const std::string& fallback_path, + std::string& preset_directory, + std::string& preset_filename) +{ + std::string preset; + + if (!preset_path.empty()) { + preset = fl_filename_absolute_str(preset_path); + preset_directory = fl_filename_path_str(preset); + preset_filename = fl_filename_name_str(preset); + } else { + if (!fallback_path.empty()) { + preset = fl_filename_absolute_str(fallback_path); + } else { + preset = fl_filename_absolute_str(Fluid.launch_path()); + } + preset_directory = preset; + preset_filename.clear(); + } + fnfc.directory(preset_directory.c_str()); + fnfc.preset_file(preset_filename.c_str()); +} + +/** + Lets the user choose a project file to open. + \param title the dialog window's title + \param preset_path the path and name to a file that should be preselected in the dialog, or an empty string to use the fallback path + \param fallback_path the path to use if no preset path is provided + \param filter the file filter to apply in the dialog + \return the absolute path to the chosen file, or an empty string if the operation was canceled + */ +std::string fluid::io::load_project_filechooser( + const std::string& title, + const std::string& preset_path, + const std::string& fallback_path, + const std::string& filter) +{ + std::string preset_directory; + std::string preset_filename; + + Fl_Native_File_Chooser fnfc; + fnfc.type(load_type); + fnfc.options(load_options); + fnfc.title(title.c_str()); + fnfc.filter(filter.c_str()); + set_paths(fnfc, preset_path, fallback_path, preset_directory, preset_filename); + + switch (fnfc.show()) { + case -1: // Error + fl_alert("Can't open project:\n%s", fnfc.errmsg()); + return ""; + case 1: // Cancelled + return ""; + default: // Success + return fl_filename_absolute_str(fnfc.filename()); + } +} + +/** + Lets the user choose a data file to inline into the code. + \param title the dialog window's title + \param preset_path the path and name to a file that should be preselected in the dialog, or an empty string to use the fallback path + \param fallback_path the path to use if no preset path is provided + \param filter the file filter to apply in the dialog + \return the relative path to the chosen file, or an empty string if the operation was canceled + */ +std::string fluid::io::load_inline_data_filechooser( + const std::string& title, + const std::string& preset_path, + const std::string& fallback_path, + const std::string& filter) +{ + std::string preset_directory; + std::string preset_filename; + + Fl_Native_File_Chooser fnfc; + fnfc.type(load_type); + fnfc.options(load_options); + fnfc.title(title.c_str()); + fnfc.filter(filter.c_str()); + set_paths(fnfc, preset_path, fallback_path, preset_directory, preset_filename); + + switch (fnfc.show()) { + case -1: // Error + fl_alert("Can't open data file:\n%s", fnfc.errmsg()); + return ""; + case 1: // Cancelled + return ""; + default: // Success + return fl_filename_relative_str(fnfc.filename()); + } +} + +/** + Lets the user choose a comment file to load. + \param title the dialog window's title + \param preset_path not useful here, we never save the path to the comment file, just import it immediately + \param fallback_path the path to use if no preset path is provided + \param filter the file filter to apply in the dialog + \return the absolute path to the chosen file, or an empty string if the operation was canceled + */ +std::string fluid::io::load_comment_filechooser( + const std::string& title, + const std::string& preset_path, + const std::string& fallback_path, + const std::string& filter) +{ + std::string preset_directory; + std::string preset_filename; + + Fl_Native_File_Chooser fnfc; + fnfc.type(load_type); + fnfc.options(load_options); + fnfc.title(title.c_str()); + fnfc.filter(filter.c_str()); + set_paths(fnfc, preset_path, fallback_path, preset_directory, preset_filename); + + switch (fnfc.show()) { + case -1: // Error + fl_alert("Can't open comment file:\n%s", fnfc.errmsg()); + return ""; + case 1: // Cancelled + return ""; + default: // Success + return fl_filename_absolute_str(fnfc.filename()); + } +} + +/** + Lets the user choose an image file to load. + \param title the dialog window's title + \param preset_path the path and name to a file that should be preselected in the dialog, or an empty string to use the fallback path + \param fallback_path the path to use if no preset path is provided + \param filter the file filter to apply in the dialog + \return the relative path to the chosen file, or an empty string if the operation was canceled + */ +std::string fluid::io::load_image_filechooser( + const std::string& title, + const std::string& preset_path, + const std::string& fallback_path, + const std::string& filter) +{ + std::string preset_directory; + std::string preset_filename; + + Fl_Native_File_Chooser fnfc; + fnfc.type(load_type); + fnfc.options(load_options); + fnfc.title(title.c_str()); + fnfc.filter(filter.c_str()); + set_paths(fnfc, preset_path, fallback_path, preset_directory, preset_filename); + + switch (fnfc.show()) { + case -1: // Error + fl_alert("Can't open image file:\n%s", fnfc.errmsg()); + return ""; + case 1: // Cancelled + return ""; + default: // Success + return fl_filename_relative_str(fnfc.filename()); + } +} diff --git a/fluid/io/file_chooser.h b/fluid/io/file_chooser.h new file mode 100644 index 000000000..67f6bf971 --- /dev/null +++ b/fluid/io/file_chooser.h @@ -0,0 +1,65 @@ +// +// Fluid C++ File Chooser header for the Fast Light Tool Kit (FLTK). +// +// Copyright 2026 by Bill Spitzak and others. +// +// This library is free software. Distribution and use rights are outlined in +// the file "COPYING" which should have been included with this file. If this +// file is missing or damaged, see the license at: +// +// https://www.fltk.org/COPYING.php +// +// Please see the following page on how to report bugs and issues: +// +// https://www.fltk.org/bugs.php +// + +#ifndef FLUID_IO_FILE_CHOOSER_H +#define FLUID_IO_FILE_CHOOSER_H + +#include + +namespace fluid { + +namespace io { + +// ✔︎ Load Inline Data +// ✔︎ Pick a comment +// ✔︎ ui_find_image (the offender here, I guess) + +// Application::save_project_file +// ✔︎ Application::open_project_filechooser +// Fd_Shell_Command_List::export_selected +// Fd_Shell_Command_List::import_from_file +// Load Layout Settings +// Save Layout Settings + +extern std::string load_project_filechooser( + const std::string& title, + const std::string& preset_path, + const std::string& fallback_path, + const std::string& filter); + +extern std::string load_inline_data_filechooser( + const std::string& title, + const std::string& preset_path, + const std::string& fallback_path, + const std::string& filter); + +extern std::string load_comment_filechooser( + const std::string& title, + const std::string& preset_path, + const std::string& fallback_path, + const std::string& filter); + +extern std::string load_image_filechooser( + const std::string& title, + const std::string& preset_path, + const std::string& fallback_path, + const std::string& filter); + +} // namespace io + +} // namespace fluid + +#endif // FLUID_IO_FILE_CHOOSER_H diff --git a/fluid/panels/widget_panel.cxx b/fluid/panels/widget_panel.cxx index c3840088e..f188472b1 100644 --- a/fluid/panels/widget_panel.cxx +++ b/fluid/panels/widget_panel.cxx @@ -23,6 +23,7 @@ #include "proj/undo.h" #include "nodes/Window_Node.h" #include "nodes/Grid_Node.h" +#include "io/file_chooser.h" #include "nodes/Menu_Node.h" #include "nodes/Function_Node.h" #include @@ -53,7 +54,7 @@ extern int haderror; Allow widget navigation on text fields with Tab. */ static int use_tab_navigation(int, Fl_Text_Editor*) { -//fl ▼ ------------------------ code --~--=--=-~=-==~~=-~=-~- ▼ fl// +//fl ▼ ------------------------ code ---~=~=-----=-=~-~=~--~- ▼ fl// return 0; //fl ▲ ----------~~-~=---~-------------~-=--~~-=~=-~~--~-~=-- ▲ fl// } @@ -2651,19 +2652,25 @@ static void cb_wp_data_filename(Fl_Input* o, void* v) { static void cb_fileopen(Fl_Button*, void* v) { //fl ▼ ---------------------- callback ~--=-~~~=~~--~=-=~-=~- ▼ fl// if (v != LOAD) { + Fluid.proj.enter_project_dir(); - const char *fn = fl_file_chooser("Load Inline Data", - nullptr, wp_data_filename->value(), 1); + std::string fn = fluid::io::load_inline_data_filechooser( + "Load Inline Data", + wp_data_filename->value(), + Fluid.proj.projectfile_path(), + "" + ); Fluid.proj.leave_project_dir(); - if (fn) { - if (strcmp(fn, wp_data_filename->value())) { + + if (!fn.empty()) { + if (strcmp(fn.c_str(), wp_data_filename->value())) { Fluid.proj.set_modflag(1); - wp_data_filename->value(fn); + wp_data_filename->value(fn.c_str()); wp_data_filename->do_callback(); } } } -//fl ▲ ----------=~~---~~=~~=----------~--~-~=~=-~=--~=-~~=-- ▲ fl// +//fl ▲ ----------=~~---~~=~~=----------~-~=--~-=~~~~--~~~=--- ▲ fl// } static void cb_Comment(Fl_Text_Editor* o, void* v) { @@ -2814,17 +2821,22 @@ static void cb_comment_load_2(Fl_Button*, void* v) { //fl ▼ ---------------------- callback ~~~~~=~-=~~-=-=-~=-=~= ▼ fl// // load a comment from disk if (v != LOAD) { - fl_file_chooser_ok_label("Load"); - const char *fname = fl_file_chooser("Pick a comment", nullptr, nullptr); - fl_file_chooser_ok_label(nullptr); - if (fname) { - if (comment_tabs_name->buffer()->loadfile(fname)) { - fl_alert("Error loading file\n%s", fname); + Fluid.proj.enter_project_dir(); + std::string fname = fluid::io::load_comment_filechooser( + "Load Comment From File", + "", + Fluid.proj.projectfile_path(), + "" + ); + Fluid.proj.leave_project_dir(); + if (!fname.empty()) { + if (comment_tabs_name->buffer()->loadfile(fname.c_str())) { + fl_alert("Error loading file\n%s", fname.c_str()); } comment_tabs_name->do_callback(); } } -//fl ▲ ----------=~--=--=~=~=----------~~-~~----=--~~-~~~=~=- ▲ fl// +//fl ▲ ----------=~--=--=~=~=----------~~=--~--~=-=-=-=--=--~ ▲ fl// } static void cb_output(Fl_Check_Button* o, void* v) { diff --git a/fluid/panels/widget_panel.fl b/fluid/panels/widget_panel.fl index 9f8871d82..0b8933366 100644 --- a/fluid/panels/widget_panel.fl +++ b/fluid/panels/widget_panel.fl @@ -57,6 +57,9 @@ decl {\#include "nodes/Window_Node.h"} {uid b4c3 private global decl {\#include "nodes/Grid_Node.h"} {uid 867b private global } +decl {\#include "io/file_chooser.h"} {uid 867e private global +} + decl {\#include "nodes/Menu_Node.h"} {uid 2b16 private global } @@ -546,7 +549,7 @@ Function {make_widget_panel()} {uid 9310 xywh {95 40 309 20} labelfont 1 labelsize 11 align 4 } { Fl_Input wp_gui_label {uid 5f88 - callback label_cb + callback label_cb selected tooltip {The label text for the widget. Use Ctrl-J for newlines.} xywh {95 40 190 20} labelfont 1 labelsize 11 when 15 textsize 11 resizable } @@ -2636,7 +2639,7 @@ if (v == LOAD) { } Fl_Tabs data_tabs {uid 3ae5 callback {if (current_node && current_node->is_a(Type::Data)) - propagate_load((Fl_Group *)o,v);} open + propagate_load((Fl_Group *)o,v);} xywh {10 10 400 350} selection_color 12 labelsize 11 labelcolor 255 hide } { Fl_Group data_tabs_data {uid b260 @@ -2818,14 +2821,20 @@ update_current(o, v, Fl_Button {} {uid a0fe label {@fileopen} callback {if (v != LOAD) { + Fluid.proj.enter_project_dir(); - const char *fn = fl_file_chooser("Load Inline Data", - nullptr, wp_data_filename->value(), 1); + std::string fn = fluid::io::load_inline_data_filechooser( + "Load Inline Data", + wp_data_filename->value(), + Fluid.proj.projectfile_path(), + "" + ); Fluid.proj.leave_project_dir(); - if (fn) { - if (strcmp(fn, wp_data_filename->value())) { + + if (!fn.empty()) { + if (strcmp(fn.c_str(), wp_data_filename->value())) { Fluid.proj.set_modflag(1); - wp_data_filename->value(fn); + wp_data_filename->value(fn.c_str()); wp_data_filename->do_callback(); } } @@ -2983,12 +2992,17 @@ if (v == LOAD) { label {Import...} callback {// load a comment from disk if (v != LOAD) { - fl_file_chooser_ok_label("Load"); - const char *fname = fl_file_chooser("Pick a comment", nullptr, nullptr); - fl_file_chooser_ok_label(nullptr); - if (fname) { - if (comment_tabs_name->buffer()->loadfile(fname)) { - fl_alert("Error loading file\\n%s", fname); + Fluid.proj.enter_project_dir(); + std::string fname = fluid::io::load_comment_filechooser( + "Load Comment From File", + "", + Fluid.proj.projectfile_path(), + "" + ); + Fluid.proj.leave_project_dir(); + if (!fname.empty()) { + if (comment_tabs_name->buffer()->loadfile(fname.c_str())) { + fl_alert("Error loading file\\n%s", fname.c_str()); } comment_tabs_name->do_callback(); } diff --git a/fluid/proj/Image_Asset.cxx b/fluid/proj/Image_Asset.cxx index fc89fe209..d5bb85857 100644 --- a/fluid/proj/Image_Asset.cxx +++ b/fluid/proj/Image_Asset.cxx @@ -17,6 +17,8 @@ #include "proj/Image_Asset.h" #include "Fluid.h" +#include "Project.h" +#include "io/file_chooser.h" #include "io/Project_Reader.h" #include "io/Project_Writer.h" #include "io/Code_Writer.h" @@ -451,16 +453,18 @@ Image_Asset::~Image_Asset() { */ std::shared_ptr ui_find_image(const char *oldname) { Fluid.proj.enter_project_dir(); - fl_file_chooser_ok_label("Use Image"); - const char *name = fl_file_chooser("Image?", - "Image Files (*.{bm,bmp,gif,jpg,pbm,pgm,png,ppm,xbm,xpm,svg" + + std::string name = fluid::io::load_image_filechooser( + "Image?", + oldname ? oldname : "", + Fluid.proj.projectfile_path(), + "Image Files\t*.{bm,bmp,gif,jpg,pbm,pgm,png,ppm,xbm,xpm,svg" #ifdef HAVE_LIBZ ",svgz" #endif - "})", - oldname,1); - fl_file_chooser_ok_label(nullptr); - std::shared_ptr ret = (name && *name) ? Fluid.proj.image_assets.find_or_create(name) : nullptr; + "}" + ); + std::shared_ptr ret = (!name.empty()) ? Fluid.proj.image_assets.find_or_create(name) : nullptr; Fluid.proj.leave_project_dir(); return ret; }