Fluid: improve filechooser and paths (#1475)

* some refactoring still needed
This commit is contained in:
Matthias Melcher
2026-06-29 01:42:24 +02:00
parent bad6c58dbb
commit ab656293a9
10 changed files with 357 additions and 65 deletions
+2
View File
@@ -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
+14 -28
View File
@@ -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.
-2
View File
@@ -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.
+7
View File
@@ -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;
+7 -1
View File
@@ -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
+198
View File
@@ -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 <FL/Fl_Native_File_Chooser.H>
#include <FL/filename.H>
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());
}
}
+65
View File
@@ -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 <string>
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
+26 -14
View File
@@ -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 <FL/Fl_Spinner.H>
@@ -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) {
+27 -13
View File
@@ -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();
}
+11 -7
View File
@@ -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<Image_Asset> 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<Image_Asset> ret = (name && *name) ? Fluid.proj.image_assets.find_or_create(name) : nullptr;
"}"
);
std::shared_ptr<Image_Asset> ret = (!name.empty()) ? Fluid.proj.image_assets.find_or_create(name) : nullptr;
Fluid.proj.leave_project_dir();
return ret;
}