summaryrefslogtreecommitdiff
path: root/fluid/proj
diff options
context:
space:
mode:
authorMatthias Melcher <github@matthiasm.com>2025-03-16 17:16:12 -0400
committerGitHub <noreply@github.com>2025-03-16 17:16:12 -0400
commit51a55bc73660f64e8f4b32b8b4d3858f2a786f7b (patch)
tree122ad9f838fcf8f61ed7cf5fa031e8ed69817e10 /fluid/proj
parent13a7073a1e007ce5b71ef70bced1a9b15158820d (diff)
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`.
Diffstat (limited to 'fluid/proj')
-rw-r--r--fluid/proj/align_widget.cxx414
-rw-r--r--fluid/proj/align_widget.h24
-rw-r--r--fluid/proj/mergeback.cxx539
-rw-r--r--fluid/proj/mergeback.h81
-rw-r--r--fluid/proj/undo.cxx271
-rw-r--r--fluid/proj/undo.h93
6 files changed, 1422 insertions, 0 deletions
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 <FL/Fl.H>
+#include <FL/Fl_Window.H>
+
+/**
+ 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()<left)
+ left = w->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()<left)
+ left = w->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()<top)
+ top = w->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()<top)
+ top = w->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()<left)
+ left = w->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()<top)
+ top = w->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 <FL/Fl_Window.H>
+#include <FL/fl_ask.H>
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <zlib.h>
+
+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<n; i++) if (proj_filename[i]=='\\') proj_filename[i] = '/';
+ preferences_get(path, "code", code_filename, "");
+ }
+#endif
+ if (code_filename.empty())
+ code_filename = Fluid.proj.codefile_path() + Fluid.proj.codefile_name();
+ if (!Fluid.batch_mode) proj.enter_project_dir();
+ int c = merge_back(code_filename, proj_filename, FD_MERGEBACK_INTERACTIVE);
+ if (!Fluid.batch_mode) proj.leave_project_dir();
+
+ if (c==0) fl_message("Comparing\n \"%s\"\nto\n \"%s\"\n\n"
+ "MergeBack found no external modifications\n"
+ "in the source code.",
+ code_filename.c_str(), proj_filename.c_str());
+ if (c==-2) fl_message("No corresponding source code file found.");
+ return c;
+}
+
+void mergeback_cb(Fl_Widget *, void *) {
+ mergeback_code_files();
+}
+#endif
+
+#endif
+
diff --git a/fluid/proj/mergeback.h b/fluid/proj/mergeback.h
new file mode 100644
index 000000000..2986d379c
--- /dev/null
+++ b/fluid/proj/mergeback.h
@@ -0,0 +1,81 @@
+//
+// MergeBack header 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
+//
+
+// Matt: disabled
+#if 0
+
+#ifndef _FLUID_MERGEBACK_H
+#define _FLUID_MERGEBACK_H
+
+#include <FL/fl_attr.h>
+
+#include <stdio.h>
+#include <string>
+
+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 <FL/Fl.H>
+#include <FL/Fl_Window.H>
+#include <FL/Fl_Preferences.H>
+#include <FL/Fl_Menu_Bar.H>
+#include <FL/fl_ask.H>
+#include "tools/filename.h"
+#include "../src/flstring.h"
+
+#if defined(_WIN32) && !defined(__CYGWIN__)
+# include <io.h>
+# include <windows.h>
+# define getpid (int)GetCurrentProcessId
+#else
+# include <unistd.h>
+#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 <FL/filename.H>
+
+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