diff --git a/fluid/app/project.cxx b/fluid/app/project.cxx new file mode 100644 index 0000000000000000000000000000000000000000..2ce91adc44e510ca7e840615eac2bdfb2376151f --- /dev/null +++ b/fluid/app/project.cxx @@ -0,0 +1,2340 @@ +// +// FLUID main entry for the Fast Light Tool Kit (FLTK). +// +// Copyright 1998-2025 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 "app/fluid.h" + +#include "app/mergeback.h" +#include "app/undo.h" +#include "io/Project_Reader.h" +#include "io/Project_Writer.h" +#include "io/Code_Writer.h" +#include "nodes/Fl_Type.h" +#include "nodes/Fl_Function_Type.h" +#include "nodes/Fl_Group_Type.h" +#include "nodes/Fl_Window_Type.h" +#include "nodes/factory.h" +#include "panels/settings_panel.h" +#include "panels/function_panel.h" +#include "panels/codeview_panel.h" +#include "panels/template_panel.h" +#include "panels/about_panel.h" +#include "rsrcs/pixmaps.h" +#include "app/shell_command.h" +#include "tools/autodoc.h" +#include "widgets/Node_Browser.h" + +#include <FL/Fl.H> +#ifdef __APPLE__ +#include <FL/platform.H> // for fl_open_callback +#endif +#include <FL/Fl_Help_Dialog.H> +#include <FL/Fl_Menu_Bar.H> +#include <FL/Fl_PNG_Image.H> +#include <FL/Fl_Native_File_Chooser.H> +#include <FL/Fl_Printer.H> +#include <FL/fl_string_functions.h> +#include <locale.h> // setlocale().. +#include "../src/flstring.h" + +extern "C" +{ +#if defined(HAVE_LIBPNG) && defined(HAVE_LIBZ) +# include <zlib.h> +# ifdef HAVE_PNG_H +# include <png.h> +# else +# include <libpng/png.h> +# endif // HAVE_PNG_H +#endif // HAVE_LIBPNG && HAVE_LIBZ +} + +/// \defgroup globals Fluid Global Variables, Functions and Callbacks +/// \{ + +// +// Globals.. +// + +/// FLUID-wide help dialog. +static Fl_Help_Dialog *help_dialog = NULL; + +/// Main app window menu bar. +Fl_Menu_Bar *main_menubar = NULL; + +/// Main app window. +Fl_Window *main_window; + +/// Fluid application preferences, always accessible, will be flushed when app closes. +Fl_Preferences fluid_prefs(Fl_Preferences::USER_L, "fltk.org", "fluid"); + +/// Show guides in the design window when positioning widgets, saved in app preferences. +int show_guides = 1; + +/// Show areas of restricted use in overlay plane. +/// Restricted areas are widget that overlap each other, widgets that are outside +/// of their parent's bounds (except children of Scroll groups), and areas +/// within an Fl_Tile that are not covered by children. +int show_restricted = 1; + +/// Show a ghosted outline for groups that have very little contrast. +/// This makes groups with NO_BOX or FLAT_BOX better editable. +int show_ghosted_outline = 1; + +/// Show widget comments in the browser, saved in app preferences. +int show_comments = 1; + +/// Use external editor for editing Fl_Code_Type, saved in app preferences. +int G_use_external_editor = 0; + +/// Debugging help for external Fl_Code_Type editor. +int G_debug = 0; + +/// Run this command to load an Fl_Code_Type into an external editor, save in app preferences. +char G_external_editor_command[512]; + + +// File history info... + +/// Stores the absolute filename of the last 10 design files, saved in app preferences. +char absolute_history[10][FL_PATH_MAX]; + +/// This list of filenames is computed from \c absolute_history and displayed in the main menu. +char relative_history[10][FL_PATH_MAX]; + +/// Menuitem to save a .fl design file, will be deactivated if the design is unchanged. +Fl_Menu_Item *save_item = NULL; + +/// First Menuitem that shows the .fl design file history. +Fl_Menu_Item *history_item = NULL; + +/// Menuitem to show or hide the widget bin, label will change if bin is visible. +Fl_Menu_Item *widgetbin_item = NULL; + +/// Menuitem to show or hide the code view, label will change if view is visible. +Fl_Menu_Item *codeview_item = NULL; + +/// Menuitem to show or hide the editing overlay, label will change if overlay visibility changes. +Fl_Menu_Item *overlay_item = NULL; + +/// Menuitem to show or hide the editing guides, label will change if overlay visibility changes. +Fl_Menu_Item *guides_item = NULL; + +/// Menuitem to show or hide the restricted area overlys, label will change if overlay visibility changes. +Fl_Menu_Item *restricted_item = NULL; + +//////////////////////////////////////////////////////////////// + +/// Filename of the current .fl project file +static const char *filename = NULL; + +/// Set if the current design has been modified compared to the associated .fl design file. +int modflag = 0; + +/// Set if the code files are older than the current design. +int modflag_c = 0; + +/// Application work directory, stored here when temporarily changing to the source code directory. +/// \see goto_source_dir() +static std::string app_work_dir; + +/// Used as a counter to set the .fl project dir as the current directory. +/// \see enter_project_dir(), leave_project_dir() +static char in_project_dir = 0; + +/// Set, if Fluid was started with the command line argument -u +int update_file = 0; // fluid -u + +/// Set, if Fluid was started with the command line argument -c +int compile_file = 0; // fluid -c + +/// Set, if Fluid was started with the command line argument -cs +int compile_strings = 0; // fluid -cs + +/// Set, if Fluid was started with the command line argument -v +int show_version = 0; // fluid -v + +/// Set, if Fluid runs in batch mode, and no user interface is activated. +int batch_mode = 0; // if set (-c, -u) don't open display + +/// command line arguments that overrides the generate code file extension or name +std::string g_code_filename_arg; + +/// command line arguments that overrides the generate header file extension or name +std::string g_header_filename_arg; + +/// current directory path at application launch +std::string g_launch_path; + +/// if set, generate images for automatic documentation in this directory +std::string g_autodoc_path; + +/// path to store temporary files during app run +/// \see tmpdir_create_called +std::string tmpdir_path; + +/// true if the temporary file path was already created +/// \see tmpdir_path +bool tmpdir_create_called = false; + + +/// Offset in pixels when adding widgets from an .fl file. +int pasteoffset = 0; + +/// Paste offset incrementing at every paste command. +static int ipasteoffset = 0; + +// ---- project settings + +/// The current project, possibly a new, empty roject +Fluid_Project g_project; + +/** + Initialize a new project. + */ +Fluid_Project::Fluid_Project() : + i18n_type(FD_I18N_NONE), + include_H_from_C(1), + use_FL_COMMAND(0), + utf8_in_src(0), + avoid_early_includes(0), + header_file_set(0), + code_file_set(0), + write_mergeback_data(0), + header_file_name(".h"), + code_file_name(".cxx") +{ } + +/** + Clear all project resources. + Not implemented. + */ +Fluid_Project::~Fluid_Project() { +} + +/** + Reset all project setting to create a new empty project. + */ +void Fluid_Project::reset() { + ::delete_all(); + i18n_type = FD_I18N_NONE; + + i18n_gnu_include = "<libintl.h>"; + i18n_gnu_conditional = ""; + i18n_gnu_function = "gettext"; + i18n_gnu_static_function = "gettext_noop"; + + i18n_pos_include = "<nl_types.h>"; + i18n_pos_conditional = ""; + i18n_pos_file = ""; + i18n_pos_set = "1"; + + include_H_from_C = 1; + use_FL_COMMAND = 0; + utf8_in_src = 0; + avoid_early_includes = 0; + header_file_set = 0; + code_file_set = 0; + header_file_name = ".h"; + code_file_name = ".cxx"; + write_mergeback_data = 0; +} + +/** + Tell the project and i18n tab of the settings dialog to refresh themselves. + */ +void Fluid_Project::update_settings_dialog() { + if (settings_window) { + w_settings_project_tab->do_callback(w_settings_project_tab, LOAD); + w_settings_i18n_tab->do_callback(w_settings_i18n_tab, LOAD); + } +} + +/** + Make sure that a path name ends with a forward slash. + \param[in] str directory or path name + \return a new string, ending with a '/' + */ +static std::string end_with_slash(const std::string &str) { + char last = str[str.size()-1]; + if (last !='/' && last != '\\') + return str + "/"; + else + return str; +} + +/** + Generate a path to a directory for temporary data storage. + The path is stored in g_tmpdir. + */ +static void create_tmpdir() { + if (tmpdir_create_called) + return; + tmpdir_create_called = true; + + char buf[128]; +#if _WIN32 + // The usual temp file locations on Windows are + // %system%\Windows\Temp + // %userprofiles%\AppData\Local + // usually resolving into + // C:/Windows/Temp/ + // C:\Users\<username>\AppData\Local\Temp + fl_snprintf(buf, sizeof(buf)-1, "fluid-%d/", (long)GetCurrentProcessId()); + std::string name = buf; + wchar_t tempdirW[FL_PATH_MAX+1]; + char tempdir[FL_PATH_MAX+1]; + unsigned len = GetTempPathW(FL_PATH_MAX, tempdirW); + if (len == 0) { + strcpy(tempdir, "c:/windows/temp/"); + } else { + unsigned wn = fl_utf8fromwc(tempdir, FL_PATH_MAX, tempdirW, len); + tempdir[wn] = 0; + } + std::string path = tempdir; + end_with_slash(path); + path += name; + fl_make_path(path.c_str()); + if (fl_access(path.c_str(), 6) == 0) tmpdir_path = path; +#else + fl_snprintf(buf, sizeof(buf)-1, "fluid-%d/", getpid()); + std::string name = buf; + std::string path = fl_getenv("TMPDIR"); + if (!path.empty()) { + end_with_slash(path); + path += name; + fl_make_path(path.c_str()); + if (fl_access(path.c_str(), 6) == 0) tmpdir_path = path; + } + if (tmpdir_path.empty()) { + path = std::string("/tmp/") + name; + fl_make_path(path.c_str()); + if (fl_access(path.c_str(), 6) == 0) tmpdir_path = path; + } +#endif + if (tmpdir_path.empty()) { + char pbuf[FL_PATH_MAX+1]; + fluid_prefs.get_userdata_path(pbuf, FL_PATH_MAX); + path = std::string(pbuf); + end_with_slash(path); + path += name; + fl_make_path(path.c_str()); + if (fl_access(path.c_str(), 6) == 0) tmpdir_path = path; + } + if (tmpdir_path.empty()) { + if (batch_mode) { + fprintf(stderr, "ERROR: Can't create directory for temporary data storage.\n"); + } else { + fl_alert("Can't create directory for temporary data storage."); + } + } +} + +/** + Delete the temporary directory that was created in set_tmpdir. + */ +static void delete_tmpdir() { + // was a temporary directory created + if (!tmpdir_create_called) + return; + if (tmpdir_path.empty()) + return; + + // first delete all files that may still be left in the temp directory + struct dirent **de; + int n_de = fl_filename_list(tmpdir_path.c_str(), &de); + if (n_de >= 0) { + for (int i=0; i<n_de; i++) { + std::string path = tmpdir_path + de[i]->d_name; + fl_unlink(path.c_str()); + } + fl_filename_free_list(&de, n_de); + } + + // then delete the directory itself + if (fl_rmdir(tmpdir_path.c_str()) < 0) { + if (batch_mode) { + fprintf(stderr, "WARNING: Can't delete tmpdir '%s': %s", tmpdir_path.c_str(), strerror(errno)); + } else { + fl_alert("WARNING: Can't delete tmpdir '%s': %s", tmpdir_path.c_str(), strerror(errno)); + } + } +} + +/** + Return the path to a temporary directory for this instance of FLUID. + Fluid will do its best to clear and delete this directory when exiting. + \return the path to the temporary directory, ending in a '/', or and empty + string if no directory could be created. + */ +const std::string &get_tmpdir() { + if (!tmpdir_create_called) + create_tmpdir(); + return tmpdir_path; +} + +/** + Give the user the opportunity to save a project before clearing it. + + If the project has unsaved changes, this function pops up a dialog, that + allows the user to save the project, continue without saving the project, + or to cancel the operation. + + If the user chooses to save, and no filename was set, a file dialog allows + the user to pick a name and location, or to cancel the operation. + + \return false if the user aborted the operation and the calling function + should abort as well + */ +bool confirm_project_clear() { + if (modflag == 0) return true; + switch (fl_choice("This project has unsaved changes. Do you want to save\n" + "the project file before proceeding?", + "Cancel", "Save", "Don't Save")) + { + case 0 : /* Cancel */ + return false; + case 1 : /* Save */ + save_cb(NULL, NULL); + if (modflag) return false; // user canceled the "Save As" dialog + } + return true; +} + +// ---- + +extern Fl_Window *the_panel; + +/** + Ensure that text widgets in the widget panel propagates apply current changes. + By temporarily clearing the text focus, all text widgets with changed text + will unfocus and call their respective callbacks, propagating those changes to + their data set. + */ +void flush_text_widgets() { + if (Fl::focus() && (Fl::focus()->top_window() == the_panel)) { + Fl_Widget *old_focus = Fl::focus(); + Fl::focus(NULL); // trigger callback of the widget that is losing focus + Fl::focus(old_focus); + } +} + +// ---- + +/** + Change the current working directory to the .fl project directory. + + Every call to enter_project_dir() must have a corresponding leave_project_dir() + call. Enter and leave calls can be nested. + + The first call to enter_project_dir() remembers the original directory, usually + the launch directory of the application. Nested calls will increment a nesting + counter. When the nesting counter is back to 0, leave_project_dir() will return + to the original directory. + + The global variable 'filename' must be set to the current project file with + absolute or relative path information. + + \see leave_project_dir(), pwd, in_project_dir + */ +void enter_project_dir() { + if (in_project_dir<0) { + fprintf(stderr, "** Fluid internal error: enter_project_dir() calls unmatched\n"); + return; + } + in_project_dir++; + // check if we are already in the project dir and do nothing if so + if (in_project_dir>1) return; + // check if there is an active project, and do nothing if there is none + if (!filename || !*filename) { + fprintf(stderr, "** Fluid internal error: enter_project_dir() no filename set\n"); + return; + } + // store the current working directory for later + app_work_dir = fl_getcwd_str(); + // set the current directory to the path of our .fl file + std::string project_path = fl_filename_path_str(fl_filename_absolute_str(filename)); + if (fl_chdir(project_path.c_str()) == -1) { + fprintf(stderr, "** Fluid internal error: enter_project_dir() can't chdir to %s: %s\n", + project_path.c_str(), strerror(errno)); + return; + } + //fprintf(stderr, "chdir from %s to %s\n", app_work_dir.c_str(), fl_getcwd().c_str()); +} + +/** + Change the current working directory to the previous directory. + \see enter_project_dir(), pwd, in_project_dir + */ +void leave_project_dir() { + if (in_project_dir == 0) { + fprintf(stderr, "** Fluid internal error: leave_project_dir() calls unmatched\n"); + return; + } + in_project_dir--; + // still nested, stay in the project directory + if (in_project_dir > 0) return; + // no longer nested, return to the original, usually the application working directory + if (fl_chdir(app_work_dir.c_str()) < 0) { + fprintf(stderr, "** Fluid internal error: leave_project_dir() can't chdir back to %s : %s\n", + app_work_dir.c_str(), strerror(errno)); + } +} + +/** + Position the given window window based on entries in the app preferences. + Customisable by user; feature can be switched off. + The window is not shown or hidden by this function, but a value is returned + to indicate the state to the caller. + \param[in] w position this window + \param[in] prefsName name of the preferences item that stores the window settings + \param[in] Visible default value if window is hidden or shown + \param[in] X, Y, W, H default size and position if nothing is specified in the preferences + \return 1 if the caller should make the window visible, 0 if hidden. + */ +char position_window(Fl_Window *w, const char *prefsName, int Visible, int X, int Y, int W, int H) { + Fl_Preferences pos(fluid_prefs, prefsName); + if (prevpos_button->value()) { + pos.get("x", X, X); + pos.get("y", Y, Y); + if ( W!=0 ) { + pos.get("w", W, W); + pos.get("h", H, H); + w->resize( X, Y, W, H ); + } + else + w->position( X, Y ); + } + pos.get("visible", Visible, Visible); + return Visible; +} + +/** + Save the position and visibility state of a window to the app preferences. + \param[in] w save this window data + \param[in] prefsName name of the preferences item that stores the window settings + */ +void save_position(Fl_Window *w, const char *prefsName) { + Fl_Preferences pos(fluid_prefs, prefsName); + pos.set("x", w->x()); + pos.set("y", w->y()); + pos.set("w", w->w()); + pos.set("h", w->h()); + pos.set("visible", (int)(w->shown() && w->visible())); +} + +/** + Return the path and filename of a temporary file for cut or duplicated data. + \param[in] which 0 gets the cut/copy/paste buffer, 1 gets the duplication buffer + \return a pointer to a string in a static buffer + */ +static char* cutfname(int which = 0) { + static char name[2][FL_PATH_MAX]; + static char beenhere = 0; + + if (!beenhere) { + beenhere = 1; + fluid_prefs.getUserdataPath(name[0], sizeof(name[0])); + strlcat(name[0], "cut_buffer", sizeof(name[0])); + fluid_prefs.getUserdataPath(name[1], sizeof(name[1])); + strlcat(name[1], "dup_buffer", sizeof(name[1])); + } + + return name[which]; +} + +/** + Timer to watch for external editor modifications. + + If one or more external editors open, check if their files were modified. + If so: reload to ram, update size/mtime records, and change fluid's + 'modified' state. + */ +static void external_editor_timer(void*) { + int editors_open = ExternalCodeEditor::editors_open(); + if ( G_debug ) printf("--- TIMER --- External editors open=%d\n", editors_open); + if ( editors_open > 0 ) { + // Walk tree looking for files modified by external editors. + int modified = 0; + for (Fl_Type *p = Fl_Type::first; p; p = p->next) { + if ( p->is_a(ID_Code) ) { + Fl_Code_Type *code = (Fl_Code_Type*)p; + // Code changed by external editor? + if ( code->handle_editor_changes() ) { // updates ram, file size/mtime + modified++; + } + if ( code->is_editing() ) { // editor open? + code->reap_editor(); // Try to reap; maybe it recently closed + } + } + } + if ( modified ) set_modflag(1); + } + // Repeat timeout if editors still open + // The ExternalCodeEditor class handles start/stopping timer, we just + // repeat_timeout() if it's already on. NOTE: above code may have reaped + // only open editor, which would disable further timeouts. So *recheck* + // if editors still open, to ensure we don't accidentally re-enable them. + // + if ( ExternalCodeEditor::editors_open() ) { + Fl::repeat_timeout(2.0, external_editor_timer); + } +} + +/** + Save the current design to the file given by \c filename. + If automatic, this overwrites an existing file. If interactive, if will + verify with the user. + \param[in] v if v is not NULL, or no filename is set, open a filechooser. + */ +void save_cb(Fl_Widget *, void *v) { + flush_text_widgets(); + Fl_Native_File_Chooser fnfc; + const char *c = filename; + if (v || !c || !*c) { + fnfc.title("Save To:"); + fnfc.type(Fl_Native_File_Chooser::BROWSE_SAVE_FILE); + fnfc.filter("FLUID Files\t*.f[ld]"); + if (fnfc.show() != 0) return; + c = fnfc.filename(); + if (!fl_access(c, 0)) { + std::string basename = fl_filename_name_str(std::string(c)); + if (fl_choice("The file \"%s\" already exists.\n" + "Do you want to replace it?", "Cancel", + "Replace", NULL, basename.c_str()) == 0) return; + } + + if (v != (void *)2) set_filename(c); + } + if (!fld::io::write_file(c)) { + fl_alert("Error writing %s: %s", c, strerror(errno)); + return; + } + + if (v != (void *)2) { + set_modflag(0, 1); + undo_save = undo_current; + } +} + +/** + Save a design template. + \todo We should document the concept of templates. + */ +void save_template_cb(Fl_Widget *, void *) { + // Setup the template panel... + if (!template_panel) make_template_panel(); + + template_clear(); + template_browser->add("New Template"); + template_load(); + + template_name->show(); + template_name->value(""); + + template_instance->hide(); + + template_delete->show(); + template_delete->deactivate(); + + template_submit->label("Save"); + template_submit->deactivate(); + + template_panel->label("Save Template"); + + // Show the panel and wait for the user to do something... + template_panel->show(); + while (template_panel->shown()) Fl::wait(); + + // Get the template name, return if it is empty... + const char *c = template_name->value(); + if (!c || !*c) return; + + // Convert template name to filename_with_underscores + char savename[FL_PATH_MAX], *saveptr; + strlcpy(savename, c, sizeof(savename)); + for (saveptr = savename; *saveptr; saveptr ++) { + if (isspace(*saveptr)) *saveptr = '_'; + } + + // Find the templates directory... + char filename[FL_PATH_MAX]; + fluid_prefs.getUserdataPath(filename, sizeof(filename)); + + strlcat(filename, "templates", sizeof(filename)); + if (fl_access(filename, 0)) fl_make_path(filename); + + strlcat(filename, "/", sizeof(filename)); + strlcat(filename, savename, sizeof(filename)); + + char *ext = filename + strlen(filename); + if (ext >= (filename + sizeof(filename) - 5)) { + fl_alert("The template name \"%s\" is too long!", c); + return; + } + + // Save the .fl file... + strcpy(ext, ".fl"); + + if (!fl_access(filename, 0)) { + if (fl_choice("The template \"%s\" already exists.\n" + "Do you want to replace it?", "Cancel", + "Replace", NULL, c) == 0) return; + } + + if (!fld::io::write_file(filename)) { + fl_alert("Error writing %s: %s", filename, strerror(errno)); + return; + } + +#if defined(HAVE_LIBPNG) && defined(HAVE_LIBZ) + // Get the screenshot, if any... + Fl_Type *t; + + for (t = Fl_Type::first; t; t = t->next) { + // Find the first window... + if (t->is_a(ID_Window)) break; + } + + if (!t) return; + + // Grab a screenshot... + Fl_Window_Type *wt = (Fl_Window_Type *)t; + uchar *pixels; + int w, h; + + if ((pixels = wt->read_image(w, h)) == NULL) return; + + // Save to a PNG file... + strcpy(ext, ".png"); + + errno = 0; + if (fl_write_png(filename, pixels, w, h, 3) != 0) { + delete[] pixels; + fl_alert("Error writing %s: %s", filename, strerror(errno)); + return; + } + +# if 0 // The original PPM output code... + strcpy(ext, ".ppm"); + fp = fl_fopen(filename, "wb"); + fprintf(fp, "P6\n%d %d 255\n", w, h); + fwrite(pixels, w * h, 3, fp); + fclose(fp); +# endif // 0 + + delete[] pixels; +#endif // HAVE_LIBPNG && HAVE_LIBZ +} + +/** + Reload the file set by \c filename, replacing the current design. + If the design was modified, a dialog will ask for confirmation. + */ +void revert_cb(Fl_Widget *,void *) { + if (modflag) { + if (!fl_choice("This user interface has been changed. Really revert?", + "Cancel", "Revert", NULL)) return; + } + undo_suspend(); + if (!fld::io::read_file(filename, 0)) { + undo_resume(); + widget_browser->rebuild(); + g_project.update_settings_dialog(); + fl_message("Can't read %s: %s", filename, strerror(errno)); + return; + } + widget_browser->rebuild(); + undo_resume(); + set_modflag(0, 0); + undo_clear(); + g_project.update_settings_dialog(); +} + +/** + Exit Fluid; we hope you had a nice experience. + If the design was modified, a dialog will ask for confirmation. + */ +void exit_cb(Fl_Widget *,void *) { + if (shell_command_running()) { + int choice = fl_choice("Previous shell command still running!", + "Cancel", + "Exit", + NULL); + if (choice == 0) { // user chose to cancel the exit operation + return; + } + } + + flush_text_widgets(); + + // verify user intention + if (confirm_project_clear() == false) + return; + + // Stop any external editor update timers + ExternalCodeEditor::stop_update_timer(); + + save_position(main_window,"main_window_pos"); + + if (widgetbin_panel) { + save_position(widgetbin_panel,"widgetbin_pos"); + delete widgetbin_panel; + } + if (codeview_panel) { + Fl_Preferences svp(fluid_prefs, "codeview"); + svp.set("autorefresh", cv_autorefresh->value()); + svp.set("autoposition", cv_autoposition->value()); + svp.set("tab", cv_tab->find(cv_tab->value())); + svp.set("code_choice", cv_code_choice); + save_position(codeview_panel,"codeview_pos"); + delete codeview_panel; + codeview_panel = 0; + } + if (shell_run_window) { + save_position(shell_run_window,"shell_run_Window_pos"); + } + + if (about_panel) + delete about_panel; + if (help_dialog) + delete help_dialog; + + if (g_shell_config) + g_shell_config->write(fluid_prefs, FD_STORE_USER); + g_layout_list.write(fluid_prefs, FD_STORE_USER); + + undo_clear(); + + // Destroy tree + // Doing so causes dtors to automatically close all external editors + // and cleans up editor tmp files. Then remove fluid tmpdir /last/. + g_project.reset(); + ExternalCodeEditor::tmpdir_clear(); + delete_tmpdir(); + + exit(0); +} + +/** + Clear the current project and create a new, empty one. + + If the current project was modified, FLUID will give the user the opportunity + to save the old project first. + + \param[in] user_must_confirm if set, a confimation dialog is presented to the + user before resetting the project. Default is `true`. + \return false if the operation was canceled + */ +bool new_project(bool user_must_confirm) { + // verify user intention + if ((user_must_confirm) && (confirm_project_clear() == false)) + return false; + + // clear the current project + g_project.reset(); + set_filename(NULL); + set_modflag(0, 0); + widget_browser->rebuild(); + g_project.update_settings_dialog(); + + // all is clear to continue + return true; +} + +/** + Open the template browser and load a new file from templates. + + If the current project was modified, FLUID will give the user the opportunity + to save the old project first. + + \return false if the operation was canceled or failed otherwise + */ +bool new_project_from_template() { + // clear the current project first + if (new_project() == false) + return false; + + // Setup the template panel... + if (!template_panel) make_template_panel(); + + template_clear(); + template_browser->add("Blank"); + template_load(); + + template_name->hide(); + template_name->value(""); + + template_instance->show(); + template_instance->deactivate(); + template_instance->value(""); + + template_delete->show(); + + template_submit->label("New"); + template_submit->deactivate(); + + template_panel->label("New"); + + //if ( template_browser->size() == 1 ) { // only one item? + template_browser->value(1); // select it + template_browser->do_callback(); + //} + + // Show the panel and wait for the user to do something... + template_panel->show(); + while (template_panel->shown()) Fl::wait(); + + // See if the user chose anything... + int item = template_browser->value(); + if (item < 1) return false; + + // Load the template, if any... + const char *tname = (const char *)template_browser->data(item); + + if (tname) { + // Grab the instance name... + const char *iname = template_instance->value(); + + if (iname && *iname) { + // Copy the template to a temp file, then read it in... + char line[1024], *ptr, *next; + FILE *infile, *outfile; + + if ((infile = fl_fopen(tname, "rb")) == NULL) { + fl_alert("Error reading template file \"%s\":\n%s", tname, + strerror(errno)); + set_modflag(0); + undo_clear(); + return false; + } + + if ((outfile = fl_fopen(cutfname(1), "wb")) == NULL) { + fl_alert("Error writing buffer file \"%s\":\n%s", cutfname(1), + strerror(errno)); + fclose(infile); + set_modflag(0); + undo_clear(); + return false; + } + + while (fgets(line, sizeof(line), infile)) { + // Replace @INSTANCE@ with the instance name... + for (ptr = line; (next = strstr(ptr, "@INSTANCE@")) != NULL; ptr = next + 10) { + fwrite(ptr, next - ptr, 1, outfile); + fputs(iname, outfile); + } + + fputs(ptr, outfile); + } + + fclose(infile); + fclose(outfile); + + undo_suspend(); + fld::io::read_file(cutfname(1), 0); + fl_unlink(cutfname(1)); + undo_resume(); + } else { + // No instance name, so read the template without replacements... + undo_suspend(); + fld::io::read_file(tname, 0); + undo_resume(); + } + } + + widget_browser->rebuild(); + g_project.update_settings_dialog(); + set_modflag(0); + undo_clear(); + + return true; +} + +/** + 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 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 (filename) { + std::string current_project_file = 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()); +} + +/** + Load a project from the give file name and path. + + The project file is inserted at the currently selected type. + + If no filename is given, FLUID will open a file chooser dialog. + + \param[in] filename_arg path and name of the new project file + \return false if the operation failed + */ +bool merge_project_file(const std::string &filename_arg) { + bool is_a_merge = (Fl_Type::first != NULL); + std::string title = is_a_merge ? "Merge Project File" : "Open Project File"; + + // ask for a filename if none was given + std::string new_filename = filename_arg; + if (new_filename.empty()) { + new_filename = open_project_filechooser(title); + if (new_filename.empty()) { + return false; + } + } + + const char *c = new_filename.c_str(); + const char *oldfilename = filename; + filename = NULL; + set_filename(c); + if (is_a_merge) undo_checkpoint(); + undo_suspend(); + if (!fld::io::read_file(c, is_a_merge)) { + undo_resume(); + widget_browser->rebuild(); + g_project.update_settings_dialog(); + fl_message("Can't read %s: %s", c, strerror(errno)); + free((void *)filename); + filename = oldfilename; + if (main_window) set_modflag(modflag); + return false; + } + undo_resume(); + widget_browser->rebuild(); + if (is_a_merge) { + // Inserting a file; restore the original filename... + set_filename(oldfilename); + set_modflag(1); + } else { + // Loaded a file; free the old filename... + set_modflag(0, 0); + undo_clear(); + } + if (oldfilename) free((void *)oldfilename); + return true; +} + +/** + Open a file chooser and load an exiting project file. + + If the current project was modified, FLUID will give the user the opportunity + to save the old project first. + + If no filename is given, FLUID will open a file chooser dialog. + + \param[in] filename_arg load from this file, or show file chooser if empty + \return false if the operation was canceled or failed otherwise + */ +bool open_project_file(const std::string &filename_arg) { + // verify user intention + if (confirm_project_clear() == false) + return false; + + // 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"); + if (new_filename.empty()) { + return false; + } + } + + // clear the project and merge a file by the given name + new_project(false); + return merge_project_file(new_filename); +} + +#ifdef __APPLE__ +/** + Handle app launch with an associated filename (macOS only). + Should there be a modified design already, Fluid asks for user confirmation. + \param[in] c the filename of the new design + */ +void apple_open_cb(const char *c) { + open_project_file(std::string(c)); +} +#endif // __APPLE__ + +/** + Get the absolute path of the project file, for example `/Users/matt/dev/`. + \return the path ending in '/' + */ +std::string Fluid_Project::projectfile_path() const { + return end_with_slash(fl_filename_absolute_str(fl_filename_path_str(filename), g_launch_path)); +} + +/** + Get the project file name including extension, for example `test.fl`. + \return the file name without path + */ +std::string Fluid_Project::projectfile_name() const { + return fl_filename_name(filename); +} + +/** + Get the absolute path of the generated C++ code file, for example `/Users/matt/dev/src/`. + \return the path ending in '/' + */ +std::string Fluid_Project::codefile_path() const { + std::string path = fl_filename_path_str(code_file_name); + if (batch_mode) + return end_with_slash(fl_filename_absolute_str(path, g_launch_path)); + else + return end_with_slash(fl_filename_absolute_str(path, projectfile_path())); +} + +/** + Get the generated C++ code file name including extension, for example `test.cxx`. + \return the file name without path + */ +std::string Fluid_Project::codefile_name() const { + std::string name = fl_filename_name_str(code_file_name); + if (name.empty()) { + return fl_filename_setext_str(fl_filename_name(filename), ".cxx"); + } else if (name[0] == '.') { + return fl_filename_setext_str(fl_filename_name(filename), code_file_name); + } else { + return name; + } +} + +/** + Get the absolute path of the generated C++ header file, for example `/Users/matt/dev/src/`. + \return the path ending in '/' + */ +std::string Fluid_Project::headerfile_path() const { + std::string path = fl_filename_path_str(header_file_name); + if (batch_mode) + return end_with_slash(fl_filename_absolute_str(path, g_launch_path)); + else + return end_with_slash(fl_filename_absolute_str(path, projectfile_path())); +} + +/** + Get the generated C++ header file name including extension, for example `test.cxx`. + \return the file name without path + */ +std::string Fluid_Project::headerfile_name() const { + std::string name = fl_filename_name_str(header_file_name); + if (name.empty()) { + return fl_filename_setext_str(fl_filename_name_str(filename), ".h"); + } else if (name[0] == '.') { + return fl_filename_setext_str(fl_filename_name_str(filename), header_file_name); + } else { + return name; + } +} + +/** + Get the absolute path of the generated i18n strings file, for example `/Users/matt/dev/`. + Although it may be more useful to put the text file into the same directory + with the source and header file, historically, the text is always saved with + the project file in interactive mode, and in the FLUID launch directory in + batch mode. + \return the path ending in '/' + */ +std::string Fluid_Project::stringsfile_path() const { + if (batch_mode) + return g_launch_path; + else + return projectfile_path(); +} + +/** + Get the generated i18n text file name including extension, for example `test.po`. + \return the file name without path + */ +std::string Fluid_Project::stringsfile_name() const { + switch (i18n_type) { + default: return fl_filename_setext_str(fl_filename_name(filename), ".txt"); + case FD_I18N_GNU: return fl_filename_setext_str(fl_filename_name(filename), ".po"); + case FD_I18N_POSIX: return fl_filename_setext_str(fl_filename_name(filename), ".msg"); + } +} + +/** + Get the name of the project file without the filename extension. + \return the file name without path or extension + */ +std::string Fluid_Project::basename() const { + return fl_filename_setext_str(fl_filename_name(filename), ""); +} + +/** + Generate the C++ source and header filenames and write those files. + + This function creates the source filename by setting the file + extension to \c code_file_name and a header filename + with the extension \c code_file_name which are both + settable by the user. + + If the code filename has not been set yet, a "save file as" dialog will be + presented to the user. + + In batch_mode, the function will either be silent, or, if opening or writing + the files fails, write an error message to \c stderr and exit with exit code 1. + + In interactive mode, it will pop up an error message, or, if the user + hasn't disabled that, pop up a confirmation message. + + \param[in] dont_show_completion_dialog don't show the completion dialog + \return 1 if the operation failed, 0 if it succeeded + */ +int write_code_files(bool dont_show_completion_dialog) +{ + // -- handle user interface issues + flush_text_widgets(); + if (!filename) { + save_cb(0,0); + if (!filename) return 1; + } + + // -- generate the file names with absolute paths + fld::io::Code_Writer f; + std::string code_filename = g_project.codefile_path() + g_project.codefile_name(); + std::string header_filename = g_project.headerfile_path() + g_project.headerfile_name(); + + // -- write the code and header files + if (!batch_mode) enter_project_dir(); + int x = f.write_code(code_filename.c_str(), header_filename.c_str()); + std::string code_filename_rel = fl_filename_relative_str(code_filename); + std::string header_filename_rel = fl_filename_relative_str(header_filename); + if (!batch_mode) leave_project_dir(); + + // -- print error message in batch mode or pop up an error or confirmation dialog box + if (batch_mode) { + if (!x) { + fprintf(stderr, "%s and %s: %s\n", + code_filename_rel.c_str(), + header_filename_rel.c_str(), + strerror(errno)); + exit(1); + } + } else { + if (!x) { + fl_message("Can't write %s or %s: %s", + code_filename_rel.c_str(), + header_filename_rel.c_str(), + strerror(errno)); + } else { + set_modflag(-1, 0); + if (dont_show_completion_dialog==false && completion_button->value()) { + fl_message("Wrote %s and %s", + code_filename_rel.c_str(), + header_filename_rel.c_str()); + } + } + } + return 0; +} + +/** + Callback to write C++ code and header files. + */ +void write_cb(Fl_Widget *, void *) { + write_code_files(); +} + +#if 0 +// Matt: disabled +/** + Merge the possibly modified content of code files back into the project. + */ +int mergeback_code_files() +{ + flush_text_widgets(); + if (!filename) return 1; + if (!g_project.write_mergeback_data) { + fl_message("MergeBack is not enabled for this project.\n" + "Please enable MergeBack in the project settings\n" + "dialog and re-save the project file and the code."); + return 0; + } + + std::string proj_filename = g_project.projectfile_path() + g_project.projectfile_name(); + std::string code_filename; +#if 1 + if (!batch_mode) { + Fl_Preferences build_records(Fl_Preferences::USER_L, "fltk.org", "fluid-build"); + Fl_Preferences path(build_records, proj_filename.c_str()); + int i, n = proj_filename.size(); + for (i=0; i<n; i++) if (proj_filename[i]=='\\') proj_filename[i] = '/'; + preferences_get(path, "code", code_filename, ""); + } +#endif + if (code_filename.empty()) + code_filename = g_project.codefile_path() + g_project.codefile_name(); + if (!batch_mode) enter_project_dir(); + int c = merge_back(code_filename, proj_filename, FD_MERGEBACK_INTERACTIVE); + if (!batch_mode) leave_project_dir(); + + if (c==0) fl_message("Comparing\n \"%s\"\nto\n \"%s\"\n\n" + "MergeBack found no external modifications\n" + "in the source code.", + code_filename.c_str(), proj_filename.c_str()); + if (c==-2) fl_message("No corresponding source code file found."); + return c; +} + +void mergeback_cb(Fl_Widget *, void *) { + mergeback_code_files(); +} +#endif + +/** + Write the strings that are used in i18n. + */ +void write_strings_cb(Fl_Widget *, void *) { + flush_text_widgets(); + if (!filename) { + save_cb(0,0); + if (!filename) return; + } + std::string filename = g_project.stringsfile_path() + g_project.stringsfile_name(); + int x = write_strings(filename); + if (batch_mode) { + if (x) { + fprintf(stderr, "%s : %s\n", filename.c_str(), strerror(errno)); + exit(1); + } + } else { + if (x) { + fl_message("Can't write %s: %s", filename.c_str(), strerror(errno)); + } else if (completion_button->value()) { + fl_message("Wrote %s", g_project.stringsfile_name().c_str()); + } + } +} + +/** + Show the editor for the \c current Fl_Type. + */ +void openwidget_cb(Fl_Widget *, void *) { + if (!Fl_Type::current) { + fl_message("Please select a widget"); + return; + } + Fl_Type::current->open(); +} + +/** + User chose to copy the currently selected widgets. + */ +void copy_cb(Fl_Widget*, void*) { + flush_text_widgets(); + if (!Fl_Type::current) { + fl_beep(); + return; + } + flush_text_widgets(); + ipasteoffset = 10; + if (!fld::io::write_file(cutfname(),1)) { + fl_message("Can't write %s: %s", cutfname(), strerror(errno)); + return; + } +} + +/** + User chose to cut the currently selected widgets. + */ +void cut_cb(Fl_Widget *, void *) { + if (!Fl_Type::current) { + fl_beep(); + return; + } + flush_text_widgets(); + if (!fld::io::write_file(cutfname(),1)) { + fl_message("Can't write %s: %s", cutfname(), strerror(errno)); + return; + } + undo_checkpoint(); + set_modflag(1); + ipasteoffset = 0; + Fl_Type *p = Fl_Type::current->parent; + while (p && p->selected) p = p->parent; + delete_all(1); + if (p) select_only(p); + widget_browser->rebuild(); +} + +/** + User chose to delete the currently selected widgets. + */ +void delete_cb(Fl_Widget *, void *) { + if (!Fl_Type::current) { + fl_beep(); + return; + } + undo_checkpoint(); + set_modflag(1); + ipasteoffset = 0; + Fl_Type *p = Fl_Type::current->parent; + while (p && p->selected) p = p->parent; + delete_all(1); + if (p) select_only(p); + widget_browser->rebuild(); +} + +/** + User chose to paste the widgets from the cut buffer. + + This function will paste the widgets in the cut buffer after the currently + selected widget. If the currently selected widget is a group widget and + it is not folded, the new widgets will be added inside the group. + */ +void paste_cb(Fl_Widget*, void*) { + pasteoffset = ipasteoffset; + undo_checkpoint(); + undo_suspend(); + Strategy strategy = Strategy::FROM_FILE_AFTER_CURRENT; + if (Fl_Type::current && Fl_Type::current->can_have_children()) { + if (Fl_Type::current->folded_ == 0) { + // If the current widget is a group widget and it is not folded, + // add the new widgets inside the group. + strategy = Strategy::FROM_FILE_AS_LAST_CHILD; + // The following alternative also works quite nicely + //strategy = Strategy::FROM_FILE_AS_FIRST_CHILD; + } + } + if (!fld::io::read_file(cutfname(), 1, strategy)) { + widget_browser->rebuild(); + fl_message("Can't read %s: %s", cutfname(), strerror(errno)); + } + undo_resume(); + widget_browser->display(Fl_Type::current); + widget_browser->rebuild(); + pasteoffset = 0; + ipasteoffset += 10; +} + +/** + Duplicate the selected widgets. + + This code is a bit complex because it needs to find the last selected + widget with the lowest level, so that the new widgets are inserted after + this one. + */ +void duplicate_cb(Fl_Widget*, void*) { + if (!Fl_Type::current) { + fl_beep(); + return; + } + + // flush the text widgets to make sure the user's changes are saved: + flush_text_widgets(); + + // find the last selected node with the lowest level: + int lowest_level = 9999; + Fl_Type *new_insert = NULL; + if (Fl_Type::current->selected) { + for (Fl_Type *t = Fl_Type::first; t; t = t->next) { + if (t->selected && (t->level <= lowest_level)) { + lowest_level = t->level; + new_insert = t; + } + } + } + if (new_insert) + Fl_Type::current = new_insert; + + // write the selected widgets to a file: + if (!fld::io::write_file(cutfname(1),1)) { + fl_message("Can't write %s: %s", cutfname(1), strerror(errno)); + return; + } + + // read the file and add the widgets after the current one: + pasteoffset = 0; + undo_checkpoint(); + undo_suspend(); + if (!fld::io::read_file(cutfname(1), 1, Strategy::FROM_FILE_AFTER_CURRENT)) { + fl_message("Can't read %s: %s", cutfname(1), strerror(errno)); + } + fl_unlink(cutfname(1)); + widget_browser->display(Fl_Type::current); + widget_browser->rebuild(); + undo_resume(); +} + +/** + User wants to sort selected widgets by y coordinate. + */ +static void sort_cb(Fl_Widget *,void *) { + undo_checkpoint(); + sort((Fl_Type*)NULL); + widget_browser->rebuild(); + set_modflag(1); +} + +/** + Open the "About" dialog. + */ +void about_cb(Fl_Widget *, void *) { + if (!about_panel) make_about_panel(); + about_panel->show(); +} + +/** + Open a dialog to show the HTML help page form the FLTK documentation folder. + \param[in] name name of the HTML help file. + */ +void show_help(const char *name) { + const char *docdir; + char helpname[FL_PATH_MAX]; + + if (!help_dialog) help_dialog = new Fl_Help_Dialog(); + + if ((docdir = fl_getenv("FLTK_DOCDIR")) == NULL) { + docdir = FLTK_DOCDIR; + } + snprintf(helpname, sizeof(helpname), "%s/%s", docdir, name); + + // make sure that we can read the file + FILE *f = fopen(helpname, "rb"); + if (f) { + fclose(f); + help_dialog->load(helpname); + } else { + // if we can not read the file, we display the canned version instead + // or ask the native browser to open the page on www.fltk.org + if (strcmp(name, "fluid.html")==0) { + if (!Fl_Shared_Image::find("embedded:/fluid_flow_chart_800.png")) + new Fl_PNG_Image("embedded:/fluid_flow_chart_800.png", fluid_flow_chart_800_png, sizeof(fluid_flow_chart_800_png)); + help_dialog->value + ( + "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n" + "<html><head><title>FLTK: Programming with FLUID</title></head><body>\n" + "<h2>What is FLUID?</h2>\n" + "The Fast Light User Interface Designer, or FLUID, is a graphical editor " + "that is used to produce FLTK source code. FLUID edits and saves its state " + "in <code>.fl</code> files. These files are text, and you can (with care) " + "edit them in a text editor, perhaps to get some special effects.<p>\n" + "FLUID can \"compile\" the <code>.fl</code> file into a <code>.cxx</code> " + "and a <code>.h</code> file. The <code>.cxx</code> file defines all the " + "objects from the <code>.fl</code> file and the <code>.h</code> file " + "declares all the global ones. FLUID also supports localization " + "(Internationalization) of label strings using message files and the GNU " + "gettext or POSIX catgets interfaces.<p>\n" + "A simple program can be made by putting all your code (including a <code>" + "main()</code> function) into the <code>.fl</code> file and thus making the " + "<code>.cxx</code> file a single source file to compile. Most programs are " + "more complex than this, so you write other <code>.cxx</code> files that " + "call the FLUID functions. These <code>.cxx</code> files must <code>" + "#include</code> the <code>.h</code> file or they can <code>#include</code> " + "the <code>.cxx</code> file so it still appears to be a single source file.<p>" + "<img src=\"embedded:/fluid_flow_chart_800.png\"></p>" + "<p>More information is available online at <a href=" + "\"https://www.fltk.org/doc-1.4/fluid.html\">https://www.fltk.org/</a>" + "</body></html>" + ); + } else if (strcmp(name, "license.html")==0) { + fl_open_uri("https://www.fltk.org/doc-1.4/license.html"); + return; + } else if (strcmp(name, "index.html")==0) { + fl_open_uri("https://www.fltk.org/doc-1.4/index.html"); + return; + } else { + snprintf(helpname, sizeof(helpname), "https://www.fltk.org/%s", name); + fl_open_uri(helpname); + return; + } + } + help_dialog->show(); +} + +/** + User wants help on Fluid. + */ +void help_cb(Fl_Widget *, void *) { + show_help("fluid.html"); +} + +/** + User wants to see the Fluid manual. + */ +void manual_cb(Fl_Widget *, void *) { + show_help("index.html"); +} + +// ---- Printing + +/** + Open the dialog to allow the user to print the current window. + */ +void print_menu_cb(Fl_Widget *, void *) { + int w, h, ww, hh; + int frompage, topage; + Fl_Type *t; // Current widget + int num_windows; // Number of windows + Fl_Window_Type *windows[1000]; // Windows to print + int winpage; // Current window page + Fl_Window *win; + + for (t = Fl_Type::first, num_windows = 0; t; t = t->next) { + if (t->is_a(ID_Window)) { + windows[num_windows] = (Fl_Window_Type *)t; + if (!((Fl_Window*)(windows[num_windows]->o))->shown()) continue; + num_windows ++; + } + } + + Fl_Printer printjob; + if ( printjob.start_job(num_windows, &frompage, &topage) ) return; + int pagecount = 0; + for (winpage = 0; winpage < num_windows; winpage++) { + float scale = 1, scale_x = 1, scale_y = 1; + if (winpage+1 < frompage || winpage+1 > topage) continue; + printjob.start_page(); + printjob.printable_rect(&w, &h); + + // Get the time and date... + time_t curtime = time(NULL); + struct tm *curdate = localtime(&curtime); + char date[1024]; + strftime(date, sizeof(date), "%c", curdate); + fl_font(FL_HELVETICA, 12); + fl_color(FL_BLACK); + fl_draw(date, (w - (int)fl_width(date))/2, fl_height()); + sprintf(date, "%d/%d", ++pagecount, topage-frompage+1); + fl_draw(date, w - (int)fl_width(date), fl_height()); + + // Get the base filename... + std::string basename = fl_filename_name_str(std::string(filename)); + fl_draw(basename.c_str(), 0, fl_height()); + + // print centered and scaled to fit in the page + win = (Fl_Window*)windows[winpage]->o; + ww = win->decorated_w(); + if(ww > w) scale_x = float(w)/ww; + hh = win->decorated_h(); + if(hh > h) scale_y = float(h)/hh; + if (scale_x < scale) scale = scale_x; + if (scale_y < scale) scale = scale_y; + if (scale < 1) { + printjob.scale(scale); + printjob.printable_rect(&w, &h); + } + printjob.origin(w/2, h/2); + printjob.print_window(win, -ww/2, -hh/2); + printjob.end_page(); + } + printjob.end_job(); +} + +// ---- Main menu bar + +extern void select_layout_preset_cb(Fl_Widget *, void *user_data); +extern void layout_suite_marker(Fl_Widget *, void *user_data); + +static void menu_file_new_cb(Fl_Widget *, void *) { new_project(); } +static void menu_file_new_from_template_cb(Fl_Widget *, void *) { new_project_from_template(); } +static void menu_file_open_cb(Fl_Widget *, void *) { open_project_file(""); } +static void menu_file_insert_cb(Fl_Widget *, void *) { merge_project_file(""); } +static void menu_file_open_history_cb(Fl_Widget *, void *v) { open_project_file(std::string((const char*)v)); } +static void menu_layout_sync_resize_cb(Fl_Menu_ *m, void*) { + if (m->mvalue()->value()) Fl_Type::allow_layout = 1; else Fl_Type::allow_layout = 0; } +/** + This is the main Fluid menu. + + Design history is manipulated right inside this menu structure. + Some menu items change or deactivate correctly, but most items just trigger + various callbacks. + + \c New_Menu creates new widgets and is explained in detail in another location. + + \see New_Menu + \todo This menu needs some major modernization. Menus are too long and their + sorting is not always obvious. + \todo Shortcuts are all over the place (Alt, Ctrl, Command, Shift-Ctrl, + function keys), and there should be a help page listing all shortcuts. + */ +Fl_Menu_Item Main_Menu[] = { +{"&File",0,0,0,FL_SUBMENU}, + {"&New", FL_COMMAND+'n', menu_file_new_cb}, + {"&Open...", FL_COMMAND+'o', menu_file_open_cb}, + {"&Insert...", FL_COMMAND+'i', menu_file_insert_cb, 0, FL_MENU_DIVIDER}, + {"&Save", FL_COMMAND+'s', save_cb, 0}, + {"Save &As...", FL_COMMAND+FL_SHIFT+'s', save_cb, (void*)1}, + {"Sa&ve A Copy...", 0, save_cb, (void*)2}, + {"&Revert...", 0, revert_cb, 0, FL_MENU_DIVIDER}, + {"New &From Template...", FL_COMMAND+'N', menu_file_new_from_template_cb, 0}, + {"Save As &Template...", 0, save_template_cb, 0, FL_MENU_DIVIDER}, + {"&Print...", FL_COMMAND+'p', print_menu_cb}, + {"Write &Code", FL_COMMAND+FL_SHIFT+'c', write_cb, 0}, +// Matt: disabled {"MergeBack Code", FL_COMMAND+FL_SHIFT+'m', mergeback_cb, 0}, + {"&Write Strings", FL_COMMAND+FL_SHIFT+'w', write_strings_cb, 0, FL_MENU_DIVIDER}, + {relative_history[0], FL_COMMAND+'1', menu_file_open_history_cb, absolute_history[0]}, + {relative_history[1], FL_COMMAND+'2', menu_file_open_history_cb, absolute_history[1]}, + {relative_history[2], FL_COMMAND+'3', menu_file_open_history_cb, absolute_history[2]}, + {relative_history[3], FL_COMMAND+'4', menu_file_open_history_cb, absolute_history[3]}, + {relative_history[4], FL_COMMAND+'5', menu_file_open_history_cb, absolute_history[4]}, + {relative_history[5], FL_COMMAND+'6', menu_file_open_history_cb, absolute_history[5]}, + {relative_history[6], FL_COMMAND+'7', menu_file_open_history_cb, absolute_history[6]}, + {relative_history[7], FL_COMMAND+'8', menu_file_open_history_cb, absolute_history[7]}, + {relative_history[8], FL_COMMAND+'9', menu_file_open_history_cb, absolute_history[8]}, + {relative_history[9], 0, menu_file_open_history_cb, absolute_history[9], FL_MENU_DIVIDER}, + {"&Quit", FL_COMMAND+'q', exit_cb}, + {0}, +{"&Edit",0,0,0,FL_SUBMENU}, + {"&Undo", FL_COMMAND+'z', undo_cb}, + {"&Redo", FL_COMMAND+FL_SHIFT+'z', redo_cb, 0, FL_MENU_DIVIDER}, + {"C&ut", FL_COMMAND+'x', cut_cb}, + {"&Copy", FL_COMMAND+'c', copy_cb}, + {"&Paste", FL_COMMAND+'v', paste_cb}, + {"Dup&licate", FL_COMMAND+'u', duplicate_cb}, + {"&Delete", FL_Delete, delete_cb, 0, FL_MENU_DIVIDER}, + {"Select &All", FL_COMMAND+'a', select_all_cb}, + {"Select &None", FL_COMMAND+FL_SHIFT+'a', select_none_cb, 0, FL_MENU_DIVIDER}, + {"Pr&operties...", FL_F+1, openwidget_cb}, + {"&Sort",0,sort_cb}, + {"&Earlier", FL_F+2, earlier_cb}, + {"&Later", FL_F+3, later_cb}, + {"&Group", FL_F+7, group_cb}, + {"Ung&roup", FL_F+8, ungroup_cb,0, FL_MENU_DIVIDER}, + {"Hide O&verlays",FL_COMMAND+FL_SHIFT+'o',toggle_overlays}, + {"Hide Guides",FL_COMMAND+FL_SHIFT+'g',toggle_guides}, + {"Hide Restricted",FL_COMMAND+FL_SHIFT+'r',toggle_restricted}, + {"Show Widget &Bin...",FL_ALT+'b',toggle_widgetbin_cb}, + {"Show Code View",FL_ALT+'c', (Fl_Callback*)toggle_codeview_cb, 0, FL_MENU_DIVIDER}, + {"Settings...",FL_ALT+'p',show_settings_cb}, + {0}, +{"&New", 0, 0, (void *)New_Menu, FL_SUBMENU_POINTER}, +{"&Layout",0,0,0,FL_SUBMENU}, + {"&Align",0,0,0,FL_SUBMENU}, + {"&Left",0,(Fl_Callback *)align_widget_cb,(void*)10}, + {"&Center",0,(Fl_Callback *)align_widget_cb,(void*)11}, + {"&Right",0,(Fl_Callback *)align_widget_cb,(void*)12}, + {"&Top",0,(Fl_Callback *)align_widget_cb,(void*)13}, + {"&Middle",0,(Fl_Callback *)align_widget_cb,(void*)14}, + {"&Bottom",0,(Fl_Callback *)align_widget_cb,(void*)15}, + {0}, + {"&Space Evenly",0,0,0,FL_SUBMENU}, + {"&Across",0,(Fl_Callback *)align_widget_cb,(void*)20}, + {"&Down",0,(Fl_Callback *)align_widget_cb,(void*)21}, + {0}, + {"&Make Same Size",0,0,0,FL_SUBMENU}, + {"&Width",0,(Fl_Callback *)align_widget_cb,(void*)30}, + {"&Height",0,(Fl_Callback *)align_widget_cb,(void*)31}, + {"&Both",0,(Fl_Callback *)align_widget_cb,(void*)32}, + {0}, + {"&Center In Group",0,0,0,FL_SUBMENU}, + {"&Horizontal",0,(Fl_Callback *)align_widget_cb,(void*)40}, + {"&Vertical",0,(Fl_Callback *)align_widget_cb,(void*)41}, + {0}, + {"Synchronized Resize", 0, (Fl_Callback*)menu_layout_sync_resize_cb, NULL, FL_MENU_TOGGLE|FL_MENU_DIVIDER }, + {"&Grid and Size Settings...",FL_COMMAND+'g',show_grid_cb, NULL, FL_MENU_DIVIDER}, + {"Presets", 0, layout_suite_marker, (void*)main_layout_submenu_, FL_SUBMENU_POINTER }, + {"Application", 0, select_layout_preset_cb, (void*)0, FL_MENU_RADIO|FL_MENU_VALUE }, + {"Dialog", 0, select_layout_preset_cb, (void*)1, FL_MENU_RADIO }, + {"Toolbox", 0, select_layout_preset_cb, (void*)2, FL_MENU_RADIO }, + {0}, +{"&Shell", 0, Fd_Shell_Command_List::menu_marker, (void*)Fd_Shell_Command_List::default_menu, FL_SUBMENU_POINTER}, +{"&Help",0,0,0,FL_SUBMENU}, + {"&Rapid development with FLUID...",0,help_cb}, + {"&FLTK Programmers Manual...",0,manual_cb, 0, FL_MENU_DIVIDER}, + {"&About FLUID...",0,about_cb}, + {0}, +{0}}; + +/** + Change the app's and hence preview the design's scheme. + + The scheme setting is stored in the app preferences + - in key \p 'scheme_name' since 1.4.0 + - in key \p 'scheme' (index: 0 - 4) in 1.3.x + + This callback is triggered by changing the scheme in the + Fl_Scheme_Choice widget (\p Edit/GUI Settings). + + \param[in] choice the calling widget + + \see init_scheme() for choice values and backwards compatibility + */ +void scheme_cb(Fl_Scheme_Choice *choice, void *) { + if (batch_mode) + return; + + // set the new scheme only if the scheme was changed + const char *new_scheme = choice->text(choice->value()); + + if (Fl::is_scheme(new_scheme)) + return; + + Fl::scheme(new_scheme); + fluid_prefs.set("scheme_name", new_scheme); + + // Backwards compatibility: store 1.3 scheme index (1-4). + // We assume that index 0-3 (base, plastic, gtk+, gleam) are in the + // same order as in 1.3.x (index 1-4), higher values are ignored + + int scheme_index = scheme_choice->value(); + if (scheme_index <= 3) // max. index for 1.3.x (Gleam) + fluid_prefs.set("scheme", scheme_index + 1); // compensate for different indexing +} + +/** + Read Fluid's scheme preferences and set the app's scheme. + + Since FLTK 1.4.0 the scheme \b name is stored as a character string + with key "scheme_name" in the preference database. + + In FLTK 1.3.x the scheme preference was stored as an integer index + with key "scheme" in the database. The known schemes were hardcoded in + Fluid's sources (here for reference): + + | Index | 1.3 Scheme Name | Choice | 1.4 Scheme Name | + |-------|-----------------|-------|-----------------| + | 0 | Default (same as None) | n/a | n/a | + | 1 | None (same as Default) | 0 | base | + | 2 | Plastic | 1 | plastic | + | 3 | GTK+ | 2 | gtk+ | + | 4 | Gleam | 3 | gleam | + | n/a | n/a | 4 | oxy | + + The new Fluid tries to keep backwards compatibility and reads both + keys (\p scheme and \p scheme_name). If the latter is defined, it is used. + If not the old \p scheme (index) is used - but we need to subtract one to + get the new Fl_Scheme_Choice index (column "Choice" above). +*/ +void init_scheme() { + int scheme_index = 0; // scheme index for backwards compatibility (1.3.x) + char *scheme_name = 0; // scheme name since 1.4.0 + fluid_prefs.get("scheme_name", scheme_name, "XXX"); // XXX means: not set => fallback 1.3.x + if (!strcmp(scheme_name, "XXX")) { + fluid_prefs.get("scheme", scheme_index, 0); + if (scheme_index > 0) { + scheme_index--; + scheme_choice->value(scheme_index); // set the choice value + } + if (scheme_index < 0) + scheme_index = 0; + else if (scheme_index > scheme_choice->size() - 1) + scheme_index = 0; + scheme_name = const_cast<char *>(scheme_choice->text(scheme_index)); + fluid_prefs.set("scheme_name", scheme_name); + } + // Set the new scheme only if it was not overridden by the -scheme + // command line option + if (Fl::scheme() == NULL) { + Fl::scheme(scheme_name); + } + free(scheme_name); +} + +/** + Show or hide the widget bin. + The state is stored in the app preferences. + */ +void toggle_widgetbin_cb(Fl_Widget *, void *) { + if (!widgetbin_panel) { + make_widgetbin(); + if (!position_window(widgetbin_panel,"widgetbin_pos", 1, 320, 30)) return; + } + + if (widgetbin_panel->visible()) { + widgetbin_panel->hide(); + widgetbin_item->label("Show Widget &Bin..."); + } else { + widgetbin_panel->show(); + widgetbin_item->label("Hide Widget &Bin"); + } +} + +/** + Show or hide the code preview window. + */ +void toggle_codeview_cb(Fl_Double_Window *, void *) { + codeview_toggle_visibility(); +} + +/** + Show or hide the code preview window, button callback. + */ +void toggle_codeview_b_cb(Fl_Button*, void *) { + codeview_toggle_visibility(); +} + +/** + Build the main app window and create a few other dialogs. + */ +void make_main_window() { + if (!batch_mode) { + fluid_prefs.get("show_guides", show_guides, 1); + fluid_prefs.get("show_restricted", show_restricted, 1); + fluid_prefs.get("show_ghosted_outline", show_ghosted_outline, 0); + fluid_prefs.get("show_comments", show_comments, 1); + make_shell_window(); + } + + if (!main_window) { + Fl_Widget *o; + loadPixmaps(); + main_window = new Fl_Double_Window(WINWIDTH,WINHEIGHT,"fluid"); + main_window->box(FL_NO_BOX); + o = make_widget_browser(0,MENUHEIGHT,BROWSERWIDTH,BROWSERHEIGHT); + o->box(FL_FLAT_BOX); + o->tooltip("Double-click to view or change an item."); + main_window->resizable(o); + main_menubar = new Fl_Menu_Bar(0,0,BROWSERWIDTH,MENUHEIGHT); + main_menubar->menu(Main_Menu); + // quick access to all dynamic menu items + save_item = (Fl_Menu_Item*)main_menubar->find_item(save_cb); + history_item = (Fl_Menu_Item*)main_menubar->find_item(menu_file_open_history_cb); + widgetbin_item = (Fl_Menu_Item*)main_menubar->find_item(toggle_widgetbin_cb); + codeview_item = (Fl_Menu_Item*)main_menubar->find_item((Fl_Callback*)toggle_codeview_cb); + overlay_item = (Fl_Menu_Item*)main_menubar->find_item((Fl_Callback*)toggle_overlays); + guides_item = (Fl_Menu_Item*)main_menubar->find_item((Fl_Callback*)toggle_guides); + restricted_item = (Fl_Menu_Item*)main_menubar->find_item((Fl_Callback*)toggle_restricted); + main_menubar->global(); + fill_in_New_Menu(); + main_window->end(); + } + + if (!batch_mode) { + load_history(); + g_shell_config = new Fd_Shell_Command_List; + widget_browser->load_prefs(); + make_settings_window(); + } +} + +/** + Load file history from preferences. + + This loads the absolute filepaths of the last 10 used design files. + It also computes and stores the relative filepaths for display in + the main menu. + */ +void load_history() { + int i; // Looping var + int max_files; + + fluid_prefs.get("recent_files", max_files, 5); + if (max_files > 10) max_files = 10; + + for (i = 0; i < max_files; i ++) { + fluid_prefs.get( Fl_Preferences::Name("file%d", i), absolute_history[i], "", sizeof(absolute_history[i])); + if (absolute_history[i][0]) { + // Make a shortened version of the filename for the menu... + std::string fn = fl_filename_shortened(absolute_history[i], 48); + strncpy(relative_history[i], fn.c_str(), sizeof(relative_history[i]) - 1); + if (i == 9) history_item[i].flags = FL_MENU_DIVIDER; + else history_item[i].flags = 0; + } else break; + } + + for (; i < 10; i ++) { + if (i) history_item[i-1].flags |= FL_MENU_DIVIDER; + history_item[i].hide(); + } +} + +/** + Update file history from preferences. + + Add this new filepath to the history and update the main menu. + Writes the new file history to the app preferences. + + \param[in] flname path or filename of .fl file, will be converted into an + absolute file path based on the current working directory. + */ +void update_history(const char *flname) { + int i; // Looping var + char absolute[FL_PATH_MAX]; + int max_files; + + + fluid_prefs.get("recent_files", max_files, 5); + if (max_files > 10) max_files = 10; + + fl_filename_absolute(absolute, sizeof(absolute), flname); +#ifdef _WIN32 + // Make path canonical. + for (char *s = absolute; *s; s++) { + if (*s == '\\') + *s = '/'; + } +#endif + + + for (i = 0; i < max_files; i ++) +#if defined(_WIN32) || defined(__APPLE__) + if (!strcasecmp(absolute, absolute_history[i])) break; +#else + if (!strcmp(absolute, absolute_history[i])) break; +#endif // _WIN32 || __APPLE__ + + if (i == 0) return; + + if (i >= max_files) i = max_files - 1; + + // Move the other flnames down in the list... + memmove(absolute_history + 1, absolute_history, + i * sizeof(absolute_history[0])); + memmove(relative_history + 1, relative_history, + i * sizeof(relative_history[0])); + + // Put the new file at the top... + strlcpy(absolute_history[0], absolute, sizeof(absolute_history[0])); + std::string fn = fl_filename_shortened(absolute_history[0], 48); + strncpy(relative_history[0], fn.c_str(), sizeof(relative_history[0]) - 1); + + // Update the menu items as needed... + for (i = 0; i < max_files; i ++) { + fluid_prefs.set( Fl_Preferences::Name("file%d", i), absolute_history[i]); + if (absolute_history[i][0]) { + if (i == 9) history_item[i].flags = FL_MENU_DIVIDER; + else history_item[i].flags = 0; + } else break; + } + + for (; i < 10; i ++) { + fluid_prefs.set( Fl_Preferences::Name("file%d", i), ""); + if (i) history_item[i-1].flags |= FL_MENU_DIVIDER; + history_item[i].hide(); + } + fluid_prefs.flush(); +} + +/** + Set the filename of the current .fl design. + \param[in] c the new absolute filename and path + */ +void set_filename(const char *c) { + if (filename) free((void *)filename); + filename = c ? fl_strdup(c) : NULL; + + if (filename && !batch_mode) + update_history(filename); + + set_modflag(modflag); +} + + +/** + Set the "modified" flag and update the title of the main window. + + The first argument sets the modification state of the current design against + the corresponding .fl design file. Any change to the widget tree will mark + the design 'modified'. Saving the design will mark it clean. + + The second argument is optional and set the modification state of the current + design against the source code and header file. Any change to the tree, + including saving the tree, will mark the code 'outdated'. Generating source + code and header files will clear this flag until the next modification. + + \param[in] mf 0 to clear the modflag, 1 to mark the design "modified", -1 to + ignore this parameter + \param[in] mfc default -1 to let \c mf control \c modflag_c, 0 to mark the + code files current, 1 to mark it out of date. -2 to ignore changes to mf. + */ +void set_modflag(int mf, int mfc) { + const char *code_ext = NULL; + char new_title[FL_PATH_MAX]; + + // Update the modflag_c to the worst possible condition. We could be a bit + // more graceful and compare modification times of the files, but C++ has + // no API for that until C++17. + if (mf!=-1) { + modflag = mf; + if (mfc==-1 && mf==1) + mfc = mf; + } + if (mfc>=0) { + modflag_c = mfc; + } + + if (main_window) { + std::string basename; + if (!filename) basename = "Untitled.fl"; + else basename = fl_filename_name_str(std::string(filename)); + code_ext = fl_filename_ext(g_project.code_file_name.c_str()); + char mod_star = modflag ? '*' : ' '; + char mod_c_star = modflag_c ? '*' : ' '; + snprintf(new_title, sizeof(new_title), "%s%c %s%c", + basename.c_str(), mod_star, code_ext, mod_c_star); + const char *old_title = main_window->label(); + // only update the title if it actually changed + if (!old_title || strcmp(old_title, new_title)) + main_window->copy_label(new_title); + } + // if the UI was modified in any way, update the Code View panel + if (codeview_panel && codeview_panel->visible() && cv_autorefresh->value()) + codeview_defer_update(); +} + +// ---- Main program entry point + +/** + Handle command line arguments. + \param[in] argc number of arguments in the list + \param[in] argv pointer to an array of arguments + \param[inout] i current argument index + \return number of arguments used; if 0, the argument is not supported + */ +static int arg(int argc, char** argv, int& i) { + if (argv[i][0] != '-') + return 0; + if (argv[i][1] == 'd' && !argv[i][2]) { + G_debug=1; + i++; return 1; + } + if (argv[i][1] == 'u' && !argv[i][2]) { + update_file++; + batch_mode++; + i++; return 1; + } + if (argv[i][1] == 'c' && !argv[i][2]) { + compile_file++; + batch_mode++; + i++; return 1; + } + if ((strcmp(argv[i], "-v")==0) || (strcmp(argv[i], "--version")==0)) { + show_version = 1; + i++; return 1; + } + if (argv[i][1] == 'c' && argv[i][2] == 's' && !argv[i][3]) { + compile_file++; + compile_strings++; + batch_mode++; + i++; return 1; + } + if (argv[i][1] == 'o' && !argv[i][2] && i+1 < argc) { + g_code_filename_arg = argv[i+1]; + batch_mode++; + i += 2; return 2; + } +#ifndef NDEBUG + if ((i+1 < argc) && (strcmp(argv[i], "--autodoc") == 0)) { + g_autodoc_path = argv[i+1]; + i += 2; return 2; + } +#endif + if (strcmp(argv[i], "--help")==0) { + return 0; + } + if (argv[i][1] == 'h' && !argv[i][2]) { + if ( (i+1 < argc) && (argv[i+1][0] != '-') ) { + g_header_filename_arg = argv[i+1]; + batch_mode++; + i += 2; + return 2; + } else { + // a lone "-h" without a filename will output the help string + return 0; + } + } + return 0; +} + +#if ! (defined(_WIN32) && !defined (__CYGWIN__)) + +int quit_flag = 0; +#include <signal.h> +#ifdef _sigargs +#define SIGARG _sigargs +#else +#ifdef __sigargs +#define SIGARG __sigargs +#else +#define SIGARG int // you may need to fix this for older systems +#endif +#endif + +extern "C" { +static void sigint(SIGARG) { + signal(SIGINT,sigint); + quit_flag = 1; +} +} + +#endif + +/** + Start Fluid. + + Fluid can run in interactive mode with a full user interface to design new + user interfaces and write the C++ files to manage them, + + Fluid can run form the command line in batch mode to convert .fl design files + into C++ source and header files. In batch mode, no display is needed, + particularly no X11 connection will be attempted on Linux/Unix. + + \param[in] argc number of arguments in the list + \param[in] argv pointer to an array of arguments + \return in batch mode, an error code will be returned via \c exit() . This + function return 1, if there was an error in the parameters list. + \todo On Windows, Fluid can under certain conditions open a dialog box, even + in batch mode. Is that intentional? Does it circumvent issues with Windows' + stderr and stdout? + */ +int fluid_main(int argc,char **argv) { + int i = 1; + + setlocale(LC_ALL, ""); // enable multi-language errors in file chooser + setlocale(LC_NUMERIC, "C"); // make sure numeric values are written correctly + g_launch_path = end_with_slash(fl_getcwd_str()); // store the current path at launch + + Fl::args_to_utf8(argc, argv); // for MSYS2/MinGW + if ( (Fl::args(argc,argv,i,arg) == 0) // unsupported argument found + || (batch_mode && (i != argc-1)) // .fl filename missing + || (!batch_mode && (i < argc-1)) // more than one filename found + || (argv[i] && (argv[i][0] == '-'))) { // unknown option + static const char *msg = + "usage: %s <switches> name.fl\n" + " -u : update .fl file and exit (may be combined with '-c' or '-cs')\n" + " -c : write .cxx and .h and exit\n" + " -cs : write .cxx and .h and strings and exit\n" + " -o <name> : .cxx output filename, or extension if <name> starts with '.'\n" + " -h <name> : .h output filename, or extension if <name> starts with '.'\n" + " --help : brief usage information\n" + " --version, -v : print fluid version number\n" + " -d : enable internal debugging\n"; + const char *app_name = NULL; + if ( (argc > 0) && argv[0] && argv[0][0] ) + app_name = fl_filename_name(argv[0]); + if ( !app_name || !app_name[0]) + app_name = "fluid"; +#ifdef _MSC_VER + // TODO: if this is fluid-cmd, use stderr and not fl_message + fl_message(msg, app_name); +#else + fprintf(stderr, msg, app_name); +#endif + return 1; + } + if (show_version) { + printf("fluid v%d.%d.%d\n", FL_MAJOR_VERSION, FL_MINOR_VERSION, FL_PATCH_VERSION); + ::exit(0); + } + + const char *c = NULL; + if (g_autodoc_path.empty()) + c = argv[i]; + + fl_register_images(); + + make_main_window(); + + if (c) set_filename(c); + if (!batch_mode) { +#ifdef __APPLE__ + fl_open_callback(apple_open_cb); +#endif // __APPLE__ + Fl::visual((Fl_Mode)(FL_DOUBLE|FL_INDEX)); + Fl_File_Icon::load_system_icons(); + main_window->callback(exit_cb); + position_window(main_window,"main_window_pos", 1, 10, 30, WINWIDTH, WINHEIGHT ); + if (g_shell_config) { + g_shell_config->read(fluid_prefs, FD_STORE_USER); + g_shell_config->update_settings_dialog(); + g_shell_config->rebuild_shell_menu(); + } + g_layout_list.read(fluid_prefs, FD_STORE_USER); + main_window->show(argc,argv); + toggle_widgetbin_cb(0,0); + toggle_codeview_cb(0,0); + if (!c && openlast_button->value() && absolute_history[0][0] && g_autodoc_path.empty()) { + // Open previous file when no file specified... + open_project_file(absolute_history[0]); + } + } + undo_suspend(); + if (c && !fld::io::read_file(c,0)) { + if (batch_mode) { + fprintf(stderr,"%s : %s\n", c, strerror(errno)); + exit(1); + } + fl_message("Can't read %s: %s", c, strerror(errno)); + } + undo_resume(); + + // command line args override code and header filenames from the project file + // in batch mode only + if (batch_mode) { + if (!g_code_filename_arg.empty()) { + g_project.code_file_set = 1; + g_project.code_file_name = g_code_filename_arg; + } + if (!g_header_filename_arg.empty()) { + g_project.header_file_set = 1; + g_project.header_file_name = g_header_filename_arg; + } + } + + if (update_file) { // fluid -u + fld::io::write_file(c,0); + if (!compile_file) + exit(0); + } + + if (compile_file) { // fluid -c[s] + if (compile_strings) + write_strings_cb(0,0); + write_cb(0,0); + exit(0); + } + + // don't lock up if inconsistent command line arguments were given + if (batch_mode) + exit(0); + + set_modflag(0); + undo_clear(); +#ifndef _WIN32 + signal(SIGINT,sigint); +#endif + + // Set (but do not start) timer callback for external editor updates + ExternalCodeEditor::set_update_timer_callback(external_editor_timer); + +#ifndef NDEBUG + // check if the user wants FLUID to generate image for the user documentation + if (!g_autodoc_path.empty()) { + run_autodoc(g_autodoc_path); + set_modflag(0, 0); + exit_cb(0,0); + return 0; + } +#endif + +#ifdef _WIN32 + Fl::run(); +#else + while (!quit_flag) Fl::wait(); + if (quit_flag) exit_cb(0,0); +#endif // _WIN32 + + undo_clear(); + return (0); +} + +/// \} + diff --git a/fluid/app/project.h b/fluid/app/project.h new file mode 100644 index 0000000000000000000000000000000000000000..3d5720330b419454f29c2f2cf1e37351ff7f7878 --- /dev/null +++ b/fluid/app/project.h @@ -0,0 +1,209 @@ +// +// FLUID main entry for the Fast Light Tool Kit (FLTK). +// +// Copyright 1998-2025 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_FLUID_H +#define _FLUID_FLUID_H + +#include "tools/filename.h" + +#include <FL/Fl_Preferences.H> +#include <FL/Fl_Menu_Item.H> +#include <FL/filename.H> + +#include <string> + +#define BROWSERWIDTH 300 +#define BROWSERHEIGHT 500 +#define WINWIDTH 300 +#define MENUHEIGHT 25 +#define WINHEIGHT (BROWSERHEIGHT+MENUHEIGHT) + +// ---- types + +class Fl_Double_Window; +class Fl_Window; +class Fl_Menu_Bar; +class Fl_Type; +class Fl_Choice; +class Fl_Button; +class Fl_Check_Button; + +/** + Indicate the storage location for tools like layout suites and shell macros. + \see class Fd_Shell_Command, class Fd_Layout_Suite + */ +typedef enum { + FD_STORE_INTERNAL, ///< stored inside FLUID app + FD_STORE_USER, ///< suite is stored in the user wide FLUID settings + FD_STORE_PROJECT, ///< suite is stored within the current .fl project file + FD_STORE_FILE ///< store suite in external file +} Fd_Tool_Store; + +// ---- global variables + +extern Fl_Preferences fluid_prefs; +extern Fl_Menu_Item Main_Menu[]; +extern Fl_Menu_Bar *main_menubar; +extern Fl_Window *main_window; + +extern int show_guides; +extern int show_restricted; +extern int show_ghosted_outline; +extern int show_comments; + +extern int G_use_external_editor; +extern int G_debug; +extern char G_external_editor_command[512]; + +// File history info... +extern char absolute_history[10][FL_PATH_MAX]; +extern char relative_history[10][FL_PATH_MAX]; +extern void load_history(); +extern void update_history(const char *); + +extern Fl_Menu_Item *save_item; +extern Fl_Menu_Item *history_item; +extern Fl_Menu_Item *widgetbin_item; +extern Fl_Menu_Item *codeview_item; +extern Fl_Menu_Item *overlay_item; +extern Fl_Button *overlay_button; +extern Fl_Menu_Item *guides_item; +extern Fl_Menu_Item *restricted_item; +extern Fl_Check_Button *guides_button; + +extern int modflag; + +extern int update_file; // fluid -u +extern int compile_file; // fluid -c +extern int compile_strings; // fluic -cs +extern int batch_mode; + +extern int pasteoffset; + +extern std::string g_code_filename_arg; +extern std::string g_header_filename_arg; +extern std::string g_launch_path; + +extern std::string g_autodoc_path; + +// ---- project class declaration + +/** + Enumeration of available internationalization types. + */ +typedef enum { + FD_I18N_NONE = 0, ///< No i18n, all strings are litearals + FD_I18N_GNU, ///< GNU gettext internationalization + FD_I18N_POSIX ///< Posix catgets internationalization +} Fd_I18n_Type; + +/** + Data and settings for a FLUID project file. + */ +class Fluid_Project { +public: + Fluid_Project(); + ~Fluid_Project(); + void reset(); + void update_settings_dialog(); + + std::string projectfile_path() const; + std::string projectfile_name() const; + std::string codefile_path() const; + std::string codefile_name() const; + std::string headerfile_path() const; + std::string headerfile_name() const; + std::string stringsfile_path() const; + std::string stringsfile_name() const; + std::string basename() const; + + /// One of the available internationalization types. + Fd_I18n_Type i18n_type; + /// Include file for GNU i18n, writes an #include statement into the source + /// file. This is usually `<libintl.h>` or `"gettext.h"` for GNU gettext. + std::string i18n_gnu_include; + // Optional name of a macro for conditional i18n compilation. + std::string i18n_gnu_conditional; + /// For the gettext/intl.h options, this is the function that translates text + /// at runtime. This is usually "gettext" or "_". + std::string i18n_gnu_function; + /// For the gettext/intl.h options, this is the function that marks the translation + /// of text at initialisation time. This is usually "gettext_noop" or "N_". + std::string i18n_gnu_static_function; + + /// Include file for Posix i18n, write a #include statement into the source + /// file. This is usually `<nl_types.h>` for Posix catgets. + std::string i18n_pos_include; + // Optional name of a macro for conditional i18n compilation. + std::string i18n_pos_conditional; + /// Name of the nl_catd database + std::string i18n_pos_file; + /// Message set ID for the catalog. + std::string i18n_pos_set; + + /// If set, generate code to include the header file form the c++ file + int include_H_from_C; + /// If set, handle keyboard shortcut Ctrl on macOS using Cmd instead + int use_FL_COMMAND; + /// Clear if UTF-8 characters in statics texts are written as escape sequences + int utf8_in_src; + /// If set, <FL/Fl.H> will not be included from the header code before anything else + int avoid_early_includes; + /// If set, command line overrides header file name in .fl file. + int header_file_set; + /// If set, command line overrides source code file name in .fl file. + int code_file_set; + int write_mergeback_data; + /// Hold the default extension for header files, or the entire filename if set via command line. + std::string header_file_name; + /// Hold the default extension for source code files, or the entire filename if set via command line. + std::string code_file_name; +}; + +extern Fluid_Project g_project; + +// ---- public functions + +extern int fluid_main(int argc,char **argv); + +extern bool new_project(bool user_must_confirm = true); +extern void enter_project_dir(); +extern void leave_project_dir(); +extern void set_filename(const char *c); +extern void set_modflag(int mf, int mfc=-1); + +extern const std::string &get_tmpdir(); + +// ---- public callback functions + +extern void save_cb(Fl_Widget *, void *v); +extern void save_template_cb(Fl_Widget *, void *); +extern void revert_cb(Fl_Widget *,void *); +extern void exit_cb(Fl_Widget *,void *); + +extern int write_code_files(bool dont_show_completion_dialog=false); +extern void write_strings_cb(Fl_Widget *, void *); +extern void align_widget_cb(Fl_Widget *, long); +extern void toggle_widgetbin_cb(Fl_Widget *, void *); + +extern char position_window(Fl_Window *w, const char *prefsName, int Visible, int X, int Y, int W=0, int H=0); + +inline int fd_min(int a, int b) { return (a < b ? a : b); } +inline int fd_max(int a, int b) { return (a > b ? a : b); } +inline int fd_min(int a, int b, int c) { return fd_min(a, fd_min(b, c)); } + +#endif // _FLUID_FLUID_H + diff --git a/fluid/io/code.cxx b/fluid/io/Code_Writer.cxx similarity index 100% rename from fluid/io/code.cxx rename to fluid/io/Code_Writer.cxx diff --git a/fluid/io/code.h b/fluid/io/Code_Writer.h similarity index 100% rename from fluid/io/code.h rename to fluid/io/Code_Writer.h diff --git a/fluid/io/file.cxx b/fluid/io/Project_Reader.cxx similarity index 100% rename from fluid/io/file.cxx rename to fluid/io/Project_Reader.cxx diff --git a/fluid/io/file.h b/fluid/io/Project_Reader.h similarity index 100% rename from fluid/io/file.h rename to fluid/io/Project_Reader.h diff --git a/fluid/io/Project_Writer.cxx b/fluid/io/Project_Writer.cxx new file mode 100644 index 0000000000000000000000000000000000000000..ba1afeb1b970ec3cb087e8f9f4659c00152afcc8 --- /dev/null +++ b/fluid/io/Project_Writer.cxx @@ -0,0 +1,983 @@ +// +// Fluid file routines for the Fast Light Tool Kit (FLTK). +// +// You may find the basic read_* and write_* routines to +// be useful for other programs. I have used them many times. +// They are somewhat similar to tcl, using matching { and } +// to quote strings. +// +// Copyright 1998-2023 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.h" + +#include "app/fluid.h" +#include "app/shell_command.h" +#include "app/undo.h" +#include "io/code.h" +#include "nodes/factory.h" +#include "nodes/Fl_Function_Type.h" +#include "nodes/Fl_Widget_Type.h" +#include "nodes/Fl_Grid_Type.h" +#include "nodes/Fl_Window_Type.h" +#include "panels/settings_panel.h" +#include "widgets/Node_Browser.h" + +#include <FL/Fl.H> +#include <FL/Fl_Group.H> +#include <FL/fl_string_functions.h> +#include <FL/fl_message.H> +#include "../src/flstring.h" + +#include <stdio.h> +#include <stdlib.h> +#include <stdarg.h> + +/// \defgroup flfile .fl Project File Operations +/// \{ + +// This file contains code to read and write .fl files. + +/// If set, we read an old fdesign file and widget y coordinates need to be flipped. +int fdesign_flip = 0; + +/** \brief Read a .fl project file. + + The .fl file format is documented in `fluid/README_fl.txt`. + + \param[in] filename read this file + \param[in] merge if this is set, merge the file into an existing project + at Fl_Type::current + \param[in] strategy add new nodes after current or as last child + \return 0 if the operation failed, 1 if it succeeded + */ +int read_file(const char *filename, int merge, Strategy strategy) { + Fd_Project_Reader f; + strategy.source(Strategy::FROM_FILE); + return f.read_project(filename, merge, strategy); +} + +/** \brief Write an .fl design description file. + + The .fl file format is documented in `fluid/README_fl.txt`. + + \param[in] filename create this file, and if it exists, overwrite it + \param[in] selected_only write only the selected nodes in the widget_tree. This + is used to implement copy and paste. + \return 0 if the operation failed, 1 if it succeeded + */ +int write_file(const char *filename, int selected_only, bool to_codeview) { + Fd_Project_Writer out; + return out.write_project(filename, selected_only, to_codeview); +} + +/** + Convert a single ASCII char, assumed to be a hex digit, into its decimal value. + \param[in] x ASCII character + \return decimal value or 20 if character is not a valid hex digit (0..9,a..f,A..F) + */ +static int hexdigit(int x) { + if ((x < 0) || (x > 127)) return 20; + if (isdigit(x)) return x-'0'; + if (isupper(x)) return x-'A'+10; + if (islower(x)) return x-'a'+10; + return 20; +} + +// ---- Fd_Project_Reader ---------------------------------------------- MARK: - + +/** + A simple growing buffer. + Oh how I wish sometimes we would upgrade to modern C++. + \param[in] length minimum length in bytes + */ +void Fd_Project_Reader::expand_buffer(int length) { + if (length >= buflen) { + if (!buflen) { + buflen = length+1; + buffer = (char*)malloc(buflen); + } else { + buflen = 2*buflen; + if (length >= buflen) buflen = length+1; + buffer = (char *)realloc((void *)buffer,buflen); + } + } +} + +/** \brief Construct local project reader. */ +Fd_Project_Reader::Fd_Project_Reader() +: fin(NULL), + lineno(0), + fname(NULL), + buffer(NULL), + buflen(0), + read_version(0.0) +{ +} + +/** \brief Release project reader resources. */ +Fd_Project_Reader::~Fd_Project_Reader() +{ + // fname is not copied, so do not free it + if (buffer) + ::free(buffer); +} + +/** + Open an .fl file for reading. + \param[in] s filename, if NULL, read from stdin instead + \return 0 if the operation failed, 1 if it succeeded + */ +int Fd_Project_Reader::open_read(const char *s) { + lineno = 1; + if (!s) { + fin = stdin; + fname = "stdin"; + } else { + FILE *f = fl_fopen(s, "rb"); + if (!f) + return 0; + fin = f; + fname = s; + } + return 1; +} + +/** + Close the .fl file. + \return 0 if the operation failed, 1 if it succeeded + */ +int Fd_Project_Reader::close_read() { + if (fin != stdin) { + int x = fclose(fin); + fin = 0; + return x >= 0; + } + return 1; +} + +/** + Return the name part of the current filename and path. + \return a pointer into a string that is not owned by this class + */ +const char *Fd_Project_Reader::filename_name() { + return fl_filename_name(fname); +} + +/** + Convert an ASCII sequence from the \.fl file following a previously read `\\` into a single character. + Conversion includes the common C style \\ characters like \\n, \\x## hex + values, and \\o### octal values. + \return a character in the ASCII range + */ +int Fd_Project_Reader::read_quoted() { // read whatever character is after a \ . + int c,d,x; + switch(c = nextchar()) { + case '\n': lineno++; return -1; + case 'a' : return('\a'); + case 'b' : return('\b'); + case 'f' : return('\f'); + case 'n' : return('\n'); + case 'r' : return('\r'); + case 't' : return('\t'); + case 'v' : return('\v'); + case 'x' : /* read hex */ + for (c=x=0; x<3; x++) { + int ch = nextchar(); + d = hexdigit(ch); + if (d > 15) {ungetc(ch,fin); break;} + c = (c<<4)+d; + } + break; + default: /* read octal */ + if (c<'0' || c>'7') break; + c -= '0'; + for (x=0; x<2; x++) { + int ch = nextchar(); + d = hexdigit(ch); + if (d>7) {ungetc(ch,fin); break;} + c = (c<<3)+d; + } + break; + } + return(c); +} + +/** + Recursively read child nodes in the .fl design file. + + If this is the first call, also read the global settings for this design. + + \param[in] p parent node or NULL + \param[in] merge if set, merge into existing design, else replace design + \param[in] strategy add nodes after current or as last child + \param[in] skip_options this is set if the options were already found in + a previous call, and there is no need to waste time searching for them. + \return the last type that was created + */ +Fl_Type *Fd_Project_Reader::read_children(Fl_Type *p, int merge, Strategy strategy, char skip_options) { + Fl_Type::current = p; + Fl_Type *last_child_read = NULL; + Fl_Type *t = NULL; + for (;;) { + const char *c = read_word(); + REUSE_C: + if (!c) { + if (p && !merge) + read_error("Missing '}'"); + break; + } + + if (!strcmp(c,"}")) { + if (!p) read_error("Unexpected '}'"); + break; + } + + // Make sure that we don't go through the list of options for child nodes + if (!skip_options) { + // this is the first word in a .fd file: + if (!strcmp(c,"Magic:")) { + read_fdesign(); + return NULL; + } + + if (!strcmp(c,"version")) { + c = read_word(); + read_version = strtod(c,0); + if (read_version<=0 || read_version>double(FL_VERSION+0.00001)) + read_error("unknown version '%s'",c); + continue; + } + + // back compatibility with Vincent Penne's original class code: + if (!p && !strcmp(c,"define_in_struct")) { + Fl_Type *t = add_new_widget_from_file("class", Strategy::FROM_FILE_AS_LAST_CHILD); + t->name(read_word()); + Fl_Type::current = p = t; + merge = 1; // stops "missing }" error + continue; + } + + if (!strcmp(c,"do_not_include_H_from_C")) { + g_project.include_H_from_C=0; + goto CONTINUE; + } + if (!strcmp(c,"use_FL_COMMAND")) { + g_project.use_FL_COMMAND=1; + goto CONTINUE; + } + if (!strcmp(c,"utf8_in_src")) { + g_project.utf8_in_src=1; + goto CONTINUE; + } + if (!strcmp(c,"avoid_early_includes")) { + g_project.avoid_early_includes=1; + goto CONTINUE; + } + if (!strcmp(c,"i18n_type")) { + g_project.i18n_type = static_cast<Fd_I18n_Type>(atoi(read_word())); + goto CONTINUE; + } + if (!strcmp(c,"i18n_gnu_function")) { + g_project.i18n_gnu_function = read_word(); + goto CONTINUE; + } + if (!strcmp(c,"i18n_gnu_static_function")) { + g_project.i18n_gnu_static_function = read_word(); + goto CONTINUE; + } + if (!strcmp(c,"i18n_pos_file")) { + g_project.i18n_pos_file = read_word(); + goto CONTINUE; + } + if (!strcmp(c,"i18n_pos_set")) { + g_project.i18n_pos_set = read_word(); + goto CONTINUE; + } + if (!strcmp(c,"i18n_include")) { + if (g_project.i18n_type == FD_I18N_GNU) + g_project.i18n_gnu_include = read_word(); + else if (g_project.i18n_type == FD_I18N_POSIX) + g_project.i18n_pos_include = read_word(); + goto CONTINUE; + } + if (!strcmp(c,"i18n_conditional")) { + if (g_project.i18n_type == FD_I18N_GNU) + g_project.i18n_gnu_conditional = read_word(); + else if (g_project.i18n_type == FD_I18N_POSIX) + g_project.i18n_pos_conditional = read_word(); + goto CONTINUE; + } + if (!strcmp(c,"header_name")) { + if (!g_project.header_file_set) g_project.header_file_name = read_word(); + else read_word(); + goto CONTINUE; + } + + if (!strcmp(c,"code_name")) { + if (!g_project.code_file_set) g_project.code_file_name = read_word(); + else read_word(); + goto CONTINUE; + } + + if (!strcmp(c, "snap")) { + g_layout_list.read(this); + goto CONTINUE; + } + + if (!strcmp(c, "gridx") || !strcmp(c, "gridy")) { + // grid settings are now global + read_word(); + goto CONTINUE; + } + + if (strcmp(c, "shell_commands")==0) { + if (g_shell_config) { + g_shell_config->read(this); + } else { + read_word(); + } + goto CONTINUE; + } + + if (!strcmp(c, "mergeback")) { + g_project.write_mergeback_data = read_int(); + goto CONTINUE; + } + } + t = add_new_widget_from_file(c, strategy); + if (!t) { + read_error("Unknown word \"%s\"", c); + continue; + } + last_child_read = t; + // After reading the first widget, we no longer need to look for options + skip_options = 1; + + t->name(read_word()); + + c = read_word(1); + if (strcmp(c,"{") && t->is_class()) { // <prefix> <name> + ((Fl_Class_Type*)t)->prefix(t->name()); + t->name(c); + c = read_word(1); + } + + if (strcmp(c,"{")) { + read_error("Missing property list for %s\n",t->title()); + goto REUSE_C; + } + + t->folded_ = 1; + for (;;) { + const char *cc = read_word(); + if (!cc || !strcmp(cc,"}")) break; + t->read_property(*this, cc); + } + + if (t->can_have_children()) { + c = read_word(1); + if (strcmp(c,"{")) { + read_error("Missing child list for %s\n",t->title()); + goto REUSE_C; + } + read_children(t, 0, Strategy::FROM_FILE_AS_LAST_CHILD, skip_options); + t->postprocess_read(); + // FIXME: this has no business in the file reader! + // TODO: this is called whenever something is pasted from the top level into a grid + // It makes sense to make this more universal for other widget types too. + if (merge && t && t->parent && t->parent->is_a(ID_Grid)) { + if (Fl_Window_Type::popupx != 0x7FFFFFFF) { + ((Fl_Grid_Type*)t->parent)->insert_child_at(((Fl_Widget_Type*)t)->o, Fl_Window_Type::popupx, Fl_Window_Type::popupy); + } else { + ((Fl_Grid_Type*)t->parent)->insert_child_at_next_free_cell(((Fl_Widget_Type*)t)->o); + } + } + + t->layout_widget(); + } + + if (strategy.placement() == Strategy::AS_FIRST_CHILD) { + strategy.placement(Strategy::AFTER_CURRENT); + } + if (strategy.placement() == Strategy::AFTER_CURRENT) { + Fl_Type::current = t; + } else { + Fl_Type::current = p; + } + + CONTINUE:; + } + if (merge && last_child_read && last_child_read->parent) { + last_child_read->parent->postprocess_read(); + last_child_read->parent->layout_widget(); + } + return last_child_read; +} + +/** \brief Read a .fl project file. + \param[in] filename read this file + \param[in] merge if this is set, merge the file into an existing project + at Fl_Type::current + \param[in] strategy add new nodes after current or as last child + \return 0 if the operation failed, 1 if it succeeded + */ +int Fd_Project_Reader::read_project(const char *filename, int merge, Strategy strategy) { + Fl_Type *o; + undo_suspend(); + read_version = 0.0; + if (!open_read(filename)) { + undo_resume(); + return 0; + } + if (merge) + deselect(); + else + g_project.reset(); + read_children(Fl_Type::current, merge, strategy); + // clear this + Fl_Type::current = 0; + // Force menu items to be rebuilt... + for (o = Fl_Type::first; o; o = o->next) { + if (o->is_a(ID_Menu_Manager_)) { + o->add_child(0,0); + } + } + for (o = Fl_Type::first; o; o = o->next) { + if (o->selected) { + Fl_Type::current = o; + break; + } + } + selection_changed(Fl_Type::current); + if (g_shell_config) { + g_shell_config->rebuild_shell_menu(); + g_shell_config->update_settings_dialog(); + } + g_layout_list.update_dialogs(); + g_project.update_settings_dialog(); + int ret = close_read(); + undo_resume(); + return ret; +} + +/** + Display an error while reading the file. + If the .fl file isn't opened for reading, pop up an FLTK dialog, otherwise + print to stdout. + \note Matt: I am not sure why it is done this way. Shouldn't this depend on \c batch_mode? + \todo Not happy about this function. Output channel should depend on `batch_mode` + as the note above already states. I want to make all file readers and writers + depend on an error handling base class that outputs a useful analysis of file + operations. + \param[in] format printf style format string, followed by an argument list + */ +void Fd_Project_Reader::read_error(const char *format, ...) { + va_list args; + va_start(args, format); + if (!fin) { // FIXME: this line suppresses any error messages in interactive mode + char buffer[1024]; // TODO: hides class member "buffer" + vsnprintf(buffer, sizeof(buffer), format, args); + fl_message("%s", buffer); + } else { + fprintf(stderr, "%s:%d: ", fname, lineno); + vfprintf(stderr, format, args); + fprintf(stderr, "\n"); + } + va_end(args); +} + +/** + Return a word read from the .fl file, or NULL at the EOF. + + This will skip all comments (# to end of line), and evaluate + all \\xxx sequences and use \\ at the end of line to remove the newline. + + A word is any one of: + - a continuous string of non-space chars except { and } and # + - everything between matching {...} (unless wantbrace != 0) + - the characters '{' and '}' + + \param[in] wantbrace if set, reading a `{` as the first non-space character + will return the string `"{"`, if clear, a `{` is seen as the start of a word + \return a pointer to the internal buffer, containing a copy of the word. + Don't free the buffer! Note that most (all?) other file operations will + overwrite this buffer. If wantbrace is not set, but we read a leading '{', + the returned string will be stripped of its leading and trailing braces. + */ +const char *Fd_Project_Reader::read_word(int wantbrace) { + int x; + + // skip all the whitespace before it: + for (;;) { + x = nextchar(); + if (x < 0 && feof(fin)) { // eof + return 0; + } else if (x == '#') { // comment + do x = nextchar(); while (x >= 0 && x != '\n'); + lineno++; + continue; + } else if (x == '\n') { + lineno++; + } else if (!isspace(x & 255)) { + break; + } + } + + expand_buffer(100); + + if (x == '{' && !wantbrace) { + + // read in whatever is between braces + int length = 0; + int nesting = 0; + for (;;) { + x = nextchar(); + if (x<0) {read_error("Missing '}'"); break;} + else if (x == '#') { // embedded comment + do x = nextchar(); while (x >= 0 && x != '\n'); + lineno++; + continue; + } else if (x == '\n') lineno++; + else if (x == '\\') {x = read_quoted(); if (x<0) continue;} + else if (x == '{') nesting++; + else if (x == '}') {if (!nesting--) break;} + buffer[length++] = x; + expand_buffer(length); + } + buffer[length] = 0; + return buffer; + + } else if (x == '{' || x == '}') { + // all the punctuation is a word: + buffer[0] = x; + buffer[1] = 0; + return buffer; + + } else { + + // read in an unquoted word: + int length = 0; + for (;;) { + if (x == '\\') {x = read_quoted(); if (x<0) continue;} + else if (x<0 || isspace(x & 255) || x=='{' || x=='}' || x=='#') break; + buffer[length++] = x; + expand_buffer(length); + x = nextchar(); + } + ungetc(x, fin); + buffer[length] = 0; + return buffer; + + } +} + +/** Read a word and interpret it as an integer value. + \return integer value, or 0 if the word is not an integer + */ +int Fd_Project_Reader::read_int() { + const char *word = read_word(); + if (word) { + return atoi(word); + } else { + return 0; + } +} + +/** Read fdesign name/value pairs. + Fdesign is the file format of the XForms UI designer. It stores lists of name + and value pairs separated by a colon: `class: FL_LABELFRAME`. + \param[out] name string + \param[out] value string + \return 0 if end of file, else 1 + */ +int Fd_Project_Reader::read_fdesign_line(const char*& name, const char*& value) { + int length = 0; + int x; + // find a colon: + for (;;) { + x = nextchar(); + if (x < 0 && feof(fin)) return 0; + if (x == '\n') {length = 0; continue;} // no colon this line... + if (!isspace(x & 255)) { + buffer[length++] = x; + expand_buffer(length); + } + if (x == ':') break; + } + int valueoffset = length; + buffer[length-1] = 0; + + // skip to start of value: + for (;;) { + x = nextchar(); + if ((x < 0 && feof(fin)) || x == '\n' || !isspace(x & 255)) break; + } + + // read the value: + for (;;) { + if (x == '\\') {x = read_quoted(); if (x<0) continue;} + else if (x == '\n') break; + buffer[length++] = x; + expand_buffer(length); + x = nextchar(); + } + buffer[length] = 0; + name = buffer; + value = buffer+valueoffset; + return 1; +} + +/// Lookup table from fdesign .fd files to .fl files +static const char *class_matcher[] = { + "FL_CHECKBUTTON", "Fl_Check_Button", + "FL_ROUNDBUTTON", "Fl_Round_Button", + "FL_ROUND3DBUTTON", "Fl_Round_Button", + "FL_LIGHTBUTTON", "Fl_Light_Button", + "FL_FRAME", "Fl_Box", + "FL_LABELFRAME", "Fl_Box", + "FL_TEXT", "Fl_Box", + "FL_VALSLIDER", "Fl_Value_Slider", + "FL_MENU", "Fl_Menu_Button", + "3", "FL_BITMAP", + "1", "FL_BOX", + "71","FL_BROWSER", + "11","FL_BUTTON", + "4", "FL_CHART", + "42","FL_CHOICE", + "61","FL_CLOCK", + "25","FL_COUNTER", + "22","FL_DIAL", + "101","FL_FREE", + "31","FL_INPUT", + "12","Fl_Light_Button", + "41","FL_MENU", + "23","FL_POSITIONER", + "13","Fl_Round_Button", + "21","FL_SLIDER", + "2", "FL_BOX", // was FL_TEXT + "62","FL_TIMER", + "24","Fl_Value_Slider", + 0}; + + +/** + Finish a group of widgets and optionally transform its children's coordinates. + + Implements the same functionality as Fl_Group::forms_end() from the forms + compatibility library would have done: + + - resize the group to surround its children if the group's w() == 0 + - optionally flip the \p y coordinates of all children relative to the group's window + - Fl_Group::end() the group + + \note Copied from forms_compatibility.cxx and modified as a static fluid + function so we don't have to link to fltk_forms. + + \param[in] g the Fl_Group widget + \param[in] flip flip children's \p y coordinates if true (non-zero) + */ +static void forms_end(Fl_Group *g, int flip) { + // set the dimensions of a group to surround its contents + const int nc = g->children(); + if (nc && !g->w()) { + Fl_Widget*const* a = g->array(); + Fl_Widget* o = *a++; + int rx = o->x(); + int ry = o->y(); + int rw = rx+o->w(); + int rh = ry+o->h(); + for (int i = nc - 1; i--;) { + o = *a++; + if (o->x() < rx) rx = o->x(); + if (o->y() < ry) ry = o->y(); + if (o->x() + o->w() > rw) rw = o->x() + o->w(); + if (o->y() + o->h() > rh) rh = o->y() + o->h(); + } + g->Fl_Widget::resize(rx, ry, rw-rx, rh-ry); + } + // flip all the children's coordinate systems: + if (nc && flip) { + Fl_Widget* o = (g->as_window()) ? g : g->window(); + int Y = o->h(); + Fl_Widget*const* a = g->array(); + for (int i = nc; i--;) { + Fl_Widget* ow = *a++; + int newy = Y - ow->y() - ow->h(); + ow->Fl_Widget::resize(ow->x(), newy, ow->w(), ow->h()); + } + } + g->end(); +} + +/** + Read a XForms design file. + .fl and .fd file start with the same header. Fluid can recognize .fd XForms + Design files by a magic number. It will read them and map XForms widgets onto + FLTK widgets. + \see http://xforms-toolkit.org + */ +void Fd_Project_Reader::read_fdesign() { + int fdesign_magic = atoi(read_word()); + fdesign_flip = (fdesign_magic < 13000); + Fl_Widget_Type *window = 0; + Fl_Widget_Type *group = 0; + Fl_Widget_Type *widget = 0; + if (!Fl_Type::current) { + Fl_Type *t = add_new_widget_from_file("Function", Strategy::FROM_FILE_AS_LAST_CHILD); + t->name("create_the_forms()"); + Fl_Type::current = t; + } + for (;;) { + const char *name; + const char *value; + if (!read_fdesign_line(name, value)) break; + + if (!strcmp(name,"Name")) { + + window = (Fl_Widget_Type*)add_new_widget_from_file("Fl_Window", Strategy::FROM_FILE_AS_LAST_CHILD); + window->name(value); + window->label(value); + Fl_Type::current = widget = window; + + } else if (!strcmp(name,"class")) { + + if (!strcmp(value,"FL_BEGIN_GROUP")) { + group = widget = (Fl_Widget_Type*)add_new_widget_from_file("Fl_Group", Strategy::FROM_FILE_AS_LAST_CHILD); + Fl_Type::current = group; + } else if (!strcmp(value,"FL_END_GROUP")) { + if (group) { + Fl_Group* g = (Fl_Group*)(group->o); + g->begin(); + forms_end(g, fdesign_flip); + Fl_Group::current(0); + } + group = widget = 0; + Fl_Type::current = window; + } else { + for (int i = 0; class_matcher[i]; i += 2) + if (!strcmp(value,class_matcher[i])) { + value = class_matcher[i+1]; break;} + widget = (Fl_Widget_Type*)add_new_widget_from_file(value, Strategy::FROM_FILE_AS_LAST_CHILD); + if (!widget) { + printf("class %s not found, using Fl_Button\n", value); + widget = (Fl_Widget_Type*)add_new_widget_from_file("Fl_Button", Strategy::FROM_FILE_AS_LAST_CHILD); + } + } + + } else if (widget) { + if (!widget->read_fdesign(name, value)) + printf("Ignoring \"%s: %s\"\n", name, value); + } + } +} + +// ---- Fd_Project_Writer ---------------------------------------------- MARK: - + +/** \brief Construct local project writer. */ +Fd_Project_Writer::Fd_Project_Writer() +: fout(NULL), + needspace(0), + write_codeview_(false) +{ +} + +/** \brief Release project writer resources. */ +Fd_Project_Writer::~Fd_Project_Writer() +{ +} + +/** + Open the .fl design file for writing. + If the filename is NULL, associate stdout instead. + \param[in] s the filename or NULL for stdout + \return 1 if successful. 0 if the operation failed + */ +int Fd_Project_Writer::open_write(const char *s) { + if (!s) { + fout = stdout; + } else { + FILE *f = fl_fopen(s,"wb"); + if (!f) return 0; + fout = f; + } + return 1; +} + +/** + Close the .fl design file. + Don't close, if data was sent to stdout. + \return 1 if succeeded, 0 if fclose failed + */ +int Fd_Project_Writer::close_write() { + if (fout != stdout) { + int x = fclose(fout); + fout = stdout; + return x >= 0; + } + return 1; +} + +/** \brief Write an .fl design description file. + \param[in] filename create this file, and if it exists, overwrite it + \param[in] selected_only write only the selected nodes in the widget_tree. This + is used to implement copy and paste. + \param[in] sv if set, this file will be used by codeview + \return 0 if the operation failed, 1 if it succeeded + */ +int Fd_Project_Writer::write_project(const char *filename, int selected_only, bool sv) { + write_codeview_ = sv; + undo_suspend(); + if (!open_write(filename)) { + undo_resume(); + return 0; + } + write_string("# data file for the Fltk User Interface Designer (fluid)\n" + "version %.4f",FL_VERSION); + if(!g_project.include_H_from_C) + write_string("\ndo_not_include_H_from_C"); + if(g_project.use_FL_COMMAND) + write_string("\nuse_FL_COMMAND"); + if (g_project.utf8_in_src) + write_string("\nutf8_in_src"); + if (g_project.avoid_early_includes) + write_string("\navoid_early_includes"); + if (g_project.i18n_type) { + write_string("\ni18n_type %d", g_project.i18n_type); + switch (g_project.i18n_type) { + case FD_I18N_NONE: + break; + case FD_I18N_GNU : /* GNU gettext */ + write_string("\ni18n_include"); write_word(g_project.i18n_gnu_include.c_str()); + write_string("\ni18n_conditional"); write_word(g_project.i18n_gnu_conditional.c_str()); + write_string("\ni18n_gnu_function"); write_word(g_project.i18n_gnu_function.c_str()); + write_string("\ni18n_gnu_static_function"); write_word(g_project.i18n_gnu_static_function.c_str()); + break; + case FD_I18N_POSIX : /* POSIX catgets */ + write_string("\ni18n_include"); write_word(g_project.i18n_pos_include.c_str()); + write_string("\ni18n_conditional"); write_word(g_project.i18n_pos_conditional.c_str()); + if (!g_project.i18n_pos_file.empty()) { + write_string("\ni18n_pos_file"); + write_word(g_project.i18n_pos_file.c_str()); + } + write_string("\ni18n_pos_set"); write_word(g_project.i18n_pos_set.c_str()); + break; + } + } + + if (!selected_only) { + write_string("\nheader_name"); write_word(g_project.header_file_name.c_str()); + write_string("\ncode_name"); write_word(g_project.code_file_name.c_str()); + g_layout_list.write(this); + if (g_shell_config) + g_shell_config->write(this); + if (g_project.write_mergeback_data) + write_string("\nmergeback %d", g_project.write_mergeback_data); + } + + for (Fl_Type *p = Fl_Type::first; p;) { + if (!selected_only || p->selected) { + p->write(*this); + write_string("\n"); + int q = p->level; + for (p = p->next; p && p->level > q; p = p->next) {/*empty*/} + } else { + p = p->next; + } + } + int ret = close_write(); + undo_resume(); + return ret; +} + +/** + Write a string to the .fl file, quoting characters if necessary. + \param[in] w NUL terminated text + */ +void Fd_Project_Writer::write_word(const char *w) { + if (needspace) putc(' ', fout); + needspace = 1; + if (!w || !*w) {fprintf(fout,"{}"); return;} + const char *p; + // see if it is a single word: + for (p = w; is_id(*p); p++) ; + if (!*p) {fprintf(fout,"%s",w); return;} + // see if there are matching braces: + int n = 0; + for (p = w; *p; p++) { + if (*p == '{') n++; + else if (*p == '}') {n--; if (n<0) break;} + } + int mismatched = (n != 0); + // write out brace-quoted string: + putc('{', fout); + for (; *w; w++) { + switch (*w) { + case '{': + case '}': + if (!mismatched) break; + case '\\': + case '#': + putc('\\',fout); + break; + } + putc(*w,fout); + } + putc('}', fout); +} + +/** + Write an arbitrary formatted word to the .fl file, or a comment, etc . + If needspace is set, then one space is written before the string + unless the format starts with a newline character \\n. + \param[in] format printf style formatting string followed by a list of arguments + */ +void Fd_Project_Writer::write_string(const char *format, ...) { + va_list args; + va_start(args, format); + if (needspace && *format != '\n') fputc(' ',fout); + vfprintf(fout, format, args); + va_end(args); + needspace = !isspace(format[strlen(format)-1] & 255); +} + +/** + Start a new line in the .fl file and indent it for a given nesting level. + \param[in] n indent level + */ +void Fd_Project_Writer::write_indent(int n) { + fputc('\n',fout); + while (n--) {fputc(' ',fout); fputc(' ',fout);} + needspace = 0; +} + +/** + Write a '{' to the .fl file at the given indenting level. + */ +void Fd_Project_Writer::write_open() { + if (needspace) fputc(' ',fout); + fputc('{',fout); + needspace = 0; +} + +/** + Write a '}' to the .fl file at the given indenting level. + \param[in] n indent level + */ +void Fd_Project_Writer::write_close(int n) { + if (needspace) write_indent(n); + fputc('}',fout); + needspace = 1; +} + +/// \} diff --git a/fluid/io/Project_Writer.h b/fluid/io/Project_Writer.h new file mode 100644 index 0000000000000000000000000000000000000000..470cc1a7b74767b16753826038b6a9f4155d12c7 --- /dev/null +++ b/fluid/io/Project_Writer.h @@ -0,0 +1,94 @@ +// +// Fluid file routines for the Fast Light Tool Kit (FLTK). +// +// Copyright 1998-2023 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_FILE_H +#define _FLUID_FILE_H + +#include "nodes/Fl_Type.h" + +#include <FL/fl_attr.h> + +class Fl_Type; + +extern int fdesign_flip; + +int read_file(const char *, int merge, Strategy strategy=Strategy::FROM_FILE_AS_LAST_CHILD); +int write_file(const char *, int selected_only = 0, bool to_codeview = false); + +class Fd_Project_Reader +{ +protected: + /// Project input file + FILE *fin; + /// Number of most recently read line + int lineno; + /// Pointer to the file path and name (not copied!) + const char *fname; + /// Expanding buffer to store the most recently read word + char *buffer; + /// Exact size of the expanding buffer in bytes + int buflen; + + void expand_buffer(int length); + + int nextchar() { for (;;) { int ret = fgetc(fin); if (ret!='\r') return ret; } } + +public: + /// Holds the file version number after reading the "version" tag + double read_version; + +public: + Fd_Project_Reader(); + ~Fd_Project_Reader(); + int open_read(const char *s); + int close_read(); + const char *filename_name(); + int read_quoted(); + Fl_Type *read_children(Fl_Type *p, int merge, Strategy strategy, char skip_options=0); + int read_project(const char *, int merge, Strategy strategy=Strategy::FROM_FILE_AS_LAST_CHILD); + void read_error(const char *format, ...); + const char *read_word(int wantbrace = 0); + int read_int(); + int read_fdesign_line(const char*& name, const char*& value); + void read_fdesign(); +}; + +class Fd_Project_Writer +{ +protected: + // Project output file, always opened in "wb" mode + FILE *fout; + /// If set, one space is written before text unless the format starts with a newline character + int needspace; + /// Set if this file will be used in the codeview dialog + bool write_codeview_; + +public: + Fd_Project_Writer(); + ~Fd_Project_Writer(); + int open_write(const char *s); + int close_write(); + int write_project(const char *filename, int selected_only, bool codeview); + void write_word(const char *); + void write_string(const char *,...) __fl_attr((__format__ (__printf__, 2, 3))); + void write_indent(int n); + void write_open(); + void write_close(int n); + FILE *file() const { return fout; } + bool write_codeview() const { return write_codeview_; } +}; + +#endif // _FLUID_FILE_H diff --git a/fluid/tools/fluid_filename.cxx b/fluid/tools/filename.cxx similarity index 100% rename from fluid/tools/fluid_filename.cxx rename to fluid/tools/filename.cxx diff --git a/fluid/tools/fluid_filename.h b/fluid/tools/filename.h similarity index 100% rename from fluid/tools/fluid_filename.h rename to fluid/tools/filename.h