From 51a55bc73660f64e8f4b32b8b4d3858f2a786f7b Mon Sep 17 00:00:00 2001 From: Matthias Melcher Date: Sun, 16 Mar 2025 17:16:12 -0400 Subject: Fluid: restructuring and rejuvenation of the source code. * Add classes for application and project * Removed all globals from Fluid.h * Extracting args and project history into their own classes * Moving globals into Application class * Initialize values inside headers for some classes. * Undo functionality wrapped in a class inside Project. * File reader and writer are now linked to a project. * Avoid global project access * Nodes (former Types) will be managed by a new Tree class. * Removed static members (hidden globals) form Node/Fl_Type. * Adding Tree iterator. * Use nullptr instead of 0, NULL, or 0L * Renamed Fl_..._Type to ..._Node, FL_OVERRIDE -> override * Renaming ..._type to ...::prototype * Splitting Widget Panel into multiple files. * Moved callback code into widget panel file. * Cleaning up Fluid_Image -> Image_asset * Moving Fd_Snap_Action into new namespace fld::app::Snap_Action etc. * Moved mergeback into proj folder. * `enum ID` is now `enum class Type`. --- fluid/proj/align_widget.cxx | 414 ++++++++++++++++++++++++++++++++++ fluid/proj/align_widget.h | 24 ++ fluid/proj/mergeback.cxx | 539 ++++++++++++++++++++++++++++++++++++++++++++ fluid/proj/mergeback.h | 81 +++++++ fluid/proj/undo.cxx | 271 ++++++++++++++++++++++ fluid/proj/undo.h | 93 ++++++++ 6 files changed, 1422 insertions(+) create mode 100644 fluid/proj/align_widget.cxx create mode 100644 fluid/proj/align_widget.h create mode 100644 fluid/proj/mergeback.cxx create mode 100644 fluid/proj/mergeback.h create mode 100644 fluid/proj/undo.cxx create mode 100644 fluid/proj/undo.h (limited to 'fluid/proj') diff --git a/fluid/proj/align_widget.cxx b/fluid/proj/align_widget.cxx new file mode 100644 index 000000000..9ce9e2b62 --- /dev/null +++ b/fluid/proj/align_widget.cxx @@ -0,0 +1,414 @@ +// +// Alignment code for the Fast Light Tool Kit (FLTK). +// +// Copyright 1998-2025 by Bill Spitzak and others. +// +// This library is free software. Distribution and use rights are outlined in +// the file "COPYING" which should have been included with this file. If this +// file is missing or damaged, see the license at: +// +// https://www.fltk.org/COPYING.php +// +// Please see the following page on how to report bugs and issues: +// +// https://www.fltk.org/bugs.php +// + +#include "proj/align_widget.h" + +#include "Fluid.h" +#include "proj/undo.h" +#include "nodes/Group_Node.h" + +#include +#include + +/** + the first behavior always uses the first selected widget as a reference + the second behavior uses the largest widget (most extreme positions) as + a reference. + */ +#define BREAK_ON_FIRST break +//#define BREAK_ON_FIRST + +void align_widget_cb(Fl_Widget*, long how) +{ + const int max = 32768, min = -32768; + int left, right, top, bot, wdt, hgt, n; + Node *o; + int changed = 0; + switch ( how ) + { + //---- align + case 10: // align left + left = max; + for (o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Widget_Node *)o)->o; + if (w->x()x(); + BREAK_ON_FIRST; + } + if (left!=max) + for (Node *o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + Fluid.proj.undo.checkpoint(); + } + Fl_Widget *w = ((Widget_Node *)o)->o; + Fluid.proj.tree.allow_layout++; + w->resize(left, w->y(), w->w(), w->h()); + Fluid.proj.tree.allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + break; + case 11: // align h.center + left = max; right = min; + for (o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Widget_Node *)o)->o; + if (w->x()x(); + if (w->x()+w->w()>right) + right = w->x()+w->w(); + BREAK_ON_FIRST; + } + if (left!=max) + { + int center2 = left+right; + for (Node *o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + Fluid.proj.undo.checkpoint(); + } + Fl_Widget *w = ((Widget_Node *)o)->o; + Fluid.proj.tree.allow_layout++; + w->resize((center2-w->w())/2, w->y(), w->w(), w->h()); + Fluid.proj.tree.allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + } + break; + case 12: // align right + right = min; + for (o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Widget_Node *)o)->o; + if (w->x()+w->w()>right) + right = w->x()+w->w(); + BREAK_ON_FIRST; + } + if (right!=min) + for (Node *o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + Fluid.proj.undo.checkpoint(); + } + Fl_Widget *w = ((Widget_Node *)o)->o; + Fluid.proj.tree.allow_layout++; + w->resize(right-w->w(), w->y(), w->w(), w->h()); + Fluid.proj.tree.allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + break; + case 13: // align top + top = max; + for (o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Widget_Node *)o)->o; + if (w->y()y(); + BREAK_ON_FIRST; + } + if (top!=max) + for (Node *o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + Fluid.proj.undo.checkpoint(); + } + Fl_Widget *w = ((Widget_Node *)o)->o; + Fluid.proj.tree.allow_layout++; + w->resize(w->x(), top, w->w(), w->h()); + Fluid.proj.tree.allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + break; + case 14: // align v.center + top = max; bot = min; + for (o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Widget_Node *)o)->o; + if (w->y()y(); + if (w->y()+w->h()>bot) + bot = w->y()+w->h(); + BREAK_ON_FIRST; + } + if (top!=max) + { + int center2 = top+bot; + for (Node *o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + Fluid.proj.undo.checkpoint(); + } + Fl_Widget *w = ((Widget_Node *)o)->o; + Fluid.proj.tree.allow_layout++; + w->resize(w->x(), (center2-w->h())/2, w->w(), w->h()); + Fluid.proj.tree.allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + } + break; + case 15: // align bottom + bot = min; + for (o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Widget_Node *)o)->o; + if (w->y()+w->h()>bot) + bot = w->y()+w->h(); + BREAK_ON_FIRST; + } + if (bot!=min) + for (Node *o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + Fluid.proj.undo.checkpoint(); + } + Fl_Widget *w = ((Widget_Node *)o)->o; + Fluid.proj.tree.allow_layout++; + w->resize( w->x(), bot-w->h(), w->w(), w->h()); + Fluid.proj.tree.allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + break; + //---- space evenly + case 20: // space evenly across + left = max; right = min; wdt = 0; n = 0; + for (o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Widget_Node *)o)->o; + if (w->x()x(); + if (w->x()+w->w()>right) + right = w->x()+w->w(); + wdt += w->w(); + n++; + } + wdt = (right-left)-wdt; + n--; + if (n>0) + { + wdt = wdt/n*n; // make sure that all gaps are the same, possibly moving the rightmost widget + int cnt = 0, wsum = 0; + for (Node *o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + Fluid.proj.undo.checkpoint(); + } + Fl_Widget *w = ((Widget_Node *)o)->o; + Fluid.proj.tree.allow_layout++; + w->resize(left+wsum+wdt*cnt/n, w->y(), w->w(), w->h()); + Fluid.proj.tree.allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + cnt++; + wsum += w->w(); + } + } + break; + case 21: // space evenly down + top = max; bot = min; hgt = 0, n = 0; + for (o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Widget_Node *)o)->o; + if (w->y()y(); + if (w->y()+w->h()>bot) + bot = w->y()+w->h(); + hgt += w->h(); + n++; + } + hgt = (bot-top)-hgt; + n--; + if (n>0) + { + hgt = hgt/n*n; // make sure that all gaps are the same, possibly moving the rightmost widget + int cnt = 0, hsum = 0; + for (Node *o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + Fluid.proj.undo.checkpoint(); + } + Fl_Widget *w = ((Widget_Node *)o)->o; + Fluid.proj.tree.allow_layout++; + w->resize(w->x(), top+hsum+hgt*cnt/n, w->w(), w->h()); + Fluid.proj.tree.allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + cnt++; + hsum += w->h(); + } + } + break; + //---- make same size + case 30: // same width + wdt = min; + for (o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Widget_Node *)o)->o; + if (w->w()>wdt) + wdt = w->w(); + BREAK_ON_FIRST; + } + if (wdt!=min) + for (Node *o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + Fluid.proj.undo.checkpoint(); + } + Fl_Widget *w = ((Widget_Node *)o)->o; + Fluid.proj.tree.allow_layout++; + w->resize(w->x(), w->y(), wdt, w->h()); + Fluid.proj.tree.allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + break; + case 31: // same height + hgt = min; + for (o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Widget_Node *)o)->o; + if (w->h()>hgt) + hgt = w->h(); + BREAK_ON_FIRST; + } + if (hgt!=min) + for (Node *o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + Fluid.proj.undo.checkpoint(); + } + Fl_Widget *w = ((Widget_Node *)o)->o; + Fluid.proj.tree.allow_layout++; + w->resize( w->x(), w->y(), w->w(), hgt); + Fluid.proj.tree.allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + break; + case 32: // same size + hgt = min; wdt = min; + for (o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Widget_Node *)o)->o; + if (w->w()>wdt) + wdt = w->w(); + if (w->h()>hgt) + hgt = w->h(); + BREAK_ON_FIRST; + } + if (hgt!=min) + for (Node *o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + Fluid.proj.undo.checkpoint(); + } + Fl_Widget *w = ((Widget_Node *)o)->o; + Fluid.proj.tree.allow_layout++; + w->resize( w->x(), w->y(), wdt, hgt); + Fluid.proj.tree.allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + break; + //---- center in group + case 40: // center hor + for (o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget() && o->parent) + { + if (!changed) { + changed = 1; + Fluid.proj.undo.checkpoint(); + } + Fl_Widget *w = ((Widget_Node *)o)->o; + Fl_Widget *p = ((Widget_Node *)o->parent)->o; + int center2; + + if (w->window() == p) center2 = p->w(); + else center2 = 2*p->x()+p->w(); + + Fluid.proj.tree.allow_layout++; + w->resize((center2-w->w())/2, w->y(), w->w(), w->h()); + Fluid.proj.tree.allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + break; + case 41: // center vert + for (o = Fluid.proj.tree.first; o; o = o->next) + if (o->selected && o->is_widget() && o->parent) + { + if (!changed) { + changed = 1; + Fluid.proj.undo.checkpoint(); + } + Fl_Widget *w = ((Widget_Node *)o)->o; + Fl_Widget *p = ((Widget_Node *)o->parent)->o; + int center2; + + if (w->window() == p) center2 = p->h(); + else center2 = 2*p->y()+p->h(); + + Fluid.proj.tree.allow_layout++; + w->resize(w->x(), (center2-w->h())/2, w->w(), w->h()); + Fluid.proj.tree.allow_layout--; + Fluid.proj.set_modflag(1); + w->redraw(); + if (w->window()) w->window()->redraw(); + } + break; + } + if (changed) + Fluid.proj.set_modflag(1); +} diff --git a/fluid/proj/align_widget.h b/fluid/proj/align_widget.h new file mode 100644 index 000000000..88860b7ce --- /dev/null +++ b/fluid/proj/align_widget.h @@ -0,0 +1,24 @@ +// +// FLUID main entry for the Fast Light Tool Kit (FLTK). +// +// Copyright 1998-2025 by Bill Spitzak and others. +// +// This library is free software. Distribution and use rights are outlined in +// the file "COPYING" which should have been included with this file. If this +// file is missing or damaged, see the license at: +// +// https://www.fltk.org/COPYING.php +// +// Please see the following page on how to report bugs and issues: +// +// https://www.fltk.org/bugs.php +// + +#ifndef _FLUID_ALIGN_WIDGET_H +#define _FLUID_ALIGN_WIDGET_H + +class Fl_Widget; + +void align_widget_cb(Fl_Widget *, long); + +#endif // _FLUID_ALIGN_WIDGET_H diff --git a/fluid/proj/mergeback.cxx b/fluid/proj/mergeback.cxx new file mode 100644 index 000000000..12f272638 --- /dev/null +++ b/fluid/proj/mergeback.cxx @@ -0,0 +1,539 @@ +// +// MergeBack code for the Fast Light Tool Kit (FLTK). +// +// Copyright 2023-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 +// + +#if 0 +// Matt: disabled + +#include "proj/mergeback.h" + +#include "Fluid.h" +#include "proj/undo.h" +#include "io/Code_Writer.h" +#include "nodes/Function_Node.h" +#include "nodes/Widget_Node.h" + +#include +#include + +#include +#include +#include +#include +#include + +extern void propagate_load(Fl_Group*, void*); +extern void load_panel(); +extern void redraw_browser(); + +// TODO: add application user setting to control mergeback +// [] new projects default to mergeback +// [] check mergeback when loading project +// [] check mergeback when app gets focus +// [] always apply if safe +// TODO: command line option for mergeback +// -mb or --merge-back +// -mbs or --merge-back-if-safe +// NOTE: automatic mergeback on timer when file changes if app focus doesn't work +// NOTE: allow the user to edit comment blocks + +/** + Merge external changes in a source code file back into the current project. + + This experimental function reads a source code file line by line. When it + encounters a special tag in a line, the crc32 stored in the tag is compared + to the crc32 that was calculated from the code lines since the previous tag. + + If the crc's differ, the user has modified the source file externally, and the + given block differs from the block as it was generated by FLUID. Depending on + the block type, the user has modified the widget code (FD_TAG_GENERIC), which + can not be transferred back into the project. + + Modifications to code blocks and callbacks (CODE, CALLBACK) can be merged back + into the project. Their corresponding Node is found using the unique + node id that is part of the tag. The block is only merged back if the crc's + from the project and from the edited block differ. + + The caller must make sure that this code file was generated by the currently + loaded project. + + The user is informed in detailed dialogs what the function discovered and + offered to merge or cancel if appropriate. Just in case this function is + destructive, "undo" restores the state before a MergeBack. + + Callers can set different task. FD_MERGEBACK_ANALYSE checks if there are any + modifications in the code file and returns -1 if there was an error, or a + bit field where bit 0 is set if internal structures were modified, bit 1 if + code was changed, and bit 2 if modified blocks were found, but no Type node. + Bit 3 is set, if code was changed in the code file *and* the project. + + FD_MERGEBACK_INTERACTIVE checks for changes and presents a status dialog box + to the user if there were conflicting changes or if a mergeback is possible, + presenting the user the option to merge or cancel. Returns 0 if the project + remains unchanged, and 1 if the user merged changes back. -1 is returned if an + invalid tag was found. + + FD_MERGEBACK_APPLY merges all changes back into the project without any + interaction. Returns 0 if nothing changed, and 1 if it merged any changes back. + + FD_MERGEBACK_APPLY_IF_SAFE merges changes back only if there are no conflicts. + Returns 0 if nothing changed, and 1 if it merged any changes back, and -1 if + there were conflicts. + + \note this function is currently part of fld::io::Code_Writer to get easy access + to our crc32 code that also wrote the code file originally. + + \param[in] s path and filename of the source code file + \param[in] task see above + \return -1 if an error was found in a tag + \return -2 if no code file was found + \return see above + */ +int merge_back(const std::string &s, const std::string &p, int task) { + if (Fluid.proj.write_mergeback_data) { + Fd_Mergeback mergeback; + return mergeback.merge_back(s, p, task); + } else { + // nothing to be done if the mergeback option is disabled in the project + return 0; + } +} + +/** Allocate and initialize MergeBack class. */ +Fd_Mergeback::Fd_Mergeback() : + code(nullptr), + line_no(0), + tag_error(0), + num_changed_code(0), + num_changed_structure(0), + num_uid_not_found(0), + num_possible_override(0) +{ +} + +/** Release allocated resources. */ +Fd_Mergeback::~Fd_Mergeback() +{ + if (code) ::fclose(code); +} + +/** Remove the first two spaces at every line start. + \param[inout] s block of C code + */ +void Fd_Mergeback::unindent(char *s) { + char *d = s; + bool line_start = true; + while (*s) { + if (line_start) { + if (*s>0 && isspace(*s)) s++; + if (*s>0 && isspace(*s)) s++; + line_start = false; + } + if (*s=='\r') s++; + if (*s=='\n') line_start = true; + *d++ = *s++; + } + *d = 0; +} + +/** + Read a block of text from the source file and remove the leading two spaces in every line. + \param[in] start start of the block within the file + \param[in] end end of text within the file + \return a string holding the text that was found in the file + */ +std::string Fd_Mergeback::read_and_unindent_block(long start, long end) { + long bsize = end-start; + long here = ::ftell(code); + ::fseek(code, start, SEEK_SET); + char *block = (char*)::malloc(bsize+1); + size_t n = ::fread(block, bsize, 1, code); + if (n!=1) + block[0] = 0; // read error + else + block[bsize] = 0; + unindent(block); + std::string str = block; + ::free(block); + ::fseek(code, here, SEEK_SET); + return str; +} + +/** Tell user the results of our MergeBack analysis and pop up a dialog to give + the user a choice to merge or cancel. + \return 1 if the user wants to merge (choice dialog was shown) + \return 0 if there is nothing to merge (no dialog was shown) + \return -1 if the user wants to cancel or an error occurred or an issue was presented + (message or choice dialog was shown) + */ +int Fd_Mergeback::ask_user_to_merge(const std::string &code_filename, const std::string &proj_filename) { + if (tag_error) { + fl_message("Comparing\n \"%s\"\nto\n \"%s\"\n\n" + "MergeBack found an error in line %d while reading tags\n" + "from the source code. Merging code back is not possible.", + code_filename.c_str(), proj_filename.c_str(), line_no); + return -1; + } + if (!num_changed_code && !num_changed_structure) { + return 0; + } + if (num_changed_structure && !num_changed_code) { + fl_message("Comparing\n \"%1$s\"\nto\n \"%2$s\"\n\n" + "MergeBack found %3$d modifications in the project structure\n" + "of the source code. These kind of changes can no be\n" + "merged back and will be lost when the source code is\n" + "generated again from the open project.", + code_filename.c_str(), proj_filename.c_str(), num_changed_structure); + return -1; + } + std::string msg = "Comparing\n \"%1$s\"\nto\n \"%2$s\"\n\n" + "MergeBack found %3$d modifications in the source code."; + if (num_possible_override) + msg += "\n\nWARNING: %6$d of these modified blocks appear to also have\n" + "changed in the project. Merging will override changes in\n" + "the project with changes from the source code file."; + if (num_uid_not_found) + msg += "\n\nWARNING: for %4$d of these modifications no Type node\n" + "can be found and these modification can't be merged back."; + if (!num_possible_override && !num_uid_not_found) + msg += "\nMerging these changes back appears to be safe."; + + if (num_changed_structure) + msg += "\n\nWARNING: %5$d modifications were found in the project\n" + "structure. These kind of changes can no be merged back\n" + "and will be lost when the source code is generated again\n" + "from the open project."; + + if (num_changed_code==num_uid_not_found) { + fl_message(msg.c_str(), + code_filename.c_str(), proj_filename.c_str(), + num_changed_code, num_uid_not_found, + num_changed_structure, num_possible_override); + return -1; + } else { + msg += "\n\nClick Cancel to abort the MergeBack operation.\n" + "Click Merge to merge all code changes back into\n" + "the open project."; + int c = fl_choice(msg.c_str(), "Cancel", "Merge", nullptr, + code_filename.c_str(), proj_filename.c_str(), + num_changed_code, num_uid_not_found, + num_changed_structure, num_possible_override); + if (c==0) return -1; + return 1; + } +} + +/** Analyse the block and its corresponding widget callback. + Return findings in num_changed_code, num_changed_code, and num_uid_not_found. + */ +void Fd_Mergeback::analyse_callback(unsigned long code_crc, unsigned long tag_crc, int uid) { + Node *tp = Node::find_by_uid(uid); + if (tp && tp->is_true_widget()) { + std::string cb = tp->callback(); cb += "\n"; + unsigned long project_crc = fld::io::Code_Writer::block_crc(cb.c_str()); + // check if the code and project crc are the same, so this modification was already applied + if (project_crc!=code_crc) { + num_changed_code++; + // check if the block change on the project side as well, so we may override changes + if (project_crc!=tag_crc) { + num_possible_override++; + } + } + } else { + num_uid_not_found++; + num_changed_code++; + } +} + +/** Analyse the block and its corresponding Code Type. + Return findings in num_changed_code, num_changed_code, and num_uid_not_found. + */ +void Fd_Mergeback::analyse_code(unsigned long code_crc, unsigned long tag_crc, int uid) { + Node *tp = Node::find_by_uid(uid); + if (tp && tp->is_a(ID_Code)) { + std::string code = tp->name(); code += "\n"; + unsigned long project_crc = fld::io::Code_Writer::block_crc(code.c_str()); + // check if the code and project crc are the same, so this modification was already applied + if (project_crc!=code_crc) { + num_changed_code++; + // check if the block change on the project side as well, so we may override changes + if (project_crc!=tag_crc) { + num_possible_override++; + } + } + } else { + num_changed_code++; + num_uid_not_found++; + } +} + + +/** Analyse the code file and return findings in class member variables. + + The code file must be open for reading already. + + * tag_error is set if a tag was found, but could not be read + * line_no returns the line where an error occurred + * num_changed_code is set to the number of changed code blocks in the file. + Code changes can be merged back to the project. + * num_changed_structure is set to the number of structural changes. + Structural changes outside of code blocks can not be read back. + * num_uid_not_found number of blocks that were modified, but the corresponding + type or widget can not be found in the project + * num_possible_override number of blocks that were changed in the code file, + but also were changed in the project. + + \return -1 if reading a tag failed, otherwise 0 + */ +int Fd_Mergeback::analyse() { + // initialize local variables + unsigned long code_crc = 0; + bool line_start = true; + char line[1024]; + // bail if the caller has not opened a file yet + if (!code) return 0; + // initialize member variables to return our findings + line_no = 0; + tag_error = 0; + num_changed_code = 0; + num_changed_structure = 0; + num_uid_not_found = 0; + num_possible_override = 0; + code_crc = 0; + // loop through all lines in the code file + ::fseek(code, 0, SEEK_SET); + for (;;) { + // get the next line until end of file + if (fgets(line, 1023, code)==0) break; + line_no++; + const char *tag = strstr(line, "//~fl~"); + if (!tag) { + // if this line has no tag, add the contents to the CRC and continue + code_crc = fld::io::Code_Writer::block_crc(line, -1, code_crc, &line_start); + } else { + // if this line has a tag, read all tag data + int tag_type = -1, uid = 0; + unsigned long tag_crc = 0; + int n = sscanf(tag, "//~fl~%d~%04x~%08lx~~", &tag_type, &uid, &tag_crc); + if (n!=3 || tag_type<0 || tag_type>FD_TAG_LAST ) { tag_error = 1; return -1; } + if (code_crc != tag_crc) { + switch (tag_type) { + case FD_TAG_GENERIC: + num_changed_structure++; + break; + case FD_TAG_MENU_CALLBACK: + case FD_TAG_WIDGET_CALLBACK: + analyse_callback(code_crc, tag_crc, uid); + break; + case FD_TAG_CODE: + analyse_code(code_crc, tag_crc, uid); + break; + } + } + // reset everything for the next block + code_crc = 0; + line_start = true; + } + } + return 0; +} + +/** Apply callback mergebacks from the code file to the project. + \return 1 if the project changed + */ +int Fd_Mergeback::apply_callback(long block_end, long block_start, unsigned long code_crc, int uid) { + Node *tp = Node::find_by_uid(uid); + if (tp && tp->is_true_widget()) { + std::string cb = tp->callback(); cb += "\n"; + unsigned long project_crc = fld::io::Code_Writer::block_crc(cb.c_str()); + if (project_crc!=code_crc) { + tp->callback(read_and_unindent_block(block_start, block_end).c_str()); + return 1; + } + } + return 0; +} + +/** Apply callback mergebacks from the code file to the project. + \return 1 if the project changed + */ +int Fd_Mergeback::apply_code(long block_end, long block_start, unsigned long code_crc, int uid) { + Node *tp = Node::find_by_uid(uid); + if (tp && tp->is_a(ID_Code)) { + std::string cb = tp->name(); cb += "\n"; + unsigned long project_crc = fld::io::Code_Writer::block_crc(cb.c_str()); + if (project_crc!=code_crc) { + tp->name(read_and_unindent_block(block_start, block_end).c_str()); + return 1; + } + } + return 0; +} + +/** Apply all possible mergebacks from the code file to the project. + The code file must be open for reading already. + \return -1 if reading a tag failed, 0 if nothing changed, 1 if the project changed + */ +int Fd_Mergeback::apply() { + // initialize local variables + unsigned long code_crc = 0; + bool line_start = true; + char line[1024]; + int changed = 0; + long block_start = 0; + long block_end = 0; + // bail if the caller has not opened a file yet + if (!code) return 0; + // initialize member variables to return our findings + line_no = 0; + tag_error = 0; + code_crc = 0; + // loop through all lines in the code file + ::fseek(code, 0, SEEK_SET); + for (;;) { + // get the next line until end of file + if (fgets(line, 1023, code)==0) break; + line_no++; + const char *tag = strstr(line, "//~fl~"); + if (!tag) { + // if this line has no tag, add the contents to the CRC and continue + code_crc = fld::io::Code_Writer::block_crc(line, -1, code_crc, &line_start); + block_end = ::ftell(code); + } else { + // if this line has a tag, read all tag data + int tag_type = -1, uid = 0; + unsigned long tag_crc = 0; + int n = sscanf(tag, "//~fl~%d~%04x~%08lx~~", &tag_type, &uid, &tag_crc); + if (n!=3 || tag_type<0 || tag_type>FD_TAG_LAST ) { tag_error = 1; return -1; } + if (code_crc != tag_crc) { + if (tag_type==FD_TAG_MENU_CALLBACK || tag_type==FD_TAG_WIDGET_CALLBACK) { + changed |= apply_callback(block_end, block_start, code_crc, uid); + } else if (tag_type==FD_TAG_CODE) { + changed |= apply_code(block_end, block_start, code_crc, uid); + } + } + // reset everything for the next block + code_crc = 0; + line_start = true; + block_start = ::ftell(code); + } + } + return changed; +} + +/** Dispatch the MergeBack into analysis, interactive, or apply directly. + \param[in] s source code filename and path + \param[in] task one of FD_MERGEBACK_ANALYSE, FD_MERGEBACK_INTERACTIVE, + FD_MERGEBACK_APPLY_IF_SAFE, or FD_MERGEBACK_APPLY + \return -1 if an error was found in a tag + \return -2 if no code file was found + \return See more at ::merge_back(const std::string &s, int task). + */ +int Fd_Mergeback::merge_back(const std::string &s, const std::string &p, int task) { + int ret = 0; + code = fl_fopen(s.c_str(), "rb"); + if (!code) return -2; + do { // no actual loop, just make sure we close the code file + if (task == FD_MERGEBACK_ANALYSE) { + analyse(); + if (tag_error) {ret = -1; break; } + if (num_changed_structure) ret |= 1; + if (num_changed_code) ret |= 2; + if (num_uid_not_found) ret |= 4; + if (num_possible_override) ret |= 8; + break; + } + if (task == FD_MERGEBACK_INTERACTIVE) { + analyse(); + ret = ask_user_to_merge(s, p); + if (ret != 1) + return ret; + task = FD_MERGEBACK_APPLY; // fall through + } + if (task == FD_MERGEBACK_APPLY_IF_SAFE) { + analyse(); + if (tag_error || num_changed_structure || num_possible_override) { + ret = -1; + break; + } + if (num_changed_code==0) { + ret = 0; + break; + } + task = FD_MERGEBACK_APPLY; // fall through + } + if (task == FD_MERGEBACK_APPLY) { + ret = apply(); + if (ret == 1) { + Fluid.proj.set_modflag(1); + redraw_browser(); + load_panel(); + } + ret = 1; // avoid message box in caller + } + } while (0); + fclose(code); + code = nullptr; + return ret; +} + +#if 0 +// Matt: disabled +/** + Merge the possibly modified content of code files back into the project. + */ +int mergeback_code_files() +{ + flush_text_widgets(); + if (!filename) return 1; + if (!Fluid.proj.write_mergeback_data) { + fl_message("MergeBack is not enabled for this project.\n" + "Please enable MergeBack in the project settings\n" + "dialog and re-save the project file and the code."); + return 0; + } + + std::string proj_filename = Fluid.proj.projectfile_path() + Fluid.proj.projectfile_name(); + std::string code_filename; +#if 1 + if (!Fluid.batch_mode) { + Fl_Preferences build_records(Fl_Preferences::USER_L, "fltk.org", "fluid-build"); + Fl_Preferences path(build_records, proj_filename.c_str()); + int i, n = proj_filename.size(); + for (i=0; i + +#include +#include + +const int FD_TAG_GENERIC = 0; +const int FD_TAG_CODE = 1; +const int FD_TAG_MENU_CALLBACK = 2; +const int FD_TAG_WIDGET_CALLBACK = 3; +const int FD_TAG_LAST = 3; + +const int FD_MERGEBACK_ANALYSE = 0; +const int FD_MERGEBACK_INTERACTIVE = 1; +const int FD_MERGEBACK_APPLY = 2; +const int FD_MERGEBACK_APPLY_IF_SAFE = 3; + +/** Class that implements the MergeBack functionality. + \see merge_back(const std::string &s, int task) + */ +class Fd_Mergeback +{ +protected: + /// Pointer to the C++ code file. + FILE *code; + /// Current line number in the C++ code file. + int line_no; + /// Set if there was an error reading a tag. + int tag_error; + /// Number of code blocks that were different than the CRC in their tag. + int num_changed_code; + /// Number of generic structure blocks that were different than the CRC in their tag. + int num_changed_structure; + /// Number of code block that were modified, but a type node by that uid was not found. + int num_uid_not_found; + /// Number of modified code block where the corresponding project block also changed. + int num_possible_override; + + void unindent(char *s); + std::string read_and_unindent_block(long start, long end); + void analyse_callback(unsigned long code_crc, unsigned long tag_crc, int uid); + void analyse_code(unsigned long code_crc, unsigned long tag_crc, int uid); + int apply_callback(long block_end, long block_start, unsigned long code_crc, int uid); + int apply_code(long block_end, long block_start, unsigned long code_crc, int uid); + +public: + Fd_Mergeback(); + ~Fd_Mergeback(); + int merge_back(const std::string &s, const std::string &p, int task); + int ask_user_to_merge(const std::string &s, const std::string &p); + int analyse(); + int apply(); +}; + +extern int merge_back(const std::string &s, const std::string &p, int task); + + +#endif // _FLUID_MERGEBACK_H + +#endif diff --git a/fluid/proj/undo.cxx b/fluid/proj/undo.cxx new file mode 100644 index 000000000..0f1a478fb --- /dev/null +++ b/fluid/proj/undo.cxx @@ -0,0 +1,271 @@ +// +// Fluid Undo code for the Fast Light Tool Kit (FLTK). +// +// Copyright 1998-2025 by Bill Spitzak and others. +// +// This library is free software. Distribution and use rights are outlined in +// the file "COPYING" which should have been included with this file. If this +// file is missing or damaged, see the license at: +// +// https://www.fltk.org/COPYING.php +// +// Please see the following page on how to report bugs and issues: +// +// https://www.fltk.org/bugs.php +// + +#include "proj/undo.h" + +#include "Fluid.h" +#include "Project.h" +#include "io/Project_Reader.h" +#include "io/Project_Writer.h" +#include "nodes/Node.h" +#include "nodes/Widget_Node.h" +#include "widgets/Node_Browser.h" + +#include +#include +#include +#include +#include +#include "tools/filename.h" +#include "../src/flstring.h" + +#if defined(_WIN32) && !defined(__CYGWIN__) +# include +# include +# define getpid (int)GetCurrentProcessId +#else +# include +#endif // _WIN32 && !__CYGWIN__ + +// This file implements an undo system using temporary files; ideally +// we'd like to do this in memory, however the current data structures +// and design aren't well-suited... Instead, we save and restore +// checkpoint files. + +extern Fl_Window* the_panel; + +using namespace fld; +using namespace fld::proj; + + +Undo::Undo(Project &p) +: proj_( p ) +{ } + +Undo::~Undo() { + // TODO: delete old undo files when calling the destructor. +} + + +// Return the undo filename. +// The filename is constructed in a static internal buffer and +// this buffer is overwritten by every call of this function. +// The return value is a pointer to this internal string. +char *Undo::filename(int level) { + if (!path_len_) { + Fluid.preferences.getUserdataPath(path_, sizeof(path_)); + path_len_ = (unsigned int)strlen(path_); + } + + // append filename: "undo_PID_LEVEL.fl" + snprintf(path_ + path_len_, + sizeof(path_) - path_len_ - 1, + "undo_%d_%d.fl", getpid(), level); + return path_; +} + + +// Redo menu callback +void Undo::redo() { + // int undo_item = main_menubar->find_index(undo_cb); + // int redo_item = main_menubar->find_index(redo_cb); + once_type_ = OnceType::ALWAYS; + + if (current_ >= last_) { + fl_beep(); + return; + } + + suspend(); + if (widget_browser) { + widget_browser->save_scroll_position(); + widget_browser->new_list(); + } + int reload_panel = (the_panel && the_panel->visible()); + if (!fld::io::read_file(proj_, filename(current_ + 1), 0)) { + // Unable to read checkpoint file, don't redo... + widget_browser->rebuild(); + proj_.update_settings_dialog(); + resume(); + return; + } + if (reload_panel) { + for (auto w: Fluid.proj.tree.all_selected_widgets()) { + w->open(); + } + } + if (widget_browser) widget_browser->restore_scroll_position(); + + current_ ++; + + // Update modified flag... + proj_.set_modflag(current_ != save_); + widget_browser->rebuild(); + proj_.update_settings_dialog(); + + // Update undo/redo menu items... + // if (current_ >= last_) main_menu[redo_item].deactivate(); + // main_menu[undo_item].activate(); + resume(); +} + +// Undo menu callback +void Undo::undo() { + // int undo_item = main_menubar->find_index(undo_cb); + // int redo_item = main_menubar->find_index(redo_cb); + once_type_ = OnceType::ALWAYS; + + if (current_ <= 0) { + fl_beep(); + return; + } + + if (current_ == last_) { + fld::io::write_file(proj_, filename(current_)); + } + + suspend(); + // Undo first deletes all widgets which resets the widget_tree browser. + // Save the current scroll position, so we don't scroll back to 0 at undo. + // TODO: make the scroll position part of the .fl project file + if (widget_browser) { + widget_browser->save_scroll_position(); + widget_browser->new_list(); + } + int reload_panel = (the_panel && the_panel->visible()); + if (!fld::io::read_file(proj_, filename(current_ - 1), 0)) { + // Unable to read checkpoint file, don't undo... + widget_browser->rebuild(); + proj_.update_settings_dialog(); + proj_.set_modflag(0, 0); + resume(); + return; + } + if (reload_panel) { + for (Node *t = Fluid.proj.tree.first; t; t=t->next) { + if (t->is_widget() && t->selected) { + t->open(); + break; + } + } + } + // Restore old browser position. + // Ideally, we would save the browser position inside the undo file. + if (widget_browser) widget_browser->restore_scroll_position(); + + current_ --; + + // Update modified flag... + proj_.set_modflag(current_ != save_); + + // Update undo/redo menu items... + // if (current_ <= 0) main_menu[undo_item].deactivate(); + // main_menu[redo_item].activate(); + widget_browser->rebuild(); + proj_.update_settings_dialog(); + resume(); +} + +/** + \param[in] type set a new type, or set to 0 to clear the once_type without setting a checkpoint + \return 1 if the checkpoint was set, 0 if this is a repeating event + */ +int Undo::checkpoint(OnceType type) { + if (type == OnceType::ALWAYS) { + once_type_ = OnceType::ALWAYS; + return 0; + } + if (paused_) return 0; + if (once_type_ != type) { + checkpoint(); + once_type_ = type; + return 1; + } else { + // do not add more checkpoints for the same undo type + return 0; + } +} + +// Save current file to undo buffer +void Undo::checkpoint() { + // printf("checkpoint(): current_=%d, paused_=%d, modflag=%d\n", + // current_, paused_, modflag); + + // Don't checkpoint if suspend() has been called... + if (paused_) return; + + // int undo_item = main_menubar->find_index(undo_cb); + // int redo_item = main_menubar->find_index(redo_cb); + once_type_ = OnceType::ALWAYS; + + // Save the current UI to a checkpoint file... + const char *file = filename(current_); + if (!fld::io::write_file(proj_, file)) { + // Don't attempt to do undo stuff if we can't write a checkpoint file... + perror(file); + return; + } + + // Update the saved level... + if (proj_.modflag && current_ <= save_) save_ = -1; + else if (!proj_.modflag) save_ = current_; + + // Update the current undo level... + current_ ++; + last_ = current_; + if (current_ > max_) max_ = current_; + + // Enable the Undo and disable the Redo menu items... + // main_menu[undo_item].activate(); + // main_menu[redo_item].deactivate(); +} + +// Clear undo buffer +void Undo::clear() { + // int undo_item = main_menubar->find_index(undo_cb); + // int redo_item = main_menubar->find_index(redo_cb); + // Remove old checkpoint files... + for (int i = 0; i <= max_; i ++) { + fl_unlink(filename(i)); + } + + // Reset current, last, and save indices... + current_ = last_ = max_ = 0; + if (proj_.modflag) save_ = -1; + else save_ = 0; + + // Disable the Undo and Redo menu items... + // main_menu[undo_item].deactivate(); + // main_menu[redo_item].deactivate(); +} + +// Resume undo checkpoints +void Undo::resume() { + paused_--; +} + +// Suspend undo checkpoints +void Undo::suspend() { + paused_++; +} + +void Undo::undo_cb(Fl_Widget *, void *) { + Fluid.proj.undo.undo(); +} + +void Undo::redo_cb(Fl_Widget *, void *) { + Fluid.proj.undo.redo(); +} diff --git a/fluid/proj/undo.h b/fluid/proj/undo.h new file mode 100644 index 000000000..f87c747bd --- /dev/null +++ b/fluid/proj/undo.h @@ -0,0 +1,93 @@ +// +// Fluid Undo header for the Fast Light Tool Kit (FLTK). +// +// Copyright 1998-2025 by Bill Spitzak and others. +// +// This library is free software. Distribution and use rights are outlined in +// the file "COPYING" which should have been included with this file. If this +// file is missing or damaged, see the license at: +// +// https://www.fltk.org/COPYING.php +// +// Please see the following page on how to report bugs and issues: +// +// https://www.fltk.org/bugs.php +// + +#ifndef undo_h +#define undo_h + +#include + +class Fl_Widget; + +namespace fld { + +class Project; + +namespace proj { + +class Undo +{ +public: + + enum class OnceType { + ALWAYS = 0, + WINDOW_RESIZE + }; + + /// Link Undo class to this project. + Project &proj_; + /// Current undo level in buffer + int current_ = 0; + /// Last undo level in buffer + int last_ = 0; + // Maximum undo level used + int max_ = 0; + /// Last undo level that was saved + int save_ = -1; + // Undo checkpointing paused? + int paused_ = 0; + // Undo file path + char path_[FL_PATH_MAX] { }; + // length w/o filename + unsigned int path_len_ = 0; + /// Suspend further undos of the same type + OnceType once_type_ = OnceType::ALWAYS; + +public: + + // Constructor. + Undo(Project &p); + // Destructor. + ~Undo(); + + // Save current file to undo buffer + void checkpoint(); + // Save undo buffer once until a different checkpoint type is called + int checkpoint(OnceType type); + // Clear undo buffer + void clear(); + // Resume undo checkpoints + void resume(); + // Suspend undo checkpoints + void suspend(); + // Return the undo filename. + char *filename(int level); + + // Redo menu callback + void redo(); + // Undo menu callback + void undo(); + + // Redo menu callback + static void redo_cb(Fl_Widget *, void *); + // Undo menu callback + static void undo_cb(Fl_Widget *, void *); +}; + +} // namespace fld +} // namespace proj + + +#endif // !undo_h -- cgit v1.2.3