diff options
| author | Matthias Melcher <github@matthiasm.com> | 2025-03-07 16:34:35 +0100 |
|---|---|---|
| committer | Matthias Melcher <github@matthiasm.com> | 2025-03-07 16:34:48 +0100 |
| commit | 1985aefc0e502048f92b91beef87c0dfbe669fed (patch) | |
| tree | af62874def4590e437a47784b4428d975ceb262f /fluid/tools | |
| parent | 42a04c064d4b31c3a85210311f3ada163c406a25 (diff) | |
Restructuring Fluid source files.
Diffstat (limited to 'fluid/tools')
| -rw-r--r-- | fluid/tools/ExternalCodeEditor_UNIX.cxx | 576 | ||||
| -rw-r--r-- | fluid/tools/ExternalCodeEditor_UNIX.h | 60 | ||||
| -rw-r--r-- | fluid/tools/ExternalCodeEditor_WIN32.cxx | 632 | ||||
| -rw-r--r-- | fluid/tools/ExternalCodeEditor_WIN32.h | 59 | ||||
| -rw-r--r-- | fluid/tools/autodoc.cxx | 613 | ||||
| -rw-r--r-- | fluid/tools/autodoc.h | 59 | ||||
| -rw-r--r-- | fluid/tools/fluid_filename.cxx | 219 | ||||
| -rw-r--r-- | fluid/tools/fluid_filename.h | 59 |
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 */ |
