// // Fluid Project File Reader code 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 // // 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. #include "io/Project_Reader.h" #include #include "Fluid.h" #include "Project.h" #include "app/shell_command.h" #include "proj/undo.h" #include "app/Snap_Action.h" #include "nodes/factory.h" #include "nodes/Function_Node.h" #include "nodes/Widget_Node.h" #include "nodes/Grid_Node.h" #include "nodes/Window_Node.h" #include "widgets/Node_Browser.h" #include #include /// \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 fld::io::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 Fluid.proj.tree.current \param[in] strategy add new nodes after current or as last child \return 0 if the operation failed, 1 if it succeeded */ int fld::io::read_file(Project &proj, const char *filename, int merge, Strategy strategy) { Project_Reader f(proj); strategy.source(Strategy::FROM_FILE); return f.read_project(filename, merge, strategy); } /** 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; } // ---- 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 fld::io::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. */ fld::io::Project_Reader::Project_Reader(Project &proj) : proj_(proj) { } /** \brief Release project reader resources. */ fld::io::Project_Reader::~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 0, read from stdin instead \return 0 if the operation failed, 1 if it succeeded */ int fld::io::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 fld::io::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 *fld::io::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 fld::io::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 0 \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 node that was created */ Node *fld::io::Project_Reader::read_children(Node *p, int merge, Strategy strategy, char skip_options) { Fluid.proj.tree.current = p; Node *last_child_read = 0; Node *t = 0; 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 0; } 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")) { Node *t = add_new_widget_from_file("class", Strategy::FROM_FILE_AS_LAST_CHILD); t->name(read_word()); Fluid.proj.tree.current = p = t; merge = 1; // stops "missing }" error continue; } if (!strcmp(c,"do_not_include_H_from_C")) { proj_.include_H_from_C=0; goto CONTINUE; } if (!strcmp(c,"use_FL_COMMAND")) { proj_.use_FL_COMMAND=1; goto CONTINUE; } if (!strcmp(c,"utf8_in_src")) { proj_.utf8_in_src=1; goto CONTINUE; } if (!strcmp(c,"avoid_early_includes")) { proj_.avoid_early_includes=1; goto CONTINUE; } if (strncmp(c, "i18n_", 5) == 0) { proj_.i18n.read(*this, c); goto CONTINUE; } if (!strcmp(c,"header_name")) { if (!proj_.header_file_set) proj_.set_header_file_name(read_word()); else read_word(); goto CONTINUE; } if (!strcmp(c,"code_name")) { if (!proj_.code_file_set) proj_.set_code_file_name(read_word()); else read_word(); goto CONTINUE; } if (!strcmp(c,"include_guard")) { proj_.set_include_guard(read_word()); goto CONTINUE; } if (!strcmp(c, "snap")) { Fluid.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")) { proj_.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); // There can actually be two keywords here. The first one used to be a // "prefix", i.e. class attributes. if (strcmp(c,"{") && t->is_class()) { // ((Class_Node*)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(FLD_NODE_TYPE_Grid)) { if (Window_Node::popupx != 0x7FFFFFFF) { ((Grid_Node*)t->parent)->insert_child_at(((Widget_Node*)t)->o, Window_Node::popupx, Window_Node::popupy); } else { ((Grid_Node*)t->parent)->insert_child_at_next_free_cell(((Widget_Node*)t)->o); } } t->layout_widget(); } if (strategy.placement() == Strategy::AS_FIRST_CHILD) { strategy.placement(Strategy::AFTER_CURRENT); } if (strategy.placement() == Strategy::AFTER_CURRENT) { Fluid.proj.tree.current = t; } else { Fluid.proj.tree.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 Fluid.proj.tree.current \param[in] strategy add new nodes after current or as last child \return 0 if the operation failed, 1 if it succeeded */ int fld::io::Project_Reader::read_project(const char *filename, int merge, Strategy strategy) { Node *o; proj_.undo.suspend(); read_version = 0.0; if (!open_read(filename)) { proj_.undo.resume(); return 0; } if (merge) deselect(); else proj_.reset(); read_children(Fluid.proj.tree.current, merge, strategy); // clear this Fluid.proj.tree.current = 0; // Force menu items to be rebuilt... for (o = Fluid.proj.tree.first; o; o = o->next) { if (o->is_a(FLD_NODE_TYPE_Menu_Manager_)) { o->add_child(0,0); } } for (o = Fluid.proj.tree.first; o; o = o->next) { if (o->selected) { Fluid.proj.tree.current = o; break; } } selection_changed(Fluid.proj.tree.current); if (g_shell_config) { g_shell_config->rebuild_shell_menu(); g_shell_config->update_settings_dialog(); } Fluid.layout_list->update_dialogs(); proj_.update_settings_dialog(); int ret = close_read(); proj_.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 Fluid.batch_mode? \todo Not happy about this function. Output channel should depend on `Fluid.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 fld::io::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 0 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 *fld::io::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 fld::io::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 fld::io::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(); int i; 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 (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 (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 fld::io::Project_Reader::read_fdesign() { int fdesign_magic = atoi(read_word()); fdesign_flip = (fdesign_magic < 13000); Widget_Node *window = 0; Widget_Node *group = 0; Widget_Node *widget = 0; if (!Fluid.proj.tree.current) { Node *t = add_new_widget_from_file("Function", Strategy::FROM_FILE_AS_LAST_CHILD); t->name("create_the_forms()"); Fluid.proj.tree.current = t; } for (;;) { const char *name; const char *value; if (!read_fdesign_line(name, value)) break; if (!strcmp(name,"Name")) { window = (Widget_Node*)add_new_widget_from_file("Fl_Window", Strategy::FROM_FILE_AS_LAST_CHILD); window->name(value); window->label(value); Fluid.proj.tree.current = widget = window; } else if (!strcmp(name,"class")) { if (!strcmp(value,"FL_BEGIN_GROUP")) { group = widget = (Widget_Node*)add_new_widget_from_file("Fl_Group", Strategy::FROM_FILE_AS_LAST_CHILD); Fluid.proj.tree.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; Fluid.proj.tree.current = window; } else { int i; for (i = 0; class_matcher[i]; i += 2) if (!strcmp(value,class_matcher[i])) { value = class_matcher[i+1]; break;} widget = (Widget_Node*)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 = (Widget_Node*)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); } } } /// \}