summaryrefslogtreecommitdiff
path: root/fluid/Fluid.cxx
diff options
context:
space:
mode:
authorMatthias Melcher <github@matthiasm.com>2025-03-16 17:16:12 -0400
committerGitHub <noreply@github.com>2025-03-16 17:16:12 -0400
commit51a55bc73660f64e8f4b32b8b4d3858f2a786f7b (patch)
tree122ad9f838fcf8f61ed7cf5fa031e8ed69817e10 /fluid/Fluid.cxx
parent13a7073a1e007ce5b71ef70bced1a9b15158820d (diff)
Fluid: restructuring and rejuvenation of the source code.
* Add classes for application and project * Removed all globals from Fluid.h * Extracting args and project history into their own classes * Moving globals into Application class * Initialize values inside headers for some classes. * Undo functionality wrapped in a class inside Project. * File reader and writer are now linked to a project. * Avoid global project access * Nodes (former Types) will be managed by a new Tree class. * Removed static members (hidden globals) form Node/Fl_Type. * Adding Tree iterator. * Use nullptr instead of 0, NULL, or 0L * Renamed Fl_..._Type to ..._Node, FL_OVERRIDE -> override * Renaming ..._type to ...::prototype * Splitting Widget Panel into multiple files. * Moved callback code into widget panel file. * Cleaning up Fluid_Image -> Image_asset * Moving Fd_Snap_Action into new namespace fld::app::Snap_Action etc. * Moved mergeback into proj folder. * `enum ID` is now `enum class Type`.
Diffstat (limited to 'fluid/Fluid.cxx')
-rw-r--r--fluid/Fluid.cxx1392
1 files changed, 1392 insertions, 0 deletions
diff --git a/fluid/Fluid.cxx b/fluid/Fluid.cxx
new file mode 100644
index 000000000..48dd7dcfb
--- /dev/null
+++ b/fluid/Fluid.cxx
@@ -0,0 +1,1392 @@
+//
+// Fluid Application code for the Fast Light Tool Kit (FLTK).
+//
+// Copyright 1998-2025 by Bill Spitzak and others.
+//
+// This library is free software. Distribution and use rights are outlined in
+// the file "COPYING" which should have been included with this file. If this
+// file is missing or damaged, see the license at:
+//
+// https://www.fltk.org/COPYING.php
+//
+// Please see the following page on how to report bugs and issues:
+//
+// https://www.fltk.org/bugs.php
+//
+
+#include "Fluid.h"
+
+#include "Project.h"
+#include "proj/mergeback.h"
+#include "app/Menu.h"
+#include "app/shell_command.h"
+#include "proj/undo.h"
+#include "io/Project_Reader.h"
+#include "io/Project_Writer.h"
+#include "io/Code_Writer.h"
+#include "nodes/Node.h"
+#include "nodes/Function_Node.h"
+#include "nodes/Group_Node.h"
+#include "nodes/Window_Node.h"
+#include "nodes/factory.h"
+#include "panels/settings_panel.h"
+#include "panels/function_panel.h"
+#include "panels/codeview_panel.h"
+#include "panels/template_panel.h"
+#include "panels/about_panel.h"
+#include "rsrcs/pixmaps.h"
+#include "tools/autodoc.h"
+#include "widgets/Node_Browser.h"
+
+#include <FL/Fl.H>
+#include <FL/fl_ask.H>
+#ifdef __APPLE__
+#include <FL/platform.H> // for fl_open_callback
+#endif
+#include <FL/Fl_Help_Dialog.H>
+#include <FL/Fl_Menu_Bar.H>
+#include <FL/Fl_PNG_Image.H>
+#include <FL/Fl_Native_File_Chooser.H>
+#include <FL/Fl_Printer.H>
+#include <FL/fl_string_functions.h>
+
+#include <locale.h> // setlocale()..
+#include "../src/flstring.h"
+
+
+fld::Application Fluid;
+
+using namespace fld;
+
+
+/**
+ Timer to watch for external editor modifications.
+
+ If one or more external editors open, check if their files were modified.
+ If so: reload to ram, update size/mtime records, and change fluid's
+ 'modified' state.
+ */
+static void external_editor_timer(void*) {
+ int editors_open = ExternalCodeEditor::editors_open();
+ if ( Fluid.debug_external_editor ) printf("--- TIMER --- External editors open=%d\n", editors_open);
+ if ( editors_open > 0 ) {
+ // Walk tree looking for files modified by external editors.
+ int modified = 0;
+ for (Node *p: Fluid.proj.tree.all_nodes()) {
+ if ( p->is_a(Type::Code) ) {
+ Code_Node *code = static_cast<Code_Node*>(p);
+ // Code changed by external editor?
+ if ( code->handle_editor_changes() ) { // updates ram, file size/mtime
+ modified++;
+ }
+ if ( code->is_editing() ) { // editor open?
+ code->reap_editor(); // Try to reap; maybe it recently closed
+ }
+ }
+ }
+ if ( modified ) Fluid.proj.set_modflag(1);
+ }
+ // Repeat timeout if editors still open
+ // The ExternalCodeEditor class handles start/stopping timer, we just
+ // repeat_timeout() if it's already on. NOTE: above code may have reaped
+ // only open editor, which would disable further timeouts. So *recheck*
+ // if editors still open, to ensure we don't accidentally re-enable them.
+ //
+ if ( ExternalCodeEditor::editors_open() ) {
+ Fl::repeat_timeout(2.0, external_editor_timer);
+ }
+}
+
+
+/**
+ Create the Fluid application.
+ This creates the basic app with an empty project and reads the Fluid
+ preferences database.
+ */
+Application::Application()
+: preferences( Fl_Preferences::USER_L, "fltk.org", "fluid" )
+{ }
+
+
+/**
+ Start Fluid.
+
+ Fluid can run in interactive mode with a full user interface to design new
+ user interfaces and write the C++ files to manage them,
+
+ Fluid can run form the command line in batch mode to convert .fl design files
+ into C++ source and header files. In batch mode, no display is needed,
+ particularly no X11 connection will be attempted on Linux/Unix.
+
+ \param[in] argc number of arguments in the list
+ \param[in] argv pointer to an array of arguments
+ \return in batch mode, an error code will be returned via \c exit() . This
+ function return 1, if there was an error in the parameters list.
+ \todo On Windows, Fluid can under certain conditions open a dialog box, even
+ in batch mode. Is that intentional? Does it circumvent issues with Windows'
+ stderr and stdout?
+ */
+int Application::run(int argc,char **argv) {
+ setlocale(LC_ALL, ""); // enable multi-language errors in file chooser
+ setlocale(LC_NUMERIC, "C"); // make sure numeric values are written correctly
+ launch_path_ = end_with_slash(fl_getcwd_str()); // store the current path at launch
+
+ int i = 1;
+ if ((i = args.load(argc, argv)) == -1)
+ return 1;
+
+ if (args.show_version) {
+ printf("fluid v%d.%d.%d\n", FL_MAJOR_VERSION, FL_MINOR_VERSION, FL_PATCH_VERSION);
+ ::exit(0);
+ }
+
+ const char *c = nullptr;
+ if (args.autodoc_path.empty())
+ c = argv[i];
+
+ fl_register_images();
+
+ make_main_window();
+
+ if (c) proj.set_filename(c);
+ if (!batch_mode) {
+#ifdef __APPLE__
+ fl_open_callback(apple_open_cb);
+#endif // __APPLE__
+ Fl::visual((Fl_Mode)(FL_DOUBLE|FL_INDEX));
+ Fl_File_Icon::load_system_icons();
+ main_window->callback(exit_cb);
+ position_window(main_window,"main_window_pos", 1, 10, 30, WINWIDTH, WINHEIGHT );
+ if (g_shell_config) {
+ g_shell_config->read(preferences, fld::Tool_Store::USER);
+ g_shell_config->update_settings_dialog();
+ g_shell_config->rebuild_shell_menu();
+ }
+ Fluid.layout_list.read(preferences, fld::Tool_Store::USER);
+ main_window->show(argc,argv);
+ toggle_widget_bin();
+ toggle_codeview_cb(nullptr,nullptr);
+ if (!c && openlast_button->value() && history.abspath[0][0] && args.autodoc_path.empty()) {
+ // Open previous file when no file specified...
+ open_project_file(history.abspath[0]);
+ }
+ }
+ proj.undo.suspend();
+ if (c && !fld::io::read_file(proj, c,0)) {
+ if (batch_mode) {
+ fprintf(stderr,"%s : %s\n", c, strerror(errno));
+ exit(1);
+ }
+ fl_message("Can't read %s: %s", c, strerror(errno));
+ }
+ proj.undo.resume();
+
+ // command line args override code and header filenames from the project file
+ // in batch mode only
+ if (batch_mode) {
+ if (!args.code_filename.empty()) {
+ proj.code_file_set = 1;
+ proj.code_file_name = args.code_filename;
+ }
+ if (!args.header_filename.empty()) {
+ proj.header_file_set = 1;
+ proj.header_file_name = args.header_filename;
+ }
+ }
+
+ if (args.update_file) { // fluid -u
+ fld::io::write_file(proj, c, 0);
+ if (!args.compile_file)
+ exit(0);
+ }
+
+ if (args.compile_file) { // fluid -c[s]
+ if (args.compile_strings)
+ proj.write_strings();
+ write_code_files();
+ exit(0);
+ }
+
+ // don't lock up if inconsistent command line arguments were given
+ if (batch_mode)
+ exit(0);
+
+ proj.set_modflag(0);
+ proj.undo.clear();
+
+ // Set (but do not start) timer callback for external editor updates
+ ExternalCodeEditor::set_update_timer_callback(external_editor_timer);
+
+#ifndef NDEBUG
+ // check if the user wants FLUID to generate image for the user documentation
+ if (!args.autodoc_path.empty()) {
+ run_autodoc(args.autodoc_path);
+ proj.set_modflag(0, 0);
+ quit();
+ return 0;
+ }
+#endif
+
+ Fl::run();
+
+ proj.undo.clear();
+ return 0;
+}
+
+
+/**
+ Exit Fluid; we hope you had a nice experience.
+ If the design was modified, a dialog will ask for confirmation.
+ */
+void Application::quit() {
+ if (shell_command_running()) {
+ int choice = fl_choice("Previous shell command still running!",
+ "Cancel",
+ "Exit",
+ nullptr);
+ if (choice == 0) { // user chose to cancel the exit operation
+ return;
+ }
+ }
+
+ flush_text_widgets();
+
+ // verify user intention
+ if (confirm_project_clear() == false)
+ return;
+
+ // Stop any external editor update timers
+ ExternalCodeEditor::stop_update_timer();
+
+ save_position(main_window,"main_window_pos");
+
+ if (widgetbin_panel) {
+ save_position(widgetbin_panel,"widgetbin_pos");
+ delete widgetbin_panel;
+ }
+ if (codeview_panel) {
+ Fl_Preferences svp(preferences, "codeview");
+ svp.set("autorefresh", cv_autorefresh->value());
+ svp.set("autoposition", cv_autoposition->value());
+ svp.set("tab", cv_tab->find(cv_tab->value()));
+ svp.set("code_choice", cv_code_choice);
+ save_position(codeview_panel,"codeview_pos");
+ delete codeview_panel;
+ codeview_panel = nullptr;
+ }
+ if (shell_run_window) {
+ save_position(shell_run_window,"shell_run_Window_pos");
+ }
+
+ if (about_panel)
+ delete about_panel;
+ if (help_dialog)
+ delete help_dialog;
+
+ if (g_shell_config)
+ g_shell_config->write(preferences, fld::Tool_Store::USER);
+ Fluid.layout_list.write(preferences, fld::Tool_Store::USER);
+
+ proj.undo.clear();
+
+ // Destroy tree
+ // Doing so causes dtors to automatically close all external editors
+ // and cleans up editor tmp files. Then remove fluid tmpdir /last/.
+ proj.reset();
+ ExternalCodeEditor::tmpdir_clear();
+ delete_tmpdir();
+
+ exit(0);
+}
+
+
+/**
+ Return the working directory path at application launch.
+ \return a reference to the '/' terminated path.
+ */
+const std::string &Application::launch_path() const {
+ return launch_path_;
+}
+
+
+/**
+ Generate a path to a directory for temporary data storage.
+ \see delete_tmpdir(), get_tmpdir()
+ \todo remove duplicate API or reuse ExternalCodeEditor::create_tmpdir()!
+ */
+void Application::create_tmpdir() {
+ if (tmpdir_create_called)
+ return;
+ tmpdir_create_called = true;
+
+ char buf[128];
+#if _WIN32
+ // The usual temp file locations on Windows are
+ // %system%\Windows\Temp
+ // %userprofiles%\AppData\Local
+ // usually resolving into
+ // C:/Windows/Temp/
+ // C:\Users\<username>\AppData\Local\Temp
+ fl_snprintf(buf, sizeof(buf)-1, "fluid-%d/", (long)GetCurrentProcessId());
+ std::string name = buf;
+ wchar_t tempdirW[FL_PATH_MAX+1];
+ char tempdir[FL_PATH_MAX+1];
+ unsigned len = GetTempPathW(FL_PATH_MAX, tempdirW);
+ if (len == 0) {
+ strcpy(tempdir, "c:/windows/temp/");
+ } else {
+ unsigned wn = fl_utf8fromwc(tempdir, FL_PATH_MAX, tempdirW, len);
+ tempdir[wn] = 0;
+ }
+ std::string path = tempdir;
+ end_with_slash(path);
+ path += name;
+ fl_make_path(path.c_str());
+ if (fl_access(path.c_str(), 6) == 0) tmpdir_path = path;
+#else
+ fl_snprintf(buf, sizeof(buf)-1, "fluid-%d/", getpid());
+ std::string name = buf;
+ auto path_temp = fl_getenv("TMPDIR");
+ std::string path = path_temp ? path_temp : "";
+ if (!path.empty()) {
+ end_with_slash(path);
+ path += name;
+ fl_make_path(path.c_str());
+ if (fl_access(path.c_str(), 6) == 0) tmpdir_path = path;
+ }
+ if (tmpdir_path.empty()) {
+ path = std::string("/tmp/") + name;
+ fl_make_path(path.c_str());
+ if (fl_access(path.c_str(), 6) == 0) tmpdir_path = path;
+ }
+#endif
+ if (tmpdir_path.empty()) {
+ char pbuf[FL_PATH_MAX+1];
+ preferences.get_userdata_path(pbuf, FL_PATH_MAX);
+ path = std::string(pbuf);
+ end_with_slash(path);
+ path += name;
+ fl_make_path(path.c_str());
+ if (fl_access(path.c_str(), 6) == 0) tmpdir_path = path;
+ }
+ if (tmpdir_path.empty()) {
+ if (batch_mode) {
+ fprintf(stderr, "ERROR: Can't create directory for temporary data storage.\n");
+ } else {
+ fl_alert("Can't create directory for temporary data storage.");
+ }
+ }
+}
+
+
+/**
+ Delete the temporary directory and all its contents.
+ \see create_tmpdir(), get_tmpdir()
+ */
+void Application::delete_tmpdir() {
+ // was a temporary directory created
+ if (!tmpdir_create_called)
+ return;
+ if (tmpdir_path.empty())
+ return;
+
+ // first delete all files that may still be left in the temp directory
+ struct dirent **de;
+ int n_de = fl_filename_list(tmpdir_path.c_str(), &de);
+ if (n_de >= 0) {
+ for (int i=0; i<n_de; i++) {
+ std::string path = tmpdir_path + de[i]->d_name;
+ fl_unlink(path.c_str());
+ }
+ fl_filename_free_list(&de, n_de);
+ }
+
+ // then delete the directory itself
+ if (fl_rmdir(tmpdir_path.c_str()) < 0) {
+ if (batch_mode) {
+ fprintf(stderr, "WARNING: Can't delete tmpdir '%s': %s", tmpdir_path.c_str(), strerror(errno));
+ } else {
+ fl_alert("WARNING: Can't delete tmpdir '%s': %s", tmpdir_path.c_str(), strerror(errno));
+ }
+ }
+}
+
+
+/**
+ Return the path to a temporary directory for this instance of Fluid.
+ Fluid will do its best to clear and delete this directory when exiting.
+ \return the path to the temporary directory, ending in a '/', or and empty
+ string if no directory could be created.
+ */
+const std::string &Application::get_tmpdir() {
+ if (!tmpdir_create_called)
+ create_tmpdir();
+ return tmpdir_path;
+}
+
+
+/**
+ Return the path and filename of a temporary file for cut or duplicated data.
+ \param[in] which 0 gets the cut/copy/paste buffer, 1 gets the duplication buffer
+ \return a pointer to a string in a static buffer
+ */
+const char *Application::cutfname(int which) {
+ static char name[2][FL_PATH_MAX];
+ static char beenhere = 0;
+
+ if (!beenhere) {
+ beenhere = 1;
+ preferences.getUserdataPath(name[0], sizeof(name[0]));
+ strlcat(name[0], "cut_buffer", sizeof(name[0]));
+ preferences.getUserdataPath(name[1], sizeof(name[1]));
+ strlcat(name[1], "dup_buffer", sizeof(name[1]));
+ }
+
+ return name[which];
+}
+
+
+/**
+ Clear the current project and create a new, empty one.
+
+ If the current project was modified, FLUID will give the user the opportunity
+ to save the old project first.
+
+ \param[in] user_must_confirm if set, a confimation dialog is presented to the
+ user before resetting the project. Default is `true`.
+ \return false if the operation was canceled
+ */
+bool Application::new_project(bool user_must_confirm) {
+ // verify user intention
+ if ((user_must_confirm) && (confirm_project_clear() == false))
+ return false;
+
+ // clear the current project
+ proj.reset();
+ proj.set_filename(nullptr);
+ proj.set_modflag(0, 0);
+ widget_browser->rebuild();
+ proj.update_settings_dialog();
+
+ // all is clear to continue
+ return true;
+}
+
+
+/**
+ Open a file chooser and load an exiting project file.
+
+ If the current project was modified, FLUID will give the user the opportunity
+ to save the old project first.
+
+ If no filename is given, FLUID will open a file chooser dialog.
+
+ \param[in] filename_arg load from this file, or show file chooser if empty
+ \return false if the operation was canceled or failed otherwise
+ */
+bool Application::open_project_file(const std::string &filename_arg) {
+ // verify user intention
+ if (confirm_project_clear() == false)
+ return false;
+
+ // ask for a filename if none was given
+ std::string new_filename = filename_arg;
+ if (new_filename.empty()) {
+ new_filename = open_project_filechooser("Open Project File");
+ if (new_filename.empty()) {
+ return false;
+ }
+ }
+
+ // clear the project and merge a file by the given name
+ new_project(false);
+ return merge_project_file(new_filename);
+}
+
+
+/**
+ Load a project from the give file name and path.
+
+ The project file is inserted at the currently selected type.
+
+ If no filename is given, FLUID will open a file chooser dialog.
+
+ \param[in] filename_arg path and name of the new project file
+ \return false if the operation failed
+ */
+bool Application::merge_project_file(const std::string &filename_arg) {
+ bool is_a_merge = (!proj.tree.empty());
+ std::string title = is_a_merge ? "Merge Project File" : "Open Project File";
+
+ // ask for a filename if none was given
+ std::string new_filename = filename_arg;
+ if (new_filename.empty()) {
+ new_filename = open_project_filechooser(title);
+ if (new_filename.empty()) {
+ return false;
+ }
+ }
+
+ const char *c = new_filename.c_str();
+ const char *oldfilename = proj.proj_filename;
+ proj.proj_filename = nullptr;
+ proj.set_filename(c);
+ if (is_a_merge) proj.undo.checkpoint();
+ proj.undo.suspend();
+ if (!fld::io::read_file(proj, c, is_a_merge)) {
+ proj.undo.resume();
+ widget_browser->rebuild();
+ proj.update_settings_dialog();
+ fl_message("Can't read %s: %s", c, strerror(errno));
+ free((void *)proj.proj_filename);
+ proj.proj_filename = oldfilename;
+ if (main_window) proj.set_modflag(proj.modflag);
+ return false;
+ }
+ proj.undo.resume();
+ widget_browser->rebuild();
+ if (is_a_merge) {
+ // Inserting a file; restore the original filename...
+ proj.set_filename(oldfilename);
+ proj.set_modflag(1);
+ } else {
+ // Loaded a file; free the old filename...
+ proj.set_modflag(0, 0);
+ proj.undo.clear();
+ }
+ if (oldfilename) free((void *)oldfilename);
+ return true;
+}
+
+
+/**
+ Save the current design to the file given by \c filename.
+ If automatic, this overwrites an existing file. If interactive, if will
+ verify with the user.
+ \param[in] v if v is not nullptr, or no filename is set, open a filechooser.
+ */
+void Application::save_project_file(void *v) {
+ flush_text_widgets();
+ Fl_Native_File_Chooser fnfc;
+ const char *c = proj.proj_filename;
+ if (v || !c || !*c) {
+ fnfc.title("Save To:");
+ fnfc.type(Fl_Native_File_Chooser::BROWSE_SAVE_FILE);
+ fnfc.filter("FLUID Files\t*.f[ld]");
+ if (fnfc.show() != 0) return;
+ c = fnfc.filename();
+ if (!fl_access(c, 0)) {
+ std::string basename = fl_filename_name_str(std::string(c));
+ if (fl_choice("The file \"%s\" already exists.\n"
+ "Do you want to replace it?", "Cancel",
+ "Replace", nullptr, basename.c_str()) == 0) return;
+ }
+
+ if (v != (void *)2) proj.set_filename(c);
+ }
+ if (!fld::io::write_file(proj, c)) {
+ fl_alert("Error writing %s: %s", c, strerror(errno));
+ return;
+ }
+
+ if (v != (void *)2) {
+ proj.set_modflag(0, 1);
+ proj.undo.save_ = proj.undo.current_;
+ }
+}
+
+
+/**
+ Reload the file set by \c filename, replacing the current design.
+ If the design was modified, a dialog will ask for confirmation.
+ */
+void Application::revert_project() {
+ if ( proj.modflag) {
+ if (!fl_choice("This user interface has been changed. Really revert?",
+ "Cancel", "Revert", nullptr)) return;
+ }
+ proj.undo.suspend();
+ if (!fld::io::read_file(proj, proj.proj_filename, 0)) {
+ proj.undo.resume();
+ widget_browser->rebuild();
+ proj.update_settings_dialog();
+ fl_message("Can't read %s: %s", proj.proj_filename, strerror(errno));
+ return;
+ }
+ widget_browser->rebuild();
+ proj.undo.resume();
+ proj.set_modflag(0, 0);
+ proj.undo.clear();
+ proj.update_settings_dialog();
+}
+
+
+/**
+ Open the template browser and load a new file from templates.
+
+ If the current project was modified, FLUID will give the user the opportunity
+ to save the old project first.
+
+ \return false if the operation was canceled or failed otherwise
+ */
+bool Application::new_project_from_template() {
+ // clear the current project first
+ if (new_project() == false)
+ return false;
+
+ // Setup the template panel...
+ if (!template_panel) make_template_panel();
+
+ template_clear();
+ template_browser->add("Blank");
+ template_load();
+
+ template_name->hide();
+ template_name->value("");
+
+ template_instance->show();
+ template_instance->deactivate();
+ template_instance->value("");
+
+ template_delete->show();
+
+ template_submit->label("New");
+ template_submit->deactivate();
+
+ template_panel->label("New");
+
+ //if ( template_browser->size() == 1 ) { // only one item?
+ template_browser->value(1); // select it
+ template_browser->do_callback();
+ //}
+
+ // Show the panel and wait for the user to do something...
+ template_panel->show();
+ while (template_panel->shown()) Fl::wait();
+
+ // See if the user chose anything...
+ int item = template_browser->value();
+ if (item < 1) return false;
+
+ // Load the template, if any...
+ const char *tname = (const char *)template_browser->data(item);
+
+ if (tname) {
+ // Grab the instance name...
+ const char *iname = template_instance->value();
+
+ if (iname && *iname) {
+ // Copy the template to a temp file, then read it in...
+ char line[1024], *ptr, *next;
+ FILE *infile, *outfile;
+
+ if ((infile = fl_fopen(tname, "rb")) == nullptr) {
+ fl_alert("Error reading template file \"%s\":\n%s", tname,
+ strerror(errno));
+ proj.set_modflag(0);
+ proj.undo.clear();
+ return false;
+ }
+
+ if ((outfile = fl_fopen(cutfname(1), "wb")) == nullptr) {
+ fl_alert("Error writing buffer file \"%s\":\n%s", cutfname(1),
+ strerror(errno));
+ fclose(infile);
+ proj.set_modflag(0);
+ proj.undo.clear();
+ return false;
+ }
+
+ while (fgets(line, sizeof(line), infile)) {
+ // Replace @INSTANCE@ with the instance name...
+ for (ptr = line; (next = strstr(ptr, "@INSTANCE@")) != nullptr; ptr = next + 10) {
+ fwrite(ptr, next - ptr, 1, outfile);
+ fputs(iname, outfile);
+ }
+
+ fputs(ptr, outfile);
+ }
+
+ fclose(infile);
+ fclose(outfile);
+
+ proj.undo.suspend();
+ fld::io::read_file(proj, cutfname(1), 0);
+ fl_unlink(cutfname(1));
+ proj.undo.resume();
+ } else {
+ // No instance name, so read the template without replacements...
+ proj.undo.suspend();
+ fld::io::read_file(proj, tname, 0);
+ proj.undo.resume();
+ }
+ }
+
+ widget_browser->rebuild();
+ proj.update_settings_dialog();
+ proj.set_modflag(0);
+ proj.undo.clear();
+
+ return true;
+}
+
+
+/**
+ Open the dialog to allow the user to print the current window.
+ */
+void Application::print_snapshots() {
+ int w, h, ww, hh;
+ int frompage, topage;
+ int num_windows = 0; // Number of windows
+ Window_Node *windows[1000]; // Windows to print
+ int winpage; // Current window page
+ Fl_Window *win;
+
+ for (auto w: proj.tree.all_widgets()) {
+ if (w->is_a(Type::Window)) {
+ Window_Node *win_t = static_cast<Window_Node*>(w);
+ windows[num_windows] = win_t;
+ Fl_Window *win = static_cast<Fl_Window*>(win_t->o);
+ if (!win->shown()) continue;
+ num_windows ++;
+ }
+ }
+
+ Fl_Printer printjob;
+ if ( printjob.start_job(num_windows, &frompage, &topage) ) return;
+ int pagecount = 0;
+ for (winpage = 0; winpage < num_windows; winpage++) {
+ float scale = 1, scale_x = 1, scale_y = 1;
+ if (winpage+1 < frompage || winpage+1 > topage) continue;
+ printjob.start_page();
+ printjob.printable_rect(&w, &h);
+
+ // Get the time and date...
+ time_t curtime = time(nullptr);
+ struct tm *curdate = localtime(&curtime);
+ char date[1024];
+ strftime(date, sizeof(date), "%c", curdate);
+ fl_font(FL_HELVETICA, 12);
+ fl_color(FL_BLACK);
+ fl_draw(date, (w - (int)fl_width(date))/2, fl_height());
+ sprintf(date, "%d/%d", ++pagecount, topage-frompage+1);
+ fl_draw(date, w - (int)fl_width(date), fl_height());
+
+ // Get the base filename...
+ std::string basename = fl_filename_name_str(std::string(proj.proj_filename));
+ fl_draw(basename.c_str(), 0, fl_height());
+
+ // print centered and scaled to fit in the page
+ win = (Fl_Window*)windows[winpage]->o;
+ ww = win->decorated_w();
+ if(ww > w) scale_x = float(w)/ww;
+ hh = win->decorated_h();
+ if(hh > h) scale_y = float(h)/hh;
+ if (scale_x < scale) scale = scale_x;
+ if (scale_y < scale) scale = scale_y;
+ if (scale < 1) {
+ printjob.scale(scale);
+ printjob.printable_rect(&w, &h);
+ }
+ printjob.origin(w/2, h/2);
+ printjob.print_window(win, -ww/2, -hh/2);
+ printjob.end_page();
+ }
+ printjob.end_job();
+}
+
+
+/**
+ Generate the C++ source and header filenames and write those files.
+
+ This function creates the source filename by setting the file
+ extension to \c code_file_name and a header filename
+ with the extension \c header_file_name which are both
+ settable by the user.
+
+ If the code filename has not been set yet, a "save file as" dialog will be
+ presented to the user.
+
+ In batch_mode, the function will either be silent, or, if opening or writing
+ the files fails, write an error message to \c stderr and exit with exit code 1.
+
+ In interactive mode, it will pop up an error message, or, if the user
+ hasn't disabled that, pop up a confirmation message.
+
+ \param[in] dont_show_completion_dialog don't show the completion dialog
+ \return 1 if the operation failed, 0 if it succeeded
+ */
+int Application::write_code_files(bool dont_show_completion_dialog)
+{
+ // -- handle user interface issues
+ flush_text_widgets();
+ if (!proj.proj_filename) {
+ save_project_file(nullptr);
+ if (!proj.proj_filename) return 1;
+ }
+
+ // -- generate the file names with absolute paths
+ fld::io::Code_Writer f(proj);
+ std::string code_filename = proj.codefile_path() + proj.codefile_name();
+ std::string header_filename = proj.headerfile_path() + proj.headerfile_name();
+
+ // -- write the code and header files
+ if (!batch_mode) proj.enter_project_dir();
+ int x = f.write_code(code_filename.c_str(), header_filename.c_str());
+ std::string code_filename_rel = fl_filename_relative_str(code_filename);
+ std::string header_filename_rel = fl_filename_relative_str(header_filename);
+ if (!batch_mode) proj.leave_project_dir();
+
+ // -- print error message in batch mode or pop up an error or confirmation dialog box
+ if (batch_mode) {
+ if (!x) {
+ fprintf(stderr, "%s and %s: %s\n",
+ code_filename_rel.c_str(),
+ header_filename_rel.c_str(),
+ strerror(errno));
+ exit(1);
+ }
+ } else {
+ if (!x) {
+ fl_message("Can't write %s or %s: %s",
+ code_filename_rel.c_str(),
+ header_filename_rel.c_str(),
+ strerror(errno));
+ } else {
+ proj.set_modflag(-1, 0);
+ if (dont_show_completion_dialog==false && completion_button->value()) {
+ fl_message("Wrote %s and %s",
+ code_filename_rel.c_str(),
+ header_filename_rel.c_str());
+ }
+ }
+ }
+ return 0;
+}
+
+
+/**
+ User chose to cut the currently selected widgets.
+ */
+void Application::cut_selected() {
+ if (!proj.tree.current) {
+ fl_beep();
+ return;
+ }
+ flush_text_widgets();
+ if (!fld::io::write_file(proj, cutfname(),1)) {
+ fl_message("Can't write %s: %s", cutfname(), strerror(errno));
+ return;
+ }
+ proj.undo.checkpoint();
+ proj.set_modflag(1);
+ ipasteoffset = 0;
+ Node *p = proj.tree.current->parent;
+ while (p && p->selected) p = p->parent;
+ delete_all(1);
+ if (p) select_only(p);
+ widget_browser->rebuild();
+}
+
+
+/**
+ User chose to copy the currently selected widgets.
+ */
+void Application::copy_selected() {
+ flush_text_widgets();
+ if (!proj.tree.current) {
+ fl_beep();
+ return;
+ }
+ flush_text_widgets();
+ ipasteoffset = 10;
+ if (!fld::io::write_file(proj, cutfname(),1)) {
+ fl_message("Can't write %s: %s", cutfname(), strerror(errno));
+ return;
+ }
+}
+
+
+/**
+ User chose to paste the widgets from the cut buffer.
+
+ This function will paste the widgets in the cut buffer after the currently
+ selected widget. If the currently selected widget is a group widget and
+ it is not folded, the new widgets will be added inside the group.
+ */
+void Application::paste_from_clipboard() {
+ pasteoffset = ipasteoffset;
+ proj.undo.checkpoint();
+ proj.undo.suspend();
+ Strategy strategy = Strategy::FROM_FILE_AFTER_CURRENT;
+ if (proj.tree.current && proj.tree.current->can_have_children()) {
+ if (proj.tree.current->folded_ == 0) {
+ // If the current widget is a group widget and it is not folded,
+ // add the new widgets inside the group.
+ strategy = Strategy::FROM_FILE_AS_LAST_CHILD;
+ // The following alternative also works quite nicely
+ //strategy = Strategy::FROM_FILE_AS_FIRST_CHILD;
+ }
+ }
+ if (!fld::io::read_file(proj, cutfname(), 1, strategy)) {
+ widget_browser->rebuild();
+ fl_message("Can't read %s: %s", cutfname(), strerror(errno));
+ }
+ proj.undo.resume();
+ widget_browser->display(proj.tree.current);
+ widget_browser->rebuild();
+ pasteoffset = 0;
+ ipasteoffset += 10;
+}
+
+
+/**
+ Duplicate the selected widgets.
+
+ This code is a bit complex because it needs to find the last selected
+ widget with the lowest level, so that the new widgets are inserted after
+ this one.
+ */
+void Application::duplicate_selected() {
+ if (!proj.tree.current) {
+ fl_beep();
+ return;
+ }
+
+ // flush the text widgets to make sure the user's changes are saved:
+ flush_text_widgets();
+
+ // find the last selected node with the lowest level:
+ int lowest_level = 9999;
+ Node *new_insert = nullptr;
+ if (proj.tree.current->selected) {
+ for (auto t: proj.tree.all_selected_nodes()) {
+ if (t->level <= lowest_level) {
+ lowest_level = t->level;
+ new_insert = t;
+ }
+ }
+ }
+ if (new_insert)
+ proj.tree.current = new_insert;
+
+ // write the selected widgets to a file:
+ if (!fld::io::write_file(proj, cutfname(1),1)) {
+ fl_message("Can't write %s: %s", cutfname(1), strerror(errno));
+ return;
+ }
+
+ // read the file and add the widgets after the current one:
+ pasteoffset = 0;
+ proj.undo.checkpoint();
+ proj.undo.suspend();
+ if (!fld::io::read_file(proj, cutfname(1), 1, Strategy::FROM_FILE_AFTER_CURRENT)) {
+ fl_message("Can't read %s: %s", cutfname(1), strerror(errno));
+ }
+ fl_unlink(cutfname(1));
+ widget_browser->display(proj.tree.current);
+ widget_browser->rebuild();
+ proj.undo.resume();
+}
+
+
+/**
+ User chose to delete the currently selected widgets.
+ */
+void Application::delete_selected() {
+ if (!proj.tree.current) {
+ fl_beep();
+ return;
+ }
+ proj.undo.checkpoint();
+ proj.set_modflag(1);
+ ipasteoffset = 0;
+ Node *p = proj.tree.current->parent;
+ while (p && p->selected) p = p->parent;
+ delete_all(1);
+ if (p) select_only(p);
+ widget_browser->rebuild();
+}
+
+
+/**
+ Show the editor for the \c current Node.
+ */
+void Application::edit_selected() {
+ if (!proj.tree.current) {
+ fl_message("Please select a widget");
+ return;
+ }
+ proj.tree.current->open();
+}
+
+
+/**
+ User wants to sort selected widgets by y coordinate.
+ */
+void Application::sort_selected() {
+ proj.undo.checkpoint();
+ sort((Node*)nullptr);
+ widget_browser->rebuild();
+ proj.set_modflag(1);
+}
+
+
+/**
+ Show or hide the widget bin.
+ The state is stored in the app preferences.
+ */
+void Application::toggle_widget_bin() {
+ if (!widgetbin_panel) {
+ make_widgetbin();
+ if (!position_window(widgetbin_panel,"widgetbin_pos", 1, 320, 30)) return;
+ }
+
+ if (widgetbin_panel->visible()) {
+ widgetbin_panel->hide();
+ widgetbin_item->label("Show Widget &Bin...");
+ } else {
+ widgetbin_panel->show();
+ widgetbin_item->label("Hide Widget &Bin");
+ }
+}
+
+
+/**
+ Open a dialog to show the HTML help page form the FLTK documentation folder.
+ \param[in] name name of the HTML help file.
+ */
+void Application::show_help(const char *name) {
+ const char *docdir;
+ char helpname[FL_PATH_MAX];
+
+ if (!help_dialog) help_dialog = new Fl_Help_Dialog();
+
+ if ((docdir = fl_getenv("FLTK_DOCDIR")) == nullptr) {
+ docdir = FLTK_DOCDIR;
+ }
+ snprintf(helpname, sizeof(helpname), "%s/%s", docdir, name);
+
+ // make sure that we can read the file
+ FILE *f = fopen(helpname, "rb");
+ if (f) {
+ fclose(f);
+ help_dialog->load(helpname);
+ } else {
+ // if we can not read the file, we display the canned version instead
+ // or ask the native browser to open the page on www.fltk.org
+ if (strcmp(name, "fluid.html")==0) {
+ if (!Fl_Shared_Image::find("embedded:/fluid_flow_chart_800.png"))
+ new Fl_PNG_Image("embedded:/fluid_flow_chart_800.png", fluid_flow_chart_800_png, sizeof(fluid_flow_chart_800_png));
+ help_dialog->value
+ (
+ "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n"
+ "<html><head><title>FLTK: Programming with FLUID</title></head><body>\n"
+ "<h2>What is FLUID?</h2>\n"
+ "The Fast Light User Interface Designer, or FLUID, is a graphical editor "
+ "that is used to produce FLTK source code. FLUID edits and saves its state "
+ "in <code>.fl</code> files. These files are text, and you can (with care) "
+ "edit them in a text editor, perhaps to get some special effects.<p>\n"
+ "FLUID can \"compile\" the <code>.fl</code> file into a <code>.cxx</code> "
+ "and a <code>.h</code> file. The <code>.cxx</code> file defines all the "
+ "objects from the <code>.fl</code> file and the <code>.h</code> file "
+ "declares all the global ones. FLUID also supports localization "
+ "(Internationalization) of label strings using message files and the GNU "
+ "gettext or POSIX catgets interfaces.<p>\n"
+ "A simple program can be made by putting all your code (including a <code>"
+ "main()</code> function) into the <code>.fl</code> file and thus making the "
+ "<code>.cxx</code> file a single source file to compile. Most programs are "
+ "more complex than this, so you write other <code>.cxx</code> files that "
+ "call the FLUID functions. These <code>.cxx</code> files must <code>"
+ "#include</code> the <code>.h</code> file or they can <code>#include</code> "
+ "the <code>.cxx</code> file so it still appears to be a single source file.<p>"
+ "<img src=\"embedded:/fluid_flow_chart_800.png\"></p>"
+ "<p>More information is available online at <a href="
+ "\"https://www.fltk.org/doc-1.5/fluid.html\">https://www.fltk.org/</a>"
+ "</body></html>"
+ );
+ } else if (strcmp(name, "license.html")==0) {
+ fl_open_uri("https://www.fltk.org/doc-1.5/license.html");
+ return;
+ } else if (strcmp(name, "index.html")==0) {
+ fl_open_uri("https://www.fltk.org/doc-1.5/index.html");
+ return;
+ } else {
+ snprintf(helpname, sizeof(helpname), "https://www.fltk.org/%s", name);
+ fl_open_uri(helpname);
+ return;
+ }
+ }
+ help_dialog->show();
+}
+
+
+/**
+ Open the "About" dialog.
+ */
+void Application::about() {
+#if 1
+ if (!about_panel) make_about_panel();
+ about_panel->show();
+#else
+ for (auto &n: proj.tree.all_nodes()) {
+ puts(n.name());
+ }
+#endif
+}
+
+
+/**
+ Build the main app window and create a few other dialogs.
+ */
+void Application::make_main_window() {
+ if (!batch_mode) {
+ preferences.get("show_guides", show_guides, 1);
+ preferences.get("show_restricted", show_restricted, 1);
+ preferences.get("show_ghosted_outline", show_ghosted_outline, 0);
+ preferences.get("show_comments", show_comments, 1);
+ make_shell_window();
+ }
+
+ if (!main_window) {
+ Fl_Widget *o;
+ loadPixmaps();
+ main_window = new Fl_Double_Window(WINWIDTH,WINHEIGHT,"fluid");
+ main_window->box(FL_NO_BOX);
+ o = make_widget_browser(0,MENUHEIGHT,BROWSERWIDTH,BROWSERHEIGHT);
+ o->box(FL_FLAT_BOX);
+ o->tooltip("Double-click to view or change an item.");
+ main_window->resizable(o);
+ main_menubar = new Fl_Menu_Bar(0,0,BROWSERWIDTH,MENUHEIGHT);
+ main_menubar->menu(main_menu);
+ // quick access to all dynamic menu items
+ save_item = (Fl_Menu_Item*)main_menubar->find_item(menu_file_save_cb);
+ history_item = (Fl_Menu_Item*)main_menubar->find_item(menu_file_open_history_cb);
+ widgetbin_item = (Fl_Menu_Item*)main_menubar->find_item(toggle_widgetbin_cb);
+ codeview_item = (Fl_Menu_Item*)main_menubar->find_item((Fl_Callback*)toggle_codeview_cb);
+ overlay_item = (Fl_Menu_Item*)main_menubar->find_item((Fl_Callback*)toggle_overlays);
+ guides_item = (Fl_Menu_Item*)main_menubar->find_item((Fl_Callback*)toggle_guides);
+ restricted_item = (Fl_Menu_Item*)main_menubar->find_item((Fl_Callback*)toggle_restricted);
+ main_menubar->global();
+ fill_in_New_Menu();
+ main_window->end();
+ }
+
+ if (!batch_mode) {
+ history.load();
+ g_shell_config = new Fd_Shell_Command_List;
+ widget_browser->load_prefs();
+ make_settings_window();
+ }
+}
+
+
+/**
+ Open a native file chooser to allow choosing a project file for reading.
+
+ Path and filename are preset with the current project filename, if there
+ is one.
+
+ \param title a text describing the action after selecting a file (load, merge, ...)
+ \return the file path and name, or an empty string if the operation was canceled
+ */
+std::string Application::open_project_filechooser(const std::string &title) {
+ Fl_Native_File_Chooser dialog;
+ dialog.title(title.c_str());
+ dialog.type(Fl_Native_File_Chooser::BROWSE_FILE);
+ dialog.filter("FLUID Files\t*.f[ld]\n");
+ if (proj.proj_filename) {
+ std::string current_project_file = proj.proj_filename;
+ dialog.directory(fl_filename_path_str(current_project_file).c_str());
+ dialog.preset_file(fl_filename_name_str(current_project_file).c_str());
+ }
+ if (dialog.show() != 0)
+ return std::string();
+ return std::string(dialog.filename());
+}
+
+
+/**
+ Give the user the opportunity to save a project before clearing it.
+
+ If the project has unsaved changes, this function pops up a dialog, that
+ allows the user to save the project, continue without saving the project,
+ or to cancel the operation.
+
+ If the user chooses to save, and no filename was set, a file dialog allows
+ the user to pick a name and location, or to cancel the operation.
+
+ \return false if the user aborted the operation and the calling function
+ should abort as well
+ */
+bool Application::confirm_project_clear() {
+ if (proj.modflag == 0) return true;
+ switch (fl_choice("This project has unsaved changes. Do you want to save\n"
+ "the project file before proceeding?",
+ "Cancel", "Save", "Don't Save"))
+ {
+ case 0 : /* Cancel */
+ return false;
+ case 1 : /* Save */
+ save_project_file(nullptr);
+ if (proj.modflag) return false; // user canceled the "Save As" dialog
+ }
+ return true;
+}
+
+
+/**
+ Ensure that text widgets in the widget panel propagates apply current changes.
+ By temporarily clearing the text focus, all text widgets with changed text
+ will unfocus and call their respective callbacks, propagating those changes to
+ their data set.
+ */
+void Application::flush_text_widgets() {
+ if (Fl::focus() && (Fl::focus()->top_window() == the_panel)) {
+ Fl_Widget *old_focus = Fl::focus();
+ Fl::focus(nullptr); // trigger callback of the widget that is losing focus
+ Fl::focus(old_focus);
+ }
+}
+
+
+/**
+ Position the given window window based on entries in the app preferences.
+ Customisable by user; feature can be switched off.
+ The window is not shown or hidden by this function, but a value is returned
+ to indicate the state to the caller.
+ \param[in] w position this window
+ \param[in] prefsName name of the preferences item that stores the window settings
+ \param[in] Visible default value if window is hidden or shown
+ \param[in] X, Y, W, H default size and position if nothing is specified in the preferences
+ \return 1 if the caller should make the window visible, 0 if hidden.
+ */
+char Application::position_window(Fl_Window *w, const char *prefsName, int Visible, int X, int Y, int W, int H) {
+ Fl_Preferences pos(preferences, prefsName);
+ if (prevpos_button->value()) {
+ pos.get("x", X, X);
+ pos.get("y", Y, Y);
+ if ( W!=0 ) {
+ pos.get("w", W, W);
+ pos.get("h", H, H);
+ w->resize( X, Y, W, H );
+ }
+ else
+ w->position( X, Y );
+ }
+ pos.get("visible", Visible, Visible);
+ return Visible;
+}
+
+
+/**
+ Save the position and visibility state of a window to the app preferences.
+ \param[in] w save this window data
+ \param[in] prefsName name of the preferences item that stores the window settings
+ */
+void Application::save_position(Fl_Window *w, const char *prefsName) {
+ Fl_Preferences pos(preferences, prefsName);
+ pos.set("x", w->x());
+ pos.set("y", w->y());
+ pos.set("w", w->w());
+ pos.set("h", w->h());
+ pos.set("visible", (int)(w->shown() && w->visible()));
+}
+
+
+/**
+ Change the app's and hence preview the design's scheme.
+
+ The scheme setting is stored in the app preferences
+ - in key \p 'scheme_name' since 1.4.0
+ - in key \p 'scheme' (index: 0 - 4) in 1.3.x
+
+ This callback is triggered by changing the scheme in the
+ Fl_Scheme_Choice widget (\p Edit/GUI Settings).
+
+ \param[in] choice the calling widget
+
+ \see init_scheme() for choice values and backwards compatibility
+ */
+void Application::set_scheme(const char *new_scheme) {
+ if (batch_mode)
+ return;
+
+ // set the new scheme only if the scheme was changed
+ if (Fl::is_scheme(new_scheme))
+ return;
+
+ Fl::scheme(new_scheme);
+ preferences.set("scheme_name", new_scheme);
+
+ // Backwards compatibility: store 1.3 scheme index (1-4).
+ // We assume that index 0-3 (base, plastic, gtk+, gleam) are in the
+ // same order as in 1.3.x (index 1-4), higher values are ignored
+
+ int scheme_index = scheme_choice->value();
+ if (scheme_index <= 3) // max. index for 1.3.x (Gleam)
+ preferences.set("scheme", scheme_index + 1); // compensate for different indexing
+}
+
+
+/**
+ Read Fluid's scheme preferences and set the app's scheme.
+
+ Since FLTK 1.4.0 the scheme \b name is stored as a character string
+ with key "scheme_name" in the preference database.
+
+ In FLTK 1.3.x the scheme preference was stored as an integer index
+ with key "scheme" in the database. The known schemes were hardcoded in
+ Fluid's sources (here for reference):
+
+ | Index | 1.3 Scheme Name | Choice | 1.4 Scheme Name |
+ |-------|-----------------|-------|-----------------|
+ | 0 | Default (same as None) | n/a | n/a |
+ | 1 | None (same as Default) | 0 | base |
+ | 2 | Plastic | 1 | plastic |
+ | 3 | GTK+ | 2 | gtk+ |
+ | 4 | Gleam | 3 | gleam |
+ | n/a | n/a | 4 | oxy |
+
+ The new Fluid tries to keep backwards compatibility and reads both
+ keys (\p scheme and \p scheme_name). If the latter is defined, it is used.
+ If not the old \p scheme (index) is used - but we need to subtract one to
+ get the new Fl_Scheme_Choice index (column "Choice" above).
+ */
+void Application::init_scheme() {
+ int scheme_index = 0; // scheme index for backwards compatibility (1.3.x)
+ char *scheme_name = nullptr; // scheme name since 1.4.0
+ preferences.get("scheme_name", scheme_name, "XXX"); // XXX means: not set => fallback 1.3.x
+ if (!strcmp(scheme_name, "XXX")) {
+ preferences.get("scheme", scheme_index, 0);
+ if (scheme_index > 0) {
+ scheme_index--;
+ scheme_choice->value(scheme_index); // set the choice value
+ }
+ if (scheme_index < 0)
+ scheme_index = 0;
+ else if (scheme_index > scheme_choice->size() - 1)
+ scheme_index = 0;
+ scheme_name = const_cast<char *>(scheme_choice->text(scheme_index));
+ preferences.set("scheme_name", scheme_name);
+ }
+ // Set the new scheme only if it was not overridden by the -scheme
+ // command line option
+ if (Fl::scheme() == nullptr) {
+ Fl::scheme(scheme_name);
+ }
+ free(scheme_name);
+}
+
+
+#ifdef __APPLE__
+/**
+ Handle app launch with an associated filename (macOS only).
+ Should there be a modified design already, Fluid asks for user confirmation.
+ \param[in] c the filename of the new design
+ */
+void Application::apple_open_cb(const char *c) {
+ Fluid.open_project_file(std::string(c));
+}
+#endif // __APPLE__
+