From ca22660bbb7efe4b38ab5af6a233a1ef5ef33389 Mon Sep 17 00:00:00 2001 From: Matthias Melcher Date: Sat, 8 Mar 2025 00:13:36 +0100 Subject: Fluid: last incremental change: restructuring --- fluid/io/Code_Writer.cxx | 1107 +++++++++++++++++++++++++++++++++++++++++++ fluid/io/Code_Writer.h | 112 +++++ fluid/io/Project_Reader.cxx | 983 ++++++++++++++++++++++++++++++++++++++ fluid/io/Project_Reader.h | 94 ++++ fluid/io/Project_Writer.cxx | 983 ++++++++++++++++++++++++++++++++++++++ fluid/io/Project_Writer.h | 94 ++++ fluid/io/code.cxx | 1107 ------------------------------------------- fluid/io/code.h | 112 ----- fluid/io/file.cxx | 983 -------------------------------------- fluid/io/file.h | 94 ---- 10 files changed, 3373 insertions(+), 2296 deletions(-) create mode 100644 fluid/io/Code_Writer.cxx create mode 100644 fluid/io/Code_Writer.h create mode 100644 fluid/io/Project_Reader.cxx create mode 100644 fluid/io/Project_Reader.h create mode 100644 fluid/io/Project_Writer.cxx create mode 100644 fluid/io/Project_Writer.h delete mode 100644 fluid/io/code.cxx delete mode 100644 fluid/io/code.h delete mode 100644 fluid/io/file.cxx delete mode 100644 fluid/io/file.h (limited to 'fluid/io') diff --git a/fluid/io/Code_Writer.cxx b/fluid/io/Code_Writer.cxx new file mode 100644 index 000000000..b77cad147 --- /dev/null +++ b/fluid/io/Code_Writer.cxx @@ -0,0 +1,1107 @@ +// +// Code output routines 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 "io/code.h" + +#include "app/fluid.h" +#include "app/undo.h" +#include "io/file.h" +#include "nodes/Fl_Group_Type.h" +#include "nodes/Fl_Window_Type.h" +#include "nodes/Fl_Function_Type.h" +#include "tools/fluid_filename.h" + +#include +#include +#include +#include "../src/flstring.h" + +#include +#include +#include + +#include + +/// \defgroup cfile C Code File Operations +/// \{ + + +/** + Return true if c can be in a C identifier. + I needed this so it is not messed up by locale settings. + \param[in] c a character, or the start of a utf-8 sequence + \return 1 if c is alphanumeric or '_' + */ +int is_id(char c) { + return (c>='a' && c<='z') || (c>='A' && c<='Z') || (c>='0' && c<='9') || c=='_'; +} + +/** + Write a string to a file, replacing all non-ASCII characters with octal codes. + \param[in] out output file + \param[in] text write this NUL terminated utf-8 string + \return EOF if any of the file access calls failed, 0 if OK + */ +int write_escaped_strings(FILE *out, const char *text) { + int ret = 0; + const unsigned char *utf8_text = (const unsigned char *)text; + for (const unsigned char *s = utf8_text; *s; ++s) { + unsigned char c = *s; + // escape control characters, delete, all utf-8, and the double quotes + // note: we should have an option in the project settings to allow utf-8 + // characters in the output text and not escape them + if (c < 32 || c > 126 || c == '\"') { + if (c == '\r') { + ret = fputs("\\r", out); + } else if (c == '\n') { + ret = fputs("\\n", out); + } else { + ret = fprintf(out, "\\%03o", c); + } + } else { + ret = putc((int)c, out); + } + } + return ret; +} + +/** + Write a file that contains all label and tooltip strings for internationalization. + The user is responsible to set the right file name extension. The file format + is determined by `g_project.i18n_type`. + \param[in] filename file path and name to a file that will hold the strings + \return 1 if the file could not be opened for writing, or the result of `fclose`. + */ +int write_strings(const std::string &filename) { + Fl_Type *p; + Fl_Widget_Type *w; + int i; + + FILE *fp = fl_fopen(filename.c_str(), "wb"); + if (!fp) return 1; + + switch (g_project.i18n_type) { + case FD_I18N_NONE : /* None, just put static text out */ + fprintf(fp, "# generated by Fast Light User Interface Designer (fluid) version %.4f\n", + FL_VERSION); + for (p = Fl_Type::first; p; p = p->next) { + if (p->is_widget()) { + w = (Fl_Widget_Type *)p; + + if (w->label()) { + write_escaped_strings(fp, w->label()); + putc('\n', fp); + } + + if (w->tooltip()) { + write_escaped_strings(fp, w->tooltip()); + putc('\n', fp); + } + } + } + break; + case FD_I18N_GNU : /* GNU gettext, put a .po file out */ + fprintf(fp, "# generated by Fast Light User Interface Designer (fluid) version %.4f\n", + FL_VERSION); + for (p = Fl_Type::first; p; p = p->next) { + if (p->is_widget()) { + w = (Fl_Widget_Type *)p; + + if (w->label()) { + fputs("msgid \"", fp); + write_escaped_strings(fp, w->label()); + fputs("\"\n", fp); + + fputs("msgstr \"", fp); + write_escaped_strings(fp, w->label()); + fputs("\"\n", fp); + } + + if (w->tooltip()) { + fputs("msgid \"", fp); + write_escaped_strings(fp, w->tooltip()); + fputs("\"\n", fp); + + fputs("msgstr \"", fp); + write_escaped_strings(fp, w->tooltip()); + fputs("\"\n", fp); + } + } + } + break; + case FD_I18N_POSIX : /* POSIX catgets, put a .msg file out */ + fprintf(fp, "$ generated by Fast Light User Interface Designer (fluid) version %.4f\n", + FL_VERSION); + fprintf(fp, "$set %s\n", g_project.i18n_pos_set.c_str()); + fputs("$quote \"\n", fp); + + for (i = 1, p = Fl_Type::first; p; p = p->next) { + if (p->is_widget()) { + w = (Fl_Widget_Type *)p; + + if (w->label()) { + fprintf(fp, "%d \"", i ++); + write_escaped_strings(fp, w->label()); + fputs("\"\n", fp); + } + + if (w->tooltip()) { + fprintf(fp, "%d \"", i ++); + write_escaped_strings(fp, w->tooltip()); + fputs("\"\n", fp); + } + } + } + break; + } + + return fclose(fp); +} + +//////////////////////////////////////////////////////////////// +// Generate unique but human-readable identifiers: + +/** A binary searchable tree storing identifiers for quick retrieval. */ +struct Fd_Identifier_Tree { + char* text; + void* object; + Fd_Identifier_Tree *left, *right; + Fd_Identifier_Tree (const char* t, void* o) : text(fl_strdup(t)), object(o) {left = right = 0;} + ~Fd_Identifier_Tree(); +}; + +Fd_Identifier_Tree::~Fd_Identifier_Tree() { + delete left; + free((void *)text); + delete right; +} + +/** \brief Return a unique name for the given object. + + This function combines the name and label into an identifier. It then checks + if that id was already taken by another object, and if so, appends a + hexadecimal value which is incremented until the id is unique in this file. + + If a new id was created, it is stored in the id tree. + + \param[in] o create an ID for this object + \param[in] type is the first word of the ID + \param[in] name if name is set, it is appended to the ID + \param[in] label else if label is set, it is appended, skipping non-keyword characters + \return buffer to a unique identifier, managed by Fd_Code_Writer, so caller must NOT free() it + */ +const char* Fd_Code_Writer::unique_id(void* o, const char* type, const char* name, const char* label) { + char buffer[128]; + char* q = buffer; + char* q_end = q + 128 - 8 - 1; // room for hex number and NUL + while (*type) *q++ = *type++; + *q++ = '_'; + const char* n = name; + if (!n || !*n) n = label; + if (n && *n) { + while (*n && !is_id(*n)) n++; + while (is_id(*n) && (q < q_end)) *q++ = *n++; + } + *q = 0; + // okay, search the tree and see if the name was already used: + Fd_Identifier_Tree** p = &id_root; + int which = 0; + while (*p) { + int i = strcmp(buffer, (*p)->text); + if (!i) { + if ((*p)->object == o) return (*p)->text; + // already used, we need to pick a new name: + sprintf(q,"%x",++which); + p = &id_root; + continue; + } + else if (i < 0) p = &((*p)->left); + else p = &((*p)->right); + } + *p = new Fd_Identifier_Tree(buffer, o); + return (*p)->text; +} + +//////////////////////////////////////////////////////////////// +// return current indentation: + + +/** + Return a C string that indents code to the given depth. + + Indentation can be changed by modifying the multiplicator (``*2`` to keep + the FLTK indent style). Changing `spaces` to a list of tabs would generate + tab indents instead. This function can also be used for fixed depth indents + in the header file. + + Do *not* ever make this a user preference, or you will end up writing a + fully featured code formatter. + + \param[in] set generate this indent depth + \return pointer to a static string + */ +const char *Fd_Code_Writer::indent(int set) { + static const char* spaces = " "; + int i = set * 2; + if (i>32) i = 32; + if (i<0) i = 0; + return spaces+32-i; +} + +/** + Return a C string that indents code to the current source file depth. + \return pointer to a static string + */ +const char *Fd_Code_Writer::indent() { + return indent(indentation); +} + +/** + Return a C string that indents code to the current source file depth plus an offset. + \param[in] offset adds a temporary offset for this call only; this does not + change the `indentation` variable; offset can be negative + \return pointer to a static string + */ +const char *Fd_Code_Writer::indent_plus(int offset) { + return indent(indentation+offset); +} + + +//////////////////////////////////////////////////////////////// +// declarations/include files: +// Each string generated by write_h_once is written only once to +// the header file. This is done by keeping a binary tree of all +// the calls so far and not printing it if it is in the tree. + +/** A binary searchable tree storing text for quick retrieval. */ +struct Fd_Text_Tree { + char *text; + Fd_Text_Tree *left, *right; + Fd_Text_Tree(const char *t) { + text = fl_strdup(t); + left = right = 0; + } + ~Fd_Text_Tree(); +}; + +Fd_Text_Tree::~Fd_Text_Tree() { + delete left; + free((void *)text); + delete right; +} + +/** A binary searchable tree storing pointers for quick retrieval. */ +struct Fd_Pointer_Tree { + void *ptr; + Fd_Pointer_Tree *left, *right; + Fd_Pointer_Tree(void *p) { + ptr = p; + left = right = 0; + } + ~Fd_Pointer_Tree(); +}; + +Fd_Pointer_Tree::~Fd_Pointer_Tree() { + delete left; + delete right; +} + +/** + Print a formatted line to the header file, unless the same line was produced before in this header file. + \note Resulting line is cropped at 1023 bytes. + \param[in] format printf-style formatting text, followed by a vararg list + \return 1 if the text was written to the file, 0 if it was previously written. + */ +int Fd_Code_Writer::write_h_once(const char *format, ...) { + va_list args; + char buf[1024]; + va_start(args, format); + vsnprintf(buf, sizeof(buf), format, args); + va_end(args); + Fd_Text_Tree **p = &text_in_header; + while (*p) { + int i = strcmp(buf,(*p)->text); + if (!i) return 0; + else if (i < 0) p = &((*p)->left); + else p = &((*p)->right); + } + fprintf(header_file,"%s\n",buf); + *p = new Fd_Text_Tree(buf); + return 1; +} + +/** + Print a formatted line to the source file, unless the same line was produced before in this code file. + \note Resulting line is cropped at 1023 bytes. + \param[in] format printf-style formatting text, followed by a vararg list + \return 1 if the text was written to the file, 0 if it was previously written. + */ +int Fd_Code_Writer::write_c_once(const char *format, ...) { + va_list args; + char buf[1024]; + va_start(args, format); + vsnprintf(buf, sizeof(buf), format, args); + va_end(args); + Fd_Text_Tree **p = &text_in_header; + while (*p) { + int i = strcmp(buf,(*p)->text); + if (!i) return 0; + else if (i < 0) p = &((*p)->left); + else p = &((*p)->right); + } + p = &text_in_code; + while (*p) { + int i = strcmp(buf,(*p)->text); + if (!i) return 0; + else if (i < 0) p = &((*p)->left); + else p = &((*p)->right); + } + crc_printf("%s\n", buf); + *p = new Fd_Text_Tree(buf); + return 1; +} + +/** + Return true if this pointer was already included in the code file. + If it was not, add it to the list and return false. + \param[in] pp ay pointer + \return true if found in the tree, false if added to the tree + */ +bool Fd_Code_Writer::c_contains(void *pp) { + Fd_Pointer_Tree **p = &ptr_in_code; + while (*p) { + if ((*p)->ptr == pp) return true; + else if ((*p)->ptr < pp) p = &((*p)->left); + else p = &((*p)->right); + } + *p = new Fd_Pointer_Tree(pp); + return false; +} + +/** + Write a C string to the code file, escaping non-ASCII characters. + + Text is broken into lines of 78 character. + FLUID " before and after every line text. + + A list of control characters and ", ', and \\ are escaped by adding a \\ in + front of them. Escape ?? by writing ?\\?. All other characters that are not + between 32 and 126 inclusive will be escaped as octal characters. + + This function is utf8 agnostic. + + \param[in] s write this string + \param[in] length write so many bytes in this string + + \see f.write_cstring(const char*) + */ +void Fd_Code_Writer::write_cstring(const char *s, int length) { + const char *next_line = "\"\n\""; + if (varused_test) { + varused = 1; + return; + } + // if we are rendering to the source code preview window, and the text is + // longer than four lines, we only render a placeholder. + if (write_codeview && ((s==NULL) || (length>300))) { + if (length>=0) + crc_printf("\" ... %d bytes of text... \"", length); + else + crc_puts("\" ... text... \""); + return; + } + if (length==-1 || s==0L) { + crc_puts("\n#error string not found\n"); + crc_puts("\" ... undefined size text... \""); + return; + } + + const char *p = s; + const char *e = s+length; + int linelength = 1; + crc_putc('\"'); + for (; p < e;) { + int c = *p++; + switch (c) { + case '\b': c = 'b'; goto QUOTED; + case '\t': c = 't'; goto QUOTED; + case '\n': c = 'n'; goto QUOTED; + case '\f': c = 'f'; goto QUOTED; + case '\r': c = 'r'; goto QUOTED; + case '\"': + case '\'': + case '\\': + QUOTED: + if (linelength >= 77) { crc_puts(next_line); linelength = 0; } + crc_putc('\\'); + crc_putc(c); + linelength += 2; + break; + case '?': // prevent trigraphs by writing ?? as ?\? + if (p-2 >= s && *(p-2) == '?') goto QUOTED; + // else fall through: + default: + if (c >= ' ' && c < 127) { + // a legal ASCII character + if (linelength >= 78) { crc_puts(next_line); linelength = 0; } + crc_putc(c); + linelength++; + break; + } + // if the UTF-8 option is checked, write unicode characters verbatim + if (g_project.utf8_in_src && (c&0x80)) { + if ((c&0x40)) { + // This is the first character in a utf-8 sequence (0b11......). + // A line break would be ok here. Do not put linebreak in front of + // following characters (0b10......) + if (linelength >= 78) { crc_puts(next_line); linelength = 0; } + } + crc_putc(c); + linelength++; + break; + } + // otherwise we must print it as an octal constant: + c &= 255; + if (linelength >= 74) { crc_puts(next_line); linelength = 0; } + crc_printf("\\%03o", c); + linelength += 4; + break; + } + } + crc_putc('\"'); +} + +/** + Write a C string, escaping non-ASCII characters. + \param[in] s write this string + \see f.write_cstring(const char*, int) + */ +void Fd_Code_Writer::write_cstring(const char *s) { + write_cstring(s, (int)strlen(s)); +} + +/** + Write an array of C binary data (does not add a null). + The output is bracketed in { and }. The content is written + as decimal bytes, i.e. `{ 1, 2, 200 }` + + \param[in] s a block of binary data, interpreted as unsigned bytes + \param[in] length size of the block in bytes + */ +void Fd_Code_Writer::write_cdata(const char *s, int length) { + if (varused_test) { + varused = 1; + return; + } + if (write_codeview) { + if (length>=0) + crc_printf("{ /* ... %d bytes of binary data... */ }", length); + else + crc_puts("{ /* ... binary data... */ }"); + return; + } + if (length==-1) { + crc_puts("\n#error data not found\n"); + crc_puts("{ /* ... undefined size binary data... */ }"); + return; + } + const unsigned char *w = (const unsigned char *)s; + const unsigned char *e = w+length; + int linelength = 1; + crc_putc('{'); + for (; w < e;) { + unsigned char c = *w++; + if (c>99) linelength += 4; + else if (c>9) linelength += 3; + else linelength += 2; + if (linelength >= 77) {crc_puts("\n"); linelength = 0;} + crc_printf("%d", c); + if (wis_a(ID_Function) + || t->is_a(ID_Decl) + || t->is_a(ID_Data); +// || t->is_a(ID_Class) // FLUID can't handle a class inside a class +// || t->is_a(ID_Widget_Class) +// || t->is_a(ID_DeclBlock) // Declaration blocks are generally not handled well +} + +/** + Return true, if this is a comment, and if it is followed by a class member. + This must only be called if q is inside a widget class. + Widget classes can have widgets and members (functions/methods, declarations, + etc.) intermixed. + \param[in] q should be a comment type + \return true if this comment is followed by a class member + \return false if it is followed by a widget or code + \see is_class_member(Fl_Type *t) + */ +bool is_comment_before_class_member(Fl_Type *q) { + if (q->is_a(ID_Comment) && q->next && q->next->level==q->level) { + if (q->next->is_a(ID_Comment)) + return is_comment_before_class_member(q->next); + if (is_class_member(q->next)) + return true; + } + return false; +} + +/** + Recursively write static code and declarations + \param[in] p write this type and all its children + \return pointer to the next sibling + */ +Fl_Type* Fd_Code_Writer::write_static(Fl_Type* p) { + if (write_codeview) p->header_static_start = (int)ftell(header_file); + if (write_codeview) p->code_static_start = (int)ftell(code_file); + p->write_static(*this); + if (write_codeview) p->code_static_end = (int)ftell(code_file); + if (write_codeview) p->header_static_end = (int)ftell(header_file); + + Fl_Type* q; + for (q = p->next; q && q->level > p->level;) { + q = write_static(q); + } + + p->write_static_after(*this); + + return q; +} + +/** + Recursively write code, putting children between the two parts of the parent code. + \param[in] p write this type and all its children + \return pointer to the next sibling + */ +Fl_Type* Fd_Code_Writer::write_code(Fl_Type* p) { + // write all code that comes before the children code + // (but don't write the last comment until the very end) + if (!(p==Fl_Type::last && p->is_a(ID_Comment))) { + if (write_codeview) p->code1_start = (int)ftell(code_file); + if (write_codeview) p->header1_start = (int)ftell(header_file); + p->write_code1(*this); + if (write_codeview) p->code1_end = (int)ftell(code_file); + if (write_codeview) p->header1_end = (int)ftell(header_file); + } + // recursively write the code of all children + Fl_Type* q; + if (p->is_widget() && p->is_class()) { + // Handle widget classes specially + for (q = p->next; q && q->level > p->level;) { + // note: maybe declaration blocks should be handled like comments in the context + if (!is_class_member(q) && !is_comment_before_class_member(q)) { + q = write_code(q); + } else { + int level = q->level; + do { + q = q->next; + } while (q && q->level > level); + } + } + + // write all code that come after the children + if (write_codeview) p->code2_start = (int)ftell(code_file); + if (write_codeview) p->header2_start = (int)ftell(header_file); + p->write_code2(*this); + if (write_codeview) p->code2_end = (int)ftell(code_file); + if (write_codeview) p->header2_end = (int)ftell(header_file); + + for (q = p->next; q && q->level > p->level;) { + if (is_class_member(q) || is_comment_before_class_member(q)) { + q = write_code(q); + } else { + int level = q->level; + do { + q = q->next; + } while (q && q->level > level); + } + } + + write_h("};\n"); + current_widget_class = 0L; + } else { + for (q = p->next; q && q->level > p->level;) q = write_code(q); + // write all code that come after the children + if (write_codeview) p->code2_start = (int)ftell(code_file); + if (write_codeview) p->header2_start = (int)ftell(header_file); + p->write_code2(*this); + if (write_codeview) p->code2_end = (int)ftell(code_file); + if (write_codeview) p->header2_end = (int)ftell(header_file); + } + return q; +} + +/** + Write the source and header files for the current design. + + If the files already exist, they will be overwritten. + + \note There is no true error checking here. + + \param[in] s filename of source code file + \param[in] t filename of the header file + \return 0 if the operation failed, 1 if it was successful + */ +int Fd_Code_Writer::write_code(const char *s, const char *t, bool to_codeview) { + write_codeview = to_codeview; + delete id_root; id_root = 0; + indentation = 0; + current_class = 0L; + current_widget_class = 0L; + if (!s) code_file = stdout; + else { + FILE *f = fl_fopen(s, "wb"); + if (!f) return 0; + code_file = f; + } + if (!t) header_file = stdout; + else { + FILE *f = fl_fopen(t, "wb"); + if (!f) {fclose(code_file); return 0;} + header_file = f; + } + // Remember the last code file location for MergeBack + if (s && g_project.write_mergeback_data && !to_codeview) { + std::string proj_filename = g_project.projectfile_path() + g_project.projectfile_name(); + int i, n = (int)proj_filename.size(); + for (i=0; iis_a(ID_Comment)) { + if (write_codeview) { + first_type->code1_start = first_type->code2_start = (int)ftell(code_file); + first_type->header1_start = first_type->header2_start = (int)ftell(header_file); + } + // it is ok to write non-recursive code here, because comments have no children or code2 blocks + first_type->write_code1(*this); + if (write_codeview) { + first_type->code1_end = first_type->code2_end = (int)ftell(code_file); + first_type->header1_end = first_type->header2_end = (int)ftell(header_file); + } + first_type = first_type->next; + } + + const char *hdr = "\ +// generated by Fast Light User Interface Designer (fluid) version %.4f\n\n"; + fprintf(header_file, hdr, FL_VERSION); + crc_printf(hdr, FL_VERSION); + + {char define_name[102]; + const char* a = fl_filename_name(t); + char* b = define_name; + if (!isalpha(*a)) {*b++ = '_';} + while (*a) {*b++ = isalnum(*a) ? *a : '_'; a++;} + *b = 0; + fprintf(header_file, "#ifndef %s\n", define_name); + fprintf(header_file, "#define %s\n", define_name); + } + + if (g_project.avoid_early_includes==0) { + write_h_once("#include "); + } + if (t && g_project.include_H_from_C) { + if (to_codeview) { + write_c("#include \"CodeView.h\"\n"); + } else if (g_project.header_file_name[0] == '.' && strchr(g_project.header_file_name.c_str(), '/') == NULL) { + write_c("#include \"%s\"\n", fl_filename_name(t)); + } else { + write_c("#include \"%s\"\n", g_project.header_file_name.c_str()); + } + } + std::string loc_include, loc_conditional; + if (g_project.i18n_type==FD_I18N_GNU) { + loc_include = g_project.i18n_gnu_include; + loc_conditional = g_project.i18n_gnu_conditional; + } else { + loc_include = g_project.i18n_pos_include; + loc_conditional = g_project.i18n_pos_conditional; + } + if (g_project.i18n_type && !loc_include.empty()) { + int conditional = !loc_conditional.empty(); + if (conditional) { + write_c("#ifdef %s\n", loc_conditional.c_str()); + indentation++; + } + if (loc_include[0] != '<' && loc_include[0] != '\"') + write_c("#%sinclude \"%s\"\n", indent(), loc_include.c_str()); + else + write_c("#%sinclude %s\n", indent(), loc_include.c_str()); + if (g_project.i18n_type == FD_I18N_POSIX) { + if (!g_project.i18n_pos_file.empty()) { + write_c("extern nl_catd %s;\n", g_project.i18n_pos_file.c_str()); + } else { + write_c("// Initialize I18N stuff now for menus...\n"); + write_c("#%sinclude \n", indent()); + write_c("static char *_locale = setlocale(LC_MESSAGES, \"\");\n"); + write_c("static nl_catd _catalog = catopen(\"%s\", 0);\n", g_project.basename().c_str()); + } + } + if (conditional) { + write_c("#else\n"); + if (g_project.i18n_type == FD_I18N_GNU) { + if (!g_project.i18n_gnu_function.empty()) { + write_c("#%sifndef %s\n", indent(), g_project.i18n_gnu_function.c_str()); + write_c("#%sdefine %s(text) text\n", indent_plus(1), g_project.i18n_gnu_function.c_str()); + write_c("#%sendif\n", indent()); + } + } + if (g_project.i18n_type == FD_I18N_POSIX) { + write_c("#%sifndef catgets\n", indent()); + write_c("#%sdefine catgets(catalog, set, msgid, text) text\n", indent_plus(1)); + write_c("#%sendif\n", indent()); + } + indentation--; + write_c("#endif\n"); + } + if (g_project.i18n_type == FD_I18N_GNU && g_project.i18n_gnu_static_function[0]) { + write_c("#ifndef %s\n", g_project.i18n_gnu_static_function.c_str()); + write_c("#%sdefine %s(text) text\n", indent_plus(1), g_project.i18n_gnu_static_function.c_str()); + write_c("#endif\n"); + } + } + for (Fl_Type* p = first_type; p;) { + // write all static data for this & all children first + write_static(p); + // then write the nested code: + p = write_code(p); + } + + if (!s) return 1; + + fprintf(header_file, "#endif\n"); + + Fl_Type* last_type = Fl_Type::last; + if (last_type && (last_type != Fl_Type::first) && last_type->is_a(ID_Comment)) { + if (write_codeview) { + last_type->code1_start = last_type->code2_start = (int)ftell(code_file); + last_type->header1_start = last_type->header2_start = (int)ftell(header_file); + } + last_type->write_code1(*this); + if (write_codeview) { + last_type->code1_end = last_type->code2_end = (int)ftell(code_file); + last_type->header1_end = last_type->header2_end = (int)ftell(header_file); + } + } + int x = 0, y = 0; + + if (code_file != stdout) + x = fclose(code_file); + code_file = 0; + if (header_file != stdout) + y = fclose(header_file); + header_file = 0; + return x >= 0 && y >= 0; +} + + +/** + Write the public/private/protected keywords inside the class. + This avoids repeating these words if the mode is already set. + \param[in] state 0 for private, 1 for public, 2 for protected + */ +void Fd_Code_Writer::write_public(int state) { + if (!current_class && !current_widget_class) return; + if (current_class && current_class->write_public_state == state) return; + if (current_widget_class && current_widget_class->write_public_state == state) return; + if (current_class) current_class->write_public_state = state; + if (current_widget_class) current_widget_class->write_public_state = state; + switch (state) { + case 0: write_h("private:\n"); break; + case 1: write_h("public:\n"); break; + case 2: write_h("protected:\n"); break; + } +} + +/** + Create and initialize a new C++ source code writer. + */ +Fd_Code_Writer::Fd_Code_Writer() +: code_file(NULL), + header_file(NULL), + id_root(NULL), + text_in_header(NULL), + text_in_code(NULL), + ptr_in_code(NULL), + block_crc_(0), + block_line_start_(true), + block_buffer_(NULL), + block_buffer_size_(0), + indentation(0), + write_codeview(false), + varused_test(0), + varused(0) +{ + block_crc_ = crc32(0, NULL, 0); +} + +/** + Release all resources. + */ +Fd_Code_Writer::~Fd_Code_Writer() +{ + delete id_root; + delete ptr_in_code; + delete text_in_code; + delete text_in_header; + if (block_buffer_) ::free(block_buffer_); +} + +/** + Write a MergeBack tag as a separate line of C++ comment. + The tag contains information about the type of tag that we are writing, a + link back to the type using its unique id, and the CRC of all code written + after the previous tag up to this point. + \param[in] type FD_TAG_GENERIC, FD_TAG_CODE, FD_TAG_MENU_CALLBACK, or FD_TAG_WIDGET_CALLBACK + \param[in] uid the unique id of the current type + */ +void Fd_Code_Writer::tag(int type, unsigned short uid) { + if (g_project.write_mergeback_data) + fprintf(code_file, "//~fl~%d~%04x~%08x~~\n", type, (int)uid, (unsigned int)block_crc_); + block_crc_ = crc32(0, NULL, 0); +} + +/** + Static function to calculate the CRC32 of a block of C source code. + Calculation of the CRC ignores leading whitespace in a line and all linefeed + characters ('\\r'). + \param[in] data a pointer to the data block + \param[in] n the size of the data in bytes, or -1 to use strlen() + \param[in] in_crc add to this CRC, 0 by default to start a new block + \param[inout] inout_line_start optional pointer to flag that determines + if we are the start of a line, used to find leading whitespace + \return the new CRC + */ +unsigned long Fd_Code_Writer::block_crc(const void *data, int n, unsigned long in_crc, bool *inout_line_start) { + if (!data) return 0; + if (n==-1) n = (int)strlen((const char*)data); + bool line_start = true; + if (inout_line_start) line_start = *inout_line_start; + const char *s = (const char*)data; + for ( ; n>0; --n, ++s) { + if (line_start) { + // don't count leading spaces and tabs in a line + while (n>0 && *s>0 && isspace(*s)) { s++; n--; } + if (*s) line_start = false; + } + // don't count '\r' that may be introduced by Windows + if (n>0 && *s=='\r') { s++; n--; } + if (n>0 && *s=='\n') line_start = true; + if (n>0) { + in_crc = crc32(in_crc, (const Bytef*)s, 1); + } + } + if (inout_line_start) *inout_line_start = line_start; + return in_crc; +} + +/** Add the following block of text to the CRC of this class. + \param[in] data a pointer to the data block + \param[in] n the size of the data in bytes, or -1 to use strlen() + */ +void Fd_Code_Writer::crc_add(const void *data, int n) { + block_crc_ = block_crc(data, n, block_crc_, &block_line_start_); +} + +/** Write formatted text to the code file. + If MergeBack is enabled, the CRC calculation is continued. + \param[in] format printf style formatting string + \return see fprintf(FILE *, *const char*, ...) + */ +int Fd_Code_Writer::crc_printf(const char *format, ...) { + va_list args; + va_start(args, format); + int ret = crc_vprintf(format, args); + va_end(args); + return ret; +} + +/** Write formatted text to the code file. + If MergeBack is enabled, the CRC calculation is continued. + \param[in] format printf style formatting string + \param[in] args list of arguments + \return see fprintf(FILE *, *const char*, ...) + */ +int Fd_Code_Writer::crc_vprintf(const char *format, va_list args) { + if (g_project.write_mergeback_data) { + int n = vsnprintf(block_buffer_, block_buffer_size_, format, args); + if (n > block_buffer_size_) { + block_buffer_size_ = n + 128; + if (block_buffer_) ::free(block_buffer_); + block_buffer_ = (char*)::malloc(block_buffer_size_+1); + n = vsnprintf(block_buffer_, block_buffer_size_, format, args); + } + crc_add(block_buffer_, n); + return fputs(block_buffer_, code_file); + } else { + return vfprintf(code_file, format, args); + } +} + +/** Write some text to the code file. + If MergeBack is enabled, the CRC calculation is continued. + \param[in] text any text, no requirements to end in a newline or such + \return see fputs(const char*, FILE*) + */ +int Fd_Code_Writer::crc_puts(const char *text) { + if (g_project.write_mergeback_data) { + crc_add(text); + } + return fputs(text, code_file); +} + +/** Write a single ASCII character to the code file. + If MergeBack is enabled, the CRC calculation is continued. + \note to write UTF-8 characters, use Fd_Code_Writer::crc_puts(const char *text) + \param[in] c any character between 0 and 127 inclusive + \return see fputc(int, FILE*) + */ +int Fd_Code_Writer::crc_putc(int c) { + if (g_project.write_mergeback_data) { + uchar uc = (uchar)c; + crc_add(&uc, 1); + } + return fputc(c, code_file); +} + +/// \} + diff --git a/fluid/io/Code_Writer.h b/fluid/io/Code_Writer.h new file mode 100644 index 000000000..758e7bf45 --- /dev/null +++ b/fluid/io/Code_Writer.h @@ -0,0 +1,112 @@ +// +// Code output routines for the Fast Light Tool Kit (FLTK). +// +// Copyright 1998-2021 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_CODE_H +#define _FLUID_CODE_H + +#include + +#include +#include +#include + +class Fl_Type; +struct Fd_Identifier_Tree; +struct Fd_Text_Tree; +struct Fd_Pointer_Tree; + +int is_id(char c); +int write_strings(const std::string &filename); + +class Fd_Code_Writer +{ +protected: + /// file pointer for the C++ code file + FILE *code_file; + /// file pointer for the C++ header file + FILE *header_file; + + /// tree of unique but human-readable identifiers + Fd_Identifier_Tree* id_root; + /// searchable text tree for text that is only written once to the header file + Fd_Text_Tree *text_in_header; + /// searchable text tree for text that is only written once to the code file + Fd_Text_Tree *text_in_code; + /// searchable tree for pointers that are only written once to the code file + Fd_Pointer_Tree *ptr_in_code; + + /// crc32 for blocks of text written to the code file + unsigned long block_crc_; + /// if set, we are at the start of a line and can ignore leading spaces in crc + bool block_line_start_; + /// expanding buffer for vsnprintf + char *block_buffer_; + /// size of expanding buffer for vsnprintf + int block_buffer_size_; + + void crc_add(const void *data, int n=-1); + int crc_printf(const char *format, ...); + int crc_vprintf(const char *format, va_list args); + int crc_puts(const char *text); + int crc_putc(int c); + +public: + /// current level of source code indentation + int indentation; + /// set if we write abbreviated file for the source code previewer + /// (disables binary data blocks, for example) + bool write_codeview; + /// silly thing to prevent declaring unused variables: + /// When this symbol is on, all attempts to write code don't write + /// anything, but set a variable if it looks like the variable "o" is used: + int varused_test; + /// set to 1 if varused_test found that a variable is actually used + int varused; + +public: + Fd_Code_Writer(); + ~Fd_Code_Writer(); + const char* unique_id(void* o, const char*, const char*, const char*); + /// Increment source code indentation level. + void indent_more() { indentation++; } + /// Decrement source code indentation level. + void indent_less() { indentation--; } + const char *indent(); + const char *indent(int set); + const char *indent_plus(int offset); + int write_h_once(const char *, ...) __fl_attr((__format__ (__printf__, 2, 3))); + int write_c_once(const char *, ...) __fl_attr((__format__ (__printf__, 2, 3))); + bool c_contains(void* ptr); + void write_cstring(const char *,int length); + void write_cstring(const char *); + void write_cdata(const char *,int length); + void vwrite_c(const char* format, va_list args); + void write_c(const char*, ...) __fl_attr((__format__ (__printf__, 2, 3))); + void write_cc(const char *, int, const char*, const char*); + void write_h(const char*, ...) __fl_attr((__format__ (__printf__, 2, 3))); + void write_hc(const char *, int, const char*, const char*); + void write_c_indented(const char *textlines, int inIndent, char inTrailwWith); + Fl_Type* write_static(Fl_Type* p); + Fl_Type* write_code(Fl_Type* p); + int write_code(const char *cfile, const char *hfile, bool to_codeview=false); + void write_public(int state); // writes pubic:/private: as needed + + void tag(int type, unsigned short uid); + + static unsigned long block_crc(const void *data, int n=-1, unsigned long in_crc=0, bool *inout_line_start=NULL); +}; + +#endif // _FLUID_CODE_H diff --git a/fluid/io/Project_Reader.cxx b/fluid/io/Project_Reader.cxx new file mode 100644 index 000000000..ba1afeb1b --- /dev/null +++ b/fluid/io/Project_Reader.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 +#include +#include +#include +#include "../src/flstring.h" + +#include +#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 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(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()) { // + ((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_Reader.h b/fluid/io/Project_Reader.h new file mode 100644 index 000000000..470cc1a7b --- /dev/null +++ b/fluid/io/Project_Reader.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 + +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/io/Project_Writer.cxx b/fluid/io/Project_Writer.cxx new file mode 100644 index 000000000..ba1afeb1b --- /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 +#include +#include +#include +#include "../src/flstring.h" + +#include +#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 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(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()) { // + ((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 000000000..470cc1a7b --- /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 + +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/io/code.cxx b/fluid/io/code.cxx deleted file mode 100644 index b77cad147..000000000 --- a/fluid/io/code.cxx +++ /dev/null @@ -1,1107 +0,0 @@ -// -// Code output routines 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 "io/code.h" - -#include "app/fluid.h" -#include "app/undo.h" -#include "io/file.h" -#include "nodes/Fl_Group_Type.h" -#include "nodes/Fl_Window_Type.h" -#include "nodes/Fl_Function_Type.h" -#include "tools/fluid_filename.h" - -#include -#include -#include -#include "../src/flstring.h" - -#include -#include -#include - -#include - -/// \defgroup cfile C Code File Operations -/// \{ - - -/** - Return true if c can be in a C identifier. - I needed this so it is not messed up by locale settings. - \param[in] c a character, or the start of a utf-8 sequence - \return 1 if c is alphanumeric or '_' - */ -int is_id(char c) { - return (c>='a' && c<='z') || (c>='A' && c<='Z') || (c>='0' && c<='9') || c=='_'; -} - -/** - Write a string to a file, replacing all non-ASCII characters with octal codes. - \param[in] out output file - \param[in] text write this NUL terminated utf-8 string - \return EOF if any of the file access calls failed, 0 if OK - */ -int write_escaped_strings(FILE *out, const char *text) { - int ret = 0; - const unsigned char *utf8_text = (const unsigned char *)text; - for (const unsigned char *s = utf8_text; *s; ++s) { - unsigned char c = *s; - // escape control characters, delete, all utf-8, and the double quotes - // note: we should have an option in the project settings to allow utf-8 - // characters in the output text and not escape them - if (c < 32 || c > 126 || c == '\"') { - if (c == '\r') { - ret = fputs("\\r", out); - } else if (c == '\n') { - ret = fputs("\\n", out); - } else { - ret = fprintf(out, "\\%03o", c); - } - } else { - ret = putc((int)c, out); - } - } - return ret; -} - -/** - Write a file that contains all label and tooltip strings for internationalization. - The user is responsible to set the right file name extension. The file format - is determined by `g_project.i18n_type`. - \param[in] filename file path and name to a file that will hold the strings - \return 1 if the file could not be opened for writing, or the result of `fclose`. - */ -int write_strings(const std::string &filename) { - Fl_Type *p; - Fl_Widget_Type *w; - int i; - - FILE *fp = fl_fopen(filename.c_str(), "wb"); - if (!fp) return 1; - - switch (g_project.i18n_type) { - case FD_I18N_NONE : /* None, just put static text out */ - fprintf(fp, "# generated by Fast Light User Interface Designer (fluid) version %.4f\n", - FL_VERSION); - for (p = Fl_Type::first; p; p = p->next) { - if (p->is_widget()) { - w = (Fl_Widget_Type *)p; - - if (w->label()) { - write_escaped_strings(fp, w->label()); - putc('\n', fp); - } - - if (w->tooltip()) { - write_escaped_strings(fp, w->tooltip()); - putc('\n', fp); - } - } - } - break; - case FD_I18N_GNU : /* GNU gettext, put a .po file out */ - fprintf(fp, "# generated by Fast Light User Interface Designer (fluid) version %.4f\n", - FL_VERSION); - for (p = Fl_Type::first; p; p = p->next) { - if (p->is_widget()) { - w = (Fl_Widget_Type *)p; - - if (w->label()) { - fputs("msgid \"", fp); - write_escaped_strings(fp, w->label()); - fputs("\"\n", fp); - - fputs("msgstr \"", fp); - write_escaped_strings(fp, w->label()); - fputs("\"\n", fp); - } - - if (w->tooltip()) { - fputs("msgid \"", fp); - write_escaped_strings(fp, w->tooltip()); - fputs("\"\n", fp); - - fputs("msgstr \"", fp); - write_escaped_strings(fp, w->tooltip()); - fputs("\"\n", fp); - } - } - } - break; - case FD_I18N_POSIX : /* POSIX catgets, put a .msg file out */ - fprintf(fp, "$ generated by Fast Light User Interface Designer (fluid) version %.4f\n", - FL_VERSION); - fprintf(fp, "$set %s\n", g_project.i18n_pos_set.c_str()); - fputs("$quote \"\n", fp); - - for (i = 1, p = Fl_Type::first; p; p = p->next) { - if (p->is_widget()) { - w = (Fl_Widget_Type *)p; - - if (w->label()) { - fprintf(fp, "%d \"", i ++); - write_escaped_strings(fp, w->label()); - fputs("\"\n", fp); - } - - if (w->tooltip()) { - fprintf(fp, "%d \"", i ++); - write_escaped_strings(fp, w->tooltip()); - fputs("\"\n", fp); - } - } - } - break; - } - - return fclose(fp); -} - -//////////////////////////////////////////////////////////////// -// Generate unique but human-readable identifiers: - -/** A binary searchable tree storing identifiers for quick retrieval. */ -struct Fd_Identifier_Tree { - char* text; - void* object; - Fd_Identifier_Tree *left, *right; - Fd_Identifier_Tree (const char* t, void* o) : text(fl_strdup(t)), object(o) {left = right = 0;} - ~Fd_Identifier_Tree(); -}; - -Fd_Identifier_Tree::~Fd_Identifier_Tree() { - delete left; - free((void *)text); - delete right; -} - -/** \brief Return a unique name for the given object. - - This function combines the name and label into an identifier. It then checks - if that id was already taken by another object, and if so, appends a - hexadecimal value which is incremented until the id is unique in this file. - - If a new id was created, it is stored in the id tree. - - \param[in] o create an ID for this object - \param[in] type is the first word of the ID - \param[in] name if name is set, it is appended to the ID - \param[in] label else if label is set, it is appended, skipping non-keyword characters - \return buffer to a unique identifier, managed by Fd_Code_Writer, so caller must NOT free() it - */ -const char* Fd_Code_Writer::unique_id(void* o, const char* type, const char* name, const char* label) { - char buffer[128]; - char* q = buffer; - char* q_end = q + 128 - 8 - 1; // room for hex number and NUL - while (*type) *q++ = *type++; - *q++ = '_'; - const char* n = name; - if (!n || !*n) n = label; - if (n && *n) { - while (*n && !is_id(*n)) n++; - while (is_id(*n) && (q < q_end)) *q++ = *n++; - } - *q = 0; - // okay, search the tree and see if the name was already used: - Fd_Identifier_Tree** p = &id_root; - int which = 0; - while (*p) { - int i = strcmp(buffer, (*p)->text); - if (!i) { - if ((*p)->object == o) return (*p)->text; - // already used, we need to pick a new name: - sprintf(q,"%x",++which); - p = &id_root; - continue; - } - else if (i < 0) p = &((*p)->left); - else p = &((*p)->right); - } - *p = new Fd_Identifier_Tree(buffer, o); - return (*p)->text; -} - -//////////////////////////////////////////////////////////////// -// return current indentation: - - -/** - Return a C string that indents code to the given depth. - - Indentation can be changed by modifying the multiplicator (``*2`` to keep - the FLTK indent style). Changing `spaces` to a list of tabs would generate - tab indents instead. This function can also be used for fixed depth indents - in the header file. - - Do *not* ever make this a user preference, or you will end up writing a - fully featured code formatter. - - \param[in] set generate this indent depth - \return pointer to a static string - */ -const char *Fd_Code_Writer::indent(int set) { - static const char* spaces = " "; - int i = set * 2; - if (i>32) i = 32; - if (i<0) i = 0; - return spaces+32-i; -} - -/** - Return a C string that indents code to the current source file depth. - \return pointer to a static string - */ -const char *Fd_Code_Writer::indent() { - return indent(indentation); -} - -/** - Return a C string that indents code to the current source file depth plus an offset. - \param[in] offset adds a temporary offset for this call only; this does not - change the `indentation` variable; offset can be negative - \return pointer to a static string - */ -const char *Fd_Code_Writer::indent_plus(int offset) { - return indent(indentation+offset); -} - - -//////////////////////////////////////////////////////////////// -// declarations/include files: -// Each string generated by write_h_once is written only once to -// the header file. This is done by keeping a binary tree of all -// the calls so far and not printing it if it is in the tree. - -/** A binary searchable tree storing text for quick retrieval. */ -struct Fd_Text_Tree { - char *text; - Fd_Text_Tree *left, *right; - Fd_Text_Tree(const char *t) { - text = fl_strdup(t); - left = right = 0; - } - ~Fd_Text_Tree(); -}; - -Fd_Text_Tree::~Fd_Text_Tree() { - delete left; - free((void *)text); - delete right; -} - -/** A binary searchable tree storing pointers for quick retrieval. */ -struct Fd_Pointer_Tree { - void *ptr; - Fd_Pointer_Tree *left, *right; - Fd_Pointer_Tree(void *p) { - ptr = p; - left = right = 0; - } - ~Fd_Pointer_Tree(); -}; - -Fd_Pointer_Tree::~Fd_Pointer_Tree() { - delete left; - delete right; -} - -/** - Print a formatted line to the header file, unless the same line was produced before in this header file. - \note Resulting line is cropped at 1023 bytes. - \param[in] format printf-style formatting text, followed by a vararg list - \return 1 if the text was written to the file, 0 if it was previously written. - */ -int Fd_Code_Writer::write_h_once(const char *format, ...) { - va_list args; - char buf[1024]; - va_start(args, format); - vsnprintf(buf, sizeof(buf), format, args); - va_end(args); - Fd_Text_Tree **p = &text_in_header; - while (*p) { - int i = strcmp(buf,(*p)->text); - if (!i) return 0; - else if (i < 0) p = &((*p)->left); - else p = &((*p)->right); - } - fprintf(header_file,"%s\n",buf); - *p = new Fd_Text_Tree(buf); - return 1; -} - -/** - Print a formatted line to the source file, unless the same line was produced before in this code file. - \note Resulting line is cropped at 1023 bytes. - \param[in] format printf-style formatting text, followed by a vararg list - \return 1 if the text was written to the file, 0 if it was previously written. - */ -int Fd_Code_Writer::write_c_once(const char *format, ...) { - va_list args; - char buf[1024]; - va_start(args, format); - vsnprintf(buf, sizeof(buf), format, args); - va_end(args); - Fd_Text_Tree **p = &text_in_header; - while (*p) { - int i = strcmp(buf,(*p)->text); - if (!i) return 0; - else if (i < 0) p = &((*p)->left); - else p = &((*p)->right); - } - p = &text_in_code; - while (*p) { - int i = strcmp(buf,(*p)->text); - if (!i) return 0; - else if (i < 0) p = &((*p)->left); - else p = &((*p)->right); - } - crc_printf("%s\n", buf); - *p = new Fd_Text_Tree(buf); - return 1; -} - -/** - Return true if this pointer was already included in the code file. - If it was not, add it to the list and return false. - \param[in] pp ay pointer - \return true if found in the tree, false if added to the tree - */ -bool Fd_Code_Writer::c_contains(void *pp) { - Fd_Pointer_Tree **p = &ptr_in_code; - while (*p) { - if ((*p)->ptr == pp) return true; - else if ((*p)->ptr < pp) p = &((*p)->left); - else p = &((*p)->right); - } - *p = new Fd_Pointer_Tree(pp); - return false; -} - -/** - Write a C string to the code file, escaping non-ASCII characters. - - Text is broken into lines of 78 character. - FLUID " before and after every line text. - - A list of control characters and ", ', and \\ are escaped by adding a \\ in - front of them. Escape ?? by writing ?\\?. All other characters that are not - between 32 and 126 inclusive will be escaped as octal characters. - - This function is utf8 agnostic. - - \param[in] s write this string - \param[in] length write so many bytes in this string - - \see f.write_cstring(const char*) - */ -void Fd_Code_Writer::write_cstring(const char *s, int length) { - const char *next_line = "\"\n\""; - if (varused_test) { - varused = 1; - return; - } - // if we are rendering to the source code preview window, and the text is - // longer than four lines, we only render a placeholder. - if (write_codeview && ((s==NULL) || (length>300))) { - if (length>=0) - crc_printf("\" ... %d bytes of text... \"", length); - else - crc_puts("\" ... text... \""); - return; - } - if (length==-1 || s==0L) { - crc_puts("\n#error string not found\n"); - crc_puts("\" ... undefined size text... \""); - return; - } - - const char *p = s; - const char *e = s+length; - int linelength = 1; - crc_putc('\"'); - for (; p < e;) { - int c = *p++; - switch (c) { - case '\b': c = 'b'; goto QUOTED; - case '\t': c = 't'; goto QUOTED; - case '\n': c = 'n'; goto QUOTED; - case '\f': c = 'f'; goto QUOTED; - case '\r': c = 'r'; goto QUOTED; - case '\"': - case '\'': - case '\\': - QUOTED: - if (linelength >= 77) { crc_puts(next_line); linelength = 0; } - crc_putc('\\'); - crc_putc(c); - linelength += 2; - break; - case '?': // prevent trigraphs by writing ?? as ?\? - if (p-2 >= s && *(p-2) == '?') goto QUOTED; - // else fall through: - default: - if (c >= ' ' && c < 127) { - // a legal ASCII character - if (linelength >= 78) { crc_puts(next_line); linelength = 0; } - crc_putc(c); - linelength++; - break; - } - // if the UTF-8 option is checked, write unicode characters verbatim - if (g_project.utf8_in_src && (c&0x80)) { - if ((c&0x40)) { - // This is the first character in a utf-8 sequence (0b11......). - // A line break would be ok here. Do not put linebreak in front of - // following characters (0b10......) - if (linelength >= 78) { crc_puts(next_line); linelength = 0; } - } - crc_putc(c); - linelength++; - break; - } - // otherwise we must print it as an octal constant: - c &= 255; - if (linelength >= 74) { crc_puts(next_line); linelength = 0; } - crc_printf("\\%03o", c); - linelength += 4; - break; - } - } - crc_putc('\"'); -} - -/** - Write a C string, escaping non-ASCII characters. - \param[in] s write this string - \see f.write_cstring(const char*, int) - */ -void Fd_Code_Writer::write_cstring(const char *s) { - write_cstring(s, (int)strlen(s)); -} - -/** - Write an array of C binary data (does not add a null). - The output is bracketed in { and }. The content is written - as decimal bytes, i.e. `{ 1, 2, 200 }` - - \param[in] s a block of binary data, interpreted as unsigned bytes - \param[in] length size of the block in bytes - */ -void Fd_Code_Writer::write_cdata(const char *s, int length) { - if (varused_test) { - varused = 1; - return; - } - if (write_codeview) { - if (length>=0) - crc_printf("{ /* ... %d bytes of binary data... */ }", length); - else - crc_puts("{ /* ... binary data... */ }"); - return; - } - if (length==-1) { - crc_puts("\n#error data not found\n"); - crc_puts("{ /* ... undefined size binary data... */ }"); - return; - } - const unsigned char *w = (const unsigned char *)s; - const unsigned char *e = w+length; - int linelength = 1; - crc_putc('{'); - for (; w < e;) { - unsigned char c = *w++; - if (c>99) linelength += 4; - else if (c>9) linelength += 3; - else linelength += 2; - if (linelength >= 77) {crc_puts("\n"); linelength = 0;} - crc_printf("%d", c); - if (wis_a(ID_Function) - || t->is_a(ID_Decl) - || t->is_a(ID_Data); -// || t->is_a(ID_Class) // FLUID can't handle a class inside a class -// || t->is_a(ID_Widget_Class) -// || t->is_a(ID_DeclBlock) // Declaration blocks are generally not handled well -} - -/** - Return true, if this is a comment, and if it is followed by a class member. - This must only be called if q is inside a widget class. - Widget classes can have widgets and members (functions/methods, declarations, - etc.) intermixed. - \param[in] q should be a comment type - \return true if this comment is followed by a class member - \return false if it is followed by a widget or code - \see is_class_member(Fl_Type *t) - */ -bool is_comment_before_class_member(Fl_Type *q) { - if (q->is_a(ID_Comment) && q->next && q->next->level==q->level) { - if (q->next->is_a(ID_Comment)) - return is_comment_before_class_member(q->next); - if (is_class_member(q->next)) - return true; - } - return false; -} - -/** - Recursively write static code and declarations - \param[in] p write this type and all its children - \return pointer to the next sibling - */ -Fl_Type* Fd_Code_Writer::write_static(Fl_Type* p) { - if (write_codeview) p->header_static_start = (int)ftell(header_file); - if (write_codeview) p->code_static_start = (int)ftell(code_file); - p->write_static(*this); - if (write_codeview) p->code_static_end = (int)ftell(code_file); - if (write_codeview) p->header_static_end = (int)ftell(header_file); - - Fl_Type* q; - for (q = p->next; q && q->level > p->level;) { - q = write_static(q); - } - - p->write_static_after(*this); - - return q; -} - -/** - Recursively write code, putting children between the two parts of the parent code. - \param[in] p write this type and all its children - \return pointer to the next sibling - */ -Fl_Type* Fd_Code_Writer::write_code(Fl_Type* p) { - // write all code that comes before the children code - // (but don't write the last comment until the very end) - if (!(p==Fl_Type::last && p->is_a(ID_Comment))) { - if (write_codeview) p->code1_start = (int)ftell(code_file); - if (write_codeview) p->header1_start = (int)ftell(header_file); - p->write_code1(*this); - if (write_codeview) p->code1_end = (int)ftell(code_file); - if (write_codeview) p->header1_end = (int)ftell(header_file); - } - // recursively write the code of all children - Fl_Type* q; - if (p->is_widget() && p->is_class()) { - // Handle widget classes specially - for (q = p->next; q && q->level > p->level;) { - // note: maybe declaration blocks should be handled like comments in the context - if (!is_class_member(q) && !is_comment_before_class_member(q)) { - q = write_code(q); - } else { - int level = q->level; - do { - q = q->next; - } while (q && q->level > level); - } - } - - // write all code that come after the children - if (write_codeview) p->code2_start = (int)ftell(code_file); - if (write_codeview) p->header2_start = (int)ftell(header_file); - p->write_code2(*this); - if (write_codeview) p->code2_end = (int)ftell(code_file); - if (write_codeview) p->header2_end = (int)ftell(header_file); - - for (q = p->next; q && q->level > p->level;) { - if (is_class_member(q) || is_comment_before_class_member(q)) { - q = write_code(q); - } else { - int level = q->level; - do { - q = q->next; - } while (q && q->level > level); - } - } - - write_h("};\n"); - current_widget_class = 0L; - } else { - for (q = p->next; q && q->level > p->level;) q = write_code(q); - // write all code that come after the children - if (write_codeview) p->code2_start = (int)ftell(code_file); - if (write_codeview) p->header2_start = (int)ftell(header_file); - p->write_code2(*this); - if (write_codeview) p->code2_end = (int)ftell(code_file); - if (write_codeview) p->header2_end = (int)ftell(header_file); - } - return q; -} - -/** - Write the source and header files for the current design. - - If the files already exist, they will be overwritten. - - \note There is no true error checking here. - - \param[in] s filename of source code file - \param[in] t filename of the header file - \return 0 if the operation failed, 1 if it was successful - */ -int Fd_Code_Writer::write_code(const char *s, const char *t, bool to_codeview) { - write_codeview = to_codeview; - delete id_root; id_root = 0; - indentation = 0; - current_class = 0L; - current_widget_class = 0L; - if (!s) code_file = stdout; - else { - FILE *f = fl_fopen(s, "wb"); - if (!f) return 0; - code_file = f; - } - if (!t) header_file = stdout; - else { - FILE *f = fl_fopen(t, "wb"); - if (!f) {fclose(code_file); return 0;} - header_file = f; - } - // Remember the last code file location for MergeBack - if (s && g_project.write_mergeback_data && !to_codeview) { - std::string proj_filename = g_project.projectfile_path() + g_project.projectfile_name(); - int i, n = (int)proj_filename.size(); - for (i=0; iis_a(ID_Comment)) { - if (write_codeview) { - first_type->code1_start = first_type->code2_start = (int)ftell(code_file); - first_type->header1_start = first_type->header2_start = (int)ftell(header_file); - } - // it is ok to write non-recursive code here, because comments have no children or code2 blocks - first_type->write_code1(*this); - if (write_codeview) { - first_type->code1_end = first_type->code2_end = (int)ftell(code_file); - first_type->header1_end = first_type->header2_end = (int)ftell(header_file); - } - first_type = first_type->next; - } - - const char *hdr = "\ -// generated by Fast Light User Interface Designer (fluid) version %.4f\n\n"; - fprintf(header_file, hdr, FL_VERSION); - crc_printf(hdr, FL_VERSION); - - {char define_name[102]; - const char* a = fl_filename_name(t); - char* b = define_name; - if (!isalpha(*a)) {*b++ = '_';} - while (*a) {*b++ = isalnum(*a) ? *a : '_'; a++;} - *b = 0; - fprintf(header_file, "#ifndef %s\n", define_name); - fprintf(header_file, "#define %s\n", define_name); - } - - if (g_project.avoid_early_includes==0) { - write_h_once("#include "); - } - if (t && g_project.include_H_from_C) { - if (to_codeview) { - write_c("#include \"CodeView.h\"\n"); - } else if (g_project.header_file_name[0] == '.' && strchr(g_project.header_file_name.c_str(), '/') == NULL) { - write_c("#include \"%s\"\n", fl_filename_name(t)); - } else { - write_c("#include \"%s\"\n", g_project.header_file_name.c_str()); - } - } - std::string loc_include, loc_conditional; - if (g_project.i18n_type==FD_I18N_GNU) { - loc_include = g_project.i18n_gnu_include; - loc_conditional = g_project.i18n_gnu_conditional; - } else { - loc_include = g_project.i18n_pos_include; - loc_conditional = g_project.i18n_pos_conditional; - } - if (g_project.i18n_type && !loc_include.empty()) { - int conditional = !loc_conditional.empty(); - if (conditional) { - write_c("#ifdef %s\n", loc_conditional.c_str()); - indentation++; - } - if (loc_include[0] != '<' && loc_include[0] != '\"') - write_c("#%sinclude \"%s\"\n", indent(), loc_include.c_str()); - else - write_c("#%sinclude %s\n", indent(), loc_include.c_str()); - if (g_project.i18n_type == FD_I18N_POSIX) { - if (!g_project.i18n_pos_file.empty()) { - write_c("extern nl_catd %s;\n", g_project.i18n_pos_file.c_str()); - } else { - write_c("// Initialize I18N stuff now for menus...\n"); - write_c("#%sinclude \n", indent()); - write_c("static char *_locale = setlocale(LC_MESSAGES, \"\");\n"); - write_c("static nl_catd _catalog = catopen(\"%s\", 0);\n", g_project.basename().c_str()); - } - } - if (conditional) { - write_c("#else\n"); - if (g_project.i18n_type == FD_I18N_GNU) { - if (!g_project.i18n_gnu_function.empty()) { - write_c("#%sifndef %s\n", indent(), g_project.i18n_gnu_function.c_str()); - write_c("#%sdefine %s(text) text\n", indent_plus(1), g_project.i18n_gnu_function.c_str()); - write_c("#%sendif\n", indent()); - } - } - if (g_project.i18n_type == FD_I18N_POSIX) { - write_c("#%sifndef catgets\n", indent()); - write_c("#%sdefine catgets(catalog, set, msgid, text) text\n", indent_plus(1)); - write_c("#%sendif\n", indent()); - } - indentation--; - write_c("#endif\n"); - } - if (g_project.i18n_type == FD_I18N_GNU && g_project.i18n_gnu_static_function[0]) { - write_c("#ifndef %s\n", g_project.i18n_gnu_static_function.c_str()); - write_c("#%sdefine %s(text) text\n", indent_plus(1), g_project.i18n_gnu_static_function.c_str()); - write_c("#endif\n"); - } - } - for (Fl_Type* p = first_type; p;) { - // write all static data for this & all children first - write_static(p); - // then write the nested code: - p = write_code(p); - } - - if (!s) return 1; - - fprintf(header_file, "#endif\n"); - - Fl_Type* last_type = Fl_Type::last; - if (last_type && (last_type != Fl_Type::first) && last_type->is_a(ID_Comment)) { - if (write_codeview) { - last_type->code1_start = last_type->code2_start = (int)ftell(code_file); - last_type->header1_start = last_type->header2_start = (int)ftell(header_file); - } - last_type->write_code1(*this); - if (write_codeview) { - last_type->code1_end = last_type->code2_end = (int)ftell(code_file); - last_type->header1_end = last_type->header2_end = (int)ftell(header_file); - } - } - int x = 0, y = 0; - - if (code_file != stdout) - x = fclose(code_file); - code_file = 0; - if (header_file != stdout) - y = fclose(header_file); - header_file = 0; - return x >= 0 && y >= 0; -} - - -/** - Write the public/private/protected keywords inside the class. - This avoids repeating these words if the mode is already set. - \param[in] state 0 for private, 1 for public, 2 for protected - */ -void Fd_Code_Writer::write_public(int state) { - if (!current_class && !current_widget_class) return; - if (current_class && current_class->write_public_state == state) return; - if (current_widget_class && current_widget_class->write_public_state == state) return; - if (current_class) current_class->write_public_state = state; - if (current_widget_class) current_widget_class->write_public_state = state; - switch (state) { - case 0: write_h("private:\n"); break; - case 1: write_h("public:\n"); break; - case 2: write_h("protected:\n"); break; - } -} - -/** - Create and initialize a new C++ source code writer. - */ -Fd_Code_Writer::Fd_Code_Writer() -: code_file(NULL), - header_file(NULL), - id_root(NULL), - text_in_header(NULL), - text_in_code(NULL), - ptr_in_code(NULL), - block_crc_(0), - block_line_start_(true), - block_buffer_(NULL), - block_buffer_size_(0), - indentation(0), - write_codeview(false), - varused_test(0), - varused(0) -{ - block_crc_ = crc32(0, NULL, 0); -} - -/** - Release all resources. - */ -Fd_Code_Writer::~Fd_Code_Writer() -{ - delete id_root; - delete ptr_in_code; - delete text_in_code; - delete text_in_header; - if (block_buffer_) ::free(block_buffer_); -} - -/** - Write a MergeBack tag as a separate line of C++ comment. - The tag contains information about the type of tag that we are writing, a - link back to the type using its unique id, and the CRC of all code written - after the previous tag up to this point. - \param[in] type FD_TAG_GENERIC, FD_TAG_CODE, FD_TAG_MENU_CALLBACK, or FD_TAG_WIDGET_CALLBACK - \param[in] uid the unique id of the current type - */ -void Fd_Code_Writer::tag(int type, unsigned short uid) { - if (g_project.write_mergeback_data) - fprintf(code_file, "//~fl~%d~%04x~%08x~~\n", type, (int)uid, (unsigned int)block_crc_); - block_crc_ = crc32(0, NULL, 0); -} - -/** - Static function to calculate the CRC32 of a block of C source code. - Calculation of the CRC ignores leading whitespace in a line and all linefeed - characters ('\\r'). - \param[in] data a pointer to the data block - \param[in] n the size of the data in bytes, or -1 to use strlen() - \param[in] in_crc add to this CRC, 0 by default to start a new block - \param[inout] inout_line_start optional pointer to flag that determines - if we are the start of a line, used to find leading whitespace - \return the new CRC - */ -unsigned long Fd_Code_Writer::block_crc(const void *data, int n, unsigned long in_crc, bool *inout_line_start) { - if (!data) return 0; - if (n==-1) n = (int)strlen((const char*)data); - bool line_start = true; - if (inout_line_start) line_start = *inout_line_start; - const char *s = (const char*)data; - for ( ; n>0; --n, ++s) { - if (line_start) { - // don't count leading spaces and tabs in a line - while (n>0 && *s>0 && isspace(*s)) { s++; n--; } - if (*s) line_start = false; - } - // don't count '\r' that may be introduced by Windows - if (n>0 && *s=='\r') { s++; n--; } - if (n>0 && *s=='\n') line_start = true; - if (n>0) { - in_crc = crc32(in_crc, (const Bytef*)s, 1); - } - } - if (inout_line_start) *inout_line_start = line_start; - return in_crc; -} - -/** Add the following block of text to the CRC of this class. - \param[in] data a pointer to the data block - \param[in] n the size of the data in bytes, or -1 to use strlen() - */ -void Fd_Code_Writer::crc_add(const void *data, int n) { - block_crc_ = block_crc(data, n, block_crc_, &block_line_start_); -} - -/** Write formatted text to the code file. - If MergeBack is enabled, the CRC calculation is continued. - \param[in] format printf style formatting string - \return see fprintf(FILE *, *const char*, ...) - */ -int Fd_Code_Writer::crc_printf(const char *format, ...) { - va_list args; - va_start(args, format); - int ret = crc_vprintf(format, args); - va_end(args); - return ret; -} - -/** Write formatted text to the code file. - If MergeBack is enabled, the CRC calculation is continued. - \param[in] format printf style formatting string - \param[in] args list of arguments - \return see fprintf(FILE *, *const char*, ...) - */ -int Fd_Code_Writer::crc_vprintf(const char *format, va_list args) { - if (g_project.write_mergeback_data) { - int n = vsnprintf(block_buffer_, block_buffer_size_, format, args); - if (n > block_buffer_size_) { - block_buffer_size_ = n + 128; - if (block_buffer_) ::free(block_buffer_); - block_buffer_ = (char*)::malloc(block_buffer_size_+1); - n = vsnprintf(block_buffer_, block_buffer_size_, format, args); - } - crc_add(block_buffer_, n); - return fputs(block_buffer_, code_file); - } else { - return vfprintf(code_file, format, args); - } -} - -/** Write some text to the code file. - If MergeBack is enabled, the CRC calculation is continued. - \param[in] text any text, no requirements to end in a newline or such - \return see fputs(const char*, FILE*) - */ -int Fd_Code_Writer::crc_puts(const char *text) { - if (g_project.write_mergeback_data) { - crc_add(text); - } - return fputs(text, code_file); -} - -/** Write a single ASCII character to the code file. - If MergeBack is enabled, the CRC calculation is continued. - \note to write UTF-8 characters, use Fd_Code_Writer::crc_puts(const char *text) - \param[in] c any character between 0 and 127 inclusive - \return see fputc(int, FILE*) - */ -int Fd_Code_Writer::crc_putc(int c) { - if (g_project.write_mergeback_data) { - uchar uc = (uchar)c; - crc_add(&uc, 1); - } - return fputc(c, code_file); -} - -/// \} - diff --git a/fluid/io/code.h b/fluid/io/code.h deleted file mode 100644 index 758e7bf45..000000000 --- a/fluid/io/code.h +++ /dev/null @@ -1,112 +0,0 @@ -// -// Code output routines for the Fast Light Tool Kit (FLTK). -// -// Copyright 1998-2021 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_CODE_H -#define _FLUID_CODE_H - -#include - -#include -#include -#include - -class Fl_Type; -struct Fd_Identifier_Tree; -struct Fd_Text_Tree; -struct Fd_Pointer_Tree; - -int is_id(char c); -int write_strings(const std::string &filename); - -class Fd_Code_Writer -{ -protected: - /// file pointer for the C++ code file - FILE *code_file; - /// file pointer for the C++ header file - FILE *header_file; - - /// tree of unique but human-readable identifiers - Fd_Identifier_Tree* id_root; - /// searchable text tree for text that is only written once to the header file - Fd_Text_Tree *text_in_header; - /// searchable text tree for text that is only written once to the code file - Fd_Text_Tree *text_in_code; - /// searchable tree for pointers that are only written once to the code file - Fd_Pointer_Tree *ptr_in_code; - - /// crc32 for blocks of text written to the code file - unsigned long block_crc_; - /// if set, we are at the start of a line and can ignore leading spaces in crc - bool block_line_start_; - /// expanding buffer for vsnprintf - char *block_buffer_; - /// size of expanding buffer for vsnprintf - int block_buffer_size_; - - void crc_add(const void *data, int n=-1); - int crc_printf(const char *format, ...); - int crc_vprintf(const char *format, va_list args); - int crc_puts(const char *text); - int crc_putc(int c); - -public: - /// current level of source code indentation - int indentation; - /// set if we write abbreviated file for the source code previewer - /// (disables binary data blocks, for example) - bool write_codeview; - /// silly thing to prevent declaring unused variables: - /// When this symbol is on, all attempts to write code don't write - /// anything, but set a variable if it looks like the variable "o" is used: - int varused_test; - /// set to 1 if varused_test found that a variable is actually used - int varused; - -public: - Fd_Code_Writer(); - ~Fd_Code_Writer(); - const char* unique_id(void* o, const char*, const char*, const char*); - /// Increment source code indentation level. - void indent_more() { indentation++; } - /// Decrement source code indentation level. - void indent_less() { indentation--; } - const char *indent(); - const char *indent(int set); - const char *indent_plus(int offset); - int write_h_once(const char *, ...) __fl_attr((__format__ (__printf__, 2, 3))); - int write_c_once(const char *, ...) __fl_attr((__format__ (__printf__, 2, 3))); - bool c_contains(void* ptr); - void write_cstring(const char *,int length); - void write_cstring(const char *); - void write_cdata(const char *,int length); - void vwrite_c(const char* format, va_list args); - void write_c(const char*, ...) __fl_attr((__format__ (__printf__, 2, 3))); - void write_cc(const char *, int, const char*, const char*); - void write_h(const char*, ...) __fl_attr((__format__ (__printf__, 2, 3))); - void write_hc(const char *, int, const char*, const char*); - void write_c_indented(const char *textlines, int inIndent, char inTrailwWith); - Fl_Type* write_static(Fl_Type* p); - Fl_Type* write_code(Fl_Type* p); - int write_code(const char *cfile, const char *hfile, bool to_codeview=false); - void write_public(int state); // writes pubic:/private: as needed - - void tag(int type, unsigned short uid); - - static unsigned long block_crc(const void *data, int n=-1, unsigned long in_crc=0, bool *inout_line_start=NULL); -}; - -#endif // _FLUID_CODE_H diff --git a/fluid/io/file.cxx b/fluid/io/file.cxx deleted file mode 100644 index ba1afeb1b..000000000 --- a/fluid/io/file.cxx +++ /dev/null @@ -1,983 +0,0 @@ -// -// 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 -#include -#include -#include -#include "../src/flstring.h" - -#include -#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 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(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()) { // - ((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/file.h b/fluid/io/file.h deleted file mode 100644 index 470cc1a7b..000000000 --- a/fluid/io/file.h +++ /dev/null @@ -1,94 +0,0 @@ -// -// 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 - -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 -- cgit v1.2.3