summaryrefslogtreecommitdiff
path: root/fluid/tools
diff options
context:
space:
mode:
authorMatthias Melcher <github@matthiasm.com>2025-03-07 16:34:35 +0100
committerMatthias Melcher <github@matthiasm.com>2025-03-07 16:34:48 +0100
commit1985aefc0e502048f92b91beef87c0dfbe669fed (patch)
treeaf62874def4590e437a47784b4428d975ceb262f /fluid/tools
parent42a04c064d4b31c3a85210311f3ada163c406a25 (diff)
Restructuring Fluid source files.
Diffstat (limited to 'fluid/tools')
-rw-r--r--fluid/tools/ExternalCodeEditor_UNIX.cxx576
-rw-r--r--fluid/tools/ExternalCodeEditor_UNIX.h60
-rw-r--r--fluid/tools/ExternalCodeEditor_WIN32.cxx632
-rw-r--r--fluid/tools/ExternalCodeEditor_WIN32.h59
-rw-r--r--fluid/tools/autodoc.cxx613
-rw-r--r--fluid/tools/autodoc.h59
-rw-r--r--fluid/tools/fluid_filename.cxx219
-rw-r--r--fluid/tools/fluid_filename.h59
8 files changed, 2277 insertions, 0 deletions
diff --git a/fluid/tools/ExternalCodeEditor_UNIX.cxx b/fluid/tools/ExternalCodeEditor_UNIX.cxx
new file mode 100644
index 000000000..09d7e6178
--- /dev/null
+++ b/fluid/tools/ExternalCodeEditor_UNIX.cxx
@@ -0,0 +1,576 @@
+//
+// External code editor management class for Unix
+//
+// Note: This entire file Unix only
+
+#include "ExternalCodeEditor_UNIX.h"
+
+#include "app/fluid.h"
+
+#include <FL/Fl.H> /* Fl_Timeout_Handler.. */
+#include <FL/fl_ask.H> /* fl_alert() */
+#include <FL/fl_string_functions.h> /* fl_strdup() */
+
+#include <errno.h> /* errno */
+#include <string.h> /* strerror() */
+#include <sys/types.h> /* stat().. */
+#include <sys/stat.h>
+#include <sys/wait.h> /* waitpid().. */
+#include <fcntl.h> /* open().. */
+#include <signal.h> /* kill().. */
+#include <unistd.h>
+#include <stdlib.h> /* free().. */
+#include <stdio.h> /* snprintf().. */
+
+// Static local data
+static int L_editors_open = 0; // keep track of #editors open
+static Fl_Timeout_Handler L_update_timer_cb = 0; // app's update timer callback
+
+// [Static/Local] See if file exists
+static int is_file(const char *filename) {
+ struct stat buf;
+ if ( stat(filename, &buf) < 0 ) return(0);
+ return(S_ISREG(buf.st_mode) ? 1 : 0); // regular file?
+}
+
+// [Static/Local] See if dir exists
+static int is_dir(const char *dirname) {
+ struct stat buf;
+ if ( stat(dirname, &buf) < 0 ) return(0);
+ return(S_ISDIR(buf.st_mode) ? 1 : 0); // a dir?
+}
+
+// ---- ExternalCodeEditor implementation
+
+/** \class ExternalCodeEditor
+ Support for an external C++ code editor for Fluid Code block.
+
+ This class can launch and quit a user defined program for editing
+ code outside of Fluid.
+ It observes changes in the external file and updates the Fluid
+ widget to stay synchronized.
+ */
+
+/**
+ Create the manager for external code editors.
+ */
+ExternalCodeEditor::ExternalCodeEditor() {
+ pid_ = -1;
+ filename_ = 0;
+ file_mtime_ = 0;
+ file_size_ = 0;
+ alert_pipe_[0] = alert_pipe_[1] = -1;
+ alert_pipe_open_ = false;
+}
+
+/**
+ Destroy the manager.
+ This also closes the external editor.
+ */
+ExternalCodeEditor::~ExternalCodeEditor() {
+ if ( G_debug )
+ printf("ExternalCodeEditor() DTOR CALLED (this=%p, pid=%ld)\n",
+ (void*)this, (long)pid_);
+ close_editor(); // close editor, delete tmp file
+ set_filename(0); // free()s filename
+
+ if (alert_pipe_open_) {
+ Fl::remove_fd(alert_pipe_[0]);
+ if (alert_pipe_[0] != -1) ::close(alert_pipe_[0]);
+ if (alert_pipe_[1] != -1) ::close(alert_pipe_[1]);
+ }
+}
+
+/**
+ Set the filename for the file we wish to edit.
+ Handles memory allocation/free.
+ If set to NULL, frees memory.
+ \param[in] val new filename
+ */
+void ExternalCodeEditor::set_filename(const char *val) {
+ if ( filename_ ) free((void*)filename_);
+ filename_ = val ? fl_strdup(val) : 0;
+}
+
+/**
+ Is editor running?
+ \return 1 if we are currently editing a file.
+ */
+int ExternalCodeEditor::is_editing() {
+ return( (pid_ != -1) ? 1 : 0 );
+}
+
+/**
+ Wait for editor to close
+ */
+void ExternalCodeEditor::close_editor() {
+ if ( G_debug ) printf("close_editor() called: pid=%ld\n", long(pid_));
+ // Wait until editor is closed + reaped
+ while ( is_editing() ) {
+ switch ( reap_editor() ) {
+ case -2: // no editor running (unlikely to happen)
+ return;
+ case -1: // error
+ fl_alert("Error reaping external editor\n"
+ "pid=%ld file=%s", long(pid_), filename());
+ break;
+ case 0: // process still running
+ switch ( fl_choice("Please close external editor\npid=%ld file=%s",
+ "Force Close", // button 0
+ "Closed", // button 1
+ 0, // button 2
+ long(pid_), filename() ) ) {
+ case 0: // Force Close
+ kill_editor();
+ continue;
+ case 1: // Closed? try to reap
+ continue;
+ }
+ break;
+ case 1: // process reaped
+ return;
+ }
+ }
+}
+
+/**
+ Kill the running editor (if any).
+
+ Kills the editor, reaps the process, and removes the tmp file.
+ The dtor calls this to ensure no editors remain running when fluid exits.
+ */
+void ExternalCodeEditor::kill_editor() {
+ if ( G_debug ) printf("kill_editor() called: pid=%ld\n", (long)pid_);
+ if ( !is_editing() ) return; // editor not running? return..
+ kill(pid_, SIGTERM); // kill editor
+ int wcount = 0;
+ while ( is_editing() ) { // and wait for editor to finish..
+ usleep(100000); // 1/10th sec delay gives editor time to close itself
+ pid_t pid_reaped;
+ switch ( reap_editor(&pid_reaped) ) {
+ case -2: // editor not running (unlikely to happen)
+ return;
+ case -1: // error
+ fl_alert("Can't seem to close editor of file: %s\n"
+ "waitpid() returned: %s\n"
+ "Please close editor and hit OK",
+ filename(), strerror(errno));
+ continue;
+ case 0: // process still running
+ if ( ++wcount > 3 ) { // retry 3x with 1/10th delay before showing dialog
+ fl_alert("Can't seem to close editor of file: %s\n"
+ "Please close editor and hit OK", filename());
+ }
+ continue;
+ case 1: // process reaped (reap_editor() sets pid_ to -1)
+ if ( G_debug )
+ printf("*** REAPED KILLED EXTERNAL EDITOR: PID %ld\n", (long)pid_reaped);
+ break;
+ }
+ }
+ return;
+}
+
+/**
+ Handle if file changed since last check, and update records if so.
+
+ Load new data into 'code', which caller must free().
+ If 'force' set, forces reload even if file size/time didn't change.
+
+ \param[in] code
+ \param[in] force
+ \return 0 if file unchanged or not editing
+ \return 1 if file changed, internal records updated, 'code' has new content
+ \return -1 error getting file info (strerror() has reason)
+*/
+int ExternalCodeEditor::handle_changes(const char **code, int force) {
+ code[0] = 0;
+ if ( !is_editing() ) return 0;
+ // Get current time/size info, see if file changed
+ int changed = 0;
+ {
+ struct stat sbuf;
+ if ( stat(filename(), &sbuf) < 0 ) return(-1); // TODO: show fl_alert(), do this in win32 too, adjust func call docs above
+ time_t now_mtime = sbuf.st_mtime;
+ size_t now_size = sbuf.st_size;
+ // OK, now see if file changed; update records if so
+ if ( now_mtime != file_mtime_ ) { changed = 1; file_mtime_ = now_mtime; }
+ if ( now_size != file_size_ ) { changed = 1; file_size_ = now_size; }
+ }
+ // No changes? done
+ if ( !changed && !force ) return 0;
+ // Changes? Load file, and fallthru to close()
+ int fd = open(filename(), O_RDONLY);
+ if ( fd < 0 ) {
+ fl_alert("ERROR: can't open '%s': %s", filename(), strerror(errno));
+ return -1;
+ }
+ int ret = 0;
+ char *buf = (char*)malloc(file_size_ + 1);
+ ssize_t count = read(fd, buf, file_size_);
+ if ( count == -1 ) {
+ fl_alert("ERROR: read() %s: %s", filename(), strerror(errno));
+ free((void*)buf);
+ ret = -1;
+ } else if ( (long)count != (long)file_size_ ) {
+ fl_alert("ERROR: read() failed for %s:\n"
+ "expected %ld bytes, only got %ld",
+ filename(), long(file_size_), long(count));
+ ret = -1;
+ } else {
+ // Success -- file loaded OK
+ buf[count] = '\0';
+ code[0] = buf; // return pointer to allocated buffer
+ ret = 1;
+ }
+ close(fd);
+ return ret;
+}
+
+/**
+ Remove the tmp file (if it exists), and zero out filename/mtime/size.
+ \return -1 on error (dialog is posted as to why)
+ \return 0 no file to remove
+ \return 1 -- file was removed
+ */
+int ExternalCodeEditor::remove_tmpfile() {
+ const char *tmpfile = filename();
+ if ( !tmpfile ) return 0;
+ // Filename set? remove (if exists) and zero filename/mtime/size
+ if ( is_file(tmpfile) ) {
+ if ( G_debug ) printf("Removing tmpfile '%s'\n", tmpfile);
+ if ( remove(tmpfile) < 0 ) {
+ fl_alert("WARNING: Can't remove() '%s': %s", tmpfile, strerror(errno));
+ return -1;
+ }
+ }
+ set_filename(0);
+ file_mtime_ = 0;
+ file_size_ = 0;
+ return 1;
+}
+
+/**
+ Return tmpdir name for this fluid instance.
+ \return pointer to static memory.
+ */
+const char* ExternalCodeEditor::tmpdir_name() {
+ static char dirname[100];
+ snprintf(dirname, sizeof(dirname), "/tmp/.fluid-%ld", (long)getpid());
+ return dirname;
+}
+
+/**
+ Clear the external editor's tempdir.
+ Static so that the main program can call it on exit to clean up.
+ */
+void ExternalCodeEditor::tmpdir_clear() {
+ const char *tmpdir = tmpdir_name();
+ if ( is_dir(tmpdir) ) {
+ if ( G_debug ) printf("Removing tmpdir '%s'\n", tmpdir);
+ if ( rmdir(tmpdir) < 0 ) {
+ fl_alert("WARNING: Can't rmdir() '%s': %s", tmpdir, strerror(errno));
+ }
+ }
+}
+
+/**
+ Creates temp dir (if doesn't exist) and returns the dirname
+ as a static string.
+ \return NULL on error, dialog shows reason.
+ */
+const char* ExternalCodeEditor::create_tmpdir() {
+ const char *dirname = tmpdir_name();
+ if ( ! is_dir(dirname) ) {
+ if ( mkdir(dirname, 0777) < 0 ) {
+ fl_alert("can't create directory '%s': %s",
+ dirname, strerror(errno));
+ return NULL;
+ }
+ }
+ return dirname;
+}
+
+/**
+ Returns temp filename in static buffer.
+ \return NULL if can't, posts dialog explaining why.
+ */
+const char* ExternalCodeEditor::tmp_filename() {
+ static char path[FL_PATH_MAX+1];
+ const char *tmpdir = create_tmpdir();
+ if ( !tmpdir ) return 0;
+ const char *ext = g_project.code_file_name.c_str(); // e.g. ".cxx"
+ snprintf(path, FL_PATH_MAX, "%s/%p%s", tmpdir, (void*)this, ext);
+ path[FL_PATH_MAX] = 0;
+ return path;
+}
+
+/**
+ Save string 'code' to 'filename', returning file's mtime/size.
+ 'code' can be NULL -- writes an empty file if so.
+ \return 0 on success
+ \return -1 on error (posts dialog with reason)
+ */
+static int save_file(const char *filename, const char *code) {
+ if ( code == 0 ) code = ""; // NULL? write an empty file
+ int fd = open(filename, O_WRONLY|O_CREAT, 0666);
+ if ( fd == -1 ) {
+ fl_alert("ERROR: open() '%s': %s", filename, strerror(errno));
+ return -1;
+ }
+ ssize_t clen = strlen(code);
+ ssize_t count = write(fd, code, clen);
+ int ret = 0;
+ if ( count == -1 ) {
+ fl_alert("ERROR: write() '%s': %s", filename, strerror(errno));
+ ret = -1; // fallthru to close()
+ } else if ( count != clen ) {
+ fl_alert("ERROR: write() '%s': wrote only %lu bytes, expected %lu",
+ filename, (unsigned long)count, (unsigned long)clen);
+ ret = -1; // fallthru to close()
+ }
+ close(fd);
+ return(ret);
+}
+
+/**
+ Convert string 's' to array of argv[], useful for execve().
+ - 's' will be modified (words will be NULL separated)
+ - argv[] will end up pointing to the words of 's'
+ - Caller must free argv with: free(argv);
+ \return -1 in case of memory allocation error
+ \return number of arguments in argv (same value as in argc)
+ */
+static int make_args(char *s, // string containing words (gets trashed!)
+ int *aargc, // pointer to argc
+ char ***aargv) { // pointer to argv
+ char *ss, **argv;
+ if ((argv=(char**)malloc(sizeof(char*) * (strlen(s)/2)))==NULL) {
+ return -1;
+ }
+ int t;
+ for(t=0; (t==0)?(ss=strtok(s," \t")):(ss=strtok(0," \t")); t++) {
+ argv[t] = ss;
+ }
+ argv[t] = 0;
+ aargv[0] = argv;
+ aargc[0] = t;
+ return(t);
+}
+
+/**
+ If no alert pipe is open yet, try to create the pipe and hook it up the the fd callback.
+
+ The alert pipe is used to communicate from the forked process to the main
+ FLTK app in case launching the editor failed.
+ */
+void ExternalCodeEditor::open_alert_pipe() {
+ if (!alert_pipe_open_) {
+ if (::pipe(alert_pipe_) == 0) {
+ Fl::add_fd(alert_pipe_[0], FL_READ, alert_pipe_cb, this);
+ alert_pipe_open_ = true;
+ } else {
+ alert_pipe_[0] = alert_pipe_[1] = -1;
+ }
+ }
+}
+
+/**
+ Start editor in background (fork/exec)
+ \return 0 on success, leaves editor child process running as 'pid_'
+ \return -1 on error, posts dialog with reason (child exits)
+ */
+int ExternalCodeEditor::start_editor(const char *editor_cmd,
+ const char *filename) {
+ if ( G_debug ) printf("start_editor() cmd='%s', filename='%s'\n",
+ editor_cmd, filename);
+ char cmd[1024];
+ snprintf(cmd, sizeof(cmd), "%s %s", editor_cmd, filename);
+ command_line_ = editor_cmd;
+ open_alert_pipe();
+ // Fork editor to background..
+ switch ( pid_ = fork() ) {
+ case -1: // error
+ fl_alert("couldn't fork(): %s", strerror(errno));
+ return -1;
+ case 0: { // child
+ // NOTE: OSX wants minimal code between fork/exec, see Apple TN2083
+ // NOTE: no FLTK calls after a fork. Use a pipe to tell the app if the
+ // command can't launch
+ int nargs;
+ char **args = 0;
+ if (make_args(cmd, &nargs, &args) > 0) {
+ execvp(args[0], args); // run command - doesn't return if succeeds
+ if (alert_pipe_open_) {
+ int err = errno;
+ if (::write(alert_pipe_[1], &err, sizeof(int)) != sizeof(int)) {
+ // should not happen, but if it does, at least we tried
+ }
+ }
+ exit(1);
+ }
+ exit(1);
+ // break;
+ }
+ default: // parent
+ if ( L_editors_open++ == 0 ) // first editor? start timers
+ { start_update_timer(); }
+ if ( G_debug )
+ printf("--- EDITOR STARTED: pid_=%ld #open=%d\n", (long)pid_, L_editors_open);
+ break;
+ }
+ return 0;
+}
+
+/**
+ Try to reap external editor process.
+
+ If 'pid_reaped' not NULL, returns PID of reaped editor.
+
+ \return -2: editor not open
+ \return -1: waitpid() failed (errno has reason)
+ \return 0: process still running
+ \return 1: process finished + reaped ('pid_reaped' has pid), pid_ set to -1.
+ Handles removing tmpfile/zeroing file_mtime/file_size/filename
+ \return If return value <=0, 'pid_reaped' is set to zero.
+ */
+int ExternalCodeEditor::reap_editor(pid_t *pid_reaped) {
+ if ( pid_reaped ) *pid_reaped = 0;
+ if ( !is_editing() ) return -2;
+ int status = 0;
+ pid_t wpid;
+ switch (wpid = waitpid(pid_, &status, WNOHANG)) {
+ case -1: // waitpid() failed
+ return -1;
+ case 0: // process didn't reap, still running
+ return 0;
+ default: // process reaped
+ if ( pid_reaped ) *pid_reaped = wpid; // return pid to caller
+ remove_tmpfile(); // also zeroes mtime/size
+ pid_ = -1;
+ if ( --L_editors_open <= 0 )
+ { stop_update_timer(); }
+ break;
+ }
+ if ( G_debug )
+ printf("*** EDITOR REAPED: pid=%ld #open=%d\n", long(wpid), L_editors_open);
+ return 1;
+}
+
+/**
+ Open external editor using 'editor_cmd' to edit 'code'.
+
+ 'code' contains multiline code to be edited as a temp file.
+ 'code' can be NULL -- edits an empty file if so.
+
+ \return 0 if succeeds
+ \return -1 if can't open editor (already open, etc),
+ errors were shown to user in a dialog
+ */
+int ExternalCodeEditor::open_editor(const char *editor_cmd,
+ const char *code) {
+ // Make sure a temp filename exists
+ if ( !filename() ) {
+ set_filename(tmp_filename());
+ if ( !filename() ) return -1;
+ }
+ // See if tmpfile already exists or editor already open
+ if ( is_file(filename()) ) {
+ if ( is_editing() ) {
+ // See if editor recently closed but not reaped; try to reap
+ pid_t wpid;
+ switch ( reap_editor(&wpid) ) {
+ case -2: // no editor running? (unlikely if is_editing() true)
+ break;
+ case -1: // waitpid() failed
+ fl_alert("ERROR: waitpid() failed: %s\nfile='%s', pid=%ld",
+ strerror(errno), filename(), (long)pid_);
+ return -1;
+ case 0: // process still running
+ fl_alert("Editor Already Open\n file='%s'\n pid=%ld",
+ filename(), (long)pid_);
+ return 0;
+ case 1: // process reaped, wpid is pid reaped
+ if ( G_debug )
+ printf("*** REAPED EXTERNAL EDITOR: PID %ld\n", (long)wpid);
+ break; // fall thru to open new editor instance
+ }
+ // Reinstate tmp filename (reap_editor() clears it)
+ set_filename(tmp_filename());
+ }
+ }
+ if ( save_file(filename(), code) < 0 ) {
+ return -1; // errors were shown in dialog
+ }
+ // Update mtime/size from closed file
+ struct stat sbuf;
+ if ( stat(filename(), &sbuf) < 0 ) {
+ fl_alert("ERROR: can't stat('%s'): %s", filename(), strerror(errno));
+ return -1;
+ }
+ file_mtime_ = sbuf.st_mtime;
+ file_size_ = sbuf.st_size;
+ if ( start_editor(editor_cmd, filename()) < 0 ) { // open file in external editor
+ if ( G_debug ) printf("Editor failed to start\n");
+ return -1; // errors were shown in dialog
+ }
+ return 0;
+}
+
+/**
+ Start update timer.
+ */
+void ExternalCodeEditor::start_update_timer() {
+ if ( !L_update_timer_cb ) return;
+ if ( G_debug ) printf("--- TIMER: STARTING UPDATES\n");
+ Fl::add_timeout(2.0, L_update_timer_cb);
+}
+
+/**
+ Stop update timer.
+ */
+void ExternalCodeEditor::stop_update_timer() {
+ if ( !L_update_timer_cb ) return;
+ if ( G_debug ) printf("--- TIMER: STOPPING UPDATES\n");
+ Fl::remove_timeout(L_update_timer_cb);
+}
+
+/**
+ Set app's external editor update timer callback.
+
+ This is the app's callback callback we start while editors are open,
+ and stop when all editors are closed.
+ */
+void ExternalCodeEditor::set_update_timer_callback(Fl_Timeout_Handler cb) {
+ L_update_timer_cb = cb;
+}
+
+/**
+ See if any external editors are open.
+ App's timer cb can see if any editors need checking..
+ */
+int ExternalCodeEditor::editors_open() {
+ return L_editors_open;
+}
+
+/**
+ It the forked process can't run the editor, it will send the errno through a pipe.
+ */
+void ExternalCodeEditor::alert_pipe_cb(FL_SOCKET s, void* d) {
+ ExternalCodeEditor* self = (ExternalCodeEditor*)d;
+ self->last_error_ = 0;
+ if (::read(s, &self->last_error_, sizeof(int)) != sizeof(int))
+ return;
+ const char* cmd = self->command_line_.c_str();
+ if (cmd && *cmd) {
+ if (cmd[0] == '/') { // is this an absolute filename?
+ fl_alert("Can't launch external editor '%s':\n%s\n\ncmd: \"%s\"",
+ fl_filename_name(cmd), strerror(self->last_error_), cmd);
+ } else {
+ char pwd[FL_PATH_MAX+1];
+ fl_getcwd(pwd, FL_PATH_MAX);
+ fl_alert("Can't launch external editor '%s':\n%s\n\ncmd: \"%s\"\npwd: \"%s\"",
+ fl_filename_name(cmd), strerror(self->last_error_), cmd, pwd);
+ }
+ }
+}
diff --git a/fluid/tools/ExternalCodeEditor_UNIX.h b/fluid/tools/ExternalCodeEditor_UNIX.h
new file mode 100644
index 000000000..644f22afa
--- /dev/null
+++ b/fluid/tools/ExternalCodeEditor_UNIX.h
@@ -0,0 +1,60 @@
+//
+// External code editor management class for Unix
+//
+// Handles starting and keeping track of an external text editor,
+// including process start, temp file creation/removal, bookkeeping, killing..
+//
+#ifndef _EXTCODEEDITOR_H
+#define _EXTCODEEDITOR_H
+
+#include "app/fluid.h"
+
+#include <FL/Fl.H>
+
+#include <errno.h> /* errno */
+#include <string.h> /* strerror() */
+#include <sys/types.h> /* stat().. */
+#include <sys/stat.h>
+#include <unistd.h>
+
+// ---- ExternalCodeEditor declaration
+
+class ExternalCodeEditor {
+ int pid_;
+ time_t file_mtime_; // last modify time of the file (used to determine if file changed)
+ size_t file_size_; // last file size (used to determine if changed)
+ const char *filename_;
+ std::string command_line_;
+ int last_error_;
+ int alert_pipe_[2];
+ bool alert_pipe_open_;
+ static void alert_pipe_cb(FL_SOCKET, void*);
+
+protected:
+ void kill_editor();
+ const char *create_tmpdir();
+ const char *tmp_filename();
+ int start_editor(const char *cmd, const char *filename);
+ void set_filename(const char *val);
+ void open_alert_pipe();
+
+public:
+ ExternalCodeEditor();
+ ~ExternalCodeEditor();
+ int is_editing();
+ int reap_editor(pid_t *pid_reaped=NULL);
+ void close_editor();
+ const char *filename() { return filename_; }
+ int open_editor(const char *editor_cmd, const char *code);
+ int handle_changes(const char **code, int force=0);
+ int remove_tmpfile();
+ // Public static methods
+ static void start_update_timer();
+ static void stop_update_timer();
+ static const char* tmpdir_name();
+ static void tmpdir_clear();
+ static int editors_open();
+ static void set_update_timer_callback(Fl_Timeout_Handler);
+};
+
+#endif /*_EXTCODEEDITOR_H */
diff --git a/fluid/tools/ExternalCodeEditor_WIN32.cxx b/fluid/tools/ExternalCodeEditor_WIN32.cxx
new file mode 100644
index 000000000..c58f22a30
--- /dev/null
+++ b/fluid/tools/ExternalCodeEditor_WIN32.cxx
@@ -0,0 +1,632 @@
+//
+// External code editor management class for Windows
+//
+// 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
+//
+
+// Note: This entire file Windows only.
+
+#include "tools/ExternalCodeEditor_WIN32.h"
+#include "app/fluid.h"
+
+#include <FL/Fl.H> // Fl_Timeout_Handler..
+#include <FL/fl_ask.H> // fl_alert()
+#include <FL/fl_utf8.h> // fl_utf8fromwc()
+#include <FL/fl_string_functions.h> // fl_strdup()
+
+#include <stdio.h> // snprintf()
+#include <stdlib.h>
+
+extern int G_debug; // defined in fluid.cxx
+
+// Static local data
+static int L_editors_open = 0; // keep track of #editors open
+static Fl_Timeout_Handler L_update_timer_cb = 0; // app's update timer callback
+static wchar_t *wbuf = NULL;
+static char *abuf = NULL;
+
+static wchar_t *utf8_to_wchar(const char *utf8, wchar_t *&wbuf, int lg = -1) {
+ unsigned len = (lg >= 0) ? (unsigned)lg : (unsigned)strlen(utf8);
+ unsigned wn = fl_utf8toUtf16(utf8, len, NULL, 0) + 1; // Query length
+ wbuf = (wchar_t *)realloc(wbuf, sizeof(wchar_t) * wn);
+ wn = fl_utf8toUtf16(utf8, len, (unsigned short *)wbuf, wn); // Convert string
+ wbuf[wn] = 0;
+ return wbuf;
+}
+
+static char *wchar_to_utf8(const wchar_t *wstr, char *&utf8) {
+ unsigned len = (unsigned)wcslen(wstr);
+ unsigned wn = fl_utf8fromwc(NULL, 0, wstr, len) + 1; // query length
+ utf8 = (char *)realloc(utf8, wn);
+ wn = fl_utf8fromwc(utf8, wn, wstr, len); // convert string
+ utf8[wn] = 0;
+ return utf8;
+}
+
+// [Static/Local] Get error message string for last failed WIN32 function.
+// Returns a string pointing to static memory.
+//
+static const char *get_ms_errmsg() {
+ static char emsg[1024];
+ DWORD lastErr = GetLastError();
+ DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER |
+ FORMAT_MESSAGE_IGNORE_INSERTS |
+ FORMAT_MESSAGE_FROM_SYSTEM;
+ DWORD langid = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT);
+ LPWSTR mbuf = 0;
+ DWORD msize = 0;
+
+ // Get error message from Windows
+ msize = FormatMessageW(flags, 0, lastErr, langid, (LPWSTR)&mbuf, 0, NULL);
+ if ( msize == 0 ) {
+ _snprintf(emsg, sizeof(emsg), "Error #%ld", (unsigned long)lastErr);
+ } else {
+ // Convert message to UTF-8
+ fl_utf8fromwc(emsg, sizeof(emsg), mbuf, msize);
+ // Remove '\r's -- they screw up fl_alert()
+ char *src = emsg, *dst = emsg;
+ for ( ; 1; src++ ) {
+ if ( *src == '\0' ) { *dst = '\0'; break; }
+ if ( *src != '\r' ) { *dst++ = *src; }
+ }
+ LocalFree(mbuf); // Free the buffer allocated by the system
+ }
+ return emsg;
+}
+
+// [Static/Local] See if file exists
+static int is_file(const char *filename) {
+ utf8_to_wchar(filename, wbuf);
+ DWORD att = GetFileAttributesW(wbuf);
+ if (att == INVALID_FILE_ATTRIBUTES)
+ return 0;
+ if ( (att & FILE_ATTRIBUTE_DIRECTORY) == 0 ) return 1; // not a dir == file
+ return 0;
+}
+
+// [Static/Local] See if dir exists
+static int is_dir(const char *dirname) {
+ utf8_to_wchar(dirname, wbuf);
+ DWORD att = GetFileAttributesW(wbuf);
+ if (att == INVALID_FILE_ATTRIBUTES) return 0;
+ if (att & FILE_ATTRIBUTE_DIRECTORY) return 1;
+ return 0;
+}
+
+// CTOR
+ExternalCodeEditor::ExternalCodeEditor() {
+ memset(&pinfo_, 0, sizeof(pinfo_));
+ memset(&file_mtime_, 0, sizeof(file_mtime_));
+ memset(&file_size_, 0, sizeof(file_size_));
+ filename_ = 0;
+}
+
+// DTOR
+ExternalCodeEditor::~ExternalCodeEditor() {
+ close_editor(); // close editor, delete tmp file
+ set_filename(0); // free()s filename
+}
+
+// [Protected] Set the filename. Handles memory allocation/free
+// If set to NULL, frees memory.
+//
+void ExternalCodeEditor::set_filename(const char *val) {
+ if ( filename_ ) free((void*)filename_);
+ filename_ = val ? fl_strdup(val) : 0;
+}
+
+// [Public] Is editor running?
+int ExternalCodeEditor::is_editing() {
+ return( (pinfo_.dwProcessId != 0) ? 1 : 0 );
+}
+
+// [Static/Local] Terminate_app()'s callback to send WM_CLOSE to a single window.
+static BOOL CALLBACK terminate_app_enum(HWND hwnd, LPARAM lParam) {
+ DWORD dwID;
+ GetWindowThreadProcessId(hwnd, &dwID);
+ if (dwID == (DWORD)lParam) {
+ PostMessage(hwnd, WM_CLOSE, 0, 0);
+ if ( G_debug )
+ printf("terminate_app_enum() sends WIN_CLOSE to hwnd=%p\n", (void*)hwnd);
+ }
+ return TRUE;
+}
+
+// [Static/Local] Handle sending WIN_CLOSE to /all/ windows matching specified pid.
+// Wait up to msecTimeout for process to close, and if it doesn't, use TerminateProcess().
+//
+static int terminate_app(DWORD pid, DWORD msecTimeout) {
+ HANDLE hProc = OpenProcess(SYNCHRONIZE|PROCESS_TERMINATE, FALSE, pid);
+ if ( !hProc ) return -1;
+ // terminate_app_enum() posts WM_CLOSE to all windows matching pid
+ EnumWindows((WNDENUMPROC)terminate_app_enum, (LPARAM) pid);
+ // Wait on handle. If it closes, great. If it times out, use TerminateProcess()
+ int ret = 0;
+ if ( WaitForSingleObject(hProc, msecTimeout) != WAIT_OBJECT_0 ) {
+ if ( G_debug ) {
+ printf("WARNING: sent WIN_CLOSE, but timeout after %ld msecs.."
+ "trying TerminateProcess\n", msecTimeout);
+ }
+ if ( TerminateProcess(hProc, 0) == 0 ) {
+ if ( G_debug ) {
+ printf("ERROR: TerminateProcess() for pid=%ld failed: %s\n",
+ long(pid), get_ms_errmsg());
+ }
+ ret = -1;
+ } else {
+ ret = 0; // TerminateProcess succeeded
+ }
+ } else {
+ ret = 0; // WaitForSingleObject() confirmed WIN_CLOSE succeeded
+ }
+ CloseHandle(hProc);
+ return ret;
+}
+
+// [Protected] Wait for editor to close
+void ExternalCodeEditor::close_editor() {
+ if ( G_debug ) printf("close_editor() called: pid=%ld\n", long(pinfo_.dwProcessId));
+ // Wait until editor is closed + reaped
+ while ( is_editing() ) {
+ switch ( reap_editor() ) {
+ case -2: // no editor running (unlikely to happen)
+ return;
+ case -1: // error
+ fl_alert("Error reaping external editor\npid=%ld file=%s\nOS error message=%s",
+ long(pinfo_.dwProcessId), filename(), get_ms_errmsg());
+ break;
+ case 0: // process still running
+ switch ( fl_choice("Please close external editor\npid=%ld file=%s",
+ "Force Close", // button 0
+ "Closed", // button 1
+ 0, // button 2
+ long(pinfo_.dwProcessId), filename() ) ) {
+ case 0: // Force Close
+ kill_editor();
+ continue;
+ case 1: // Closed? try to reap
+ continue;
+ }
+ break;
+ case 1: // process reaped
+ return;
+ }
+ }
+}
+
+// [Protected] Kill the running editor (if any) and cleanup
+// Kills the editor, reaps the process, and removes the tmp file.
+// The dtor calls this to ensure no editors remain running when fluid exits.
+//
+void ExternalCodeEditor::kill_editor() {
+ if ( G_debug )
+ printf("kill_editor() called: pid=%ld\n", (long)pinfo_.dwProcessId);
+ if ( !is_editing() ) return;
+ switch ( terminate_app(pinfo_.dwProcessId, 500) ) { // kill editor, wait up to 1/2 sec to die
+ case -1: { // error
+ fl_alert("Can't seem to close editor of file: %s\n"
+ "Please close editor and hit OK", filename());
+ break;
+ }
+ case 0: { // success -- process reaped
+ DWORD pid = pinfo_.dwProcessId; // save pid
+ reap_cleanup(); // clears pinfo_
+ if ( G_debug )
+ printf("*** kill_editor() REAP pid=%ld #open=%ld\n",
+ long(pid), long(L_editors_open));
+ break;
+ }
+ }
+ return;
+}
+
+// [Public] Handle if file changed since last check, and update records if so.
+// Load new data into 'code', which caller must free().
+// If 'force' set, forces reload even if file size/time didn't change.
+//
+// Returns:
+// 0 -- file unchanged or not editing
+// 1 -- file changed, internal records updated, 'code' has new content
+// -1 -- error getting file info (get_ms_errmsg() has reason)
+//
+// OPTIONAL TODO:
+// Ignore changes made within the last 2 seconds,
+// to give editor time to fully write out the file.
+//
+int ExternalCodeEditor::handle_changes(const char **code, int force) {
+ code[0] = 0;
+ if ( !is_editing() ) return 0;
+ // Sigh, have to open file to get file time/size :/
+ utf8_to_wchar(filename(), wbuf);
+ HANDLE fh = CreateFileW(wbuf, // file to read
+ GENERIC_READ, // reading only
+ FILE_SHARE_READ, // sharing -- allow read share; just getting file size
+ NULL, // security
+ OPEN_EXISTING, // create flags -- must exist
+ 0, // misc flags
+ NULL); // templates
+ if ( fh == INVALID_HANDLE_VALUE ) return -1;
+ LARGE_INTEGER fsize;
+ // Get file size
+ if ( GetFileSizeEx(fh, &fsize) == 0 ) {
+ DWORD err = GetLastError();
+ CloseHandle(fh);
+ SetLastError(err); // return error from GetFileSizeEx(), not CloseHandle()
+ return -1;
+ }
+ // Get file time
+ FILETIME ftCreate, ftAccess, ftWrite;
+ if ( GetFileTime(fh, &ftCreate, &ftAccess, &ftWrite) == 0 ) {
+ DWORD err = GetLastError();
+ CloseHandle(fh);
+ SetLastError(err); // return error from GetFileTime(), not CloseHandle()
+ return -1;
+ }
+ // OK, now see if file changed; update records if so
+ int changed = 0;
+ if ( fsize.QuadPart != file_size_.QuadPart )
+ { changed = 1; file_size_ = fsize; }
+ if ( CompareFileTime(&ftWrite, &file_mtime_) != 0 )
+ { changed = 1; file_mtime_ = ftWrite; }
+ // Changes? Load file. Be sure to fallthru to CloseHandle()
+ int ret = 0;
+ if ( changed || force ) {
+ DWORD buflen = (DWORD)fsize.QuadPart;
+ char *buf = (char*)malloc((size_t)buflen + 1);
+ DWORD count;
+ if ( ReadFile(fh, buf, buflen, &count, 0) == 0 ) {
+ fl_alert("ERROR: ReadFile() failed for %s: %s",
+ filename(), get_ms_errmsg());
+ free((void*)buf); buf = 0;
+ ret = -1; // fallthru to CloseHandle()
+ } else if ( count != buflen ) {
+ fl_alert("ERROR: ReadFile() failed for %s:\n"
+ "expected %ld bytes, got %ld",
+ filename(), long(buflen), long(count));
+ free((void*)buf); buf = 0;
+ ret = -1; // fallthru to CloseHandle()
+ } else {
+ // Successfully read changed file
+ buf[count] = '\0';
+ code[0] = buf; // return pointer to allocated buffer
+ ret = 1; // fallthru to CloseHandle()
+ }
+ }
+ CloseHandle(fh);
+ return ret;
+}
+
+// [Public] Remove the tmp file (if it exists), and zero out filename/mtime/size
+// Returns:
+// -1 -- on error (dialog is posted as to why)
+// 0 -- no file to remove
+// 1 -- file was removed
+//
+int ExternalCodeEditor::remove_tmpfile() {
+ const char *tmpfile = filename();
+ if ( G_debug ) printf("remove_tmpfile() '%s'\n", tmpfile ? tmpfile : "(empty)");
+ if ( !tmpfile ) return 0;
+ // Filename set? remove (if exists) and zero filename/mtime/size
+ if ( is_file(tmpfile) ) {
+ if ( G_debug ) printf("Removing tmpfile '%s'\n", tmpfile);
+ utf8_to_wchar(tmpfile, wbuf);
+ if (DeleteFileW(wbuf) == 0) {
+ fl_alert("WARNING: Can't DeleteFile() '%s': %s", tmpfile, get_ms_errmsg());
+ return -1;
+ }
+ } else {
+ if ( G_debug ) printf("remove_tmpfile(): is_file(%s) failed\n", tmpfile);
+ }
+ set_filename(0);
+ memset(&file_mtime_, 0, sizeof(file_mtime_));
+ memset(&file_size_, 0, sizeof(file_size_));
+ return 1;
+}
+
+// [Static/Public] Return tmpdir name for this fluid instance.
+// Returns pointer to static memory.
+//
+const char* ExternalCodeEditor::tmpdir_name() {
+ wchar_t tempdirW[FL_PATH_MAX+1];
+ char tempdir[FL_PATH_MAX+1];
+ if (GetTempPathW(FL_PATH_MAX, tempdirW) == 0) {
+ strcpy(tempdir, "c:\\windows\\temp"); // fallback
+ } else {
+ strcpy(tempdir, wchar_to_utf8(tempdirW, abuf));
+ }
+ static char dirname[100];
+ _snprintf(dirname, sizeof(dirname), "%s.fluid-%ld",
+ tempdir, (long)GetCurrentProcessId());
+ if ( G_debug ) printf("tmpdir_name(): '%s'\n", dirname);
+ return dirname;
+}
+
+// [Static/Public] Clear the external editor's tempdir
+// Static so that the main program can call it on exit to clean up.
+//
+void ExternalCodeEditor::tmpdir_clear() {
+ const char *tmpdir = tmpdir_name();
+ if ( is_dir(tmpdir) ) {
+ if ( G_debug ) printf("Removing tmpdir '%s'\n", tmpdir);
+ utf8_to_wchar(tmpdir, wbuf);
+ if ( RemoveDirectoryW(wbuf) == 0 ) {
+ fl_alert("WARNING: Can't RemoveDirectory() '%s': %s",
+ tmpdir, get_ms_errmsg());
+ }
+ }
+}
+
+// [Protected] Creates temp dir (if doesn't exist) and returns the dirname
+// as a static string. Returns NULL on error, dialog shows reason.
+//
+const char* ExternalCodeEditor::create_tmpdir() {
+ const char *dirname = tmpdir_name();
+ if ( ! is_dir(dirname) ) {
+ utf8_to_wchar(dirname, wbuf);
+ if (CreateDirectoryW(wbuf, 0) == 0) {
+ fl_alert("can't create directory '%s': %s",
+ dirname, get_ms_errmsg());
+ return NULL;
+ }
+ }
+ return dirname;
+}
+
+// [Protected] Returns temp filename in static buffer.
+// Returns NULL if can't, posts dialog explaining why.
+//
+const char* ExternalCodeEditor::tmp_filename() {
+ static char path[512];
+ const char *tmpdir = create_tmpdir();
+ if ( !tmpdir ) return 0;
+ const char *ext = g_project.code_file_name.c_str(); // e.g. ".cxx"
+ _snprintf(path, sizeof(path), "%s\\%p%s", tmpdir, (void*)this, ext);
+ path[sizeof(path)-1] = 0;
+ return path;
+}
+
+// [Static/Local] Save string 'code' to 'filename', returning file's mtime/size
+// 'code' can be NULL -- writes an empty file if so.
+// Returns:
+// 0 on success
+// -1 on error (posts dialog with reason)
+//
+static int save_file(const char *filename,
+ const char *code,
+ FILETIME &file_mtime, // return these since in win32 it's..
+ LARGE_INTEGER &file_size) { // ..efficient to get while file open
+ if ( code == 0 ) code = ""; // NULL? write an empty file
+ memset(&file_mtime, 0, sizeof(file_mtime));
+ memset(&file_size, 0, sizeof(file_size));
+ utf8_to_wchar(filename, wbuf);
+ HANDLE fh = CreateFileW(wbuf, // filename
+ GENERIC_WRITE, // write only
+ 0, // sharing -- no share during write
+ NULL, // security
+ CREATE_ALWAYS, // create flags -- recreate
+ FILE_ATTRIBUTE_NORMAL, // misc flags
+ NULL); // templates
+ if ( fh == INVALID_HANDLE_VALUE ) {
+ fl_alert("ERROR: couldn't create file '%s': %s",
+ filename, get_ms_errmsg());
+ return(-1);
+ }
+ // Write the file, being careful to CloseHandle() even on errs
+ DWORD clen = (DWORD)strlen(code);
+ DWORD count = 0;
+ int ret = 0;
+ if ( WriteFile(fh, code, clen, &count, NULL) == 0 ) {
+ fl_alert("ERROR: WriteFile() '%s': %s", filename, get_ms_errmsg());
+ ret = -1; // fallthru to CloseHandle()
+ } else if ( count != clen ) {
+ fl_alert("ERROR: WriteFile() '%s': wrote only %lu bytes, expected %lu",
+ filename, (unsigned long)count, (unsigned long)clen);
+ ret = -1; // fallthru to CloseHandle()
+ }
+ // Get mtime/size before closing
+ {
+ FILETIME ftCreate, ftAccess, ftWrite;
+ if ( GetFileSizeEx(fh, &file_size) == 0 ) {
+ fl_alert("ERROR: save_file(%s): GetFileSizeEx() failed: %s\n",
+ filename, get_ms_errmsg());
+ }
+ if ( GetFileTime(fh, &ftCreate, &ftAccess, &ftWrite) == 0 ) {
+ fl_alert("ERROR: save_file(%s): GetFileTime() failed: %s\n",
+ filename, get_ms_errmsg());
+ }
+ file_mtime = ftWrite;
+ }
+ // Close, done
+ CloseHandle(fh);
+ return(ret);
+}
+
+// [Protected] Start editor
+// Returns:
+// > 0 on success, leaves editor child process running as 'pinfo_'
+// > -1 on error, posts dialog with reason (child exits)
+//
+int ExternalCodeEditor::start_editor(const char *editor_cmd,
+ const char *filename) {
+ if ( G_debug ) printf("start_editor() cmd='%s', filename='%s'\n",
+ editor_cmd, filename);
+ // Startup info
+ STARTUPINFOW sinfo;
+ memset(&sinfo, 0, sizeof(sinfo));
+ sinfo.cb = sizeof(sinfo);
+ sinfo.dwFlags = 0;
+ sinfo.wShowWindow = 0;
+ // Process info
+ memset(&pinfo_, 0, sizeof(pinfo_));
+ // Command
+ char cmd[1024];
+ _snprintf(cmd, sizeof(cmd), "%s %s", editor_cmd, filename);
+ utf8_to_wchar(cmd, wbuf);
+ // Start editor process
+ if (CreateProcessW(NULL, // app name
+ wbuf, // command to exec
+ NULL, // secure attribs
+ NULL, // thread secure attribs
+ FALSE, // handle inheritance
+ 0, // creation flags
+ NULL, // environ block
+ NULL, // current dir
+ &sinfo, // startup info
+ &pinfo_) == 0 ) { // process info
+ fl_alert("CreateProcess() failed to start '%s': %s",
+ cmd, get_ms_errmsg());
+ return(-1);
+ }
+ if ( L_editors_open++ == 0 ) // first editor? start timers
+ { start_update_timer(); }
+ if ( G_debug )
+ printf("--- EDITOR STARTED: pid_=%ld #open=%d\n",
+ (long)pinfo_.dwProcessId, L_editors_open);
+ return 0;
+}
+
+// [Protected] Cleanup after editor reaped:
+// > Remove tmpfile, zeroes mtime/size/filename
+// > Close process handles
+// > Zeroes out pinfo_
+// > Decrease editor count
+//
+void ExternalCodeEditor::reap_cleanup() {
+ remove_tmpfile(); // also zeroes mtime/size/filename
+ CloseHandle(pinfo_.hProcess); // close process handle
+ CloseHandle(pinfo_.hThread); // close thread handle
+ memset(&pinfo_, 0, sizeof(pinfo_)); // clear pinfo_
+ if ( --L_editors_open <= 0 )
+ { stop_update_timer(); }
+}
+
+// [Public] Try to reap external editor process
+// If 'pid_reaped' not NULL, returns PID of reaped editor.
+// Returns:
+// -2 -- editor not open
+// -1 -- WaitForSingleObject() failed (get_ms_errmsg() has reason)
+// 0 -- process still running
+// 1 -- process finished + reaped ('pid_reaped' has pid), pinfo_ set to 0.
+// Handles removing tmpfile/zeroing file_mtime/file_size/filename
+//
+// If return value <=0, 'pid_reaped' is set to zero.
+//
+int ExternalCodeEditor::reap_editor(DWORD *pid_reaped) {
+ if ( pid_reaped ) *pid_reaped = 0;
+ if ( !is_editing() ) return -2;
+ DWORD msecs_wait = 50; // .05 sec
+ switch ( WaitForSingleObject(pinfo_.hProcess, msecs_wait) ) {
+ case WAIT_TIMEOUT: { // process didn't reap, still running
+ return 0;
+ }
+ case WAIT_OBJECT_0: { // reaped
+ DWORD wpid = pinfo_.dwProcessId; // save pid
+ reap_cleanup(); // clears pinfo_
+ if ( pid_reaped ) *pid_reaped = wpid; // return pid to caller
+ if ( G_debug ) printf("*** EDITOR REAPED: pid=%ld #open=%d\n",
+ long(wpid), L_editors_open);
+ return 1;
+ }
+ case WAIT_FAILED: { // failed
+ return -1;
+ }
+ }
+ return -1; // any other return unexpected
+}
+
+// [Public] Open external editor using 'editor_cmd' to edit 'code'.
+//
+// 'code' contains multiline code to be edited as a temp file.
+// 'code' can be NULL -- edits an empty file if so.
+//
+// Returns:
+// 0 if succeeds
+// -1 if can't open editor (already open, etc),
+// errors were shown to user in a dialog
+//
+int ExternalCodeEditor::open_editor(const char *editor_cmd,
+ const char *code) {
+ // Make sure a temp filename exists
+ if ( !filename() ) {
+ set_filename(tmp_filename());
+ if ( !filename() ) return -1;
+ }
+ // See if tmpfile already exists or editor already open
+ if ( is_file(filename()) ) {
+ if ( is_editing() ) {
+ // See if editor recently closed but not reaped; try to reap
+ DWORD wpid;
+ switch ( reap_editor(&wpid) ) {
+ case -2: // no editor running (unlikely to happen)
+ break;
+ case -1: // wait failed
+ fl_alert("ERROR: WaitForSingleObject() failed: %s\nfile='%s', pid=%ld",
+ get_ms_errmsg(), filename(), long(pinfo_.dwProcessId));
+ return -1;
+ case 0: // process still running
+ fl_alert("Editor Already Open\n file='%s'\n pid=%ld",
+ filename(), long(pinfo_.dwProcessId));
+ return 0;
+ case 1: // process reaped, wpid is pid reaped
+ if ( G_debug )
+ printf("*** REAPED EXTERNAL EDITOR: PID %ld\n", long(wpid));
+ break; // fall thru to open new editor instance
+ }
+ // Reinstate tmp filename (reap_editor() clears it)
+ set_filename(tmp_filename());
+ }
+ }
+ // Save code to tmpfile, getting mtime/size
+ if ( save_file(filename(), code, file_mtime_, file_size_) < 0 ) {
+ return -1; // errors were shown in dialog
+ }
+ if ( start_editor(editor_cmd, filename()) < 0 ) { // open file in external editor
+ if ( G_debug ) printf("Editor failed to start\n");
+ return -1; // errors were shown in dialog
+ }
+ // New editor opened -- start update timer (if not already)
+ if ( L_update_timer_cb && !Fl::has_timeout(L_update_timer_cb) ) {
+ if ( G_debug ) printf("--- Editor opened: STARTING UPDATE TIMER\n");
+ Fl::add_timeout(2.0, L_update_timer_cb);
+ }
+ return 0;
+}
+
+// [Public/Static] Start update timer
+void ExternalCodeEditor::start_update_timer() {
+ if ( !L_update_timer_cb ) return;
+ if ( G_debug ) printf("--- TIMER: STARTING UPDATES\n");
+ Fl::add_timeout(2.0, L_update_timer_cb);
+}
+
+// [Public/Static] Stop update timer
+void ExternalCodeEditor::stop_update_timer() {
+ if ( !L_update_timer_cb ) return;
+ if ( G_debug ) printf("--- TIMER: STOPPING UPDATES\n");
+ Fl::remove_timeout(L_update_timer_cb);
+}
+
+// [Public/Static] Set app's external editor update timer callback
+// This is the app's callback callback we start while editors are open,
+// and stop when all editors are closed.
+//
+void ExternalCodeEditor::set_update_timer_callback(Fl_Timeout_Handler cb) {
+ L_update_timer_cb = cb;
+}
+
+// [Static/Public] See if any external editors are open.
+// App's timer cb can see if any editors need checking..
+//
+int ExternalCodeEditor::editors_open() {
+ return L_editors_open;
+}
diff --git a/fluid/tools/ExternalCodeEditor_WIN32.h b/fluid/tools/ExternalCodeEditor_WIN32.h
new file mode 100644
index 000000000..de4c2c4a7
--- /dev/null
+++ b/fluid/tools/ExternalCodeEditor_WIN32.h
@@ -0,0 +1,59 @@
+//
+// External code editor management class for Windows
+//
+// Handles starting and keeping track of an external text editor,
+// including process start, temp file creation/removal, bookkeeping, killing..
+//
+#ifndef _EXTCODEEDITOR_H
+#define _EXTCODEEDITOR_H
+
+/* We require at least Windows 2000 (WINVER == 0x0500) for GetFileSizeEx(). */
+/* This must be defined before #include <windows.h> - MinGW doesn't do that. */
+#if !defined(WINVER) || (WINVER < 0x0500)
+# ifdef WINVER
+# undef WINVER
+# endif
+# define WINVER 0x0500
+#endif
+#if !defined(_WIN32_WINNT) || (_WIN32_WINNT < 0x0500)
+# ifdef _WIN32_WINNT
+# undef _WIN32_WINNT
+# endif
+# define _WIN32_WINNT 0x0500
+#endif
+
+#include <windows.h> /* CreateFile().. */
+#include <string.h> /* sprintf().. */
+
+class ExternalCodeEditor {
+ PROCESS_INFORMATION pinfo_; // CreateProcess() handle to running process
+ FILETIME file_mtime_; // last modify time of the file (used to determine if file changed)
+ LARGE_INTEGER file_size_; // last file size (used to determine if changed)
+ const char * filename_; // tmpfilename editor uses
+protected:
+ void kill_editor();
+ void reap_cleanup();
+ const char *create_tmpdir();
+ const char *tmp_filename();
+ int start_editor(const char *cmd, const char *filename);
+ void set_filename(const char *val);
+public:
+ ExternalCodeEditor();
+ ~ExternalCodeEditor();
+ int is_editing();
+ int reap_editor(DWORD *pid_reaped=NULL);
+ void close_editor();
+ const char *filename() { return filename_; }
+ int open_editor(const char *editor_cmd, const char *code);
+ int handle_changes(const char **code, int force=0);
+ int remove_tmpfile();
+ // Public static methods
+ static void start_update_timer();
+ static void stop_update_timer();
+ static const char* tmpdir_name();
+ static void tmpdir_clear();
+ static int editors_open();
+ static void set_update_timer_callback(Fl_Timeout_Handler);
+};
+
+#endif /*_EXTCODEEDITOR_H */
diff --git a/fluid/tools/autodoc.cxx b/fluid/tools/autodoc.cxx
new file mode 100644
index 000000000..59f9a7b27
--- /dev/null
+++ b/fluid/tools/autodoc.cxx
@@ -0,0 +1,613 @@
+//
+// Self-generate snapshots of user interface for FLUID documentation.
+//
+// Copyright 2024-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 NDEBUG
+
+#include "tools/autodoc.h"
+
+#include "app/fluid.h"
+#include "nodes/factory.h"
+#include "nodes/Fl_Widget_Type.h"
+#include "nodes/Fl_Window_Type.h"
+#include "panels/widget_panel.h"
+#include "panels/function_panel.h"
+#include "panels/settings_panel.h"
+#include "panels/codeview_panel.h"
+#include "widgets/widget_browser.h"
+
+#include <FL/Enumerations.H>
+#include <FL/fl_draw.H>
+#include <FL/Fl_Image_Surface.H>
+#include <FL/Fl_PNG_Image.H>
+#include <FL/Fl_Menu_Bar.H>
+
+extern Fl_Double_Window *settings_window;
+
+/** \file autodoc.cxx
+
+ \todo Implement a function to snapshot a window including decoration
+ - see: void Fl_Widget_Surface::draw_decorated_window(Fl_Window *win, int win_offset_x, int win_offset_y)
+ - see: void Fl_Widget_Surface::origin(int x, int y)
+ - see: void Fl_Widget_Surface::draw(Fl_Widget* widget, int delta_x, int delta_y)
+ - see: void Fl_Widget_Surface::print_window_part(Fl_Window *win, int x, int y, int w, int h, int delta_x, int delta_y)
+
+ \todo Implement a version that snaps multiple windows in a desktop style situation.
+
+ \todo a version that takes snapshots of a range of menu items
+
+ \todo implement FL_SNAP_TO_GROUP, possibly with a number on how many groups up in the hierarchy
+ */
+
+/** \addtogroup fl_drawings
+ @{
+ */
+
+const int FL_SNAP_TO_WINDOW = 0x01000000;
+
+static Fl_Box snap_clear_(0, 0, 0, 0);
+Fl_Widget *FL_SNAP_AREA_CLEAR = &snap_clear_;
+
+static inline int fl_min(int a, int b) { return a < b ? a : b; }
+static inline uchar fl_min(uchar a, uchar b) { return a < b ? a : b; }
+static inline int fl_max(int a, int b) { return a > b ? a : b; }
+
+/**
+ Create a rect by providing a margin around a zero size rectangle.
+ \param[in] dx, dy positive integers, move margin up and left
+ \param[in] dr, db move margin to the right and down
+ */
+Fl_Margin::Fl_Margin(int dx, int dy, int dr, int db)
+ : Fl_Rect(-dx, -dy, dx+dr, dy+db)
+{
+}
+
+/**
+ Convert an RGB image into an RGBA image.
+ \param[inout] image pointer to an RGB image, deletes the RGB image, returns the RGBA image
+ \return 0 if the image is now in RGBA format, or -1 if it can't be converted
+ */
+static int convert_RGB_to_RGBA(Fl_RGB_Image *&img) {
+ if (img->d() == 4)
+ return 0;
+ if (img->d() != 3)
+ return -1;
+
+ // Copy pixel data from RGB to RGBA raw data
+ int img_w = img->w();
+ int img_h = img->h();
+ uchar *data = new uchar[img_w * img_h * 4], *dst = data;
+ int ld = img->ld(); if (ld == 0) ld = img_w * 3;
+ int i, j;
+ for (i=0; i<img_h; i++) {
+ const uchar *src = (const uchar*)img->data()[0] + i * ld;
+ for (j=0; j<img_w; j++) {
+ *dst++ = *src++;
+ *dst++ = *src++;
+ *dst++ = *src++;
+ *dst++ = 255;
+ }
+ }
+
+ // Delete the old image
+ delete img;
+ // Create the new image
+ img = new Fl_RGB_Image(data, img_w, img_h, 4);
+ img->alloc_array = 1;
+ return 0;
+}
+
+/**
+ Blend the left side lines of the alpha channel of an RBGA image to full transparency.
+ \param[in] img must be an RGBA image
+ \param[in] dx number of lines to blend
+ */
+void blend_alpha_left(const Fl_RGB_Image *img, int dx) {
+ if (img->d() != 4)
+ return;
+ if (dx > img->w())
+ return;
+ if (dx > 0) {
+ int max_x = dx, max_y = img->h();
+ int ld = img->ld(); if (ld == 0) ld = img->w() * img->d();
+ float a = 255.0f/static_cast<float>(max_x);
+ for (int i = 0; i < max_x; i++) {
+ uchar *rgba = (uchar*)img->data()[0] + i * img->d();
+ uchar alpha = static_cast<uchar>(i * a);
+ for (int j = 0; j < max_y; j++) {
+ rgba[3] = fl_min(alpha, rgba[3]);
+ rgba += ld;
+ }
+ }
+ }
+}
+
+/**
+ Blend the top lines of the alpha channel of an RBGA image to full transparency.
+ \param[in] img must be an RGBA image
+ \param[in] dy number of lines to blend
+ */
+void blend_alpha_top(const Fl_RGB_Image *img, int dy) {
+ if (img->d() != 4)
+ return;
+ if (dy > img->h())
+ return;
+ if (dy > 0) {
+ int max_x = img->w(), max_y = dy;
+ int ld = img->ld(); if (ld == 0) ld = img->w() * img->d();
+ float a = 255.0f/static_cast<float>(max_y);
+ for (int i = 0; i < max_y; i++) {
+ uchar *rgba = (uchar*)img->data()[0] + i * ld;
+ uchar alpha = static_cast<uchar>(i * a);
+ for (int j = 0; j < max_x; j++) {
+ rgba[3] = fl_min(alpha, rgba[3]);
+ rgba += 4;
+ }
+ }
+ }
+}
+
+/**
+ Blend the right side lines of the alpha channel of an RBGA image to full transparency.
+ \param[in] img must be an RGBA image
+ \param[in] dx number of lines to blend
+ */
+void blend_alpha_right(const Fl_RGB_Image *img, int dx) {
+ if (img->d() != 4)
+ return;
+ if (dx > img->w())
+ return;
+ if (dx > 0) {
+ int max_x = dx, max_y = img->h();
+ int ld = img->ld(); if (ld == 0) ld = img->w() * img->d();
+ float a = 255.0f/static_cast<float>(max_x);
+ for (int i = 0; i < max_x; i++) {
+ uchar *rgba = (uchar*)img->data()[0] + (img->w()-i-1) * img->d();
+ uchar alpha = static_cast<uchar>(i * a);
+ for (int j = 0; j < max_y; j++) {
+ rgba[3] = fl_min(alpha, rgba[3]);
+ rgba += ld;
+ }
+ }
+ }
+}
+
+/**
+ Blend the bottom lines of the alpha channel of an RBGA image to full transparency.
+ \param[in] img must be an RGBA image
+ \param[in] dy number of lines to blend
+ */
+void blend_alpha_bottom(const Fl_RGB_Image *img, int dy) {
+ if (img->d() != 4)
+ return;
+ if (dy > img->h())
+ return;
+ if (dy > 0) {
+ int max_x = img->w(), max_y = dy;
+ int ld = img->ld(); if (ld == 0) ld = img->w() * img->d();
+ float a = 255.0f/static_cast<float>(max_y);
+ for (int i = 0; i < max_y; i++) {
+ uchar *rgba = (uchar*)img->data()[0] + (img->h()-i-1) * ld;
+ uchar alpha = static_cast<uchar>(i * a);
+ for (int j = 0; j < max_x; j++) {
+ rgba[3] = fl_min(alpha, rgba[3]);
+ rgba += 4;
+ }
+ }
+ }
+}
+
+/**
+ Take a snapshot of a number of widgets and save it as a png image.
+
+ Draw a rectangular snapshot that fits around all widgets inside a window.
+ All widgets must be inside the same window. It's up to the caller to ensure
+ that widgets are visible. This includes children of `Fl_Tabs`.
+
+ Outside labels of widgets are not taken into account, but a `frame` can be
+ provided to grow the snapshot rectangle. Setting individual parameters of the
+ frame to `FL_SNAP_TO_WINDOW` will extend the snapshot to the borders of the
+ top level window.
+
+ Another `blend` frame can be added around the image that fades to full
+ transparency on selected sides.
+
+ Use `Fl_Margin` to create `frame` and `blend` using positive integers to grow
+ the rectangle to the left, top, right, and bottom.
+
+ The image can be scaled after all processing. Note that snapshot is always
+ created in FLTK resolution, even if the screen uses a higher resolution.
+
+ \param[in] filename the snapshot will be written to this file in png format
+ \param[in] w draw a bounding box around all widgets in the NULL terminated list
+ \param[in] frame add a margin around the bounding box
+ \param[in] blend add another margin around the bounding box that fades to full transparency
+ \param[in] scale scale everything by this factor before saving it
+ \return the result of fl_write_png or -3 if another error occurred
+ */
+int fl_snapshot(const char *filename, Fl_Widget **w,
+ const Fl_Rect &frame,
+ const Fl_Rect &blend,
+ double scale)
+{
+ int i, min_x = 0, min_y = 0, max_x = 0, max_y = 0, bb_w, bb_h, img_w, img_h;
+
+ // Get the bounding box for all widgets and make sure that all widgets are shown
+ for (i=0; w[i]; i++) {
+ int x, y;
+ Fl_Widget *ww = w[i];
+ if (ww == FL_SNAP_AREA_CLEAR) {
+ min_x = max_x = 0;
+ min_y = max_y = 0;
+ } else {
+ ww->top_window_offset(x, y);
+ if (i==0) {
+ min_x = x; max_x = x + ww->w();
+ min_y = y; max_y = y + ww->h();
+ } else {
+ min_x = fl_min(min_x, x); max_x = fl_max(max_x, x + ww->w());
+ min_y = fl_min(min_y, y); max_y = fl_max(max_y, y + ww->h());
+ }
+ }
+
+ // this does not help us with Fl_Tab groups
+ while (ww) { ww->show(); ww = ww->parent(); }
+ }
+
+ // Check for special values in frame and adjust bounding box
+ Fl_Rect c_frame = frame;
+ if (frame.x() == -FL_SNAP_TO_WINDOW) c_frame.x(-min_x);
+ if (frame.y() == -FL_SNAP_TO_WINDOW) c_frame.y(-min_y);
+ if (frame.r() == FL_SNAP_TO_WINDOW) c_frame.r(w[0]->top_window()->w()-max_x);
+ if (frame.b() == FL_SNAP_TO_WINDOW) c_frame.b(w[0]->top_window()->h()-max_y);
+
+ min_x += c_frame.x(); max_x += c_frame.r();
+ min_y += c_frame.y(); max_y += c_frame.b();
+ bb_w = max_x - min_x; bb_h = max_y - min_y;
+ img_w = bb_w + blend.w();
+ img_h = bb_h + blend.h();
+
+ // Generate the Image Surface
+ Fl_Image_Surface *srfc = new Fl_Image_Surface(img_w, img_h);
+ Fl_Image_Surface::push_current(srfc);
+
+ // Draw the background
+ fl_rectf(0, 0, img_w, img_h, 0x1395bf00);
+
+ // Draw the top level window
+ srfc->draw(w[0]->top_window(), -blend.x()-min_x, -blend.y()-min_y);
+ Fl_Image_Surface::pop_current();
+ Fl_RGB_Image *img = srfc->image();
+
+ // Do we want an alpha blended extension of the frame?
+ if ((blend.x()<0 || blend.y()<0 || blend.r()>0 || blend.b()>0)) {
+ if (convert_RGB_to_RGBA(img) == -1) {
+ delete img;
+ delete srfc;
+ return -3;
+ }
+ if (blend.x() < 0) blend_alpha_left(img, -blend.x());
+ if (blend.y() < 0) blend_alpha_top(img, -blend.y());
+ if (blend.r() > 0) blend_alpha_right(img, blend.r());
+ if (blend.b() > 0) blend_alpha_bottom(img, blend.b());
+ }
+
+ // If scale is set, scale the image
+ if (scale != 1.0) {
+ Fl_Image::scaling_algorithm(FL_RGB_SCALING_BILINEAR);
+ int scaled_img_w = static_cast<int>(img->w()*scale);
+ int scaled_img_h = static_cast<int>(img->h()*scale);
+ Fl_RGB_Image *scaled_img =
+ static_cast<Fl_RGB_Image*>(img->copy(scaled_img_w, scaled_img_h));
+ delete img;
+ img = scaled_img;
+ }
+
+ // Write the image to disk
+ int ret = fl_write_png(filename, img);
+
+ // Clean up
+ delete img;
+ delete srfc;
+ return ret;
+}
+
+/**
+ Take a snapshot of the size of the bounding box around two widgets and save it as a png image.
+
+ \param[in] filename the snapshot will be written to this file in png format
+ \param[in] w1, w2 top left and bottom right widget
+ \param[in] frame add a margin around the bounding box
+ \param[in] blend add another margin around the bounding box that fades to full transparency
+ \param[in] scale scale everything by this factor before saving it
+ \return the result of fl_write_png or -3 if another error occurred
+
+ \see fl_snapshot(const char*, Fl_Widget**, const Fl_Rect&, const Fl_Rect&, double)
+ */
+int fl_snapshot(const char *filename, Fl_Widget *w1, Fl_Widget *w2,
+ const Fl_Rect &frame,
+ const Fl_Rect &blend,
+ double scale)
+{
+ Fl_Widget *ww[3] = { w1, w2, NULL };
+ return fl_snapshot(filename, ww, frame, blend, scale);
+}
+
+/**
+ Take a snapshot of a widget inside its window and save it as a png image.
+
+ \param[in] filename the snapshot will be written to this file in png format
+ \param[in] w snap this window, can also be a groups
+ \param[in] frame add a margin around the bounding box
+ \param[in] blend add another margin around the bounding box that fades to full transparency
+ \param[in] scale scale everything by this factor before saving it
+ \return the result of fl_write_png or -3 if another error occurred
+
+ \see fl_snapshot(const char*, Fl_Widget**, const Fl_Rect&, const Fl_Rect&, double)
+ */
+int fl_snapshot(const char *filename, Fl_Widget *w,
+ const Fl_Rect &frame,
+ const Fl_Rect &blend,
+ double scale)
+{
+ Fl_Widget *ww[2] = { w, NULL };
+ return fl_snapshot(filename, ww, frame, blend, scale);
+}
+
+/** @} */
+
+
+void run_autodoc(const std::string &target_dir) {
+ // A list of all the margins we will use later
+ Fl_Margin win_margin(0, 0, 0, 0);
+ Fl_Margin win_blend(10, 10, 10, 10);
+ Fl_Margin tab_margin(FL_SNAP_TO_WINDOW, 32, FL_SNAP_TO_WINDOW, 4);
+ Fl_Margin xtab_margin(FL_SNAP_TO_WINDOW, 50, FL_SNAP_TO_WINDOW, 4);
+ Fl_Margin row_margin(FL_SNAP_TO_WINDOW, 4, FL_SNAP_TO_WINDOW, 4);
+ Fl_Margin xrow_margin(FL_SNAP_TO_WINDOW, 14, FL_SNAP_TO_WINDOW, 4);
+ Fl_Margin row_blend(0, 10, 0, 10);
+
+// Fl::scheme("gtk+");
+
+ // Create a silly project that contains all widgets that we want to document
+ new_project(false);
+
+ /*Fl_Type *t_func = */ add_new_widget_from_user("Function", Strategy::AS_LAST_CHILD, false);
+ Fl_Window_Type *t_win = (Fl_Window_Type*)add_new_widget_from_user("Fl_Window", Strategy::AS_LAST_CHILD, false);
+ t_win->label("My Main Window");
+ Fl_Widget_Type *t_grp = (Fl_Widget_Type*)add_new_widget_from_user("Fl_Group", Strategy::AS_LAST_CHILD, false);
+ t_grp->public_ = 0;
+ Fl_Widget_Type *t_btn = (Fl_Widget_Type*)add_new_widget_from_user("Fl_Button", Strategy::AS_LAST_CHILD, false);
+ t_btn->comment("Don't press this button!");
+ t_btn->name("emergency_btn");
+ ((Fl_Button*)t_btn->o)->shortcut(FL_COMMAND|'g');
+ Fl_Type *t_sldr = add_new_widget_from_user("Fl_Slider", Strategy::AS_LAST_CHILD, false);
+ Fl_Type *t_inp = add_new_widget_from_user("Fl_Input", Strategy::AS_LAST_CHILD, false);
+ Fl_Type *t_flx = add_new_widget_from_user("Fl_Flex", Strategy::AS_LAST_CHILD, false);
+ Fl_Type *t_flxc = add_new_widget_from_user("Fl_Button", Strategy::AS_LAST_CHILD, false);
+ select_only(t_grp);
+ Fl_Type *t_grd = add_new_widget_from_user("Fl_Grid", Strategy::AS_LAST_CHILD, false);
+ Fl_Type *t_grdc = add_new_widget_from_user("Fl_Button", Strategy::AS_LAST_CHILD, false);
+
+ widget_browser->rebuild();
+ g_project.update_settings_dialog();
+
+ // TODO: FLUID overview
+
+ // TODO: explain FLUID command line usage
+
+ // TODO: take a snapshot of FLUID in a desktop situation
+ // (main, toolbar, document, widget editor, code view)
+
+ // ---- main window
+ // explain titlebar
+ // explain menubar?
+ // explain widget browser
+ // explain widget browser entry
+ main_window->size(350, 320);
+ fl_snapshot((target_dir + "main_window.png").c_str(), main_window, win_margin, win_blend);
+ fl_snapshot((target_dir + "main_menubar.png").c_str(), main_menubar, row_margin, row_blend);
+ fl_snapshot((target_dir + "main_browser.png").c_str(), widget_browser, FL_SNAP_AREA_CLEAR,
+ Fl_Rect(0, 30, FL_SNAP_TO_WINDOW, 100), row_blend, 2.0);
+
+
+ // TODO: document view
+ // explain dnd
+ // explain selection, multiple selection, keyboard shortcuts
+ // explain mouse functionality and alignment
+ // explain live resize
+ // arrow: move by 1
+ // shift: resize by one
+ // Meta: move by Widget Gap
+ // Shift Meta: resize by Widget Increment
+
+ // ---- widget bin
+ // show grouping
+ // explain non-widget types and where they will be located
+ // explain widgets types an their dnd option
+ // explain menu arrays
+ // list exceptions (subwindow, scroll)
+ Fl::wait(0.2);
+ Fl::flush();
+ fl_snapshot((target_dir + "widgetbin_panel.png").c_str(), widgetbin_panel, win_margin, win_blend);
+
+ // ---- code view
+ // explain functionality
+ // explain live update and choices
+ // show various tabs
+ // explain find and locate
+ if (!codeview_panel) make_codeview();
+ codeview_panel->show();
+ Fl::wait(0.2);
+ Fl::flush();
+ update_codeview_cb(NULL, NULL); // must be visible on screen for this to work
+ cv_tab->value(cv_source_tab);
+ codeview_panel->redraw();
+ Fl::flush();
+ fl_snapshot((target_dir + "codeview_panel.png").c_str(), codeview_panel, win_margin, win_blend);
+ fl_snapshot((target_dir + "cv_find_row.png").c_str(), cv_find_row, row_margin, row_blend);
+ fl_snapshot((target_dir + "cv_settings_row.png").c_str(), cv_settings_row, row_margin, row_blend);
+
+ // ---- settings dialog
+ // show and explain all tabs
+ fl_snapshot((target_dir + "w_settings.png").c_str(), settings_window, win_margin, win_blend);
+ fl_snapshot((target_dir + "w_settings_general_tab.png").c_str(), w_settings_general_tab, xtab_margin, row_blend);
+ w_settings_tabs->value(w_settings_project_tab);
+ fl_snapshot((target_dir + "w_settings_project_tab.png").c_str(), w_settings_project_tab, xtab_margin, row_blend);
+ w_settings_tabs->value(w_settings_layout_tab);
+ fl_snapshot((target_dir + "w_settings_layout_tab.png").c_str(), w_settings_layout_tab, xtab_margin, row_blend);
+ w_settings_tabs->value(w_settings_shell_tab);
+ w_settings_shell_list->value(1);
+ w_settings_shell_list->do_callback();
+ fl_snapshot((target_dir + "w_settings_shell_tab.png").c_str(), w_settings_shell_tab, xtab_margin, row_blend);
+ w_settings_tabs->value(w_settings_i18n_tab);
+ i18n_type_chooser->value(1);
+ i18n_type_chooser->do_callback();
+ fl_snapshot((target_dir + "w_settings_i18n_gnu.png").c_str(), i18n_type_chooser, i18n_gnu_static_function_input, row_margin, row_blend);
+ i18n_type_chooser->value(2);
+ i18n_type_chooser->do_callback();
+ fl_snapshot((target_dir + "w_settings_i18n_psx.png").c_str(), i18n_type_chooser, i18n_pos_set_input, row_margin, row_blend);
+ w_settings_tabs->value(w_settings_user_tab);
+ fl_snapshot((target_dir + "w_settings_user_tab.png").c_str(), w_settings_user_tab, xtab_margin, row_blend);
+
+
+ // ---- dialog types
+ // list and show all non-widget types and their respective dialog boxes
+
+ // -- ID_Function
+ Fl_Window *adoc_function_panel = make_function_panel();
+ f_name_input->value("count_trees(const char *forest_name)");
+ f_return_type_input->value("unsigned int");
+ fl_snapshot((target_dir + "function_panel.png").c_str(), adoc_function_panel, win_margin, win_blend);
+ adoc_function_panel->hide();
+
+ // -- ID_Code
+ Fl_Window *adoc_code_panel = make_code_panel();
+ code_input->buffer()->text("// increment user count\nif (new_user) {\n user_count++;\n}\n");
+ fl_snapshot((target_dir + "code_panel.png").c_str(), adoc_code_panel, win_margin, win_blend);
+ adoc_code_panel->hide();
+
+ // -- ID_CodeBlock
+ Fl_Window *adoc_codeblock_panel = make_codeblock_panel();
+ code_before_input->value("if (test())");
+ code_after_input->value("// test widgets added...");
+ fl_snapshot((target_dir + "codeblock_panel.png").c_str(), adoc_codeblock_panel, win_margin, win_blend);
+ adoc_codeblock_panel->hide();
+
+ // -- ID_Decl
+ Fl_Window *adoc_decl_panel = make_decl_panel();
+ decl_class_choice->hide();
+ decl_input->buffer()->text("const char *damage = \"'tis but a scratch\";");
+ fl_snapshot((target_dir + "decl_panel.png").c_str(), adoc_decl_panel, win_margin, win_blend);
+ adoc_decl_panel->hide();
+
+ // -- ID_DeclBlock
+ Fl_Window *adoc_declblock_panel = make_declblock_panel();
+ declblock_before_input->value("#ifdef NDEBUG");
+ declblock_after_input->value("#endif // NDEBUG");
+ fl_snapshot((target_dir + "declblock_panel.png").c_str(), adoc_declblock_panel, win_margin, win_blend);
+ adoc_declblock_panel->hide();
+
+ // -- ID_Class
+ Fl_Window *adoc_class_panel = make_class_panel();
+ decl_class_choice->hide();
+ c_name_input->value("Zoo_Giraffe");
+ c_subclass_input->value("Zoo_Animal");
+ fl_snapshot((target_dir + "class_panel.png").c_str(), adoc_class_panel, win_margin, win_blend);
+ adoc_class_panel->hide();
+
+ // -- ID_Widget_Class is handled like Fl_Window_Type
+
+ // -- ID_Comment
+ Fl_Window *adoc_comment_panel = make_comment_panel();
+ comment_input->buffer()->text("Make sure that the giraffe gets enough hay,\nbut the monkey can't reach it.");
+ fl_snapshot((target_dir + "comment_panel.png").c_str(), adoc_comment_panel, win_margin, win_blend);
+ adoc_comment_panel->hide();
+
+ // -- ID_Data
+ Fl_Window *adoc_data_panel = make_data_panel();
+ data_class_choice->hide();
+ data_input->value("emulated_ROM");
+ data_filename->value("./ROM.bin");
+ fl_snapshot((target_dir + "data_panel.png").c_str(), adoc_data_panel, win_margin, win_blend);
+ adoc_data_panel->hide();
+
+
+ // ---- widget dialog
+ t_win->open(); // open the window
+ t_win->open(); // open the panel
+ select_only(t_win);
+
+ // -- snapshot of the widget properties panel
+ fl_snapshot((target_dir + "widget_panel.png").c_str(), the_panel, win_margin, win_blend);
+ fl_snapshot((target_dir + "wLiveMode.png").c_str(), wLiveMode, row_margin, row_blend);
+
+ // -- snapshot of the GUI tab
+ widget_tabs->value(wp_gui_tab);
+ fl_snapshot((target_dir + "wp_gui_tab.png").c_str(), wp_gui_tab, tab_margin, row_blend);
+ fl_snapshot((target_dir + "wp_gui_label.png").c_str(), wp_gui_label, row_margin, row_blend);
+ select_only(t_btn);
+ fl_snapshot((target_dir + "wp_gui_image.png").c_str(), widget_image_input, widget_deimage_input, row_margin, row_blend);
+ fl_snapshot((target_dir + "wp_gui_alignment.png").c_str(), wp_gui_alignment, row_margin, row_blend);
+ fl_snapshot((target_dir + "wp_gui_size.png").c_str(), widget_x_input, xrow_margin, row_blend);
+ select_only(t_sldr);
+ fl_snapshot((target_dir + "wp_gui_values.png").c_str(), wp_gui_values, xrow_margin, row_blend);
+ select_only(t_flxc);
+ fl_snapshot((target_dir + "wp_gui_flexp.png").c_str(), wp_gui_flexp, xrow_margin, row_blend);
+ select_only(t_flx);
+ fl_snapshot((target_dir + "wp_gui_margins.png").c_str(), wp_gui_margins, xrow_margin, row_blend);
+ select_only(t_win);
+ fl_snapshot((target_dir + "wp_gui_sizerange.png").c_str(), wp_gui_sizerange, xrow_margin, row_blend);
+ select_only(t_btn);
+ fl_snapshot((target_dir + "wp_gui_shortcut.png").c_str(), wp_gui_shortcut, row_margin, row_blend);
+ select_only(t_win);
+ fl_snapshot((target_dir + "wp_gui_xclass.png").c_str(), wp_gui_xclass, row_margin, row_blend);
+ select_only(t_btn);
+ fl_snapshot((target_dir + "wp_gui_attributes.png").c_str(), wp_gui_attributes, row_margin, row_blend);
+ fl_snapshot((target_dir + "wp_gui_tooltip.png").c_str(), wp_gui_tooltip, row_margin, row_blend);
+
+ // -- snapshot of the style tab
+ widget_tabs->value(wp_style_tab);
+ select_only(t_inp);
+ fl_snapshot((target_dir + "wp_style_tab.png").c_str(), wp_style_tab, tab_margin, row_blend);
+ fl_snapshot((target_dir + "wp_style_label.png").c_str(), wp_style_label, row_margin, row_blend);
+ select_only(t_btn);
+ fl_snapshot((target_dir + "wp_style_box.png").c_str(), wp_style_box, wp_style_downbox, row_margin, row_blend);
+ select_only(t_inp);
+ fl_snapshot((target_dir + "wp_style_text.png").c_str(), wp_style_text, row_margin, row_blend);
+
+ // -- snapshot of the C++ tab
+ widget_tabs->value(wp_cpp_tab);
+ select_only(t_btn);
+ fl_snapshot((target_dir + "wp_cpp_tab.png").c_str(), wp_cpp_tab, tab_margin, row_blend);
+ fl_snapshot((target_dir + "wp_cpp_class.png").c_str(), wp_cpp_class, row_margin, row_blend);
+ fl_snapshot((target_dir + "wp_cpp_name.png").c_str(), wp_cpp_name, row_margin, row_blend);
+ fl_snapshot((target_dir + "v_input.png").c_str(), v_input[0], v_input[3], row_margin, row_blend);
+ fl_snapshot((target_dir + "wComment.png").c_str(), wComment, row_margin, row_blend);
+ fl_snapshot((target_dir + "wp_cpp_callback.png").c_str(), wCallback, w_when_box, row_margin, row_blend);
+
+ // -- snapshot of the Grid tab
+ select_only(t_grd);
+ widget_tabs->value(widget_tab_grid);
+ fl_snapshot((target_dir + "wp_grid_tab.png").c_str(), widget_tab_grid, tab_margin, row_blend);
+
+ // -- snapshot of the Grid Child tab
+ select_only(t_grdc);
+ widget_tabs->value(widget_tab_grid_child);
+ fl_snapshot((target_dir + "wp_gridc_tab.png").c_str(), widget_tab_grid_child, tab_margin, row_blend);
+}
+
+
+#endif // NDEBUG
diff --git a/fluid/tools/autodoc.h b/fluid/tools/autodoc.h
new file mode 100644
index 000000000..33b0bd6fc
--- /dev/null
+++ b/fluid/tools/autodoc.h
@@ -0,0 +1,59 @@
+//
+// Widget snapshot header-only file 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
+//
+
+/**
+ \file autodoc.h
+ \brief tools to take snapshots of UI elements for documentation purposes
+ */
+
+#ifndef fl_screenshot_H
+#define fl_screenshot_H
+
+#include <FL/Fl_Export.H>
+#include <FL/Fl_Window.H>
+#include <FL/Fl_Rect.H>
+
+#include <string>
+
+/** Class to initialize a Rect by providing the margin around a rect. */
+class Fl_Margin : public Fl_Rect {
+public:
+ Fl_Margin(int dx, int dy, int dr, int db);
+};
+
+int fl_snapshot(const char *filename, Fl_Widget **w,
+ const Fl_Rect &frame = Fl_Margin(4, 4, 4, 4),
+ const Fl_Rect &blend = Fl_Margin(4, 4, 4, 4),
+ double scale=1.0);
+
+int fl_snapshot(const char *filename, Fl_Widget *w1, Fl_Widget *w2,
+ const Fl_Rect &frame = Fl_Margin(4, 4, 4, 4),
+ const Fl_Rect &blend = Fl_Margin(4, 4, 4, 4),
+ double scale=1.0);
+
+int fl_snapshot(const char *filename, Fl_Widget *w,
+ const Fl_Rect &frame = Fl_Margin(4, 4, 4, 4),
+ const Fl_Rect &blend = Fl_Margin(4, 4, 4, 4),
+ double scale=1.0);
+
+extern const int FL_SNAP_TO_WINDOW;
+
+extern Fl_Widget *FL_SNAP_AREA_CLEAR;
+
+extern void run_autodoc(const std::string &target_dir);
+
+#endif
+
diff --git a/fluid/tools/fluid_filename.cxx b/fluid/tools/fluid_filename.cxx
new file mode 100644
index 000000000..cd9dadfa6
--- /dev/null
+++ b/fluid/tools/fluid_filename.cxx
@@ -0,0 +1,219 @@
+//
+// Filename expansion 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
+//
+
+/** \file fluid/fluid_filename.cxx
+
+ \brief File names and URI utility functions for FLUID only.
+
+ This file defines all fl_filename* functions using std::string and also
+ includes the main header file <FL/filename.H>.
+
+ \note This file contains some filename functions using std::string which
+ which are used in FLTK 1.4.x but will be removed in the next minor
+ or major release after 1.4.x (i.e. 1.5 or maybe 4.0).
+
+ \note This entire file should become obsolete in 1.5 or higher, whatever
+ the next release after 1.4.x will be. We'll use std::string instead!
+*/
+
+#include <FL/filename.H>
+#include <FL/Fl.H>
+#include <FL/fl_string_functions.h>
+#include "../src/flstring.h"
+
+#include <stdlib.h>
+#include <string>
+
+/**
+ Return a new string that contains the name part of the filename.
+ \param[in] filename file path and name
+ \return the name part of a filename
+ \see fl_filename_name(const char *filename)
+ */
+std::string fl_filename_name(const std::string &filename) {
+ return std::string(fl_filename_name(filename.c_str()));
+}
+
+/**
+ Return a new string that contains the path part of the filename.
+ \param[in] filename file path and name
+ \return the path part of a filename without the name
+ \see fl_filename_name(const char *filename)
+ */
+std::string fl_filename_path(const std::string &filename) {
+ const char *base = filename.c_str();
+ const char *name = fl_filename_name(base);
+ if (name) {
+ return std::string(base, (int)(name-base));
+ } else {
+ return std::string();
+ }
+}
+
+/**
+ Return a new string that contains the filename extension.
+ \param[in] filename file path and name
+ \return the filename extension including the prepending '.', or an empty
+ string if the filename has no extension
+ \see fl_filename_ext(const char *buf)
+ */
+std::string fl_filename_ext(const std::string &filename) {
+ return std::string(fl_filename_ext(filename.c_str()));
+}
+
+/**
+ Return a copy of the old filename with the new extension.
+ \param[in] filename file path and name
+ \param[in] new_extension new filename extension, starts with a '.'
+ \return the new filename
+ \see fl_filename_setext(char *to, int tolen, const char *ext)
+ */
+std::string fl_filename_setext(const std::string &filename, const std::string &new_extension) {
+ char buffer[FL_PATH_MAX];
+ fl_strlcpy(buffer, filename.c_str(), FL_PATH_MAX);
+ fl_filename_setext(buffer, FL_PATH_MAX, new_extension.c_str());
+ return std::string(buffer);
+}
+
+/**
+ Expands a filename containing shell variables and tilde (~).
+ \param[in] from file path and name
+ \return the new, expanded filename
+ \see fl_filename_expand(char *to, int tolen, const char *from)
+*/
+std::string fl_filename_expand(const std::string &from) {
+ char buffer[FL_PATH_MAX];
+ fl_filename_expand(buffer, FL_PATH_MAX, from.c_str());
+ return std::string(buffer);
+}
+
+/**
+ Makes a filename absolute from a filename relative to the current working directory.
+ \param[in] from relative filename
+ \return the new, absolute filename
+ \see fl_filename_absolute(char *to, int tolen, const char *from)
+ */
+std::string fl_filename_absolute(const std::string &from) {
+ char buffer[FL_PATH_MAX];
+ fl_filename_absolute(buffer, FL_PATH_MAX, from.c_str());
+ return std::string(buffer);
+}
+
+/**
+ Append the relative filename `from` to the absolute filename `base` to form
+ the new absolute path.
+ \param[in] from relative filename
+ \param[in] base `from` is relative to this absolute file path
+ \return the new, absolute filename
+ \see fl_filename_absolute(char *to, int tolen, const char *from, const char *base)
+ */
+std::string fl_filename_absolute(const std::string &from, const std::string &base) {
+ char buffer[FL_PATH_MAX];
+ fl_filename_absolute(buffer, FL_PATH_MAX, from.c_str(), base.c_str());
+ return std::string(buffer);
+}
+
+/**
+ Makes a filename relative to the current working directory.
+ \param[in] from file path and name
+ \return the new, relative filename
+ \see fl_filename_relative(char *to, int tolen, const char *from)
+ */
+std::string fl_filename_relative(const std::string &from) {
+ char buffer[FL_PATH_MAX];
+ fl_filename_relative(buffer, FL_PATH_MAX, from.c_str());
+ return std::string(buffer);
+}
+
+/**
+ Makes a filename relative to any directory.
+ \param[in] from file path and name
+ \param[in] base relative to this absolute path
+ \return the new, relative filename
+ \see fl_filename_relative(char *to, int tolen, const char *from, const char *base)
+ */
+std::string fl_filename_relative(const std::string &from, const std::string &base) {
+ char buffer[FL_PATH_MAX];
+ fl_filename_relative(buffer, FL_PATH_MAX, from.c_str(), base.c_str());
+ return std::string(buffer);
+}
+
+/** Cross-platform function to get the current working directory
+ as a UTF-8 encoded value in an std::string.
+ \return the CWD encoded as UTF-8
+ */
+std::string fl_getcwd() {
+ char buffer[FL_PATH_MAX];
+ fl_getcwd(buffer, FL_PATH_MAX);
+ return std::string(buffer);
+}
+
+/**
+ Return a shortened filename for limited display width.
+
+ Replace the start uf a path with "~" if it matches the home directory.
+ If the remaining filename has more than the give number of characters, it will
+ be shortened by replacing parts of the path with an ellipsis ("...").
+
+ The shortened name can no longer be used to open a file. This is purely to
+ make as much information visible while fitting into a give space.
+
+ \param[in] filename absolute path and name, UTF-8 aware
+ \param[in[ max_chars maximum number of characters in result, including ellipsis
+ \return shortened file path and name
+ */
+std::string fl_filename_shortened(const std::string &filename, int max_chars) {
+ // Insert this as the ellipsis
+ static const char *ell = "...";
+ static const int ell_bytes = 3;
+ // Replace the start of a path with "~" if it matches the home directory
+ static std::string tilde = "~/";
+ static std::string home;
+ static int home_chars = -1;
+ if (home_chars==-1) {
+ home = fl_filename_expand(tilde);
+ home_chars = fl_utf_nb_char((const uchar*)home.c_str(), (int)home.size());
+ }
+ std::string homed_filename;
+#if defined(_WIN32) || defined(__APPLE__)
+ bool starts_with_home = fl_utf_strncasecmp(home.c_str(), filename.c_str(), home_chars)==0;
+#else
+ bool starts_with_home = ::strncmp(home.c_str(), filename.c_str(), home.size())==0;
+#endif
+ if (starts_with_home) {
+ homed_filename = tilde + filename.substr(home.size());
+ } else {
+ homed_filename = filename;
+ }
+ // C style pointer will stay valid until filename is modified.
+ const unsigned char *u8str = reinterpret_cast<const unsigned char *>(homed_filename.c_str());
+ // Count the number of UTF-8 characters in the name.
+ int num_chars = fl_utf_nb_char(u8str, (int)homed_filename.size());
+ if (num_chars+ell_bytes-1 > max_chars) {
+ // Create a new string by replacing characters in the middle.
+ int remove_chars = num_chars - max_chars + ell_bytes;
+ int left_chars = (max_chars - ell_bytes)/2;
+// int right_chars = max_chars - left_chars - 3;
+// int right_start_char = num_chars - right_chars;
+ // Convert character counts into byte counts.
+ int left_bytes = fl_utf8strlen(homed_filename.c_str(), left_chars);
+ int right_start_byte = fl_utf8strlen(homed_filename.c_str()+left_bytes, remove_chars) + left_bytes;
+ return homed_filename.substr(0, left_bytes) + ell + homed_filename.substr(right_start_byte);
+ } else {
+ // Nothing to change.
+ return homed_filename;
+ }
+}
diff --git a/fluid/tools/fluid_filename.h b/fluid/tools/fluid_filename.h
new file mode 100644
index 000000000..1486e61ab
--- /dev/null
+++ b/fluid/tools/fluid_filename.h
@@ -0,0 +1,59 @@
+/*
+ * Filename header file 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
+ */
+
+/** \file fluid/fluid_filename.h
+
+ \brief File names and URI utility functions for FLUID only.
+
+ This file declares all fl_filename* functions using std::string and also
+ includes the main header file <FL/filename.H>.
+
+ \note This file contains some filename functions using std::string which
+ which are used in FLTK 1.4.x but will be removed in the next minor
+ or major release after 1.4.x (i.e. 1.5 or maybe 4.0).
+
+ \note This entire file should become obsolete in 1.5 or higher, whatever
+ the next release after 1.4.x will be. We'll use std::string instead!
+*/
+
+#ifndef FLUID_FILENAME_H
+# define FLUID_FILENAME_H
+
+#include <FL/Fl_Export.H>
+#include <FL/platform_types.h>
+#include <FL/filename.H>
+
+# if defined(__cplusplus)
+
+#include <string>
+
+std::string fl_filename_shortened(const std::string &filename, int maxchars);
+std::string fl_filename_name(const std::string &filename);
+std::string fl_filename_path(const std::string &filename);
+std::string fl_filename_ext(const std::string &filename);
+std::string fl_filename_setext(const std::string &filename, const std::string &new_extension);
+std::string fl_filename_expand(const std::string &from);
+std::string fl_filename_absolute(const std::string &from);
+std::string fl_filename_absolute(const std::string &from, const std::string &base);
+std::string fl_filename_relative(const std::string &from);
+std::string fl_filename_relative(const std::string &from, const std::string &base);
+std::string fl_getcwd();
+
+# endif
+
+/** @} */
+
+#endif /* FLUID_FILENAME_H */