diff options
Diffstat (limited to 'fluid/app')
| -rw-r--r-- | fluid/app/Fd_Snap_Action.cxx | 1816 | ||||
| -rw-r--r-- | fluid/app/Fd_Snap_Action.h | 193 | ||||
| -rw-r--r-- | fluid/app/Fluid_Image.cxx | 412 | ||||
| -rw-r--r-- | fluid/app/Fluid_Image.h | 60 | ||||
| -rw-r--r-- | fluid/app/align_widget.cxx | 414 | ||||
| -rw-r--r-- | fluid/app/align_widget.h | 24 | ||||
| -rw-r--r-- | fluid/app/fluid.cxx | 2339 | ||||
| -rw-r--r-- | fluid/app/fluid.h | 208 | ||||
| -rw-r--r-- | fluid/app/mergeback.cxx | 493 | ||||
| -rw-r--r-- | fluid/app/mergeback.h | 81 | ||||
| -rw-r--r-- | fluid/app/shell_command.cxx | 1005 | ||||
| -rw-r--r-- | fluid/app/shell_command.h | 148 | ||||
| -rw-r--r-- | fluid/app/undo.cxx | 263 | ||||
| -rw-r--r-- | fluid/app/undo.h | 37 |
14 files changed, 7493 insertions, 0 deletions
diff --git a/fluid/app/Fd_Snap_Action.cxx b/fluid/app/Fd_Snap_Action.cxx new file mode 100644 index 000000000..c093af1ff --- /dev/null +++ b/fluid/app/Fd_Snap_Action.cxx @@ -0,0 +1,1816 @@ +// +// Snap action code file for the Fast Light Tool Kit (FLTK). +// +// Copyright 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 +// + +#include "app/Fd_Snap_Action.h" + +#include "io/file.h" +#include "nodes/Fl_Group_Type.h" +#include "panels/settings_panel.h" + +#include <FL/fl_draw.H> +#include <FL/Fl_Menu_Bar.H> +#include <FL/fl_string_functions.h> +#include <math.h> +#include <string.h> +#include <assert.h> + +// TODO: warning if the user wants to change builtin layouts +// TODO: move panel to global settings panel (move load & save to main pulldown, or to toolbox?) +// INFO: how about a small tool box for quick preset selection and disabling of individual snaps? + +void select_layout_suite_cb(Fl_Widget *, void *user_data); + +int Fd_Snap_Action::eex = 0; +int Fd_Snap_Action::eey = 0; + +static Fd_Layout_Preset fltk_app = { + 15, 15, 15, 15, 0, 0, // window: l, r, t, b, gx, gy + 10, 10, 10, 10, 0, 0, // group: l, r, t, b, gx, gy + 25, 25, // tabs: t, b + 20, 10, 4, // widget_x: min, inc, gap + 20, 4, 8, // widget_y: min, inc, gap + 0, 14, -1, 14 // labelfont/size, textfont/size +}; +static Fd_Layout_Preset fltk_dlg = { + 10, 10, 10, 10, 0, 0, // window: l, r, t, b, gx, gy + 10, 10, 10, 10, 0, 0, // group: l, r, t, b, gx, gy + 20, 20, // tabs: t, b + 20, 10, 5, // widget_x: min, inc, gap + 20, 5, 5, // widget_y: min, inc, gap + 0, 11, -1, 11 // labelfont/size, textfont/size +}; +static Fd_Layout_Preset fltk_tool = { + 10, 10, 10, 10, 0, 0, // window: l, r, t, b, gx, gy + 10, 10, 10, 10, 0, 0, // group: l, r, t, b, gx, gy + 18, 18, // tabs: t, b + 16, 8, 2, // widget_x: min, inc, gap + 16, 4, 2, // widget_y: min, inc, gap + 0, 10, -1, 10 // labelfont/size, textfont/size +}; + +static Fd_Layout_Preset grid_app = { + 12, 12, 12, 12, 12, 12, // window: l, r, t, b, gx, gy + 12, 12, 12, 12, 12, 12, // group: l, r, t, b, gx, gy + 24, 24, // tabs: t, b + 12, 6, 6, // widget_x: min, inc, gap + 12, 6, 6, // widget_y: min, inc, gap + 0, 14, -1, 14 // labelfont/size, textfont/size +}; + +static Fd_Layout_Preset grid_dlg = { + 10, 10, 10, 10, 10, 10, // window: l, r, t, b, gx, gy + 10, 10, 10, 10, 10, 10, // group: l, r, t, b, gx, gy + 20, 20, // tabs: t, b + 10, 5, 5, // widget_x: min, inc, gap + 10, 5, 5, // widget_y: min, inc, gap + 0, 12, -1, 12 // labelfont/size, textfont/size +}; + +static Fd_Layout_Preset grid_tool = { + 8, 8, 8, 8, 8, 8, // window: l, r, t, b, gx, gy + 8, 8, 8, 8, 8, 8, // group: l, r, t, b, gx, gy + 16, 16, // tabs: t, b + 8, 4, 4, // widget_x: min, inc, gap + 8, 4, 4, // widget_y: min, inc, gap + 0, 10, -1, 10 // labelfont/size, textfont/size +}; + +static Fd_Layout_Suite static_suite_list[] = { + { (char*)"FLTK", (char*)"@fd_beaker FLTK", { &fltk_app, &fltk_dlg, &fltk_tool }, FD_STORE_INTERNAL }, + { (char*)"Grid", (char*)"@fd_beaker Grid", { &grid_app, &grid_dlg, &grid_tool }, FD_STORE_INTERNAL } +}; + +Fl_Menu_Item main_layout_submenu_[] = { + { static_suite_list[0].menu_label, 0, select_layout_suite_cb, (void*)0, FL_MENU_RADIO|FL_MENU_VALUE }, + { static_suite_list[1].menu_label, 0, select_layout_suite_cb, (void*)1, FL_MENU_RADIO }, + { NULL } +}; + +static Fl_Menu_Item static_choice_menu[] = { + { static_suite_list[0].menu_label }, + { static_suite_list[1].menu_label }, + { NULL } +}; + +Fd_Layout_Preset *layout = &fltk_app; +Fd_Layout_List g_layout_list; + +// ---- Callbacks ------------------------------------------------------ MARK: - + +void layout_suite_marker(Fl_Widget *, void *) { + // intentionally left empty +} + +void select_layout_suite_cb(Fl_Widget *, void *user_data) { + int index = (int)(fl_intptr_t)user_data; + assert(index >= 0); + assert(index < g_layout_list.list_size_); + g_layout_list.current_suite(index); + g_layout_list.update_dialogs(); +} + +void select_layout_preset_cb(Fl_Widget *, void *user_data) { + int index = (int)(fl_intptr_t)user_data; + assert(index >= 0); + assert(index < 3); + g_layout_list.current_preset(index); + g_layout_list.update_dialogs(); +} + +void edit_layout_preset_cb(Fl_Button *w, long user_data) { + int index = (int)w->argument(); + assert(index >= 0); + assert(index < 3); + if (user_data == (long)(fl_intptr_t)LOAD) { + w->value(g_layout_list.current_preset() == index); + } else { + g_layout_list.current_preset(index); + g_layout_list.update_dialogs(); + } +} + +// ---- Fd_Layout_Suite ------------------------------------------------ MARK: - + +/** + Write presets to a Preferences database. + */ +void Fd_Layout_Preset::write(Fl_Preferences &prefs) { + assert(this); + Fl_Preferences p_win(prefs, "Window"); + p_win.set("left_margin", left_window_margin); + p_win.set("right_margin", right_window_margin); + p_win.set("top_margin", top_window_margin); + p_win.set("bottom_margin", bottom_window_margin); + p_win.set("grid_x", window_grid_x); + p_win.set("grid_y", window_grid_y); + + Fl_Preferences p_grp(prefs, "Group"); + p_grp.set("left_margin", left_group_margin); + p_grp.set("right_margin", right_group_margin); + p_grp.set("top_margin", top_group_margin); + p_grp.set("bottom_margin", bottom_group_margin); + p_grp.set("grid_x", group_grid_x); + p_grp.set("grid_y", group_grid_y); + + Fl_Preferences p_tbs(prefs, "Tabs"); + p_tbs.set("top_margin", top_tabs_margin); + p_tbs.set("bottom_margin", bottom_tabs_margin); + + Fl_Preferences p_wgt(prefs, "Widget"); + p_wgt.set("min_w", widget_min_w); + p_wgt.set("inc_w", widget_inc_w); + p_wgt.set("gap_x", widget_gap_x); + p_wgt.set("min_h", widget_min_h); + p_wgt.set("inc_h", widget_inc_h); + p_wgt.set("gap_y", widget_gap_y); + + Fl_Preferences p_lyt(prefs, "Layout"); + p_lyt.set("labelfont", labelfont); + p_lyt.set("labelsize", labelsize); + p_lyt.set("textfont", textfont); + p_lyt.set("textsize", textsize); +} + +/** + Read presets from a Preferences database. + */ +void Fd_Layout_Preset::read(Fl_Preferences &prefs) { + assert(this); + Fl_Preferences p_win(prefs, "Window"); + p_win.get("left_margin", left_window_margin, 15); + p_win.get("right_margin", right_window_margin, 15); + p_win.get("top_margin", top_window_margin, 15); + p_win.get("bottom_margin", bottom_window_margin, 15); + p_win.get("grid_x", window_grid_x, 0); + p_win.get("grid_y", window_grid_y, 0); + + Fl_Preferences p_grp(prefs, "Group"); + p_grp.get("left_margin", left_group_margin, 10); + p_grp.get("right_margin", right_group_margin, 10); + p_grp.get("top_margin", top_group_margin, 10); + p_grp.get("bottom_margin", bottom_group_margin, 10); + p_grp.get("grid_x", group_grid_x, 0); + p_grp.get("grid_y", group_grid_y, 0); + + Fl_Preferences p_tbs(prefs, "Tabs"); + p_tbs.get("top_margin", top_tabs_margin, 25); + p_tbs.get("bottom_margin", bottom_tabs_margin, 25); + + Fl_Preferences p_wgt(prefs, "Widget"); + p_wgt.get("min_w", widget_min_w, 20); + p_wgt.get("inc_w", widget_inc_w, 10); + p_wgt.get("gap_x", widget_gap_x, 4); + p_wgt.get("min_h", widget_min_h, 20); + p_wgt.get("inc_h", widget_inc_h, 4); + p_wgt.get("gap_y", widget_gap_y, 8); + + Fl_Preferences p_lyt(prefs, "Layout"); + p_lyt.get("labelfont", labelfont, 0); + p_lyt.get("labelsize", labelsize, 14); + p_lyt.get("textfont", textfont, 0); + p_lyt.get("textsize", textsize, 14); +} + +/** + Write presets to an .fl project file. + */ +void Fd_Layout_Preset::write(Fd_Project_Writer *out) { + out->write_string(" preset { 1\n"); // preset format version + out->write_string(" %d %d %d %d %d %d\n", + left_window_margin, right_window_margin, + top_window_margin, bottom_window_margin, + window_grid_x, window_grid_y); + out->write_string(" %d %d %d %d %d %d\n", + left_group_margin, right_group_margin, + top_group_margin, bottom_group_margin, + group_grid_x, group_grid_y); + out->write_string(" %d %d\n", top_tabs_margin, bottom_tabs_margin); + out->write_string(" %d %d %d %d %d %d\n", + widget_min_w, widget_inc_w, widget_gap_x, + widget_min_h, widget_inc_h, widget_gap_y); + out->write_string(" %d %d %d %d\n", + labelfont, labelsize, textfont, textsize); + out->write_string(" }\n"); // preset format version +} + +/** + Read presets from an .fl project file. + */ +void Fd_Layout_Preset::read(Fd_Project_Reader *in) { + const char *key; + key = in->read_word(1); + if (key && !strcmp(key, "{")) { + for (;;) { + key = in->read_word(); + if (!key) return; + if (key[0] == '}') break; + int ver = atoi(key); + if (ver == 0) { + continue; + } else if (ver == 1) { + left_window_margin = in->read_int(); + right_window_margin = in->read_int(); + top_window_margin = in->read_int(); + bottom_window_margin = in->read_int(); + window_grid_x = in->read_int(); + window_grid_y = in->read_int(); + + left_group_margin = in->read_int(); + right_group_margin = in->read_int(); + top_group_margin = in->read_int(); + bottom_group_margin = in->read_int(); + group_grid_x = in->read_int(); + group_grid_y = in->read_int(); + + top_tabs_margin = in->read_int(); + bottom_tabs_margin = in->read_int(); + + widget_min_w = in->read_int(); + widget_inc_w = in->read_int(); + widget_gap_x = in->read_int(); + widget_min_h = in->read_int(); + widget_inc_h = in->read_int(); + widget_gap_y = in->read_int(); + + labelfont = in->read_int(); + labelsize = in->read_int(); + textfont = in->read_int(); + textsize = in->read_int(); + } else { // skip unknown chunks + for (;;) { + key = in->read_word(1); + if (key && (key[0] == '}')) + return; + } + } + } + } else { + // format error + } +} + +/** + Return the preferred text size, but make sure it's not 0. + */ +int Fd_Layout_Preset::textsize_not_null() { + // try the user selected text size + if (textsize > 0) return textsize; + // if the user did not set one, try the label size + if (labelsize > 0) return labelsize; + // if that doesn;t work, fall back to the default value + return 14; +} + + +// ---- Fd_Layout_Suite ------------------------------------------------ MARK: - + +/** + Write a presets suite to a Preferences database. + */ +void Fd_Layout_Suite::write(Fl_Preferences &prefs) { + assert(this); + assert(name_); + prefs.set("name", name_); + for (int i = 0; i < 3; ++i) { + Fl_Preferences prefs_preset(prefs, Fl_Preferences::Name(i)); + assert(layout[i]); + layout[i]->write(prefs_preset); + } +} + +/** + Read a presets suite from a Preferences database. + */ +void Fd_Layout_Suite::read(Fl_Preferences &prefs) { + assert(this); + for (int i = 0; i < 3; ++i) { + Fl_Preferences prefs_preset(prefs, Fl_Preferences::Name(i)); + assert(layout[i]); + layout[i]->read(prefs_preset); + } +} + +/** + Write a presets suite to an .fl project file. + */ +void Fd_Layout_Suite::write(Fd_Project_Writer *out) { + out->write_string(" suite {\n"); + out->write_string(" name "); out->write_word(name_); out->write_string("\n"); + for (int i = 0; i < 3; ++i) { + layout[i]->write(out); + } + out->write_string(" }\n"); +} + +/** + Read a presets suite from an .fl project file. + */ +void Fd_Layout_Suite::read(Fd_Project_Reader *in) { + const char *key; + key = in->read_word(1); + if (key && !strcmp(key, "{")) { + int ix = 0; + for (;;) { + key = in->read_word(); + if (!key) return; + if (!strcmp(key, "name")) { + name(in->read_word()); + } else if (!strcmp(key, "preset")) { + if (ix >= 3) return; // file format error + layout[ix++]->read(in); + } else if (!strcmp(key, "}")) { + break; + } else { + in->read_word(); // unknown key, ignore, hopefully a key-value pair + } + } + } else { + // file format error + } +} + +/** + \brief Update the menu_label to show a symbol representing the storage location. + Also updates the FLUID user interface. + */ +void Fd_Layout_Suite::update_label() { + std::string sym; + switch (storage_) { + case FD_STORE_INTERNAL: sym.assign("@fd_beaker "); break; + case FD_STORE_USER: sym.assign("@fd_user "); break; + case FD_STORE_PROJECT: sym.assign("@fd_project "); break; + case FD_STORE_FILE: sym.assign("@fd_file "); break; + } + sym.append(name_); + if (menu_label) + ::free(menu_label); + menu_label = fl_strdup(sym.c_str()); + g_layout_list.update_menu_labels(); +} + +/** + \brief Update the Suite name and the Suite menu_label. + Also updates the FLUID user interface. + */ +void Fd_Layout_Suite::name(const char *n) { + if (name_) + ::free(name_); + if (n) + name_ = fl_strdup(n); + else + name_ = NULL; + update_label(); +} + +/** + Initialize the class for first use. + */ +void Fd_Layout_Suite::init() { + name_ = NULL; + menu_label = NULL; + layout[0] = layout[1] = layout[2] = NULL; + storage_ = FD_STORE_INTERNAL; +} + +/** + Free all allocated resources. + */ +Fd_Layout_Suite::~Fd_Layout_Suite() { + if (storage_ == FD_STORE_INTERNAL) return; + if (name_) ::free(name_); + for (int i = 0; i < 3; ++i) { + delete layout[i]; + } +} + +// ---- Fd_Layout_List ------------------------------------------------- MARK: - + +/** + Draw a little FLUID beaker symbol. + */ +static void fd_beaker(Fl_Color c) { + fl_color(221); + fl_begin_polygon(); + fl_vertex(-0.6, 0.2); + fl_vertex(-0.9, 0.8); + fl_vertex(-0.8, 0.9); + fl_vertex( 0.8, 0.9); + fl_vertex( 0.9, 0.8); + fl_vertex( 0.6, 0.2); + fl_end_polygon(); + fl_color(c); + fl_begin_line(); + fl_vertex(-0.3, -0.9); + fl_vertex(-0.2, -0.8); + fl_vertex(-0.2, -0.2); + fl_vertex(-0.9, 0.8); + fl_vertex(-0.8, 0.9); + fl_vertex( 0.8, 0.9); + fl_vertex( 0.9, 0.8); + fl_vertex( 0.2, -0.2); + fl_vertex( 0.2, -0.8); + fl_vertex( 0.3, -0.9); + fl_end_line(); +} + +/** + Draw a user silhouette symbol + */ +static void fd_user(Fl_Color c) { + fl_color(245); + fl_begin_complex_polygon(); + fl_arc( 0.1, 0.9, 0.8, 0.0, 80.0); + fl_arc( 0.0, -0.5, 0.4, -65.0, 245.0); + fl_arc(-0.1, 0.9, 0.8, 100.0, 180.0); + fl_end_complex_polygon(); + fl_color(c); + fl_begin_line(); + fl_arc( 0.1, 0.9, 0.8, 0.0, 80.0); + fl_arc( 0.0, -0.5, 0.4, -65.0, 245.0); + fl_arc(-0.1, 0.9, 0.8, 100.0, 180.0); + fl_end_line(); +} + +/** + Draw a document symbol. + */ +static void fd_project(Fl_Color c) { + Fl_Color fc = FL_LIGHT2; + fl_color(fc); + fl_begin_complex_polygon(); + fl_vertex(-0.7, -1.0); + fl_vertex(0.1, -1.0); + fl_vertex(0.1, -0.4); + fl_vertex(0.7, -0.4); + fl_vertex(0.7, 1.0); + fl_vertex(-0.7, 1.0); + fl_end_complex_polygon(); + + fl_color(fl_lighter(fc)); + fl_begin_polygon(); + fl_vertex(0.1, -1.0); + fl_vertex(0.1, -0.4); + fl_vertex(0.7, -0.4); + fl_end_polygon(); + + fl_color(fl_darker(c)); + fl_begin_loop(); + fl_vertex(-0.7, -1.0); + fl_vertex(0.1, -1.0); + fl_vertex(0.1, -0.4); + fl_vertex(0.7, -0.4); + fl_vertex(0.7, 1.0); + fl_vertex(-0.7, 1.0); + fl_end_loop(); + + fl_begin_line(); + fl_vertex(0.1, -1.0); + fl_vertex(0.7, -0.4); + fl_end_line(); +} + +/** + Draw a 3 1/2" floppy symbol. + */ +void fd_file(Fl_Color c) { + Fl_Color fl = FL_LIGHT2; + Fl_Color fc = FL_DARK3; + fl_color(fc); + fl_begin_polygon(); // case + fl_vertex(-0.9, -1.0); + fl_vertex(0.9, -1.0); + fl_vertex(1.0, -0.9); + fl_vertex(1.0, 0.9); + fl_vertex(0.9, 1.0); + fl_vertex(-0.9, 1.0); + fl_vertex(-1.0, 0.9); + fl_vertex(-1.0, -0.9); + fl_end_polygon(); + + fl_color(fl_lighter(fl)); + fl_begin_polygon(); + fl_vertex(-0.7, -1.0); // slider + fl_vertex(0.7, -1.0); + fl_vertex(0.7, -0.4); + fl_vertex(-0.7, -0.4); + fl_end_polygon(); + + fl_begin_polygon(); // label + fl_vertex(-0.7, 0.0); + fl_vertex(0.7, 0.0); + fl_vertex(0.7, 1.0); + fl_vertex(-0.7, 1.0); + fl_end_polygon(); + + fl_color(fc); + fl_begin_polygon(); + fl_vertex(-0.5, -0.9); // slot + fl_vertex(-0.3, -0.9); + fl_vertex(-0.3, -0.5); + fl_vertex(-0.5, -0.5); + fl_end_polygon(); + + fl_color(fl_darker(c)); + fl_begin_loop(); + fl_vertex(-0.9, -1.0); + fl_vertex(0.9, -1.0); + fl_vertex(1.0, -0.9); + fl_vertex(1.0, 0.9); + fl_vertex(0.9, 1.0); + fl_vertex(-0.9, 1.0); + fl_vertex(-1.0, 0.9); + fl_vertex(-1.0, -0.9); + fl_end_loop(); +} + +/** + Instantiate the class that holds a list of all layouts and manages the UI. + */ +Fd_Layout_List::Fd_Layout_List() +: main_menu_(main_layout_submenu_), + choice_menu_(static_choice_menu), + list_(static_suite_list), + list_size_(2), + list_capacity_(2), + list_is_static_(true), + current_suite_(0), + current_preset_(0) +{ + fl_add_symbol("fd_beaker", fd_beaker, 1); + fl_add_symbol("fd_user", fd_user, 1); + fl_add_symbol("fd_project", fd_project, 1); + fl_add_symbol("fd_file", fd_file, 1); +} + +/** + Release allocated resources. + */ +Fd_Layout_List::~Fd_Layout_List() { + assert(this); + if (!list_is_static_) { + ::free(main_menu_); + ::free(choice_menu_); + for (int i = 0; i < list_size_; i++) { + Fd_Layout_Suite &suite = list_[i]; + if (suite.storage_ != FD_STORE_INTERNAL) + suite.~Fd_Layout_Suite(); + } + ::free(list_); + } +} + +/** + Update the Setting dialog and menus to reflect the current Layout selection state. + */ +void Fd_Layout_List::update_dialogs() { + static Fl_Menu_Item *preset_menu = NULL; + if (!preset_menu) { + preset_menu = (Fl_Menu_Item*)main_menubar->find_item(select_layout_preset_cb); + assert(preset_menu); + } + assert(this); + assert(current_suite_ >= 0 ); + assert(current_suite_ < list_size_); + assert(current_preset_ >= 0 ); + assert(current_preset_ < 3); + layout = list_[current_suite_].layout[current_preset_]; + assert(layout); + if (w_settings_layout_tab) { + w_settings_layout_tab->do_callback(w_settings_layout_tab, LOAD); + layout_choice->redraw(); + } + preset_menu[current_preset_].setonly(preset_menu); + main_menu_[current_suite_].setonly(main_menu_); +} + +/** + Refresh the label pointers for both pulldown menus. + */ +void Fd_Layout_List::update_menu_labels() { + for (int i=0; i<list_size_; i++) { + main_menu_[i].label(list_[i].menu_label); + choice_menu_[i].label(list_[i].menu_label); + } +} + +/** + Load all user layouts from the FLUID user preferences. + */ +int Fd_Layout_List::load(const std::string &filename) { + remove_all(FD_STORE_FILE); + Fl_Preferences prefs(filename.c_str(), "layout.fluid.fltk.org", NULL, Fl_Preferences::C_LOCALE); + read(prefs, FD_STORE_FILE); + return 0; +} + +/** + Save all user layouts to the FLUID user preferences. + */ +int Fd_Layout_List::save(const std::string &filename) { + assert(this); + Fl_Preferences prefs(filename.c_str(), "layout.fluid.fltk.org", NULL, (Fl_Preferences::Root)(Fl_Preferences::C_LOCALE|Fl_Preferences::CLEAR)); + prefs.clear(); + write(prefs, FD_STORE_FILE); + return 0; +} + +/** + Write Suite and Layout selection and selected layout data to Preferences database. + */ +void Fd_Layout_List::write(Fl_Preferences &prefs, Fd_Tool_Store storage) { + Fl_Preferences prefs_list(prefs, "Layouts"); + prefs_list.clear(); + prefs_list.set("current_suite", list_[current_suite()].name_); + prefs_list.set("current_preset", current_preset()); + int n = 0; + for (int i = 0; i < list_size_; ++i) { + Fd_Layout_Suite &suite = list_[i]; + if (suite.storage_ == storage) { + Fl_Preferences prefs_suite(prefs_list, Fl_Preferences::Name(n++)); + suite.write(prefs_suite); + } + } +} + +/** + Read Suite and Layout selection and selected layout data to Preferences database. + */ +void Fd_Layout_List::read(Fl_Preferences &prefs, Fd_Tool_Store storage) { + Fl_Preferences prefs_list(prefs, "Layouts"); + std::string cs; + int cp = 0; + preferences_get(prefs_list, "current_suite", cs, ""); + prefs_list.get("current_preset", cp, 0); + for (int i = 0; i < prefs_list.groups(); ++i) { + Fl_Preferences prefs_suite(prefs_list, Fl_Preferences::Name(i)); + char *new_name = NULL; + prefs_suite.get("name", new_name, NULL); + if (new_name) { + int n = add(new_name); + list_[n].read(prefs_suite); + list_[n].storage(storage); + ::free(new_name); + } + } + current_suite(cs); + current_preset(cp); + update_dialogs(); +} + +/** + Write Suite and Layout selection and project layout data to an .fl project file. + */ +void Fd_Layout_List::write(Fd_Project_Writer *out) { + // Don't write the Snap field if no custom layout was used + if ((current_suite()==0) && (current_preset()==0)) { + int nSuite = 0; + for (int i=0; i<list_size_; i++) { + if (list_[i].storage_ == FD_STORE_PROJECT) nSuite++; + } + if (nSuite == 0) return; + } + out->write_string("\nsnap {\n ver 1\n"); + out->write_string(" current_suite "); out->write_word(list_[current_suite()].name_); out->write_string("\n"); + out->write_string(" current_preset %d\n", current_preset()); + for (int i=0; i<list_size_; i++) { + Fd_Layout_Suite &suite = list_[i]; + if (suite.storage_ == FD_STORE_PROJECT) + suite.write(out); + } + out->write_string("}"); +} + +/** + Read Suite and Layout selection and project layout data from an .fl project file. + */ +void Fd_Layout_List::read(Fd_Project_Reader *in) { + const char *key; + key = in->read_word(1); + if (key && !strcmp(key, "{")) { + std::string cs; + int cp = 0; + for (;;) { + key = in->read_word(); + if (!key) return; + if (!strcmp(key, "ver")) { + in->read_int(); + } else if (!strcmp(key, "current_suite")) { + cs = in->read_word(); + } else if (!strcmp(key, "current_preset")) { + cp = in->read_int(); + } else if (!strcmp(key, "suite")) { + int n = add(in->filename_name()); + list_[n].read(in); + list_[n].storage(FD_STORE_PROJECT); + } else if (!strcmp(key, "}")) { + break; + } else { + in->read_word(); // unknown key, ignore, hopefully a key-value pair + } + } + current_suite(cs); + current_preset(cp); + update_dialogs(); + } else { + // old style "snap" is followed by an integer. Ignore. + } +} + +/** + Set the current Suite. + \param[in] ix index into list of suites + */ +void Fd_Layout_List::current_suite(int ix) { + assert(ix >= 0); + assert(ix < list_size_); + current_suite_ = ix; + layout = list_[current_suite_].layout[current_preset_]; +} + +/** + Set the current Suite. + \param[in] arg_name name of the selected suite + \return if no name is given or the name is not found, keep the current suite selected + */ +void Fd_Layout_List::current_suite(std::string arg_name) { + if (arg_name.empty()) return; + for (int i = 0; i < list_size_; ++i) { + Fd_Layout_Suite &suite = list_[i]; + if (suite.name_ && (strcmp(suite.name_, arg_name.c_str()) == 0)) { + current_suite(i); + break; + } + } +} + +/** + Select a Preset within the current Suite. + \param[in] ix 0 = application, 1 = dialog, 2 = toolbox + */ +void Fd_Layout_List::current_preset(int ix) { + assert(ix >= 0); + assert(ix < 3); + current_preset_ = ix; + layout = list_[current_suite_].layout[current_preset_]; +} + +/** + Allocate enough space for n entries in the list. + */ +void Fd_Layout_List::capacity(int n) { + static Fl_Menu_Item *suite_menu = NULL; + if (!suite_menu) + suite_menu = (Fl_Menu_Item*)main_menubar->find_item(layout_suite_marker); + + int old_n = list_size_; + int i; + + Fd_Layout_Suite *new_list = (Fd_Layout_Suite*)::calloc(n, sizeof(Fd_Layout_Suite)); + for (i = 0; i < old_n; i++) + new_list[i] = list_[i]; + if (!list_is_static_) ::free(list_); + list_ = new_list; + + Fl_Menu_Item *new_main_menu = (Fl_Menu_Item*)::calloc(n+1, sizeof(Fl_Menu_Item)); + for (i = 0; i < old_n; i++) + new_main_menu[i] = main_menu_[i]; + if (!list_is_static_) ::free(main_menu_); + main_menu_ = new_main_menu; + suite_menu->user_data(main_menu_); + + Fl_Menu_Item *new_choice_menu = (Fl_Menu_Item*)::calloc(n+1, sizeof(Fl_Menu_Item)); + for (i = 0; i < old_n; i++) + new_choice_menu[i] = choice_menu_[i]; + if (!list_is_static_) ::free(choice_menu_); + choice_menu_ = new_choice_menu; + if (layout_choice) layout_choice->menu(choice_menu_); + + list_capacity_ = n; + list_is_static_ = false; +} + +/** + \brief Clone the currently selected suite and append it to the list. + Selects the new layout and updates the UI. + */ +int Fd_Layout_List::add(const char *name) { + if (list_size_ == list_capacity_) { + capacity(list_capacity_ * 2); + } + int n = list_size_; + Fd_Layout_Suite &old_suite = list_[current_suite_]; + Fd_Layout_Suite &new_suite = list_[n]; + new_suite.init(); + new_suite.name(name); + for (int i=0; i<3; ++i) { + new_suite.layout[i] = new Fd_Layout_Preset; + ::memcpy(new_suite.layout[i], old_suite.layout[i], sizeof(Fd_Layout_Preset)); + } + Fd_Tool_Store new_storage = old_suite.storage_; + if (new_storage == FD_STORE_INTERNAL) + new_storage = FD_STORE_USER; + new_suite.storage(new_storage); + main_menu_[n].label(new_suite.menu_label); + main_menu_[n].callback(main_menu_[0].callback()); + main_menu_[n].argument(n); + main_menu_[n].flags = main_menu_[0].flags; + choice_menu_[n].label(new_suite.menu_label); + list_size_++; + current_suite(n); + return n; +} + +/** + Rename the current Suite. + */ +void Fd_Layout_List::rename(const char *name) { + int n = current_suite(); + list_[n].name(name); + main_menu_[n].label(list_[n].menu_label); + choice_menu_[n].label(list_[n].menu_label); +} + +/** + Remove the given suite. + \param[in] ix index into list of suites + */ +void Fd_Layout_List::remove(int ix) { + int tail = list_size_-ix-1; + if (tail) { + for (int i = ix; i < list_size_-1; i++) + list_[i] = list_[i+1]; + } + ::memmove(main_menu_+ix, main_menu_+ix+1, (tail+1) * sizeof(Fl_Menu_Item)); + ::memmove(choice_menu_+ix, choice_menu_+ix+1, (tail+1) * sizeof(Fl_Menu_Item)); + list_size_--; + if (current_suite() >= list_size_) + current_suite(list_size_ - 1); +} + +/** + Remove all Suites that use the given storage attribute. + \param[in] storage storage attribute, see FD_STORE_INTERNAL, etc. + */ +void Fd_Layout_List::remove_all(Fd_Tool_Store storage) { + for (int i=list_size_-1; i>=0; --i) { + if (list_[i].storage_ == storage) + remove(i); + } +} + +// ---- Helper --------------------------------------------------------- MARK: - + +static void draw_h_arrow(int, int, int); +static void draw_v_arrow(int x, int y1, int y2); +static void draw_left_brace(const Fl_Widget *w); +static void draw_right_brace(const Fl_Widget *w); +static void draw_top_brace(const Fl_Widget *w); +static void draw_bottom_brace(const Fl_Widget *w); +static void draw_grid(int x, int y, int dx, int dy); +void draw_width(int x, int y, int r, Fl_Align a); +void draw_height(int x, int y, int b, Fl_Align a); + +static int nearest(int x, int left, int grid, int right=0x7fff) { + int grid_x = ((x-left+grid/2)/grid)*grid+left; + if (grid_x < left+grid/2) return left; // left+grid/2; + if (grid_x > right-grid/2) return right; // right-grid/2; + return grid_x; +} + +static bool in_window(Fd_Snap_Data &d) { + return (d.wgt && d.wgt->parent == d.win); +} + +static bool in_group(Fd_Snap_Data &d) { + return (d.wgt && d.wgt->parent && d.wgt->parent->is_a(ID_Group) && d.wgt->parent != d.win); +} + +static bool in_tabs(Fd_Snap_Data &d) { + return (d.wgt && d.wgt->parent && d.wgt->parent->is_a(ID_Tabs)); +} + +static Fl_Group *parent(Fd_Snap_Data &d) { + return (d.wgt->o->parent()); +} + +// ---- Fd_Snap_Action ------------------------------------------------- MARK: - + +/** \class Fd_Snap_Action + + When a user drags one or more widgets, snap actions can be defined that provide + hints if a preferred widget position or size is nearby. The user's motion is + then directed towards the nearest preferred position, and the widget selection + snaps into place. + + FLUID provides a list of various snap actions. Every snap action uses the data + from the motion event and combines it with the sizes and positions of all other + widgets in the layout. + + Common snap actions include gaps and margins, but also alignments and + simple grid positions. + */ + +/** + \brief Check if a snap action has reached a preferred x position. + \param[inout] d current event data + \param[in] x_ref position of moving point + \param[in] x_snap position of target point + \return 1 if the points are not within range and won;t be considered + \return 0 if the point is as close as another in a previous action + \return -1 if this point is closer than any previous check, and this is the + new distance to beat. + */ +int Fd_Snap_Action::check_x_(Fd_Snap_Data &d, int x_ref, int x_snap) { + int dd = x_ref + d.dx - x_snap; + int d2 = abs(dd); + if (d2 > d.x_dist) return 1; + dx = d.dx_out = d.dx - dd; + ex = d.ex_out = x_snap; + if (d2 == d.x_dist) return 0; + d.x_dist = d2; + return -1; +} + +/** + \brief Check if a snap action has reached a preferred y position. + \see Fd_Snap_Action::check_x_(Fd_Snap_Data &d, int x_ref, int x_snap) + */ +int Fd_Snap_Action::check_y_(Fd_Snap_Data &d, int y_ref, int y_snap) { + int dd = y_ref + d.dy - y_snap; + int d2 = abs(dd); + if (d2 > d.y_dist) return 1; + dy = d.dy_out = d.dy - dd; + ey = d.ey_out = y_snap; + if (d2 == d.y_dist) return 0; + d.y_dist = d2; + return -1; +} + +/** + \brief Check if a snap action has reached a preferred x and y position. + \see Fd_Snap_Action::check_x_(Fd_Snap_Data &d, int x_ref, int x_snap) + */ +void Fd_Snap_Action::check_x_y_(Fd_Snap_Data &d, int x_ref, int x_snap, int y_ref, int y_snap) { + int ddx = x_ref + d.dx - x_snap; + int d2x = abs(ddx); + int ddy = y_ref + d.dy - y_snap; + int d2y = abs(ddy); + if ((d2x <= d.x_dist) && (d2y <= d.y_dist)) { + dx = d.dx_out = d.dx - ddx; + ex = d.ex_out = x_snap; + d.x_dist = d2x; + dy = d.dy_out = d.dy - ddy; + ey = d.ey_out = y_snap; + d.y_dist = d2y; + } +} + +/** + \brief Check if a snap action was applied to the current event. + This method is used to determine if a visual indicator for this snap action + should be drawn. + \param[inout] d current event data + */ +bool Fd_Snap_Action::matches(Fd_Snap_Data &d) { + switch (type) { + case 1: return (d.drag & mask) && (eex == ex) && (d.dx == dx); + case 2: return (d.drag & mask) && (eey == ey) && (d.dy == dy); + case 3: return (d.drag & mask) && (eex == ex) && (d.dx == dx) && (eey == ey) && (d.dy == dy); + } + return false; +} + +/** + \brief Run through all possible snap actions and store the winning coordinates in eex and eey. + \param[inout] d current event data + */ +void Fd_Snap_Action::check_all(Fd_Snap_Data &data) { + for (int i=0; list[i]; i++) { + if (list[i]->mask & data.drag) + list[i]->check(data); + } + eex = data.ex_out; + eey = data.ey_out; +} + +/** + \brief Draw a visual indicator for all snap actions that were applied during the last check. + Only one snap coordinate can win. FLUID chooses the one that is closest to + the current user event. If two or more snap actions suggest the same + coordinate, all of them will be drawn. + \param[inout] d current event data + */ +void Fd_Snap_Action::draw_all(Fd_Snap_Data &data) { + for (int i=0; list[i]; i++) { + if (list[i]->matches(data)) + list[i]->draw(data); + } +} + +/** Return a sensible step size for resizing a widget. */ +void Fd_Snap_Action::get_resize_stepsize(int &x_step, int &y_step) { + if ((layout->widget_inc_w > 1) && (layout->widget_inc_h > 1)) { + x_step = layout->widget_inc_w; + y_step = layout->widget_inc_h; + } else if ((layout->group_grid_x > 1) && (layout->group_grid_y > 1)) { + x_step = layout->group_grid_x; + y_step = layout->group_grid_y; + } else { + x_step = layout->window_grid_x; + y_step = layout->window_grid_y; + } +} + +/** Return a sensible step size for moving a widget. */ +void Fd_Snap_Action::get_move_stepsize(int &x_step, int &y_step) { + if ((layout->group_grid_x > 1) && (layout->group_grid_y > 1)) { + x_step = layout->group_grid_x; + y_step = layout->group_grid_y; + } else if ((layout->window_grid_x > 1) && (layout->window_grid_y > 1)) { + x_step = layout->window_grid_x; + y_step = layout->window_grid_y; + } else { + x_step = layout->widget_gap_x; + y_step = layout->widget_gap_y; + } +} + +/** Fix the given size to the same or next bigger snap position. */ +void Fd_Snap_Action::better_size(int &w, int &h) { + int x_min = 1, y_min = 1, x_inc = 1, y_inc = 1; + get_resize_stepsize(x_inc, y_inc); + if (x_inc < 1) x_inc = 1; + if (y_inc < 1) y_inc = 1; + if ((layout->widget_min_w > 1) && (layout->widget_min_h > 1)) { + x_min = layout->widget_min_w; + y_min = layout->widget_min_h; + } else if ((layout->group_grid_x > 1) && (layout->group_grid_y > 1)) { + x_min = layout->group_grid_x; + y_min = layout->group_grid_y; + } else { + x_min = x_inc; + y_min = y_inc; + } + int ww = fd_max(w - x_min, 0); w = (w - ww + x_inc - 1) / x_inc; w = w * x_inc; w = w + ww; + int hh = fd_max(h - y_min, 0); h = (h - hh + y_inc - 1) / y_inc; h = h * y_inc; h = h + hh; +} + + +// ---- snapping prototypes -------------------------------------------- MARK: - + +/** + Base class for all actions that drag the left side or the entire widget. + */ +class Fd_Snap_Left : public Fd_Snap_Action { +public: + Fd_Snap_Left() { type = 1; mask = FD_LEFT|FD_DRAG; } +}; + +/** + Base class for all actions that drag the right side or the entire widget. + */ +class Fd_Snap_Right : public Fd_Snap_Action { +public: + Fd_Snap_Right() { type = 1; mask = FD_RIGHT|FD_DRAG; } +}; + +/** + Base class for all actions that drag the top side or the entire widget. + */ +class Fd_Snap_Top : public Fd_Snap_Action { +public: + Fd_Snap_Top() { type = 2; mask = FD_TOP|FD_DRAG; } +}; + +/** + Base class for all actions that drag the bottom side or the entire widget. + */ +class Fd_Snap_Bottom : public Fd_Snap_Action { +public: + Fd_Snap_Bottom() { type = 2; mask = FD_BOTTOM|FD_DRAG; } +}; + +// ---- window snapping ------------------------------------------------ MARK: - + +/** + Check if the widget hits the left window edge. + */ +class Fd_Snap_Left_Window_Edge : public Fd_Snap_Left { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { clr(); check_x_(d, d.bx, 0); } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { draw_left_brace(d.win->o); }; +}; +Fd_Snap_Left_Window_Edge snap_left_window_edge; + +/** + Check if the widget hits the right window edge. + */ +class Fd_Snap_Right_Window_Edge : public Fd_Snap_Right { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { clr(); check_x_(d, d.br, d.win->o->w()); } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { draw_right_brace(d.win->o); }; +}; +Fd_Snap_Right_Window_Edge snap_right_window_edge; + +/** + Check if the widget hits the top window edge. + */ +class Fd_Snap_Top_Window_Edge : public Fd_Snap_Top { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { clr(); check_y_(d, d.by, 0); } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { draw_top_brace(d.win->o); }; +}; +Fd_Snap_Top_Window_Edge snap_top_window_edge; + +/** + Check if the widget hits the bottom window edge. + */ +class Fd_Snap_Bottom_Window_Edge : public Fd_Snap_Bottom { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { clr(); check_y_(d, d.bt, d.win->o->h()); } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { draw_bottom_brace(d.win->o); }; +}; +Fd_Snap_Bottom_Window_Edge snap_bottom_window_edge; + +/** + Check if the widget hits the left window edge plus a user defined margin. + */ +class Fd_Snap_Left_Window_Margin : public Fd_Snap_Left { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { + clr(); + if (in_window(d)) check_x_(d, d.bx, layout->left_window_margin); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + draw_h_arrow(d.bx, (d.by+d.bt)/2, 0); + }; +}; +Fd_Snap_Left_Window_Margin snap_left_window_margin; + +class Fd_Snap_Right_Window_Margin : public Fd_Snap_Right { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { + clr(); + if (in_window(d)) check_x_(d, d.br, d.win->o->w()-layout->right_window_margin); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + draw_h_arrow(d.br, (d.by+d.bt)/2, d.win->o->w()-1); + }; +}; +Fd_Snap_Right_Window_Margin snap_right_window_margin; + +class Fd_Snap_Top_Window_Margin : public Fd_Snap_Top { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { + clr(); + if (in_window(d)) check_y_(d, d.by, layout->top_window_margin); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + draw_v_arrow((d.bx+d.br)/2, d.by, 0); + }; +}; +Fd_Snap_Top_Window_Margin snap_top_window_margin; + +class Fd_Snap_Bottom_Window_Margin : public Fd_Snap_Bottom { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { + clr(); + if (in_window(d)) check_y_(d, d.bt, d.win->o->h()-layout->bottom_window_margin); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + draw_v_arrow((d.bx+d.br)/2, d.bt, d.win->o->h()-1); + }; +}; +Fd_Snap_Bottom_Window_Margin snap_bottom_window_margin; + +// ---- group snapping ------------------------------------------------- MARK: - + +/** + Check if the widget hits the left group edge. + */ +class Fd_Snap_Left_Group_Edge : public Fd_Snap_Left { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { + clr(); + if (in_group(d)) check_x_(d, d.bx, parent(d)->x()); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + draw_left_brace(parent(d)); + }; +}; +Fd_Snap_Left_Group_Edge snap_left_group_edge; + +class Fd_Snap_Right_Group_Edge : public Fd_Snap_Right { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { + clr(); + if (in_group(d)) check_x_(d, d.br, parent(d)->x() + parent(d)->w()); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + draw_right_brace(parent(d)); + }; +}; +Fd_Snap_Right_Group_Edge snap_right_group_edge; + +class Fd_Snap_Top_Group_Edge : public Fd_Snap_Top { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { + clr(); + if (in_group(d)) check_y_(d, d.by, parent(d)->y()); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + draw_top_brace(parent(d)); + }; +}; +Fd_Snap_Top_Group_Edge snap_top_group_edge; + +class Fd_Snap_Bottom_Group_Edge : public Fd_Snap_Bottom { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { + clr(); + if (in_group(d)) check_y_(d, d.bt, parent(d)->y() + parent(d)->h()); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + draw_bottom_brace(parent(d)); + }; +}; +Fd_Snap_Bottom_Group_Edge snap_bottom_group_edge; + + +/** + Check if the widget hits the left group edge plus a user defined margin. + */ +class Fd_Snap_Left_Group_Margin : public Fd_Snap_Left { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { + clr(); + if (in_group(d)) check_x_(d, d.bx, parent(d)->x() + layout->left_group_margin); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + draw_left_brace(parent(d)); + draw_h_arrow(d.bx, (d.by+d.bt)/2, parent(d)->x()); + }; +}; +Fd_Snap_Left_Group_Margin snap_left_group_margin; + +class Fd_Snap_Right_Group_Margin : public Fd_Snap_Right { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { + clr(); + if (in_group(d)) check_x_(d, d.br, parent(d)->x()+parent(d)->w()-layout->right_group_margin); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + draw_right_brace(parent(d)); + draw_h_arrow(d.br, (d.by+d.bt)/2, parent(d)->x()+parent(d)->w()-1); + }; +}; +Fd_Snap_Right_Group_Margin snap_right_group_margin; + +class Fd_Snap_Top_Group_Margin : public Fd_Snap_Top { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { + clr(); + if (in_group(d) && !in_tabs(d)) check_y_(d, d.by, parent(d)->y()+layout->top_group_margin); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + draw_top_brace(parent(d)); + draw_v_arrow((d.bx+d.br)/2, d.by, parent(d)->y()); + }; +}; +Fd_Snap_Top_Group_Margin snap_top_group_margin; + +class Fd_Snap_Bottom_Group_Margin : public Fd_Snap_Bottom { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { + clr(); + if (in_group(d) && !in_tabs(d)) check_y_(d, d.bt, parent(d)->y()+parent(d)->h()-layout->bottom_group_margin); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + draw_bottom_brace(parent(d)); + draw_v_arrow((d.bx+d.br)/2, d.bt, parent(d)->y()+parent(d)->h()-1); + }; +}; +Fd_Snap_Bottom_Group_Margin snap_bottom_group_margin; + +// ----- tabs snapping ------------------------------------------------- MARK: - + +/** + Check if the widget top hits the Fl_Tabs group top edge plus a user defined margin. + */ +class Fd_Snap_Top_Tabs_Margin : public Fd_Snap_Top_Group_Margin { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { + clr(); + if (in_tabs(d)) check_y_(d, d.by, parent(d)->y()+layout->top_tabs_margin); + } +}; +Fd_Snap_Top_Tabs_Margin snap_top_tabs_margin; + +class Fd_Snap_Bottom_Tabs_Margin : public Fd_Snap_Bottom_Group_Margin { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { + clr(); + if (in_tabs(d)) check_y_(d, d.bt, parent(d)->y()+parent(d)->h()-layout->bottom_tabs_margin); + } +}; +Fd_Snap_Bottom_Tabs_Margin snap_bottom_tabs_margin; + +// ----- grid snapping ------------------------------------------------- MARK: - + +/** + Base class for grid based snapping. + */ +class Fd_Snap_Grid : public Fd_Snap_Action { +protected: + int nearest_x, nearest_y; +public: + Fd_Snap_Grid() { type = 3; mask = FD_LEFT|FD_TOP|FD_DRAG; } + void check_grid(Fd_Snap_Data &d, int left, int grid_x, int right, int top, int grid_y, int bottom) { + if ((grid_x <= 1) || (grid_y <= 1)) return; + int suggested_x = d.bx + d.dx; + nearest_x = nearest(suggested_x, left, grid_x, right); + int suggested_y = d.by + d.dy; + nearest_y = nearest(suggested_y, top, grid_y, bottom); + if (d.drag == FD_LEFT) + check_x_(d, d.bx, nearest_x); + else if (d.drag == FD_TOP) + check_y_(d, d.by, nearest_y); + else + check_x_y_(d, d.bx, nearest_x, d.by, nearest_y); + } + bool matches(Fd_Snap_Data &d) FL_OVERRIDE { + if (d.drag == FD_LEFT) return (eex == ex); + if (d.drag == FD_TOP) return (eey == ey) && (d.dx == dx); + return (d.drag & mask) && (eex == ex) && (d.dx == dx) && (eey == ey) && (d.dy == dy); + } +}; + +/** + Check if the widget hits window grid coordinates. + */ +class Fd_Snap_Window_Grid : public Fd_Snap_Grid { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { + clr(); + if (in_window(d)) check_grid(d, layout->left_window_margin, layout->window_grid_x, d.win->o->w()-layout->right_window_margin, + layout->top_window_margin, layout->window_grid_y, d.win->o->h()-layout->bottom_window_margin); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + draw_grid(nearest_x, nearest_y, layout->window_grid_x, layout->window_grid_y); + }; +}; +Fd_Snap_Window_Grid snap_window_grid; + +/** + Check if the widget hits group grid coordinates. + */ +class Fd_Snap_Group_Grid : public Fd_Snap_Grid { +public: + void check(Fd_Snap_Data &d) FL_OVERRIDE { + if (in_group(d)) { + clr(); + Fl_Widget *g = parent(d); + check_grid(d, g->x()+layout->left_group_margin, layout->group_grid_x, g->x()+g->w()-layout->right_group_margin, + g->y()+layout->top_group_margin, layout->group_grid_y, g->y()+g->h()-layout->bottom_group_margin); + } + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + draw_grid(nearest_x, nearest_y, layout->group_grid_x, layout->group_grid_y); + }; +}; +Fd_Snap_Group_Grid snap_group_grid; + +// ----- sibling snapping ---------------------------------------------- MARK: - + +/** + Base class the check distance to other widgets in the same group. + */ +class Fd_Snap_Sibling : public Fd_Snap_Action { +protected: + Fl_Widget *best_match; +public: + Fd_Snap_Sibling() : best_match(NULL) { } + virtual int sibling_check(Fd_Snap_Data &d, Fl_Widget *s) = 0; + void check(Fd_Snap_Data &d) FL_OVERRIDE { + clr(); + best_match = NULL; + if (!d.wgt) return; + if (!d.wgt->parent->is_a(ID_Group)) return; + int dsib_min = 1024; + Fl_Group_Type *gt = (Fl_Group_Type*)d.wgt->parent; + Fl_Group *g = (Fl_Group*)gt->o; + Fl_Widget *w = d.wgt->o; + for (int i=0; i<g->children(); i++) { + Fl_Widget *c = g->child(i); + if (c == w) continue; + int sret = sibling_check(d, c); + if (sret < 1) { + int dsib; + if (type==1) + dsib = abs( ((d.by+d.bt)/2+d.dy) - (c->y()+c->h()/2) ); + else + dsib = abs( ((d.bx+d.br)/2+d.dx) - (c->x()+c->w()/2) ); + if (sret == -1 || (dsib < dsib_min)) { + dsib_min = dsib; + best_match = c; + } + } + } + } +}; + +/** + Check if widgets have the same x coordinate, so they can be vertically aligned. + */ +class Fd_Snap_Siblings_Left_Same : public Fd_Snap_Sibling { +public: + Fd_Snap_Siblings_Left_Same() { type = 1; mask = FD_LEFT|FD_DRAG; } + int sibling_check(Fd_Snap_Data &d, Fl_Widget *s) FL_OVERRIDE { + return check_x_(d, d.bx, s->x()); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + if (best_match) draw_left_brace(best_match); + }; +}; +Fd_Snap_Siblings_Left_Same snap_siblings_left_same; + +/** + Check if widgets touch left to right, or have a user selected gap left to right. + */ +class Fd_Snap_Siblings_Left : public Fd_Snap_Sibling { +public: + Fd_Snap_Siblings_Left() { type = 1; mask = FD_LEFT|FD_DRAG; } + int sibling_check(Fd_Snap_Data &d, Fl_Widget *s) FL_OVERRIDE { + return fd_min(check_x_(d, d.bx, s->x()+s->w()), + check_x_(d, d.bx, s->x()+s->w()+layout->widget_gap_x) ); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + if (best_match) draw_right_brace(best_match); + }; +}; +Fd_Snap_Siblings_Left snap_siblings_left; + +class Fd_Snap_Siblings_Right_Same : public Fd_Snap_Sibling { +public: + Fd_Snap_Siblings_Right_Same() { type = 1; mask = FD_RIGHT|FD_DRAG; } + int sibling_check(Fd_Snap_Data &d, Fl_Widget *s) FL_OVERRIDE { + return check_x_(d, d.br, s->x()+s->w()); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + if (best_match) draw_right_brace(best_match); + }; +}; +Fd_Snap_Siblings_Right_Same snap_siblings_right_same; + +class Fd_Snap_Siblings_Right : public Fd_Snap_Sibling { +public: + Fd_Snap_Siblings_Right() { type = 1; mask = FD_RIGHT|FD_DRAG; } + int sibling_check(Fd_Snap_Data &d, Fl_Widget *s) FL_OVERRIDE { + return fd_min(check_x_(d, d.br, s->x()), + check_x_(d, d.br, s->x()-layout->widget_gap_x)); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + if (best_match) draw_left_brace(best_match); + }; +}; +Fd_Snap_Siblings_Right snap_siblings_right; + +class Fd_Snap_Siblings_Top_Same : public Fd_Snap_Sibling { +public: + Fd_Snap_Siblings_Top_Same() { type = 2; mask = FD_TOP|FD_DRAG; } + int sibling_check(Fd_Snap_Data &d, Fl_Widget *s) FL_OVERRIDE { + return check_y_(d, d.by, s->y()); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + if (best_match) draw_top_brace(best_match); + }; +}; +Fd_Snap_Siblings_Top_Same snap_siblings_top_same; + +class Fd_Snap_Siblings_Top : public Fd_Snap_Sibling { +public: + Fd_Snap_Siblings_Top() { type = 2; mask = FD_TOP|FD_DRAG; } + int sibling_check(Fd_Snap_Data &d, Fl_Widget *s) FL_OVERRIDE { + return fd_min(check_y_(d, d.by, s->y()+s->h()), + check_y_(d, d.by, s->y()+s->h()+layout->widget_gap_y)); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + if (best_match) draw_bottom_brace(best_match); + }; +}; +Fd_Snap_Siblings_Top snap_siblings_top; + +class Fd_Snap_Siblings_Bottom_Same : public Fd_Snap_Sibling { +public: + Fd_Snap_Siblings_Bottom_Same() { type = 2; mask = FD_BOTTOM|FD_DRAG; } + int sibling_check(Fd_Snap_Data &d, Fl_Widget *s) FL_OVERRIDE { + return check_y_(d, d.bt, s->y()+s->h()); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + if (best_match) draw_bottom_brace(best_match); + }; +}; +Fd_Snap_Siblings_Bottom_Same snap_siblings_bottom_same; + +class Fd_Snap_Siblings_Bottom : public Fd_Snap_Sibling { +public: + Fd_Snap_Siblings_Bottom() { type = 2; mask = FD_BOTTOM|FD_DRAG; } + int sibling_check(Fd_Snap_Data &d, Fl_Widget *s) FL_OVERRIDE { + return fd_min(check_y_(d, d.bt, s->y()), + check_y_(d, d.bt, s->y()-layout->widget_gap_y)); + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + if (best_match) draw_top_brace(best_match); + }; +}; +Fd_Snap_Siblings_Bottom snap_siblings_bottom; + + +// ------ widget snapping ---------------------------------------------- MARK: - + +/** + Snap horizontal resizing to min_w or min_w and a multiple of inc_w. + */ +class Fd_Snap_Widget_Ideal_Width : public Fd_Snap_Action { +public: + Fd_Snap_Widget_Ideal_Width() { type = 1; mask = FD_LEFT|FD_RIGHT; } + void check(Fd_Snap_Data &d) FL_OVERRIDE { + clr(); + if (!d.wgt) return; + int iw = 15, ih = 15; + d.wgt->ideal_size(iw, ih); + if (d.drag == FD_RIGHT) { + check_x_(d, d.br, d.bx+iw); + iw = layout->widget_min_w; + if (iw > 0) iw = nearest(d.br-d.bx+d.dx, layout->widget_min_w, layout->widget_inc_w); + check_x_(d, d.br, d.bx+iw); + } else { + check_x_(d, d.bx, d.br-iw); + iw = layout->widget_min_w; + if (iw > 0) iw = nearest(d.br-d.bx-d.dx, layout->widget_min_w, layout->widget_inc_w); + check_x_(d, d.bx, d.br-iw); + } + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + draw_width(d.bx, d.bt+7, d.br, 0); + }; +}; +Fd_Snap_Widget_Ideal_Width snap_widget_ideal_width; + +class Fd_Snap_Widget_Ideal_Height : public Fd_Snap_Action { +public: + Fd_Snap_Widget_Ideal_Height() { type = 2; mask = FD_TOP|FD_BOTTOM; } + void check(Fd_Snap_Data &d) FL_OVERRIDE { + clr(); + if (!d.wgt) return; + int iw, ih; + d.wgt->ideal_size(iw, ih); + if (d.drag == FD_BOTTOM) { + check_y_(d, d.bt, d.by+ih); + ih = layout->widget_min_h; + if (ih > 0) ih = nearest(d.bt-d.by+d.dy, layout->widget_min_h, layout->widget_inc_h); + check_y_(d, d.bt, d.by+ih); + } else { + check_y_(d, d.by, d.bt-ih); + ih = layout->widget_min_h; + if (ih > 0) ih = nearest(d.bt-d.by-d.dy, layout->widget_min_h, layout->widget_inc_h); + check_y_(d, d.by, d.bt-ih); + } + } + void draw(Fd_Snap_Data &d) FL_OVERRIDE { + draw_height(d.br+7, d.by, d.bt, 0); + }; +}; +Fd_Snap_Widget_Ideal_Height snap_widget_ideal_height; + +// ---- snap actions list ---------------------------------------------- MARK: - + +/** + /brief The list of all snap actions available to FLUID. + New snap actions can be appended to the list. If multiple snap actions + with different coordinates, but the same snap distance are found, the last + action in the list wins. All snap actions with the same distance and same + winning coordinates are drawn in the overlay plane. + */ +Fd_Snap_Action *Fd_Snap_Action::list[] = { + &snap_left_window_edge, + &snap_right_window_edge, + &snap_top_window_edge, + &snap_bottom_window_edge, + + &snap_left_window_margin, + &snap_right_window_margin, + &snap_top_window_margin, + &snap_bottom_window_margin, + + &snap_window_grid, + &snap_group_grid, + + &snap_left_group_edge, + &snap_right_group_edge, + &snap_top_group_edge, + &snap_bottom_group_edge, + + &snap_left_group_margin, + &snap_right_group_margin, + &snap_top_group_margin, + &snap_bottom_group_margin, + + &snap_top_tabs_margin, + &snap_bottom_tabs_margin, + + &snap_siblings_left_same, &snap_siblings_left, + &snap_siblings_right_same, &snap_siblings_right, + &snap_siblings_top_same, &snap_siblings_top, + &snap_siblings_bottom_same, &snap_siblings_bottom, + + &snap_widget_ideal_width, + &snap_widget_ideal_height, + + NULL +}; + +// ---- draw alignment marks ------------------------------------------- MARK: - + +static void draw_v_arrow(int x, int y1, int y2) { + int dy = (y1>y2) ? -1 : 1 ; + fl_yxline(x, y1, y2); + fl_xyline(x-4, y2, x+4); + fl_line(x-2, y2-dy*5, x, y2-dy); + fl_line(x+2, y2-dy*5, x, y2-dy); +} + +static void draw_h_arrow(int x1, int y, int x2) { + int dx = (x1>x2) ? -1 : 1 ; + fl_xyline(x1, y, x2); + fl_yxline(x2, y-4, y+4); + fl_line(x2-dx*5, y-2, x2-dx, y); + fl_line(x2-dx*5, y+2, x2-dx, y); +} + +static void draw_top_brace(const Fl_Widget *w) { + int x = w->as_window() ? 0 : w->x(); + int y = w->as_window() ? 0 : w->y(); + fl_yxline(x, y-2, y+6); + fl_yxline(x+w->w()-1, y-2, y+6); + fl_xyline(x-2, y, x+w->w()+1); +} + +static void draw_left_brace(const Fl_Widget *w) { + int x = w->as_window() ? 0 : w->x(); + int y = w->as_window() ? 0 : w->y(); + fl_xyline(x-2, y, x+6); + fl_xyline(x-2, y+w->h()-1, x+6); + fl_yxline(x, y-2, y+w->h()+1); +} + +static void draw_right_brace(const Fl_Widget *w) { + int x = w->as_window() ? w->w() - 1 : w->x() + w->w() - 1; + int y = w->as_window() ? 0 : w->y(); + fl_xyline(x-6, y, x+2); + fl_xyline(x-6, y+w->h()-1, x+2); + fl_yxline(x, y-2, y+w->h()+1); +} + +static void draw_bottom_brace(const Fl_Widget *w) { + int x = w->as_window() ? 0 : w->x(); + int y = w->as_window() ? w->h() - 1 : w->y() + w->h() - 1; + fl_yxline(x, y-6, y+2); + fl_yxline(x+w->w()-1, y-6, y+2); + fl_xyline(x-2, y, x+w->w()+1); +} + +void draw_height(int x, int y, int b, Fl_Align a) { + char buf[16]; + int h = b - y; + sprintf(buf, "%d", h); + fl_font(FL_HELVETICA, 9); + int lw = (int)fl_width(buf); + int lx; + + b --; + if (h < 30) { + // Move height to the side... + if (a == FL_ALIGN_LEFT) lx = x - lw - 2; + else lx = x + 2; + fl_yxline(x, y, b); + } else { + // Put height inside the arrows... + if (a == FL_ALIGN_LEFT) lx = x - lw + 2; + else lx = x - lw / 2; + fl_yxline(x, y, y + (h - 11) / 2); + fl_yxline(x, y + (h + 11) / 2, b); + } + + // Draw the height... + fl_draw(buf, lx, y + (h + 7) / 2); + + // Draw the arrowheads... + fl_line(x-2, y+5, x, y+1, x+2, y+5); + fl_line(x-2, b-5, x, b-1, x+2, b-5); + + // Draw the end lines... + fl_xyline(x - 4, y, x + 4); + fl_xyline(x - 4, b, x + 4); +} + +void draw_width(int x, int y, int r, Fl_Align a) { + char buf[16]; + int w = r-x; + sprintf(buf, "%d", w); + fl_font(FL_HELVETICA, 9); + int lw = (int)fl_width(buf); + int ly = y + 4; + + r--; + + if (lw > (w - 20)) { + // Move width above/below the arrows... + if (a == FL_ALIGN_TOP) ly -= 10; + else ly += 10; + + fl_xyline(x, y, r); + } else { + // Put width inside the arrows... + fl_xyline(x, y, x + (w - lw - 2) / 2); + fl_xyline(x + (w + lw + 2) / 2, y, r); + } + + // Draw the width... + fl_draw(buf, x + (w - lw) / 2, ly-2); + + // Draw the arrowheads... + fl_line(x+5, y-2, x+1, y, x+5, y+2); + fl_line(r-5, y-2, r-1, y, r-5, y+2); + + // Draw the end lines... + fl_yxline(x, y - 4, y + 4); + fl_yxline(r, y - 4, y + 4); +} + +static void draw_grid(int x, int y, int dx, int dy) { + int dx2 = 1, dy2 = 1; + const int n = 2; + for (int i=-n; i<=n; i++) { + for (int j=-n; j<=n; j++) { + if (abs(i)+abs(j) < 4) { + int xx = x + i*dx , yy = y + j*dy; + fl_xyline(xx-dx2, yy, xx+dx2); + fl_yxline(xx, yy-dy2, yy+dy2); + } + } + } +} diff --git a/fluid/app/Fd_Snap_Action.h b/fluid/app/Fd_Snap_Action.h new file mode 100644 index 000000000..50b520319 --- /dev/null +++ b/fluid/app/Fd_Snap_Action.h @@ -0,0 +1,193 @@ +// +// Snap action header file for the Fast Light Tool Kit (FLTK). +// +// Copyright 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 +// + +#ifndef _FLUID_FD_SNAP_ACTION_H +#define _FLUID_FD_SNAP_ACTION_H + +#include "app/fluid.h" +#include "nodes/Fl_Window_Type.h" + +#include <string> + +struct Fl_Menu_Item; + +extern Fl_Menu_Item main_layout_submenu_[]; + +/** + \brief Collection of layout settings. + + Presets contain default fonts and font sizes for labels and text. They + can be used to guide widget positions using margins, grids, and gap sizes. + There are three Presets available in one Suite, marked "application", + "dialog", and "toolbox". + */ +class Fd_Layout_Preset { +public: + int left_window_margin; ///< gap between the window border and the widget + int right_window_margin; + int top_window_margin; + int bottom_window_margin; + int window_grid_x; ///< a regular grid across the window with its origin in the top left window corner + int window_grid_y; + + int left_group_margin; ///< gap between the border of a widget and its parent group + int right_group_margin; + int top_group_margin; + int bottom_group_margin; + int group_grid_x; ///< a regular grid across the group with its origin in the top left group corner + int group_grid_y; + + int top_tabs_margin; ///< preferred top edge tab size inside Fl_Tabs + int bottom_tabs_margin; ///< preferred bottom edge tab size inside Fl_Tabs + + int widget_min_w; ///< minimum widget width + int widget_inc_w; ///< widget width increments starting from widget_min_w + int widget_gap_x; ///< preferred horizontal gap between widgets + int widget_min_h; + int widget_inc_h; + int widget_gap_y; + + int labelfont; ///< preferred font for labels + int labelsize; ///< preferred size for labels + int textfont; ///< preferred font for text elements + int textsize; ///< preferred size for text elements + + void write(Fl_Preferences &prefs); + void read(Fl_Preferences &prefs); + void write(Fd_Project_Writer*); + void read(Fd_Project_Reader*); + + int textsize_not_null(); +}; + +extern Fd_Layout_Preset *layout; + +/** + \brief A collection of layout presets. + + A suite of layout presets is designed to cover various use cases when + designing UI layouts for applications. + There are three Presets available in one Suite, marked "application", + "dialog", and "toolbox". + */ +class Fd_Layout_Suite { +public: + char *name_; ///< name of the suite + char *menu_label; ///< label text used in pulldown menu + Fd_Layout_Preset *layout[3]; ///< presets for application, dialog, and toolbox windows + Fd_Tool_Store storage_; ///< storage location (see FD_STORE_INTERNAL, etc.) + void write(Fl_Preferences &prefs); + void read(Fl_Preferences &prefs); + void write(Fd_Project_Writer*); + void read(Fd_Project_Reader*); + void update_label(); + void storage(Fd_Tool_Store s) { storage_ = s; update_label(); } + void name(const char *n); + void init(); + ~Fd_Layout_Suite(); +public: + +}; + +/** + \brief Manage all layout suites that are available to the user. + + FLUID has two built-in suites. More suites can be cloned or added and stored + as a user preference, as part of an .fl project file, or in a separate file + for import/export and sharing. + */ +class Fd_Layout_List { +public: + Fl_Menu_Item *main_menu_; + Fl_Menu_Item *choice_menu_; + Fd_Layout_Suite *list_; + int list_size_; + int list_capacity_; + bool list_is_static_; + int current_suite_; + int current_preset_; + std::string filename_; +public: + Fd_Layout_List(); + ~Fd_Layout_List(); + void update_dialogs(); + void update_menu_labels(); + int current_suite() const { return current_suite_; } + void current_suite(int ix); + void current_suite(std::string); + int current_preset() const { return current_preset_; } + void current_preset(int ix); + Fd_Layout_Suite &operator[](int ix) { return list_[ix]; } + int add(const char *name); + void rename(const char *name); + void capacity(int); + + int load(const std::string &filename); + int save(const std::string &filename); + void write(Fl_Preferences &prefs, Fd_Tool_Store storage); + void read(Fl_Preferences &prefs, Fd_Tool_Store storage); + void write(Fd_Project_Writer*); + void read(Fd_Project_Reader*); + int add(Fd_Layout_Suite*); + void remove(int index); + void remove_all(Fd_Tool_Store storage); + Fd_Layout_Preset *at(int); + int size(); +}; + +extern Fd_Layout_List g_layout_list; + +/** + \brief Structure holding all the data to perform interactive alignment operations. + */ +typedef struct Fd_Snap_Data { + int dx, dy; ///< distance of the mouse from its initial PUSH event + int bx, by, br, bt; ///< bounding box of the original push event or current bounding box when drawing + int drag; ///< drag event mask + int x_dist, y_dist; ///< current closest snapping distance in x and y + int dx_out, dy_out; ///< current closest snapping point as a delta + Fl_Widget_Type *wgt; ///< first selected widget + Fl_Window_Type *win; ///< window that handles the drag action + int ex_out, ey_out; ///< chosen snap position +} Fd_Snap_Data; + +/** + \brief Find points of interest when moving the bounding box of all selected widgets. + */ +class Fd_Snap_Action { +protected: + int check_x_(Fd_Snap_Data &d, int x_ref, int x_snap); + int check_y_(Fd_Snap_Data &d, int y_ref, int y_snap); + void check_x_y_(Fd_Snap_Data &d, int x_ref, int x_snap, int y_ref, int y_snap); + void clr() { ex = dx = 0x7fff; } +public: + int ex, ey, dx, dy, type, mask; + Fd_Snap_Action() : ex(0x7fff), ey(0x7fff), dx(128), dy(128), type(0), mask(0) { } + virtual ~Fd_Snap_Action() { } + virtual void check(Fd_Snap_Data &d) = 0; + virtual void draw(Fd_Snap_Data &d) { } + virtual bool matches(Fd_Snap_Data &d); +public: + static int eex, eey; + static Fd_Snap_Action *list[]; + static void check_all(Fd_Snap_Data &d); + static void draw_all(Fd_Snap_Data &d); + static void get_resize_stepsize(int &x_step, int &y_step); + static void get_move_stepsize(int &x_step, int &y_step); + static void better_size(int &w, int &h); +}; + +#endif // _FLUID_FD_SNAP_ACTION_H diff --git a/fluid/app/Fluid_Image.cxx b/fluid/app/Fluid_Image.cxx new file mode 100644 index 000000000..d25f17023 --- /dev/null +++ b/fluid/app/Fluid_Image.cxx @@ -0,0 +1,412 @@ +// +// Pixmap (and other images) label support for the Fast Light Tool Kit (FLTK). +// +// Copyright 1998-2022 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 "app/Fluid_Image.h" + +#include "app/fluid.h" +#include "io/file.h" +#include "io/code.h" +#include "nodes/Fl_Group_Type.h" +#include "nodes/Fl_Window_Type.h" +#include "tools/fluid_filename.h" + +#include <FL/Fl.H> +#include <FL/Fl_Widget.H> +#include <FL/Fl_Window.H> +#include <FL/fl_string_functions.h> +#include <FL/fl_utf8.h> // fl_fopen() +#include <FL/Fl_File_Chooser.H> +#include <FL/Fl_SVG_Image.H> +#include <FL/Fl_Anim_GIF_Image.H> +#include "../src/flstring.h" + +#include <stdio.h> +#include <errno.h> +#include <stdlib.h> +#include <stdarg.h> + +void Fluid_Image::image(Fl_Widget *o) { + if (o->window() != o) o->image(img); +} + +void Fluid_Image::deimage(Fl_Widget *o) { + if (o->window() != o) o->deimage(img); +} + +/** Write the contents of the name() file as binary source code. + \param fmt short name of file contents for error message + \return 0 if the file could not be opened or read */ +size_t Fluid_Image::write_static_binary(Fd_Code_Writer& f, const char* fmt) { + size_t nData = 0; + enter_project_dir(); + FILE *in = fl_fopen(name(), "rb"); + leave_project_dir(); + if (!in) { + write_file_error(f, fmt); + return 0; + } else { + fseek(in, 0, SEEK_END); + nData = ftell(in); + fseek(in, 0, SEEK_SET); + if (nData) { + char *data = (char*)calloc(nData, 1); + if (fread(data, nData, 1, in)==0) { /* ignore */ } + f.write_cdata(data, (int)nData); + free(data); + } + fclose(in); + } + return nData; +} + +/** Write the contents of the name() file as textual source code. + \param fmt short name of file contents for error message + \return 0 if the file could not be opened or read */ +size_t Fluid_Image::write_static_text(Fd_Code_Writer& f, const char* fmt) { + size_t nData = 0; + enter_project_dir(); + FILE *in = fl_fopen(name(), "rb"); + leave_project_dir(); + if (!in) { + write_file_error(f, fmt); + return 0; + } else { + fseek(in, 0, SEEK_END); + nData = ftell(in); + fseek(in, 0, SEEK_SET); + if (nData) { + char *data = (char*)calloc(nData+1, 1); + if (fread(data, nData, 1, in)==0) { /* ignore */ } + f.write_cstring(data, (int)nData); + free(data); + } + fclose(in); + } + return nData; +} + +void Fluid_Image::write_static_rgb(Fd_Code_Writer& f, const char* idata_name) { + // Write image data... + f.write_c("\n"); + f.write_c_once("#include <FL/Fl_Image.H>\n"); + f.write_c("static const unsigned char %s[] =\n", idata_name); + const int extra_data = img->ld() ? (img->ld()-img->w()*img->d()) : 0; + f.write_cdata(img->data()[0], (img->w() * img->d() + extra_data) * img->h()); + f.write_c(";\n"); + write_initializer(f, "Fl_RGB_Image", "%s, %d, %d, %d, %d", idata_name, img->w(), img->h(), img->d(), img->ld()); +} + +/** + Write the static image data into the soutrce file. + + If \p compressed is set, write the original image format, which requires + linking the matching image reader at runtime, or if we want to store the raw + uncompressed pixels, which makes images fast, needs no reader, but takes a + lot of memory (current default for PNG) + + \param compressed write data in the original compressed file format + */ +void Fluid_Image::write_static(Fd_Code_Writer& f, int compressed) { + if (!img) return; + const char *idata_name = f.unique_id(this, "idata", fl_filename_name(name()), 0); + function_name_ = f.unique_id(this, "image", fl_filename_name(name()), 0); + + if (is_animated_gif_) { + // Write animated gif image data... + f.write_c("\n"); + f.write_c_once("#include <FL/Fl_Anim_GIF_Image.H>\n"); + f.write_c("static const unsigned char %s[] =\n", idata_name); + size_t nData = write_static_binary(f, "AnimGIF"); + f.write_c(";\n"); + write_initializer(f, "Fl_Anim_GIF_Image", "\"%s\", %s, %d", fl_filename_name(name()), idata_name, nData); + } else if (compressed && fl_ascii_strcasecmp(fl_filename_ext(name()), ".gif")==0) { + // Write gif image data... + f.write_c("\n"); + f.write_c_once("#include <FL/Fl_GIF_Image.H>\n"); + f.write_c("static const unsigned char %s[] =\n", idata_name); + size_t nData = write_static_binary(f, "GIF"); + f.write_c(";\n"); + write_initializer(f, "Fl_GIF_Image", "\"%s\", %s, %d", fl_filename_name(name()), idata_name, nData); + } else if (compressed && fl_ascii_strcasecmp(fl_filename_ext(name()), ".bmp")==0) { + // Write bmp image data... + f.write_c("\n"); + f.write_c_once("#include <FL/Fl_BMP_Image.H>\n"); + f.write_c("static const unsigned char %s[] =\n", idata_name); + size_t nData = write_static_binary(f, "BMP"); + f.write_c(";\n"); + write_initializer(f, "Fl_BMP_Image", "\"%s\", %s, %d", fl_filename_name(name()), idata_name, nData); + } else if (img->count() > 1) { + // Write Pixmap data... + f.write_c("\n"); + f.write_c_once("#include <FL/Fl_Pixmap.H>\n"); + f.write_c("static const char *%s[] = {\n", idata_name); + f.write_cstring(img->data()[0], (int)strlen(img->data()[0])); + + int i; + int ncolors, chars_per_color; + sscanf(img->data()[0], "%*d%*d%d%d", &ncolors, &chars_per_color); + + if (ncolors < 0) { + f.write_c(",\n"); + f.write_cstring(img->data()[1], ncolors * -4); + i = 2; + } else { + for (i = 1; i <= ncolors; i ++) { + f.write_c(",\n"); + f.write_cstring(img->data()[i], (int)strlen(img->data()[i])); + } + } + for (; i < img->count(); i ++) { + f.write_c(",\n"); + f.write_cstring(img->data()[i], img->w() * chars_per_color); + } + f.write_c("\n};\n"); + write_initializer(f, "Fl_Pixmap", "%s", idata_name); + } else if (img->d() == 0) { + // Write Bitmap data... + f.write_c("\n"); + f.write_c_once("#include <FL/Fl_Bitmap.H>\n"); + f.write_c("static const unsigned char %s[] =\n", idata_name); + f.write_cdata(img->data()[0], ((img->w() + 7) / 8) * img->h()); + f.write_c(";\n"); + write_initializer(f, "Fl_Bitmap", "%s, %d, %d, %d", idata_name, ((img->w() + 7) / 8) * img->h(), img->w(), img->h()); + } else if (compressed && fl_ascii_strcasecmp(fl_filename_ext(name()), ".jpg")==0) { + // Write jpeg image data... + f.write_c("\n"); + f.write_c_once("#include <FL/Fl_JPEG_Image.H>\n"); + f.write_c("static const unsigned char %s[] =\n", idata_name); + size_t nData = write_static_binary(f, "JPEG"); + f.write_c(";\n"); + write_initializer(f, "Fl_JPEG_Image", "\"%s\", %s, %d", fl_filename_name(name()), idata_name, nData); + } else if (compressed && fl_ascii_strcasecmp(fl_filename_ext(name()), ".png")==0) { + // Write png image data... + f.write_c("\n"); + f.write_c_once("#include <FL/Fl_PNG_Image.H>\n"); + f.write_c("static const unsigned char %s[] =\n", idata_name); + size_t nData = write_static_binary(f, "PNG"); + f.write_c(";\n"); + write_initializer(f, "Fl_PNG_Image", "\"%s\", %s, %d", fl_filename_name(name()), idata_name, nData); + } +#ifdef FLTK_USE_SVG + else if (fl_ascii_strcasecmp(fl_filename_ext(name()), ".svg")==0 || fl_ascii_strcasecmp(fl_filename_ext(name()), ".svgz")==0) { + bool gzipped = (strcmp(fl_filename_ext(name()), ".svgz") == 0); + // Write svg image data... + if (compressed) { + f.write_c("\n"); + f.write_c_once("#include <FL/Fl_SVG_Image.H>\n"); + if (gzipped) { + f.write_c("static const unsigned char %s[] =\n", idata_name); + size_t nData = write_static_binary(f, "SVGZ"); + f.write_c(";\n"); + write_initializer(f, "Fl_SVG_Image", "\"%s\", %s, %ld", fl_filename_name(name()), idata_name, nData); + } else { + f.write_c("static const char %s[] =\n", idata_name); + write_static_text(f, "SVG"); + f.write_c(";\n"); + write_initializer(f, "Fl_SVG_Image", "\"%s\", %s", fl_filename_name(name()), idata_name); + } + } else { + // if FLUID runs from the command line, make sure that the image is not + // only loaded but also rasterized, so we can write the RGB image data + Fl_RGB_Image* rgb_image = NULL; + Fl_SVG_Image* svg_image = NULL; + if (img->d()>0) + rgb_image = (Fl_RGB_Image*)img->image(); + if (rgb_image) + svg_image = rgb_image->as_svg_image(); + if (svg_image) { + svg_image->resize(svg_image->w(), svg_image->h()); + write_static_rgb(f, idata_name); + } else { + write_file_error(f, "RGB_from_SVG"); + } + } + } +#endif // FLTK_USE_SVG + else { + write_static_rgb(f, idata_name); + } +} + +void Fluid_Image::write_file_error(Fd_Code_Writer& f, const char *fmt) { + f.write_c("#warning Cannot read %s file \"%s\": %s\n", fmt, name(), strerror(errno)); + enter_project_dir(); + f.write_c("// Searching in path \"%s\"\n", fl_getcwd(0, FL_PATH_MAX)); + leave_project_dir(); +} + +void Fluid_Image::write_initializer(Fd_Code_Writer& f, const char *type_name, const char *format, ...) { + /* Outputs code that returns (and initializes if needed) an Fl_Image as follows: + static Fl_Image *'function_name_'() { + static Fl_Image *image = NULL; + if (!image) + image = new 'type_name'('product of format and remaining args'); + return image; + } */ + va_list ap; + va_start(ap, format); + f.write_c("static Fl_Image *%s() {\n", function_name_); + if (is_animated_gif_) + f.write_c("%sFl_GIF_Image::animate = true;\n", f.indent(1)); + f.write_c("%sstatic Fl_Image *image = NULL;\n", f.indent(1)); + f.write_c("%sif (!image)\n", f.indent(1)); + f.write_c("%simage = new %s(", f.indent(2), type_name); + f.vwrite_c(format, ap); + f.write_c(");\n"); + f.write_c("%sreturn image;\n", f.indent(1)); + f.write_c("}\n"); + va_end(ap); +} + +void Fluid_Image::write_code(Fd_Code_Writer& f, int bind, const char *var, int inactive) { + /* Outputs code that attaches an image to an Fl_Widget or Fl_Menu_Item. + This code calls a function output before by Fluid_Image::write_initializer() */ + if (img) { + f.write_c("%s%s->%s%s( %s() );\n", f.indent(), var, bind ? "bind_" : "", inactive ? "deimage" : "image", function_name_); + if (is_animated_gif_) + f.write_c("%s((Fl_Anim_GIF_Image*)(%s()))->canvas(%s, Fl_Anim_GIF_Image::DONT_RESIZE_CANVAS);\n", f.indent(), function_name_, var); + } +} + +void Fluid_Image::write_inline(Fd_Code_Writer& f, int inactive) { + if (img) + f.write_c("%s()", function_name_); +} + + +//////////////////////////////////////////////////////////////// + +static Fluid_Image** images = 0; // sorted list +static int numimages = 0; +static int tablesize = 0; + +Fluid_Image* Fluid_Image::find(const char *iname) { + if (!iname || !*iname) return 0; + + // first search to see if it exists already: + int a = 0; + int b = numimages; + while (a < b) { + int c = (a+b)/2; + int i = strcmp(iname,images[c]->name_); + if (i < 0) b = c; + else if (i > 0) a = c+1; + else return images[c]; + } + + // no, so now see if the file exists: + + enter_project_dir(); + FILE *f = fl_fopen(iname,"rb"); + if (!f) { + if (batch_mode) + fprintf(stderr, "Can't open image file:\n%s\n%s",iname,strerror(errno)); + else + fl_message("Can't open image file:\n%s\n%s",iname,strerror(errno)); + leave_project_dir(); + return 0; + } + fclose(f); + + Fluid_Image *ret = new Fluid_Image(iname); + + if (!ret->img || !ret->img->w() || !ret->img->h()) { + delete ret; + ret = 0; + if (batch_mode) + fprintf(stderr, "Can't read image file:\n%s\nunrecognized image format",iname); + else + fl_message("Can't read image file:\n%s\nunrecognized image format",iname); + } + leave_project_dir(); + if (!ret) return 0; + + // make a new entry in the table: + numimages++; + if (numimages > tablesize) { + tablesize = tablesize ? 2*tablesize : 16; + if (images) images = (Fluid_Image**)realloc(images, tablesize*sizeof(Fluid_Image*)); + else images = (Fluid_Image**)malloc(tablesize*sizeof(Fluid_Image*)); + } + for (b = numimages-1; b > a; b--) images[b] = images[b-1]; + images[a] = ret; + + return ret; +} + +Fluid_Image::Fluid_Image(const char *iname) + : is_animated_gif_(false) +{ + name_ = fl_strdup(iname); + written = 0; + refcount = 0; + img = Fl_Shared_Image::get(iname); + if (img && iname) { + const char *ext = fl_filename_ext(iname); + if (fl_ascii_strcasecmp(ext, ".gif")==0) { + int fc = Fl_Anim_GIF_Image::frame_count(iname); + if (fc > 0) is_animated_gif_ = true; + } + } + function_name_ = NULL; +} + +void Fluid_Image::increment() { + ++refcount; +} + +void Fluid_Image::decrement() { + --refcount; + if (refcount > 0) return; + delete this; +} + +Fluid_Image::~Fluid_Image() { + int a; + if (images) { + for (a = 0; a<numimages; a++) { + if (images[a] == this) { + numimages--; + for (; a < numimages; a++) { + images[a] = images[a+1]; + } + break; + } + } + } + if (img) img->release(); + free((void*)name_); +} + +//////////////////////////////////////////////////////////////// + +const char *ui_find_image_name; +Fluid_Image *ui_find_image(const char *oldname) { + enter_project_dir(); + fl_file_chooser_ok_label("Use Image"); + const char *name = fl_file_chooser("Image?", + "Image Files (*.{bm,bmp,gif,jpg,pbm,pgm,png,ppm,xbm,xpm,svg" +#ifdef HAVE_LIBZ + ",svgz" +#endif + "})", + oldname,1); + fl_file_chooser_ok_label(NULL); + ui_find_image_name = name; + Fluid_Image *ret = (name && *name) ? Fluid_Image::find(name) : 0; + leave_project_dir(); + return ret; +} diff --git a/fluid/app/Fluid_Image.h b/fluid/app/Fluid_Image.h new file mode 100644 index 000000000..e3b5faaac --- /dev/null +++ b/fluid/app/Fluid_Image.h @@ -0,0 +1,60 @@ +// +// Image header file for the Fast Light Tool Kit (FLTK). +// +// This class stores the image labels for widgets in fluid. This is +// not a class in FLTK itself, and will produce different types of +// code depending on what the image type is. +// +// Copyright 1998-2010 by Bill Spitzak and others. +// +// This library is free software. Distribution and use rights are outlined in +// the file "COPYING" which should have been included with this file. If this +// file is missing or damaged, see the license at: +// +// https://www.fltk.org/COPYING.php +// +// Please see the following page on how to report bugs and issues: +// +// https://www.fltk.org/bugs.php +// + +#ifndef FLUID_IMAGE_H +#define FLUID_IMAGE_H + +#include "io/code.h" + +#include <FL/Fl_Shared_Image.H> + +class Fluid_Image { + bool is_animated_gif_; + const char *name_; + int refcount; + Fl_Shared_Image *img; + const char *function_name_; +protected: + Fluid_Image(const char *name); // no public constructor + ~Fluid_Image(); // no public destructor + size_t write_static_binary(Fd_Code_Writer& f, const char* fmt); + size_t write_static_text(Fd_Code_Writer& f, const char* fmt); + void write_static_rgb(Fd_Code_Writer& f, const char* idata_name); +public: + int written; + static Fluid_Image* find(const char *); + void decrement(); // reference counting & automatic free + void increment(); + void image(Fl_Widget *); // set the image of this widget + void deimage(Fl_Widget *); // set the deimage of this widget + void write_static(Fd_Code_Writer& f, int compressed); + void write_initializer(Fd_Code_Writer& f, const char *type_name, const char *format, ...); + void write_code(Fd_Code_Writer& f, int bind, const char *var, int inactive = 0); + void write_inline(Fd_Code_Writer& f, int inactive = 0); + void write_file_error(Fd_Code_Writer& f, const char *fmt); + const char *name() const {return name_;} +}; + +// pop up file chooser and return a legal image selected by user, +// or zero for any errors: +Fluid_Image *ui_find_image(const char *); +extern const char *ui_find_image_name; + +#endif diff --git a/fluid/app/align_widget.cxx b/fluid/app/align_widget.cxx new file mode 100644 index 000000000..a9badf9e4 --- /dev/null +++ b/fluid/app/align_widget.cxx @@ -0,0 +1,414 @@ +// +// Alignment code 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 +// + +#include "app/align_widget.h" + +#include "app/fluid.h" +#include "app/undo.h" +#include "nodes/Fl_Group_Type.h" + +#include <FL/Fl.H> +#include <FL/Fl_Window.H> + +/** + the first behavior always uses the first selected widget as a reference + the second behavior uses the largest widget (most extreme positions) as + a reference. + */ +#define BREAK_ON_FIRST break +//#define BREAK_ON_FIRST + +void align_widget_cb(Fl_Widget*, long how) +{ + const int max = 32768, min = -32768; + int left, right, top, bot, wdt, hgt, n; + Fl_Type *o; + int changed = 0; + switch ( how ) + { + //---- align + case 10: // align left + left = max; + for (o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + if (w->x()<left) + left = w->x(); + BREAK_ON_FIRST; + } + if (left!=max) + for (Fl_Type *o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + undo_checkpoint(); + } + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + Fl_Type::allow_layout++; + w->resize(left, w->y(), w->w(), w->h()); + Fl_Type::allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + break; + case 11: // align h.center + left = max; right = min; + for (o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + if (w->x()<left) + left = w->x(); + if (w->x()+w->w()>right) + right = w->x()+w->w(); + BREAK_ON_FIRST; + } + if (left!=max) + { + int center2 = left+right; + for (Fl_Type *o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + undo_checkpoint(); + } + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + Fl_Type::allow_layout++; + w->resize((center2-w->w())/2, w->y(), w->w(), w->h()); + Fl_Type::allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + } + break; + case 12: // align right + right = min; + for (o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + if (w->x()+w->w()>right) + right = w->x()+w->w(); + BREAK_ON_FIRST; + } + if (right!=min) + for (Fl_Type *o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + undo_checkpoint(); + } + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + Fl_Type::allow_layout++; + w->resize(right-w->w(), w->y(), w->w(), w->h()); + Fl_Type::allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + break; + case 13: // align top + top = max; + for (o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + if (w->y()<top) + top = w->y(); + BREAK_ON_FIRST; + } + if (top!=max) + for (Fl_Type *o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + undo_checkpoint(); + } + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + Fl_Type::allow_layout++; + w->resize(w->x(), top, w->w(), w->h()); + Fl_Type::allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + break; + case 14: // align v.center + top = max; bot = min; + for (o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + if (w->y()<top) + top = w->y(); + if (w->y()+w->h()>bot) + bot = w->y()+w->h(); + BREAK_ON_FIRST; + } + if (top!=max) + { + int center2 = top+bot; + for (Fl_Type *o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + undo_checkpoint(); + } + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + Fl_Type::allow_layout++; + w->resize(w->x(), (center2-w->h())/2, w->w(), w->h()); + Fl_Type::allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + } + break; + case 15: // align bottom + bot = min; + for (o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + if (w->y()+w->h()>bot) + bot = w->y()+w->h(); + BREAK_ON_FIRST; + } + if (bot!=min) + for (Fl_Type *o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + undo_checkpoint(); + } + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + Fl_Type::allow_layout++; + w->resize( w->x(), bot-w->h(), w->w(), w->h()); + Fl_Type::allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + break; + //---- space evenly + case 20: // space evenly across + left = max; right = min; wdt = 0; n = 0; + for (o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + if (w->x()<left) + left = w->x(); + if (w->x()+w->w()>right) + right = w->x()+w->w(); + wdt += w->w(); + n++; + } + wdt = (right-left)-wdt; + n--; + if (n>0) + { + wdt = wdt/n*n; // make sure that all gaps are the same, possibly moving the rightmost widget + int cnt = 0, wsum = 0; + for (Fl_Type *o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + undo_checkpoint(); + } + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + Fl_Type::allow_layout++; + w->resize(left+wsum+wdt*cnt/n, w->y(), w->w(), w->h()); + Fl_Type::allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + cnt++; + wsum += w->w(); + } + } + break; + case 21: // space evenly down + top = max; bot = min; hgt = 0, n = 0; + for (o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + if (w->y()<top) + top = w->y(); + if (w->y()+w->h()>bot) + bot = w->y()+w->h(); + hgt += w->h(); + n++; + } + hgt = (bot-top)-hgt; + n--; + if (n>0) + { + hgt = hgt/n*n; // make sure that all gaps are the same, possibly moving the rightmost widget + int cnt = 0, hsum = 0; + for (Fl_Type *o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + undo_checkpoint(); + } + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + Fl_Type::allow_layout++; + w->resize(w->x(), top+hsum+hgt*cnt/n, w->w(), w->h()); + Fl_Type::allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + cnt++; + hsum += w->h(); + } + } + break; + //---- make same size + case 30: // same width + wdt = min; + for (o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + if (w->w()>wdt) + wdt = w->w(); + BREAK_ON_FIRST; + } + if (wdt!=min) + for (Fl_Type *o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + undo_checkpoint(); + } + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + Fl_Type::allow_layout++; + w->resize(w->x(), w->y(), wdt, w->h()); + Fl_Type::allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + break; + case 31: // same height + hgt = min; + for (o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + if (w->h()>hgt) + hgt = w->h(); + BREAK_ON_FIRST; + } + if (hgt!=min) + for (Fl_Type *o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + undo_checkpoint(); + } + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + Fl_Type::allow_layout++; + w->resize( w->x(), w->y(), w->w(), hgt); + Fl_Type::allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + break; + case 32: // same size + hgt = min; wdt = min; + for (o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + if (w->w()>wdt) + wdt = w->w(); + if (w->h()>hgt) + hgt = w->h(); + BREAK_ON_FIRST; + } + if (hgt!=min) + for (Fl_Type *o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget()) + { + if (!changed) { + changed = 1; + undo_checkpoint(); + } + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + Fl_Type::allow_layout++; + w->resize( w->x(), w->y(), wdt, hgt); + Fl_Type::allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + break; + //---- center in group + case 40: // center hor + for (o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget() && o->parent) + { + if (!changed) { + changed = 1; + undo_checkpoint(); + } + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + Fl_Widget *p = ((Fl_Widget_Type *)o->parent)->o; + int center2; + + if (w->window() == p) center2 = p->w(); + else center2 = 2*p->x()+p->w(); + + Fl_Type::allow_layout++; + w->resize((center2-w->w())/2, w->y(), w->w(), w->h()); + Fl_Type::allow_layout--; + w->redraw(); + if (w->window()) w->window()->redraw(); + } + break; + case 41: // center vert + for (o = Fl_Type::first; o; o = o->next) + if (o->selected && o->is_widget() && o->parent) + { + if (!changed) { + changed = 1; + undo_checkpoint(); + } + Fl_Widget *w = ((Fl_Widget_Type *)o)->o; + Fl_Widget *p = ((Fl_Widget_Type *)o->parent)->o; + int center2; + + if (w->window() == p) center2 = p->h(); + else center2 = 2*p->y()+p->h(); + + Fl_Type::allow_layout++; + w->resize(w->x(), (center2-w->h())/2, w->w(), w->h()); + Fl_Type::allow_layout--; + set_modflag(1); + w->redraw(); + if (w->window()) w->window()->redraw(); + } + break; + } + if (changed) + set_modflag(1); +} diff --git a/fluid/app/align_widget.h b/fluid/app/align_widget.h new file mode 100644 index 000000000..f04372215 --- /dev/null +++ b/fluid/app/align_widget.h @@ -0,0 +1,24 @@ +// +// FLUID main entry for the Fast Light Tool Kit (FLTK). +// +// Copyright 1998-2021 by Bill Spitzak and others. +// +// This library is free software. Distribution and use rights are outlined in +// the file "COPYING" which should have been included with this file. If this +// file is missing or damaged, see the license at: +// +// https://www.fltk.org/COPYING.php +// +// Please see the following page on how to report bugs and issues: +// +// https://www.fltk.org/bugs.php +// + +#ifndef _FLUID_ALIGN_WIDGET_H +#define _FLUID_ALIGN_WIDGET_H + +class Fl_Widget; + +void align_widget_cb(Fl_Widget *, long); + +#endif // _FLUID_ALIGN_WIDGET_H diff --git a/fluid/app/fluid.cxx b/fluid/app/fluid.cxx new file mode 100644 index 000000000..e9d473fb2 --- /dev/null +++ b/fluid/app/fluid.cxx @@ -0,0 +1,2339 @@ +// +// FLUID main entry for the Fast Light Tool Kit (FLTK). +// +// Copyright 1998-2025 by Bill Spitzak and others. +// +// This library is free software. Distribution and use rights are outlined in +// the file "COPYING" which should have been included with this file. If this +// file is missing or damaged, see the license at: +// +// https://www.fltk.org/COPYING.php +// +// Please see the following page on how to report bugs and issues: +// +// https://www.fltk.org/bugs.php +// + +#include "app/fluid.h" + +#include "app/mergeback.h" +#include "app/undo.h" +#include "io/file.h" +#include "io/code.h" +#include "nodes/Fl_Type.h" +#include "nodes/Fl_Function_Type.h" +#include "nodes/Fl_Group_Type.h" +#include "nodes/Fl_Window_Type.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 "app/shell_command.h" +#include "tools/autodoc.h" +#include "widgets/widget_browser.h" + +#include <FL/Fl.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" + +extern "C" +{ +#if defined(HAVE_LIBPNG) && defined(HAVE_LIBZ) +# include <zlib.h> +# ifdef HAVE_PNG_H +# include <png.h> +# else +# include <libpng/png.h> +# endif // HAVE_PNG_H +#endif // HAVE_LIBPNG && HAVE_LIBZ +} + +/// \defgroup globals Fluid Global Variables, Functions and Callbacks +/// \{ + +// +// Globals.. +// + +/// FLUID-wide help dialog. +static Fl_Help_Dialog *help_dialog = NULL; + +/// Main app window menu bar. +Fl_Menu_Bar *main_menubar = NULL; + +/// Main app window. +Fl_Window *main_window; + +/// Fluid application preferences, always accessible, will be flushed when app closes. +Fl_Preferences fluid_prefs(Fl_Preferences::USER_L, "fltk.org", "fluid"); + +/// Show guides in the design window when positioning widgets, saved in app preferences. +int show_guides = 1; + +/// Show areas of restricted use in overlay plane. +/// Restricted areas are widget that overlap each other, widgets that are outside +/// of their parent's bounds (except children of Scroll groups), and areas +/// within an Fl_Tile that are not covered by children. +int show_restricted = 1; + +/// Show a ghosted outline for groups that have very little contrast. +/// This makes groups with NO_BOX or FLAT_BOX better editable. +int show_ghosted_outline = 1; + +/// Show widget comments in the browser, saved in app preferences. +int show_comments = 1; + +/// Use external editor for editing Fl_Code_Type, saved in app preferences. +int G_use_external_editor = 0; + +/// Debugging help for external Fl_Code_Type editor. +int G_debug = 0; + +/// Run this command to load an Fl_Code_Type into an external editor, save in app preferences. +char G_external_editor_command[512]; + + +// File history info... + +/// Stores the absolute filename of the last 10 design files, saved in app preferences. +char absolute_history[10][FL_PATH_MAX]; + +/// This list of filenames is computed from \c absolute_history and displayed in the main menu. +char relative_history[10][FL_PATH_MAX]; + +/// Menuitem to save a .fl design file, will be deactivated if the design is unchanged. +Fl_Menu_Item *save_item = NULL; + +/// First Menuitem that shows the .fl design file history. +Fl_Menu_Item *history_item = NULL; + +/// Menuitem to show or hide the widget bin, label will change if bin is visible. +Fl_Menu_Item *widgetbin_item = NULL; + +/// Menuitem to show or hide the code view, label will change if view is visible. +Fl_Menu_Item *codeview_item = NULL; + +/// Menuitem to show or hide the editing overlay, label will change if overlay visibility changes. +Fl_Menu_Item *overlay_item = NULL; + +/// Menuitem to show or hide the editing guides, label will change if overlay visibility changes. +Fl_Menu_Item *guides_item = NULL; + +/// Menuitem to show or hide the restricted area overlys, label will change if overlay visibility changes. +Fl_Menu_Item *restricted_item = NULL; + +//////////////////////////////////////////////////////////////// + +/// Filename of the current .fl project file +static const char *filename = NULL; + +/// Set if the current design has been modified compared to the associated .fl design file. +int modflag = 0; + +/// Set if the code files are older than the current design. +int modflag_c = 0; + +/// Application work directory, stored here when temporarily changing to the source code directory. +/// \see goto_source_dir() +static std::string app_work_dir; + +/// Used as a counter to set the .fl project dir as the current directory. +/// \see enter_project_dir(), leave_project_dir() +static char in_project_dir = 0; + +/// Set, if Fluid was started with the command line argument -u +int update_file = 0; // fluid -u + +/// Set, if Fluid was started with the command line argument -c +int compile_file = 0; // fluid -c + +/// Set, if Fluid was started with the command line argument -cs +int compile_strings = 0; // fluid -cs + +/// Set, if Fluid was started with the command line argument -v +int show_version = 0; // fluid -v + +/// Set, if Fluid runs in batch mode, and no user interface is activated. +int batch_mode = 0; // if set (-c, -u) don't open display + +/// command line arguments that overrides the generate code file extension or name +std::string g_code_filename_arg; + +/// command line arguments that overrides the generate header file extension or name +std::string g_header_filename_arg; + +/// current directory path at application launch +std::string g_launch_path; + +/// if set, generate images for automatic documentation in this directory +std::string g_autodoc_path; + +/// path to store temporary files during app run +/// \see tmpdir_create_called +std::string tmpdir_path; + +/// true if the temporary file path was already created +/// \see tmpdir_path +bool tmpdir_create_called = false; + + +/// Offset in pixels when adding widgets from an .fl file. +int pasteoffset = 0; + +/// Paste offset incrementing at every paste command. +static int ipasteoffset = 0; + +// ---- project settings + +/// The current project, possibly a new, empty roject +Fluid_Project g_project; + +/** + Initialize a new project. + */ +Fluid_Project::Fluid_Project() : + i18n_type(FD_I18N_NONE), + include_H_from_C(1), + use_FL_COMMAND(0), + utf8_in_src(0), + avoid_early_includes(0), + header_file_set(0), + code_file_set(0), + write_mergeback_data(0), + header_file_name(".h"), + code_file_name(".cxx") +{ } + +/** + Clear all project resources. + Not implemented. + */ +Fluid_Project::~Fluid_Project() { +} + +/** + Reset all project setting to create a new empty project. + */ +void Fluid_Project::reset() { + ::delete_all(); + i18n_type = FD_I18N_NONE; + + i18n_gnu_include = "<libintl.h>"; + i18n_gnu_conditional = ""; + i18n_gnu_function = "gettext"; + i18n_gnu_static_function = "gettext_noop"; + + i18n_pos_include = "<nl_types.h>"; + i18n_pos_conditional = ""; + i18n_pos_file = ""; + i18n_pos_set = "1"; + + include_H_from_C = 1; + use_FL_COMMAND = 0; + utf8_in_src = 0; + avoid_early_includes = 0; + header_file_set = 0; + code_file_set = 0; + header_file_name = ".h"; + code_file_name = ".cxx"; + write_mergeback_data = 0; +} + +/** + Tell the project and i18n tab of the settings dialog to refresh themselves. + */ +void Fluid_Project::update_settings_dialog() { + if (settings_window) { + w_settings_project_tab->do_callback(w_settings_project_tab, LOAD); + w_settings_i18n_tab->do_callback(w_settings_i18n_tab, LOAD); + } +} + +/** + Make sure that a path name ends with a forward slash. + \param[in] str directory or path name + \return a new string, ending with a '/' + */ +static std::string end_with_slash(const std::string &str) { + char last = str[str.size()-1]; + if (last !='/' && last != '\\') + return str + "/"; + else + return str; +} + +/** + Generate a path to a directory for temporary data storage. + The path is stored in g_tmpdir. + */ +static void 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; + std::string path = fl_getenv("TMPDIR"); + 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]; + fluid_prefs.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 that was created in set_tmpdir. + */ +static void 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 &get_tmpdir() { + if (!tmpdir_create_called) + create_tmpdir(); + return tmpdir_path; +} + +/** + 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 confirm_project_clear() { + if (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_cb(NULL, NULL); + if (modflag) return false; // user canceled the "Save As" dialog + } + return true; +} + +// ---- + +extern Fl_Window *the_panel; + +/** + 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 flush_text_widgets() { + if (Fl::focus() && (Fl::focus()->top_window() == the_panel)) { + Fl_Widget *old_focus = Fl::focus(); + Fl::focus(NULL); // trigger callback of the widget that is losing focus + Fl::focus(old_focus); + } +} + +// ---- + +/** + Change the current working directory to the .fl project directory. + + Every call to enter_project_dir() must have a corresponding leave_project_dir() + call. Enter and leave calls can be nested. + + The first call to enter_project_dir() remembers the original directory, usually + the launch directory of the application. Nested calls will increment a nesting + counter. When the nesting counter is back to 0, leave_project_dir() will return + to the original directory. + + The global variable 'filename' must be set to the current project file with + absolute or relative path information. + + \see leave_project_dir(), pwd, in_project_dir + */ +void enter_project_dir() { + if (in_project_dir<0) { + fprintf(stderr, "** Fluid internal error: enter_project_dir() calls unmatched\n"); + return; + } + in_project_dir++; + // check if we are already in the project dir and do nothing if so + if (in_project_dir>1) return; + // check if there is an active project, and do nothing if there is none + if (!filename || !*filename) { + fprintf(stderr, "** Fluid internal error: enter_project_dir() no filename set\n"); + return; + } + // store the current working directory for later + app_work_dir = fl_getcwd(); + // set the current directory to the path of our .fl file + std::string project_path = fl_filename_path(fl_filename_absolute(filename)); + if (fl_chdir(project_path.c_str()) == -1) { + fprintf(stderr, "** Fluid internal error: enter_project_dir() can't chdir to %s: %s\n", + project_path.c_str(), strerror(errno)); + return; + } + //fprintf(stderr, "chdir from %s to %s\n", app_work_dir.c_str(), fl_getcwd().c_str()); +} + +/** + Change the current working directory to the previous directory. + \see enter_project_dir(), pwd, in_project_dir + */ +void leave_project_dir() { + if (in_project_dir == 0) { + fprintf(stderr, "** Fluid internal error: leave_project_dir() calls unmatched\n"); + return; + } + in_project_dir--; + // still nested, stay in the project directory + if (in_project_dir > 0) return; + // no longer nested, return to the original, usually the application working directory + if (fl_chdir(app_work_dir.c_str()) < 0) { + fprintf(stderr, "** Fluid internal error: leave_project_dir() can't chdir back to %s : %s\n", + app_work_dir.c_str(), strerror(errno)); + } +} + +/** + 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 position_window(Fl_Window *w, const char *prefsName, int Visible, int X, int Y, int W, int H) { + Fl_Preferences pos(fluid_prefs, 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 save_position(Fl_Window *w, const char *prefsName) { + Fl_Preferences pos(fluid_prefs, 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())); +} + +/** + 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 + */ +static char* cutfname(int which = 0) { + static char name[2][FL_PATH_MAX]; + static char beenhere = 0; + + if (!beenhere) { + beenhere = 1; + fluid_prefs.getUserdataPath(name[0], sizeof(name[0])); + strlcat(name[0], "cut_buffer", sizeof(name[0])); + fluid_prefs.getUserdataPath(name[1], sizeof(name[1])); + strlcat(name[1], "dup_buffer", sizeof(name[1])); + } + + return name[which]; +} + +/** + 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 ( G_debug ) 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 (Fl_Type *p = Fl_Type::first; p; p = p->next) { + if ( p->is_a(ID_Code) ) { + Fl_Code_Type *code = (Fl_Code_Type*)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 ) 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); + } +} + +/** + 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 NULL, or no filename is set, open a filechooser. + */ +void save_cb(Fl_Widget *, void *v) { + flush_text_widgets(); + Fl_Native_File_Chooser fnfc; + const char *c = 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(std::string(c)); + if (fl_choice("The file \"%s\" already exists.\n" + "Do you want to replace it?", "Cancel", + "Replace", NULL, basename.c_str()) == 0) return; + } + + if (v != (void *)2) set_filename(c); + } + if (!write_file(c)) { + fl_alert("Error writing %s: %s", c, strerror(errno)); + return; + } + + if (v != (void *)2) { + set_modflag(0, 1); + undo_save = undo_current; + } +} + +/** + Save a design template. + \todo We should document the concept of templates. + */ +void save_template_cb(Fl_Widget *, void *) { + // Setup the template panel... + if (!template_panel) make_template_panel(); + + template_clear(); + template_browser->add("New Template"); + template_load(); + + template_name->show(); + template_name->value(""); + + template_instance->hide(); + + template_delete->show(); + template_delete->deactivate(); + + template_submit->label("Save"); + template_submit->deactivate(); + + template_panel->label("Save Template"); + + // Show the panel and wait for the user to do something... + template_panel->show(); + while (template_panel->shown()) Fl::wait(); + + // Get the template name, return if it is empty... + const char *c = template_name->value(); + if (!c || !*c) return; + + // Convert template name to filename_with_underscores + char savename[FL_PATH_MAX], *saveptr; + strlcpy(savename, c, sizeof(savename)); + for (saveptr = savename; *saveptr; saveptr ++) { + if (isspace(*saveptr)) *saveptr = '_'; + } + + // Find the templates directory... + char filename[FL_PATH_MAX]; + fluid_prefs.getUserdataPath(filename, sizeof(filename)); + + strlcat(filename, "templates", sizeof(filename)); + if (fl_access(filename, 0)) fl_make_path(filename); + + strlcat(filename, "/", sizeof(filename)); + strlcat(filename, savename, sizeof(filename)); + + char *ext = filename + strlen(filename); + if (ext >= (filename + sizeof(filename) - 5)) { + fl_alert("The template name \"%s\" is too long!", c); + return; + } + + // Save the .fl file... + strcpy(ext, ".fl"); + + if (!fl_access(filename, 0)) { + if (fl_choice("The template \"%s\" already exists.\n" + "Do you want to replace it?", "Cancel", + "Replace", NULL, c) == 0) return; + } + + if (!write_file(filename)) { + fl_alert("Error writing %s: %s", filename, strerror(errno)); + return; + } + +#if defined(HAVE_LIBPNG) && defined(HAVE_LIBZ) + // Get the screenshot, if any... + Fl_Type *t; + + for (t = Fl_Type::first; t; t = t->next) { + // Find the first window... + if (t->is_a(ID_Window)) break; + } + + if (!t) return; + + // Grab a screenshot... + Fl_Window_Type *wt = (Fl_Window_Type *)t; + uchar *pixels; + int w, h; + + if ((pixels = wt->read_image(w, h)) == NULL) return; + + // Save to a PNG file... + strcpy(ext, ".png"); + + errno = 0; + if (fl_write_png(filename, pixels, w, h, 3) != 0) { + delete[] pixels; + fl_alert("Error writing %s: %s", filename, strerror(errno)); + return; + } + +# if 0 // The original PPM output code... + strcpy(ext, ".ppm"); + fp = fl_fopen(filename, "wb"); + fprintf(fp, "P6\n%d %d 255\n", w, h); + fwrite(pixels, w * h, 3, fp); + fclose(fp); +# endif // 0 + + delete[] pixels; +#endif // HAVE_LIBPNG && HAVE_LIBZ +} + +/** + Reload the file set by \c filename, replacing the current design. + If the design was modified, a dialog will ask for confirmation. + */ +void revert_cb(Fl_Widget *,void *) { + if (modflag) { + if (!fl_choice("This user interface has been changed. Really revert?", + "Cancel", "Revert", NULL)) return; + } + undo_suspend(); + if (!read_file(filename, 0)) { + undo_resume(); + widget_browser->rebuild(); + g_project.update_settings_dialog(); + fl_message("Can't read %s: %s", filename, strerror(errno)); + return; + } + widget_browser->rebuild(); + undo_resume(); + set_modflag(0, 0); + undo_clear(); + g_project.update_settings_dialog(); +} + +/** + Exit Fluid; we hope you had a nice experience. + If the design was modified, a dialog will ask for confirmation. + */ +void exit_cb(Fl_Widget *,void *) { + if (shell_command_running()) { + int choice = fl_choice("Previous shell command still running!", + "Cancel", + "Exit", + NULL); + 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(fluid_prefs, "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 = 0; + } + 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(fluid_prefs, FD_STORE_USER); + g_layout_list.write(fluid_prefs, FD_STORE_USER); + + 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/. + g_project.reset(); + ExternalCodeEditor::tmpdir_clear(); + delete_tmpdir(); + + exit(0); +} + +/** + 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 new_project(bool user_must_confirm) { + // verify user intention + if ((user_must_confirm) && (confirm_project_clear() == false)) + return false; + + // clear the current project + g_project.reset(); + set_filename(NULL); + set_modflag(0, 0); + widget_browser->rebuild(); + g_project.update_settings_dialog(); + + // all is clear to continue + return true; +} + +/** + 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 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")) == NULL) { + fl_alert("Error reading template file \"%s\":\n%s", tname, + strerror(errno)); + set_modflag(0); + undo_clear(); + return false; + } + + if ((outfile = fl_fopen(cutfname(1), "wb")) == NULL) { + fl_alert("Error writing buffer file \"%s\":\n%s", cutfname(1), + strerror(errno)); + fclose(infile); + set_modflag(0); + undo_clear(); + return false; + } + + while (fgets(line, sizeof(line), infile)) { + // Replace @INSTANCE@ with the instance name... + for (ptr = line; (next = strstr(ptr, "@INSTANCE@")) != NULL; ptr = next + 10) { + fwrite(ptr, next - ptr, 1, outfile); + fputs(iname, outfile); + } + + fputs(ptr, outfile); + } + + fclose(infile); + fclose(outfile); + + undo_suspend(); + read_file(cutfname(1), 0); + fl_unlink(cutfname(1)); + undo_resume(); + } else { + // No instance name, so read the template without replacements... + undo_suspend(); + read_file(tname, 0); + undo_resume(); + } + } + + widget_browser->rebuild(); + g_project.update_settings_dialog(); + set_modflag(0); + undo_clear(); + + return true; +} + +/** + 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 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 (filename) { + std::string current_project_file = filename; + dialog.directory(fl_filename_path(current_project_file).c_str()); + dialog.preset_file(fl_filename_name(current_project_file).c_str()); + } + if (dialog.show() != 0) + return std::string(); + return std::string(dialog.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 merge_project_file(const std::string &filename_arg) { + bool is_a_merge = (Fl_Type::first != NULL); + 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 = filename; + filename = NULL; + set_filename(c); + if (is_a_merge) undo_checkpoint(); + undo_suspend(); + if (!read_file(c, is_a_merge)) { + undo_resume(); + widget_browser->rebuild(); + g_project.update_settings_dialog(); + fl_message("Can't read %s: %s", c, strerror(errno)); + free((void *)filename); + filename = oldfilename; + if (main_window) set_modflag(modflag); + return false; + } + undo_resume(); + widget_browser->rebuild(); + if (is_a_merge) { + // Inserting a file; restore the original filename... + set_filename(oldfilename); + set_modflag(1); + } else { + // Loaded a file; free the old filename... + set_modflag(0, 0); + undo_clear(); + } + if (oldfilename) free((void *)oldfilename); + 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 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); +} + +#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 apple_open_cb(const char *c) { + open_project_file(std::string(c)); +} +#endif // __APPLE__ + +/** + Get the absolute path of the project file, for example `/Users/matt/dev/`. + \return the path ending in '/' + */ +std::string Fluid_Project::projectfile_path() const { + return end_with_slash(fl_filename_absolute(fl_filename_path(filename), g_launch_path)); +} + +/** + Get the project file name including extension, for example `test.fl`. + \return the file name without path + */ +std::string Fluid_Project::projectfile_name() const { + return fl_filename_name(filename); +} + +/** + Get the absolute path of the generated C++ code file, for example `/Users/matt/dev/src/`. + \return the path ending in '/' + */ +std::string Fluid_Project::codefile_path() const { + std::string path = fl_filename_path(code_file_name); + if (batch_mode) + return end_with_slash(fl_filename_absolute(path, g_launch_path)); + else + return end_with_slash(fl_filename_absolute(path, projectfile_path())); +} + +/** + Get the generated C++ code file name including extension, for example `test.cxx`. + \return the file name without path + */ +std::string Fluid_Project::codefile_name() const { + std::string name = fl_filename_name(code_file_name); + if (name.empty()) { + return fl_filename_setext(fl_filename_name(filename), ".cxx"); + } else if (name[0] == '.') { + return fl_filename_setext(fl_filename_name(filename), code_file_name); + } else { + return name; + } +} + +/** + Get the absolute path of the generated C++ header file, for example `/Users/matt/dev/src/`. + \return the path ending in '/' + */ +std::string Fluid_Project::headerfile_path() const { + std::string path = fl_filename_path(header_file_name); + if (batch_mode) + return end_with_slash(fl_filename_absolute(path, g_launch_path)); + else + return end_with_slash(fl_filename_absolute(path, projectfile_path())); +} + +/** + Get the generated C++ header file name including extension, for example `test.cxx`. + \return the file name without path + */ +std::string Fluid_Project::headerfile_name() const { + std::string name = fl_filename_name(header_file_name); + if (name.empty()) { + return fl_filename_setext(fl_filename_name(filename), ".h"); + } else if (name[0] == '.') { + return fl_filename_setext(fl_filename_name(filename), header_file_name); + } else { + return name; + } +} + +/** + Get the absolute path of the generated i18n strings file, for example `/Users/matt/dev/`. + Although it may be more useful to put the text file into the same directory + with the source and header file, historically, the text is always saved with + the project file in interactive mode, and in the FLUID launch directory in + batch mode. + \return the path ending in '/' + */ +std::string Fluid_Project::stringsfile_path() const { + if (batch_mode) + return g_launch_path; + else + return projectfile_path(); +} + +/** + Get the generated i18n text file name including extension, for example `test.po`. + \return the file name without path + */ +std::string Fluid_Project::stringsfile_name() const { + switch (i18n_type) { + default: return fl_filename_setext(fl_filename_name(filename), ".txt"); + case FD_I18N_GNU: return fl_filename_setext(fl_filename_name(filename), ".po"); + case FD_I18N_POSIX: return fl_filename_setext(fl_filename_name(filename), ".msg"); + } +} + +/** + Get the name of the project file without the filename extension. + \return the file name without path or extension + */ +std::string Fluid_Project::basename() const { + return fl_filename_setext(fl_filename_name(filename), ""); +} + +/** + 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 code_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 write_code_files(bool dont_show_completion_dialog) +{ + // -- handle user interface issues + flush_text_widgets(); + if (!filename) { + save_cb(0,0); + if (!filename) return 1; + } + + // -- generate the file names with absolute paths + Fd_Code_Writer f; + std::string code_filename = g_project.codefile_path() + g_project.codefile_name(); + std::string header_filename = g_project.headerfile_path() + g_project.headerfile_name(); + + // -- write the code and header files + if (!batch_mode) enter_project_dir(); + int x = f.write_code(code_filename.c_str(), header_filename.c_str()); + std::string code_filename_rel = fl_filename_relative(code_filename); + std::string header_filename_rel = fl_filename_relative(header_filename); + if (!batch_mode) 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 { + 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; +} + +/** + Callback to write C++ code and header files. + */ +void write_cb(Fl_Widget *, void *) { + write_code_files(); +} + +#if 0 +// Matt: disabled +/** + Merge the possibly modified content of code files back into the project. + */ +int mergeback_code_files() +{ + flush_text_widgets(); + if (!filename) return 1; + if (!g_project.write_mergeback_data) { + fl_message("MergeBack is not enabled for this project.\n" + "Please enable MergeBack in the project settings\n" + "dialog and re-save the project file and the code."); + return 0; + } + + std::string proj_filename = g_project.projectfile_path() + g_project.projectfile_name(); + std::string code_filename; +#if 1 + if (!batch_mode) { + Fl_Preferences build_records(Fl_Preferences::USER_L, "fltk.org", "fluid-build"); + Fl_Preferences path(build_records, proj_filename.c_str()); + int i, n = proj_filename.size(); + for (i=0; i<n; i++) if (proj_filename[i]=='\\') proj_filename[i] = '/'; + preferences_get(path, "code", code_filename, ""); + } +#endif + if (code_filename.empty()) + code_filename = g_project.codefile_path() + g_project.codefile_name(); + if (!batch_mode) enter_project_dir(); + int c = merge_back(code_filename, proj_filename, FD_MERGEBACK_INTERACTIVE); + if (!batch_mode) leave_project_dir(); + + if (c==0) fl_message("Comparing\n \"%s\"\nto\n \"%s\"\n\n" + "MergeBack found no external modifications\n" + "in the source code.", + code_filename.c_str(), proj_filename.c_str()); + if (c==-2) fl_message("No corresponding source code file found."); + return c; +} + +void mergeback_cb(Fl_Widget *, void *) { + mergeback_code_files(); +} +#endif + +/** + Write the strings that are used in i18n. + */ +void write_strings_cb(Fl_Widget *, void *) { + flush_text_widgets(); + if (!filename) { + save_cb(0,0); + if (!filename) return; + } + std::string filename = g_project.stringsfile_path() + g_project.stringsfile_name(); + int x = write_strings(filename); + if (batch_mode) { + if (x) { + fprintf(stderr, "%s : %s\n", filename.c_str(), strerror(errno)); + exit(1); + } + } else { + if (x) { + fl_message("Can't write %s: %s", filename.c_str(), strerror(errno)); + } else if (completion_button->value()) { + fl_message("Wrote %s", g_project.stringsfile_name().c_str()); + } + } +} + +/** + Show the editor for the \c current Fl_Type. + */ +void openwidget_cb(Fl_Widget *, void *) { + if (!Fl_Type::current) { + fl_message("Please select a widget"); + return; + } + Fl_Type::current->open(); +} + +/** + User chose to copy the currently selected widgets. + */ +void copy_cb(Fl_Widget*, void*) { + flush_text_widgets(); + if (!Fl_Type::current) { + fl_beep(); + return; + } + flush_text_widgets(); + ipasteoffset = 10; + if (!write_file(cutfname(),1)) { + fl_message("Can't write %s: %s", cutfname(), strerror(errno)); + return; + } +} + +/** + User chose to cut the currently selected widgets. + */ +void cut_cb(Fl_Widget *, void *) { + if (!Fl_Type::current) { + fl_beep(); + return; + } + flush_text_widgets(); + if (!write_file(cutfname(),1)) { + fl_message("Can't write %s: %s", cutfname(), strerror(errno)); + return; + } + undo_checkpoint(); + set_modflag(1); + ipasteoffset = 0; + Fl_Type *p = Fl_Type::current->parent; + while (p && p->selected) p = p->parent; + delete_all(1); + if (p) select_only(p); + widget_browser->rebuild(); +} + +/** + User chose to delete the currently selected widgets. + */ +void delete_cb(Fl_Widget *, void *) { + if (!Fl_Type::current) { + fl_beep(); + return; + } + undo_checkpoint(); + set_modflag(1); + ipasteoffset = 0; + Fl_Type *p = Fl_Type::current->parent; + while (p && p->selected) p = p->parent; + delete_all(1); + if (p) select_only(p); + widget_browser->rebuild(); +} + +/** + 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 paste_cb(Fl_Widget*, void*) { + pasteoffset = ipasteoffset; + undo_checkpoint(); + undo_suspend(); + Strategy strategy = Strategy::FROM_FILE_AFTER_CURRENT; + if (Fl_Type::current && Fl_Type::current->can_have_children()) { + if (Fl_Type::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 (!read_file(cutfname(), 1, strategy)) { + widget_browser->rebuild(); + fl_message("Can't read %s: %s", cutfname(), strerror(errno)); + } + undo_resume(); + widget_browser->display(Fl_Type::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 duplicate_cb(Fl_Widget*, void*) { + if (!Fl_Type::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; + Fl_Type *new_insert = NULL; + if (Fl_Type::current->selected) { + for (Fl_Type *t = Fl_Type::first; t; t = t->next) { + if (t->selected && (t->level <= lowest_level)) { + lowest_level = t->level; + new_insert = t; + } + } + } + if (new_insert) + Fl_Type::current = new_insert; + + // write the selected widgets to a file: + if (!write_file(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; + undo_checkpoint(); + undo_suspend(); + if (!read_file(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(Fl_Type::current); + widget_browser->rebuild(); + undo_resume(); +} + +/** + User wants to sort selected widgets by y coordinate. + */ +static void sort_cb(Fl_Widget *,void *) { + undo_checkpoint(); + sort((Fl_Type*)NULL); + widget_browser->rebuild(); + set_modflag(1); +} + +/** + Open the "About" dialog. + */ +void about_cb(Fl_Widget *, void *) { + if (!about_panel) make_about_panel(); + about_panel->show(); +} + +/** + Open a dialog to show the HTML help page form the FLTK documentation folder. + \param[in] name name of the HTML help file. + */ +void 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")) == NULL) { + 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.4/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.4/license.html"); + return; + } else if (strcmp(name, "index.html")==0) { + fl_open_uri("https://www.fltk.org/doc-1.4/index.html"); + return; + } else { + snprintf(helpname, sizeof(helpname), "https://www.fltk.org/%s", name); + fl_open_uri(helpname); + return; + } + } + help_dialog->show(); +} + +/** + User wants help on Fluid. + */ +void help_cb(Fl_Widget *, void *) { + show_help("fluid.html"); +} + +/** + User wants to see the Fluid manual. + */ +void manual_cb(Fl_Widget *, void *) { + show_help("index.html"); +} + +// ---- Printing + +/** + Open the dialog to allow the user to print the current window. + */ +void print_menu_cb(Fl_Widget *, void *) { + int w, h, ww, hh; + int frompage, topage; + Fl_Type *t; // Current widget + int num_windows; // Number of windows + Fl_Window_Type *windows[1000]; // Windows to print + int winpage; // Current window page + Fl_Window *win; + + for (t = Fl_Type::first, num_windows = 0; t; t = t->next) { + if (t->is_a(ID_Window)) { + windows[num_windows] = (Fl_Window_Type *)t; + if (!((Fl_Window*)(windows[num_windows]->o))->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(NULL); + 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(std::string(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(); +} + +// ---- Main menu bar + +extern void select_layout_preset_cb(Fl_Widget *, void *user_data); +extern void layout_suite_marker(Fl_Widget *, void *user_data); + +static void menu_file_new_cb(Fl_Widget *, void *) { new_project(); } +static void menu_file_new_from_template_cb(Fl_Widget *, void *) { new_project_from_template(); } +static void menu_file_open_cb(Fl_Widget *, void *) { open_project_file(""); } +static void menu_file_insert_cb(Fl_Widget *, void *) { merge_project_file(""); } +static void menu_file_open_history_cb(Fl_Widget *, void *v) { open_project_file(std::string((const char*)v)); } +static void menu_layout_sync_resize_cb(Fl_Menu_ *m, void*) { + if (m->mvalue()->value()) Fl_Type::allow_layout = 1; else Fl_Type::allow_layout = 0; } +/** + This is the main Fluid menu. + + Design history is manipulated right inside this menu structure. + Some menu items change or deactivate correctly, but most items just trigger + various callbacks. + + \c New_Menu creates new widgets and is explained in detail in another location. + + \see New_Menu + \todo This menu needs some major modernization. Menus are too long and their + sorting is not always obvious. + \todo Shortcuts are all over the place (Alt, Ctrl, Command, Shift-Ctrl, + function keys), and there should be a help page listing all shortcuts. + */ +Fl_Menu_Item Main_Menu[] = { +{"&File",0,0,0,FL_SUBMENU}, + {"&New", FL_COMMAND+'n', menu_file_new_cb}, + {"&Open...", FL_COMMAND+'o', menu_file_open_cb}, + {"&Insert...", FL_COMMAND+'i', menu_file_insert_cb, 0, FL_MENU_DIVIDER}, + {"&Save", FL_COMMAND+'s', save_cb, 0}, + {"Save &As...", FL_COMMAND+FL_SHIFT+'s', save_cb, (void*)1}, + {"Sa&ve A Copy...", 0, save_cb, (void*)2}, + {"&Revert...", 0, revert_cb, 0, FL_MENU_DIVIDER}, + {"New &From Template...", FL_COMMAND+'N', menu_file_new_from_template_cb, 0}, + {"Save As &Template...", 0, save_template_cb, 0, FL_MENU_DIVIDER}, + {"&Print...", FL_COMMAND+'p', print_menu_cb}, + {"Write &Code", FL_COMMAND+FL_SHIFT+'c', write_cb, 0}, +// Matt: disabled {"MergeBack Code", FL_COMMAND+FL_SHIFT+'m', mergeback_cb, 0}, + {"&Write Strings", FL_COMMAND+FL_SHIFT+'w', write_strings_cb, 0, FL_MENU_DIVIDER}, + {relative_history[0], FL_COMMAND+'1', menu_file_open_history_cb, absolute_history[0]}, + {relative_history[1], FL_COMMAND+'2', menu_file_open_history_cb, absolute_history[1]}, + {relative_history[2], FL_COMMAND+'3', menu_file_open_history_cb, absolute_history[2]}, + {relative_history[3], FL_COMMAND+'4', menu_file_open_history_cb, absolute_history[3]}, + {relative_history[4], FL_COMMAND+'5', menu_file_open_history_cb, absolute_history[4]}, + {relative_history[5], FL_COMMAND+'6', menu_file_open_history_cb, absolute_history[5]}, + {relative_history[6], FL_COMMAND+'7', menu_file_open_history_cb, absolute_history[6]}, + {relative_history[7], FL_COMMAND+'8', menu_file_open_history_cb, absolute_history[7]}, + {relative_history[8], FL_COMMAND+'9', menu_file_open_history_cb, absolute_history[8]}, + {relative_history[9], 0, menu_file_open_history_cb, absolute_history[9], FL_MENU_DIVIDER}, + {"&Quit", FL_COMMAND+'q', exit_cb}, + {0}, +{"&Edit",0,0,0,FL_SUBMENU}, + {"&Undo", FL_COMMAND+'z', undo_cb}, + {"&Redo", FL_COMMAND+FL_SHIFT+'z', redo_cb, 0, FL_MENU_DIVIDER}, + {"C&ut", FL_COMMAND+'x', cut_cb}, + {"&Copy", FL_COMMAND+'c', copy_cb}, + {"&Paste", FL_COMMAND+'v', paste_cb}, + {"Dup&licate", FL_COMMAND+'u', duplicate_cb}, + {"&Delete", FL_Delete, delete_cb, 0, FL_MENU_DIVIDER}, + {"Select &All", FL_COMMAND+'a', select_all_cb}, + {"Select &None", FL_COMMAND+FL_SHIFT+'a', select_none_cb, 0, FL_MENU_DIVIDER}, + {"Pr&operties...", FL_F+1, openwidget_cb}, + {"&Sort",0,sort_cb}, + {"&Earlier", FL_F+2, earlier_cb}, + {"&Later", FL_F+3, later_cb}, + {"&Group", FL_F+7, group_cb}, + {"Ung&roup", FL_F+8, ungroup_cb,0, FL_MENU_DIVIDER}, + {"Hide O&verlays",FL_COMMAND+FL_SHIFT+'o',toggle_overlays}, + {"Hide Guides",FL_COMMAND+FL_SHIFT+'g',toggle_guides}, + {"Hide Restricted",FL_COMMAND+FL_SHIFT+'r',toggle_restricted}, + {"Show Widget &Bin...",FL_ALT+'b',toggle_widgetbin_cb}, + {"Show Code View",FL_ALT+'c', (Fl_Callback*)toggle_codeview_cb, 0, FL_MENU_DIVIDER}, + {"Settings...",FL_ALT+'p',show_settings_cb}, + {0}, +{"&New", 0, 0, (void *)New_Menu, FL_SUBMENU_POINTER}, +{"&Layout",0,0,0,FL_SUBMENU}, + {"&Align",0,0,0,FL_SUBMENU}, + {"&Left",0,(Fl_Callback *)align_widget_cb,(void*)10}, + {"&Center",0,(Fl_Callback *)align_widget_cb,(void*)11}, + {"&Right",0,(Fl_Callback *)align_widget_cb,(void*)12}, + {"&Top",0,(Fl_Callback *)align_widget_cb,(void*)13}, + {"&Middle",0,(Fl_Callback *)align_widget_cb,(void*)14}, + {"&Bottom",0,(Fl_Callback *)align_widget_cb,(void*)15}, + {0}, + {"&Space Evenly",0,0,0,FL_SUBMENU}, + {"&Across",0,(Fl_Callback *)align_widget_cb,(void*)20}, + {"&Down",0,(Fl_Callback *)align_widget_cb,(void*)21}, + {0}, + {"&Make Same Size",0,0,0,FL_SUBMENU}, + {"&Width",0,(Fl_Callback *)align_widget_cb,(void*)30}, + {"&Height",0,(Fl_Callback *)align_widget_cb,(void*)31}, + {"&Both",0,(Fl_Callback *)align_widget_cb,(void*)32}, + {0}, + {"&Center In Group",0,0,0,FL_SUBMENU}, + {"&Horizontal",0,(Fl_Callback *)align_widget_cb,(void*)40}, + {"&Vertical",0,(Fl_Callback *)align_widget_cb,(void*)41}, + {0}, + {"Synchronized Resize", 0, (Fl_Callback*)menu_layout_sync_resize_cb, NULL, FL_MENU_TOGGLE|FL_MENU_DIVIDER }, + {"&Grid and Size Settings...",FL_COMMAND+'g',show_grid_cb, NULL, FL_MENU_DIVIDER}, + {"Presets", 0, layout_suite_marker, (void*)main_layout_submenu_, FL_SUBMENU_POINTER }, + {"Application", 0, select_layout_preset_cb, (void*)0, FL_MENU_RADIO|FL_MENU_VALUE }, + {"Dialog", 0, select_layout_preset_cb, (void*)1, FL_MENU_RADIO }, + {"Toolbox", 0, select_layout_preset_cb, (void*)2, FL_MENU_RADIO }, + {0}, +{"&Shell", 0, Fd_Shell_Command_List::menu_marker, (void*)Fd_Shell_Command_List::default_menu, FL_SUBMENU_POINTER}, +{"&Help",0,0,0,FL_SUBMENU}, + {"&Rapid development with FLUID...",0,help_cb}, + {"&FLTK Programmers Manual...",0,manual_cb, 0, FL_MENU_DIVIDER}, + {"&About FLUID...",0,about_cb}, + {0}, +{0}}; + +/** + 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 scheme_cb(Fl_Scheme_Choice *choice, void *) { + if (batch_mode) + return; + + // set the new scheme only if the scheme was changed + const char *new_scheme = choice->text(choice->value()); + + if (Fl::is_scheme(new_scheme)) + return; + + Fl::scheme(new_scheme); + fluid_prefs.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) + fluid_prefs.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 init_scheme() { + int scheme_index = 0; // scheme index for backwards compatibility (1.3.x) + char *scheme_name = 0; // scheme name since 1.4.0 + fluid_prefs.get("scheme_name", scheme_name, "XXX"); // XXX means: not set => fallback 1.3.x + if (!strcmp(scheme_name, "XXX")) { + fluid_prefs.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)); + fluid_prefs.set("scheme_name", scheme_name); + } + // Set the new scheme only if it was not overridden by the -scheme + // command line option + if (Fl::scheme() == NULL) { + Fl::scheme(scheme_name); + } + free(scheme_name); +} + +/** + Show or hide the widget bin. + The state is stored in the app preferences. + */ +void toggle_widgetbin_cb(Fl_Widget *, void *) { + 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"); + } +} + +/** + Show or hide the code preview window. + */ +void toggle_codeview_cb(Fl_Double_Window *, void *) { + codeview_toggle_visibility(); +} + +/** + Show or hide the code preview window, button callback. + */ +void toggle_codeview_b_cb(Fl_Button*, void *) { + codeview_toggle_visibility(); +} + +/** + Build the main app window and create a few other dialogs. + */ +void make_main_window() { + if (!batch_mode) { + fluid_prefs.get("show_guides", show_guides, 1); + fluid_prefs.get("show_restricted", show_restricted, 1); + fluid_prefs.get("show_ghosted_outline", show_ghosted_outline, 0); + fluid_prefs.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(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) { + load_history(); + g_shell_config = new Fd_Shell_Command_List; + widget_browser->load_prefs(); + make_settings_window(); + } +} + +/** + Load file history from preferences. + + This loads the absolute filepaths of the last 10 used design files. + It also computes and stores the relative filepaths for display in + the main menu. + */ +void load_history() { + int i; // Looping var + int max_files; + + fluid_prefs.get("recent_files", max_files, 5); + if (max_files > 10) max_files = 10; + + for (i = 0; i < max_files; i ++) { + fluid_prefs.get( Fl_Preferences::Name("file%d", i), absolute_history[i], "", sizeof(absolute_history[i])); + if (absolute_history[i][0]) { + // Make a shortened version of the filename for the menu... + std::string fn = fl_filename_shortened(absolute_history[i], 48); + strncpy(relative_history[i], fn.c_str(), sizeof(relative_history[i]) - 1); + if (i == 9) history_item[i].flags = FL_MENU_DIVIDER; + else history_item[i].flags = 0; + } else break; + } + + for (; i < 10; i ++) { + if (i) history_item[i-1].flags |= FL_MENU_DIVIDER; + history_item[i].hide(); + } +} + +/** + Update file history from preferences. + + Add this new filepath to the history and update the main menu. + Writes the new file history to the app preferences. + + \param[in] flname path or filename of .fl file, will be converted into an + absolute file path based on the current working directory. + */ +void update_history(const char *flname) { + int i; // Looping var + char absolute[FL_PATH_MAX]; + int max_files; + + + fluid_prefs.get("recent_files", max_files, 5); + if (max_files > 10) max_files = 10; + + fl_filename_absolute(absolute, sizeof(absolute), flname); +#ifdef _WIN32 + // Make path canonical. + for (char *s = absolute; *s; s++) { + if (*s == '\\') + *s = '/'; + } +#endif + + + for (i = 0; i < max_files; i ++) +#if defined(_WIN32) || defined(__APPLE__) + if (!strcasecmp(absolute, absolute_history[i])) break; +#else + if (!strcmp(absolute, absolute_history[i])) break; +#endif // _WIN32 || __APPLE__ + + if (i == 0) return; + + if (i >= max_files) i = max_files - 1; + + // Move the other flnames down in the list... + memmove(absolute_history + 1, absolute_history, + i * sizeof(absolute_history[0])); + memmove(relative_history + 1, relative_history, + i * sizeof(relative_history[0])); + + // Put the new file at the top... + strlcpy(absolute_history[0], absolute, sizeof(absolute_history[0])); + std::string fn = fl_filename_shortened(absolute_history[0], 48); + strncpy(relative_history[0], fn.c_str(), sizeof(relative_history[0]) - 1); + + // Update the menu items as needed... + for (i = 0; i < max_files; i ++) { + fluid_prefs.set( Fl_Preferences::Name("file%d", i), absolute_history[i]); + if (absolute_history[i][0]) { + if (i == 9) history_item[i].flags = FL_MENU_DIVIDER; + else history_item[i].flags = 0; + } else break; + } + + for (; i < 10; i ++) { + fluid_prefs.set( Fl_Preferences::Name("file%d", i), ""); + if (i) history_item[i-1].flags |= FL_MENU_DIVIDER; + history_item[i].hide(); + } + fluid_prefs.flush(); +} + +/** + Set the filename of the current .fl design. + \param[in] c the new absolute filename and path + */ +void set_filename(const char *c) { + if (filename) free((void *)filename); + filename = c ? fl_strdup(c) : NULL; + + if (filename && !batch_mode) + update_history(filename); + + set_modflag(modflag); +} + + +/** + Set the "modified" flag and update the title of the main window. + + The first argument sets the modification state of the current design against + the corresponding .fl design file. Any change to the widget tree will mark + the design 'modified'. Saving the design will mark it clean. + + The second argument is optional and set the modification state of the current + design against the source code and header file. Any change to the tree, + including saving the tree, will mark the code 'outdated'. Generating source + code and header files will clear this flag until the next modification. + + \param[in] mf 0 to clear the modflag, 1 to mark the design "modified", -1 to + ignore this parameter + \param[in] mfc default -1 to let \c mf control \c modflag_c, 0 to mark the + code files current, 1 to mark it out of date. -2 to ignore changes to mf. + */ +void set_modflag(int mf, int mfc) { + const char *code_ext = NULL; + char new_title[FL_PATH_MAX]; + + // Update the modflag_c to the worst possible condition. We could be a bit + // more graceful and compare modification times of the files, but C++ has + // no API for that until C++17. + if (mf!=-1) { + modflag = mf; + if (mfc==-1 && mf==1) + mfc = mf; + } + if (mfc>=0) { + modflag_c = mfc; + } + + if (main_window) { + std::string basename; + if (!filename) basename = "Untitled.fl"; + else basename = fl_filename_name(std::string(filename)); + code_ext = fl_filename_ext(g_project.code_file_name.c_str()); + char mod_star = modflag ? '*' : ' '; + char mod_c_star = modflag_c ? '*' : ' '; + snprintf(new_title, sizeof(new_title), "%s%c %s%c", + basename.c_str(), mod_star, code_ext, mod_c_star); + const char *old_title = main_window->label(); + // only update the title if it actually changed + if (!old_title || strcmp(old_title, new_title)) + main_window->copy_label(new_title); + } + // if the UI was modified in any way, update the Code View panel + if (codeview_panel && codeview_panel->visible() && cv_autorefresh->value()) + codeview_defer_update(); +} + +// ---- Main program entry point + +/** + Handle command line arguments. + \param[in] argc number of arguments in the list + \param[in] argv pointer to an array of arguments + \param[inout] i current argument index + \return number of arguments used; if 0, the argument is not supported + */ +static int arg(int argc, char** argv, int& i) { + if (argv[i][0] != '-') + return 0; + if (argv[i][1] == 'd' && !argv[i][2]) { + G_debug=1; + i++; return 1; + } + if (argv[i][1] == 'u' && !argv[i][2]) { + update_file++; + batch_mode++; + i++; return 1; + } + if (argv[i][1] == 'c' && !argv[i][2]) { + compile_file++; + batch_mode++; + i++; return 1; + } + if ((strcmp(argv[i], "-v")==0) || (strcmp(argv[i], "--version")==0)) { + show_version = 1; + i++; return 1; + } + if (argv[i][1] == 'c' && argv[i][2] == 's' && !argv[i][3]) { + compile_file++; + compile_strings++; + batch_mode++; + i++; return 1; + } + if (argv[i][1] == 'o' && !argv[i][2] && i+1 < argc) { + g_code_filename_arg = argv[i+1]; + batch_mode++; + i += 2; return 2; + } +#ifndef NDEBUG + if ((i+1 < argc) && (strcmp(argv[i], "--autodoc") == 0)) { + g_autodoc_path = argv[i+1]; + i += 2; return 2; + } +#endif + if (strcmp(argv[i], "--help")==0) { + return 0; + } + if (argv[i][1] == 'h' && !argv[i][2]) { + if ( (i+1 < argc) && (argv[i+1][0] != '-') ) { + g_header_filename_arg = argv[i+1]; + batch_mode++; + i += 2; + return 2; + } else { + // a lone "-h" without a filename will output the help string + return 0; + } + } + return 0; +} + +#if ! (defined(_WIN32) && !defined (__CYGWIN__)) + +int quit_flag = 0; +#include <signal.h> +#ifdef _sigargs +#define SIGARG _sigargs +#else +#ifdef __sigargs +#define SIGARG __sigargs +#else +#define SIGARG int // you may need to fix this for older systems +#endif +#endif + +extern "C" { +static void sigint(SIGARG) { + signal(SIGINT,sigint); + quit_flag = 1; +} +} + +#endif + +/** + 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 fluid_main(int argc,char **argv) { + int i = 1; + + setlocale(LC_ALL, ""); // enable multi-language errors in file chooser + setlocale(LC_NUMERIC, "C"); // make sure numeric values are written correctly + g_launch_path = end_with_slash(fl_getcwd()); // store the current path at launch + + Fl::args_to_utf8(argc, argv); // for MSYS2/MinGW + if ( (Fl::args(argc,argv,i,arg) == 0) // unsupported argument found + || (batch_mode && (i != argc-1)) // .fl filename missing + || (!batch_mode && (i < argc-1)) // more than one filename found + || (argv[i] && (argv[i][0] == '-'))) { // unknown option + static const char *msg = + "usage: %s <switches> name.fl\n" + " -u : update .fl file and exit (may be combined with '-c' or '-cs')\n" + " -c : write .cxx and .h and exit\n" + " -cs : write .cxx and .h and strings and exit\n" + " -o <name> : .cxx output filename, or extension if <name> starts with '.'\n" + " -h <name> : .h output filename, or extension if <name> starts with '.'\n" + " --help : brief usage information\n" + " --version, -v : print fluid version number\n" + " -d : enable internal debugging\n"; + const char *app_name = NULL; + if ( (argc > 0) && argv[0] && argv[0][0] ) + app_name = fl_filename_name(argv[0]); + if ( !app_name || !app_name[0]) + app_name = "fluid"; +#ifdef _MSC_VER + // TODO: if this is fluid-cmd, use stderr and not fl_message + fl_message(msg, app_name); +#else + fprintf(stderr, msg, app_name); +#endif + return 1; + } + if (show_version) { + printf("fluid v%d.%d.%d\n", FL_MAJOR_VERSION, FL_MINOR_VERSION, FL_PATCH_VERSION); + ::exit(0); + } + + const char *c = NULL; + if (g_autodoc_path.empty()) + c = argv[i]; + + fl_register_images(); + + make_main_window(); + + if (c) 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(fluid_prefs, FD_STORE_USER); + g_shell_config->update_settings_dialog(); + g_shell_config->rebuild_shell_menu(); + } + g_layout_list.read(fluid_prefs, FD_STORE_USER); + main_window->show(argc,argv); + toggle_widgetbin_cb(0,0); + toggle_codeview_cb(0,0); + if (!c && openlast_button->value() && absolute_history[0][0] && g_autodoc_path.empty()) { + // Open previous file when no file specified... + open_project_file(absolute_history[0]); + } + } + undo_suspend(); + if (c && !read_file(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)); + } + undo_resume(); + + // command line args override code and header filenames from the project file + // in batch mode only + if (batch_mode) { + if (!g_code_filename_arg.empty()) { + g_project.code_file_set = 1; + g_project.code_file_name = g_code_filename_arg; + } + if (!g_header_filename_arg.empty()) { + g_project.header_file_set = 1; + g_project.header_file_name = g_header_filename_arg; + } + } + + if (update_file) { // fluid -u + write_file(c,0); + if (!compile_file) + exit(0); + } + + if (compile_file) { // fluid -c[s] + if (compile_strings) + write_strings_cb(0,0); + write_cb(0,0); + exit(0); + } + + // don't lock up if inconsistent command line arguments were given + if (batch_mode) + exit(0); + + set_modflag(0); + undo_clear(); +#ifndef _WIN32 + signal(SIGINT,sigint); +#endif + + // 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 (!g_autodoc_path.empty()) { + run_autodoc(g_autodoc_path); + set_modflag(0, 0); + exit_cb(0,0); + return 0; + } +#endif + +#ifdef _WIN32 + Fl::run(); +#else + while (!quit_flag) Fl::wait(); + if (quit_flag) exit_cb(0,0); +#endif // _WIN32 + + undo_clear(); + return (0); +} + +/// \} + diff --git a/fluid/app/fluid.h b/fluid/app/fluid.h new file mode 100644 index 000000000..d02ae79cc --- /dev/null +++ b/fluid/app/fluid.h @@ -0,0 +1,208 @@ +// +// FLUID main entry for the Fast Light Tool Kit (FLTK). +// +// Copyright 1998-2025 by Bill Spitzak and others. +// +// This library is free software. Distribution and use rights are outlined in +// the file "COPYING" which should have been included with this file. If this +// file is missing or damaged, see the license at: +// +// https://www.fltk.org/COPYING.php +// +// Please see the following page on how to report bugs and issues: +// +// https://www.fltk.org/bugs.php +// + +#ifndef _FLUID_FLUID_H +#define _FLUID_FLUID_H + +#include "tools/fluid_filename.h" + +#include <FL/Fl_Preferences.H> +#include <FL/Fl_Menu_Item.H> + +#include <string> + +#define BROWSERWIDTH 300 +#define BROWSERHEIGHT 500 +#define WINWIDTH 300 +#define MENUHEIGHT 25 +#define WINHEIGHT (BROWSERHEIGHT+MENUHEIGHT) + +// ---- types + +class Fl_Double_Window; +class Fl_Window; +class Fl_Menu_Bar; +class Fl_Type; +class Fl_Choice; +class Fl_Button; +class Fl_Check_Button; + +/** + Indicate the storage location for tools like layout suites and shell macros. + \see class Fd_Shell_Command, class Fd_Layout_Suite + */ +typedef enum { + FD_STORE_INTERNAL, ///< stored inside FLUID app + FD_STORE_USER, ///< suite is stored in the user wide FLUID settings + FD_STORE_PROJECT, ///< suite is stored within the current .fl project file + FD_STORE_FILE ///< store suite in external file +} Fd_Tool_Store; + +// ---- global variables + +extern Fl_Preferences fluid_prefs; +extern Fl_Menu_Item Main_Menu[]; +extern Fl_Menu_Bar *main_menubar; +extern Fl_Window *main_window; + +extern int show_guides; +extern int show_restricted; +extern int show_ghosted_outline; +extern int show_comments; + +extern int G_use_external_editor; +extern int G_debug; +extern char G_external_editor_command[512]; + +// File history info... +extern char absolute_history[10][FL_PATH_MAX]; +extern char relative_history[10][FL_PATH_MAX]; +extern void load_history(); +extern void update_history(const char *); + +extern Fl_Menu_Item *save_item; +extern Fl_Menu_Item *history_item; +extern Fl_Menu_Item *widgetbin_item; +extern Fl_Menu_Item *codeview_item; +extern Fl_Menu_Item *overlay_item; +extern Fl_Button *overlay_button; +extern Fl_Menu_Item *guides_item; +extern Fl_Menu_Item *restricted_item; +extern Fl_Check_Button *guides_button; + +extern int modflag; + +extern int update_file; // fluid -u +extern int compile_file; // fluid -c +extern int compile_strings; // fluic -cs +extern int batch_mode; + +extern int pasteoffset; + +extern std::string g_code_filename_arg; +extern std::string g_header_filename_arg; +extern std::string g_launch_path; + +extern std::string g_autodoc_path; + +// ---- project class declaration + +/** + Enumeration of available internationalization types. + */ +typedef enum { + FD_I18N_NONE = 0, ///< No i18n, all strings are litearals + FD_I18N_GNU, ///< GNU gettext internationalization + FD_I18N_POSIX ///< Posix catgets internationalization +} Fd_I18n_Type; + +/** + Data and settings for a FLUID project file. + */ +class Fluid_Project { +public: + Fluid_Project(); + ~Fluid_Project(); + void reset(); + void update_settings_dialog(); + + std::string projectfile_path() const; + std::string projectfile_name() const; + std::string codefile_path() const; + std::string codefile_name() const; + std::string headerfile_path() const; + std::string headerfile_name() const; + std::string stringsfile_path() const; + std::string stringsfile_name() const; + std::string basename() const; + + /// One of the available internationalization types. + Fd_I18n_Type i18n_type; + /// Include file for GNU i18n, writes an #include statement into the source + /// file. This is usually `<libintl.h>` or `"gettext.h"` for GNU gettext. + std::string i18n_gnu_include; + // Optional name of a macro for conditional i18n compilation. + std::string i18n_gnu_conditional; + /// For the gettext/intl.h options, this is the function that translates text + /// at runtime. This is usually "gettext" or "_". + std::string i18n_gnu_function; + /// For the gettext/intl.h options, this is the function that marks the translation + /// of text at initialisation time. This is usually "gettext_noop" or "N_". + std::string i18n_gnu_static_function; + + /// Include file for Posix i18n, write a #include statement into the source + /// file. This is usually `<nl_types.h>` for Posix catgets. + std::string i18n_pos_include; + // Optional name of a macro for conditional i18n compilation. + std::string i18n_pos_conditional; + /// Name of the nl_catd database + std::string i18n_pos_file; + /// Message set ID for the catalog. + std::string i18n_pos_set; + + /// If set, generate code to include the header file form the c++ file + int include_H_from_C; + /// If set, handle keyboard shortcut Ctrl on macOS using Cmd instead + int use_FL_COMMAND; + /// Clear if UTF-8 characters in statics texts are written as escape sequences + int utf8_in_src; + /// If set, <FL/Fl.H> will not be included from the header code before anything else + int avoid_early_includes; + /// If set, command line overrides header file name in .fl file. + int header_file_set; + /// If set, command line overrides source code file name in .fl file. + int code_file_set; + int write_mergeback_data; + /// Hold the default extension for header files, or the entire filename if set via command line. + std::string header_file_name; + /// Hold the default extension for source code files, or the entire filename if set via command line. + std::string code_file_name; +}; + +extern Fluid_Project g_project; + +// ---- public functions + +extern int fluid_main(int argc,char **argv); + +extern bool new_project(bool user_must_confirm = true); +extern void enter_project_dir(); +extern void leave_project_dir(); +extern void set_filename(const char *c); +extern void set_modflag(int mf, int mfc=-1); + +extern const std::string &get_tmpdir(); + +// ---- public callback functions + +extern void save_cb(Fl_Widget *, void *v); +extern void save_template_cb(Fl_Widget *, void *); +extern void revert_cb(Fl_Widget *,void *); +extern void exit_cb(Fl_Widget *,void *); + +extern int write_code_files(bool dont_show_completion_dialog=false); +extern void write_strings_cb(Fl_Widget *, void *); +extern void align_widget_cb(Fl_Widget *, long); +extern void toggle_widgetbin_cb(Fl_Widget *, void *); + +extern char position_window(Fl_Window *w, const char *prefsName, int Visible, int X, int Y, int W=0, int H=0); + +inline int fd_min(int a, int b) { return (a < b ? a : b); } +inline int fd_max(int a, int b) { return (a > b ? a : b); } +inline int fd_min(int a, int b, int c) { return fd_min(a, fd_min(b, c)); } + +#endif // _FLUID_FLUID_H + diff --git a/fluid/app/mergeback.cxx b/fluid/app/mergeback.cxx new file mode 100644 index 000000000..f914f6e1e --- /dev/null +++ b/fluid/app/mergeback.cxx @@ -0,0 +1,493 @@ +// +// MergeBack 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 +// + +#if 0 +// Matt: disabled + +#include "app/mergeback.h" + +#include "app/fluid.h" +#include "app/undo.h" +#include "io/code.h" +#include "nodes/Fl_Function_Type.h" +#include "nodes/Fl_Widget_Type.h" + +#include <FL/Fl_Window.H> +#include <FL/fl_ask.H> + +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <ctype.h> +#include <zlib.h> + +extern void propagate_load(Fl_Group*, void*); +extern void load_panel(); +extern void redraw_browser(); + +// TODO: add application user setting to control mergeback +// [] new projects default to mergeback +// [] check mergeback when loading project +// [] check mergeback when app gets focus +// [] always apply if safe +// TODO: command line option for mergeback +// -mb or --merge-back +// -mbs or --merge-back-if-safe +// NOTE: automatic mergeback on timer when file changes if app focus doesn't work +// NOTE: allow the user to edit comment blocks + +/** + Merge external changes in a source code file back into the current project. + + This experimental function reads a source code file line by line. When it + encounters a special tag in a line, the crc32 stored in the tag is compared + to the crc32 that was calculated from the code lines since the previous tag. + + If the crc's differ, the user has modified the source file externally, and the + given block differs from the block as it was generated by FLUID. Depending on + the block type, the user has modified the widget code (FD_TAG_GENERIC), which + can not be transferred back into the project. + + Modifications to code blocks and callbacks (CODE, CALLBACK) can be merged back + into the project. Their corresponding Fl_Type is found using the unique + node id that is part of the tag. The block is only merged back if the crc's + from the project and from the edited block differ. + + The caller must make sure that this code file was generated by the currently + loaded project. + + The user is informed in detailed dialogs what the function discovered and + offered to merge or cancel if appropriate. Just in case this function is + destructive, "undo" restores the state before a MergeBack. + + Callers can set different task. FD_MERGEBACK_ANALYSE checks if there are any + modifications in the code file and returns -1 if there was an error, or a + bit field where bit 0 is set if internal structures were modified, bit 1 if + code was changed, and bit 2 if modified blocks were found, but no Type node. + Bit 3 is set, if code was changed in the code file *and* the project. + + FD_MERGEBACK_INTERACTIVE checks for changes and presents a status dialog box + to the user if there were conflicting changes or if a mergeback is possible, + presenting the user the option to merge or cancel. Returns 0 if the project + remains unchanged, and 1 if the user merged changes back. -1 is returned if an + invalid tag was found. + + FD_MERGEBACK_APPLY merges all changes back into the project without any + interaction. Returns 0 if nothing changed, and 1 if it merged any changes back. + + FD_MERGEBACK_APPLY_IF_SAFE merges changes back only if there are no conflicts. + Returns 0 if nothing changed, and 1 if it merged any changes back, and -1 if + there were conflicts. + + \note this function is currently part of Fd_Code_Writer to get easy access + to our crc32 code that also wrote the code file originally. + + \param[in] s path and filename of the source code file + \param[in] task see above + \return -1 if an error was found in a tag + \return -2 if no code file was found + \return see above + */ +int merge_back(const std::string &s, const std::string &p, int task) { + if (g_project.write_mergeback_data) { + Fd_Mergeback mergeback; + return mergeback.merge_back(s, p, task); + } else { + // nothing to be done if the mergeback option is disabled in the project + return 0; + } +} + +/** Allocate and initialize MergeBack class. */ +Fd_Mergeback::Fd_Mergeback() : + code(NULL), + line_no(0), + tag_error(0), + num_changed_code(0), + num_changed_structure(0), + num_uid_not_found(0), + num_possible_override(0) +{ +} + +/** Release allocated resources. */ +Fd_Mergeback::~Fd_Mergeback() +{ + if (code) ::fclose(code); +} + +/** Remove the first two spaces at every line start. + \param[inout] s block of C code + */ +void Fd_Mergeback::unindent(char *s) { + char *d = s; + bool line_start = true; + while (*s) { + if (line_start) { + if (*s>0 && isspace(*s)) s++; + if (*s>0 && isspace(*s)) s++; + line_start = false; + } + if (*s=='\r') s++; + if (*s=='\n') line_start = true; + *d++ = *s++; + } + *d = 0; +} + +/** + Read a block of text from the source file and remove the leading two spaces in every line. + \param[in] start start of the block within the file + \param[in] end end of text within the file + \return a string holding the text that was found in the file + */ +std::string Fd_Mergeback::read_and_unindent_block(long start, long end) { + long bsize = end-start; + long here = ::ftell(code); + ::fseek(code, start, SEEK_SET); + char *block = (char*)::malloc(bsize+1); + size_t n = ::fread(block, bsize, 1, code); + if (n!=1) + block[0] = 0; // read error + else + block[bsize] = 0; + unindent(block); + std::string str = block; + ::free(block); + ::fseek(code, here, SEEK_SET); + return str; +} + +/** Tell user the results of our MergeBack analysis and pop up a dialog to give + the user a choice to merge or cancel. + \return 1 if the user wants to merge (choice dialog was shown) + \return 0 if there is nothing to merge (no dialog was shown) + \return -1 if the user wants to cancel or an error occurred or an issue was presented + (message or choice dialog was shown) + */ +int Fd_Mergeback::ask_user_to_merge(const std::string &code_filename, const std::string &proj_filename) { + if (tag_error) { + fl_message("Comparing\n \"%s\"\nto\n \"%s\"\n\n" + "MergeBack found an error in line %d while reading tags\n" + "from the source code. Merging code back is not possible.", + code_filename.c_str(), proj_filename.c_str(), line_no); + return -1; + } + if (!num_changed_code && !num_changed_structure) { + return 0; + } + if (num_changed_structure && !num_changed_code) { + fl_message("Comparing\n \"%1$s\"\nto\n \"%2$s\"\n\n" + "MergeBack found %3$d modifications in the project structure\n" + "of the source code. These kind of changes can no be\n" + "merged back and will be lost when the source code is\n" + "generated again from the open project.", + code_filename.c_str(), proj_filename.c_str(), num_changed_structure); + return -1; + } + std::string msg = "Comparing\n \"%1$s\"\nto\n \"%2$s\"\n\n" + "MergeBack found %3$d modifications in the source code."; + if (num_possible_override) + msg += "\n\nWARNING: %6$d of these modified blocks appear to also have\n" + "changed in the project. Merging will override changes in\n" + "the project with changes from the source code file."; + if (num_uid_not_found) + msg += "\n\nWARNING: for %4$d of these modifications no Type node\n" + "can be found and these modification can't be merged back."; + if (!num_possible_override && !num_uid_not_found) + msg += "\nMerging these changes back appears to be safe."; + + if (num_changed_structure) + msg += "\n\nWARNING: %5$d modifications were found in the project\n" + "structure. These kind of changes can no be merged back\n" + "and will be lost when the source code is generated again\n" + "from the open project."; + + if (num_changed_code==num_uid_not_found) { + fl_message(msg.c_str(), + code_filename.c_str(), proj_filename.c_str(), + num_changed_code, num_uid_not_found, + num_changed_structure, num_possible_override); + return -1; + } else { + msg += "\n\nClick Cancel to abort the MergeBack operation.\n" + "Click Merge to merge all code changes back into\n" + "the open project."; + int c = fl_choice(msg.c_str(), "Cancel", "Merge", NULL, + code_filename.c_str(), proj_filename.c_str(), + num_changed_code, num_uid_not_found, + num_changed_structure, num_possible_override); + if (c==0) return -1; + return 1; + } +} + +/** Analyse the block and its corresponding widget callback. + Return findings in num_changed_code, num_changed_code, and num_uid_not_found. + */ +void Fd_Mergeback::analyse_callback(unsigned long code_crc, unsigned long tag_crc, int uid) { + Fl_Type *tp = Fl_Type::find_by_uid(uid); + if (tp && tp->is_true_widget()) { + std::string cb = tp->callback(); cb += "\n"; + unsigned long project_crc = Fd_Code_Writer::block_crc(cb.c_str()); + // check if the code and project crc are the same, so this modification was already applied + if (project_crc!=code_crc) { + num_changed_code++; + // check if the block change on the project side as well, so we may override changes + if (project_crc!=tag_crc) { + num_possible_override++; + } + } + } else { + num_uid_not_found++; + num_changed_code++; + } +} + +/** Analyse the block and its corresponding Code Type. + Return findings in num_changed_code, num_changed_code, and num_uid_not_found. + */ +void Fd_Mergeback::analyse_code(unsigned long code_crc, unsigned long tag_crc, int uid) { + Fl_Type *tp = Fl_Type::find_by_uid(uid); + if (tp && tp->is_a(ID_Code)) { + std::string code = tp->name(); code += "\n"; + unsigned long project_crc = Fd_Code_Writer::block_crc(code.c_str()); + // check if the code and project crc are the same, so this modification was already applied + if (project_crc!=code_crc) { + num_changed_code++; + // check if the block change on the project side as well, so we may override changes + if (project_crc!=tag_crc) { + num_possible_override++; + } + } + } else { + num_changed_code++; + num_uid_not_found++; + } +} + + +/** Analyse the code file and return findings in class member variables. + + The code file must be open for reading already. + + * tag_error is set if a tag was found, but could not be read + * line_no returns the line where an error occurred + * num_changed_code is set to the number of changed code blocks in the file. + Code changes can be merged back to the project. + * num_changed_structure is set to the number of structural changes. + Structural changes outside of code blocks can not be read back. + * num_uid_not_found number of blocks that were modified, but the corresponding + type or widget can not be found in the project + * num_possible_override number of blocks that were changed in the code file, + but also were changed in the project. + + \return -1 if reading a tag failed, otherwise 0 + */ +int Fd_Mergeback::analyse() { + // initialize local variables + unsigned long code_crc = 0; + bool line_start = true; + char line[1024]; + // bail if the caller has not opened a file yet + if (!code) return 0; + // initialize member variables to return our findings + line_no = 0; + tag_error = 0; + num_changed_code = 0; + num_changed_structure = 0; + num_uid_not_found = 0; + num_possible_override = 0; + code_crc = 0; + // loop through all lines in the code file + ::fseek(code, 0, SEEK_SET); + for (;;) { + // get the next line until end of file + if (fgets(line, 1023, code)==0) break; + line_no++; + const char *tag = strstr(line, "//~fl~"); + if (!tag) { + // if this line has no tag, add the contents to the CRC and continue + code_crc = Fd_Code_Writer::block_crc(line, -1, code_crc, &line_start); + } else { + // if this line has a tag, read all tag data + int tag_type = -1, uid = 0; + unsigned long tag_crc = 0; + int n = sscanf(tag, "//~fl~%d~%04x~%08lx~~", &tag_type, &uid, &tag_crc); + if (n!=3 || tag_type<0 || tag_type>FD_TAG_LAST ) { tag_error = 1; return -1; } + if (code_crc != tag_crc) { + switch (tag_type) { + case FD_TAG_GENERIC: + num_changed_structure++; + break; + case FD_TAG_MENU_CALLBACK: + case FD_TAG_WIDGET_CALLBACK: + analyse_callback(code_crc, tag_crc, uid); + break; + case FD_TAG_CODE: + analyse_code(code_crc, tag_crc, uid); + break; + } + } + // reset everything for the next block + code_crc = 0; + line_start = true; + } + } + return 0; +} + +/** Apply callback mergebacks from the code file to the project. + \return 1 if the project changed + */ +int Fd_Mergeback::apply_callback(long block_end, long block_start, unsigned long code_crc, int uid) { + Fl_Type *tp = Fl_Type::find_by_uid(uid); + if (tp && tp->is_true_widget()) { + std::string cb = tp->callback(); cb += "\n"; + unsigned long project_crc = Fd_Code_Writer::block_crc(cb.c_str()); + if (project_crc!=code_crc) { + tp->callback(read_and_unindent_block(block_start, block_end).c_str()); + return 1; + } + } + return 0; +} + +/** Apply callback mergebacks from the code file to the project. + \return 1 if the project changed + */ +int Fd_Mergeback::apply_code(long block_end, long block_start, unsigned long code_crc, int uid) { + Fl_Type *tp = Fl_Type::find_by_uid(uid); + if (tp && tp->is_a(ID_Code)) { + std::string cb = tp->name(); cb += "\n"; + unsigned long project_crc = Fd_Code_Writer::block_crc(cb.c_str()); + if (project_crc!=code_crc) { + tp->name(read_and_unindent_block(block_start, block_end).c_str()); + return 1; + } + } + return 0; +} + +/** Apply all possible mergebacks from the code file to the project. + The code file must be open for reading already. + \return -1 if reading a tag failed, 0 if nothing changed, 1 if the project changed + */ +int Fd_Mergeback::apply() { + // initialize local variables + unsigned long code_crc = 0; + bool line_start = true; + char line[1024]; + int changed = 0; + long block_start = 0; + long block_end = 0; + // bail if the caller has not opened a file yet + if (!code) return 0; + // initialize member variables to return our findings + line_no = 0; + tag_error = 0; + code_crc = 0; + // loop through all lines in the code file + ::fseek(code, 0, SEEK_SET); + for (;;) { + // get the next line until end of file + if (fgets(line, 1023, code)==0) break; + line_no++; + const char *tag = strstr(line, "//~fl~"); + if (!tag) { + // if this line has no tag, add the contents to the CRC and continue + code_crc = Fd_Code_Writer::block_crc(line, -1, code_crc, &line_start); + block_end = ::ftell(code); + } else { + // if this line has a tag, read all tag data + int tag_type = -1, uid = 0; + unsigned long tag_crc = 0; + int n = sscanf(tag, "//~fl~%d~%04x~%08lx~~", &tag_type, &uid, &tag_crc); + if (n!=3 || tag_type<0 || tag_type>FD_TAG_LAST ) { tag_error = 1; return -1; } + if (code_crc != tag_crc) { + if (tag_type==FD_TAG_MENU_CALLBACK || tag_type==FD_TAG_WIDGET_CALLBACK) { + changed |= apply_callback(block_end, block_start, code_crc, uid); + } else if (tag_type==FD_TAG_CODE) { + changed |= apply_code(block_end, block_start, code_crc, uid); + } + } + // reset everything for the next block + code_crc = 0; + line_start = true; + block_start = ::ftell(code); + } + } + return changed; +} + +/** Dispatch the MergeBack into analysis, interactive, or apply directly. + \param[in] s source code filename and path + \param[in] task one of FD_MERGEBACK_ANALYSE, FD_MERGEBACK_INTERACTIVE, + FD_MERGEBACK_APPLY_IF_SAFE, or FD_MERGEBACK_APPLY + \return -1 if an error was found in a tag + \return -2 if no code file was found + \return See more at ::merge_back(const std::string &s, int task). + */ +int Fd_Mergeback::merge_back(const std::string &s, const std::string &p, int task) { + int ret = 0; + code = fl_fopen(s.c_str(), "rb"); + if (!code) return -2; + do { // no actual loop, just make sure we close the code file + if (task == FD_MERGEBACK_ANALYSE) { + analyse(); + if (tag_error) {ret = -1; break; } + if (num_changed_structure) ret |= 1; + if (num_changed_code) ret |= 2; + if (num_uid_not_found) ret |= 4; + if (num_possible_override) ret |= 8; + break; + } + if (task == FD_MERGEBACK_INTERACTIVE) { + analyse(); + ret = ask_user_to_merge(s, p); + if (ret != 1) + return ret; + task = FD_MERGEBACK_APPLY; // fall through + } + if (task == FD_MERGEBACK_APPLY_IF_SAFE) { + analyse(); + if (tag_error || num_changed_structure || num_possible_override) { + ret = -1; + break; + } + if (num_changed_code==0) { + ret = 0; + break; + } + task = FD_MERGEBACK_APPLY; // fall through + } + if (task == FD_MERGEBACK_APPLY) { + ret = apply(); + if (ret == 1) { + set_modflag(1); + redraw_browser(); + load_panel(); + } + ret = 1; // avoid message box in caller + } + } while (0); + fclose(code); + code = NULL; + return ret; +} + +#endif + diff --git a/fluid/app/mergeback.h b/fluid/app/mergeback.h new file mode 100644 index 000000000..f828e9669 --- /dev/null +++ b/fluid/app/mergeback.h @@ -0,0 +1,81 @@ +// +// MergeBack routines for the Fast Light Tool Kit (FLTK). +// +// Copyright 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 +// + +// Matt: disabled +#if 0 + +#ifndef _FLUID_MERGEBACK_H +#define _FLUID_MERGEBACK_H + +#include <FL/fl_attr.h> + +#include <stdio.h> +#include <string> + +const int FD_TAG_GENERIC = 0; +const int FD_TAG_CODE = 1; +const int FD_TAG_MENU_CALLBACK = 2; +const int FD_TAG_WIDGET_CALLBACK = 3; +const int FD_TAG_LAST = 3; + +const int FD_MERGEBACK_ANALYSE = 0; +const int FD_MERGEBACK_INTERACTIVE = 1; +const int FD_MERGEBACK_APPLY = 2; +const int FD_MERGEBACK_APPLY_IF_SAFE = 3; + +/** Class that implements the MergeBack functionality. + \see merge_back(const std::string &s, int task) + */ +class Fd_Mergeback +{ +protected: + /// Pointer to the C++ code file. + FILE *code; + /// Current line number in the C++ code file. + int line_no; + /// Set if there was an error reading a tag. + int tag_error; + /// Number of code blocks that were different than the CRC in their tag. + int num_changed_code; + /// Number of generic structure blocks that were different than the CRC in their tag. + int num_changed_structure; + /// Number of code block that were modified, but a type node by that uid was not found. + int num_uid_not_found; + /// Number of modified code block where the corresponding project block also changed. + int num_possible_override; + + void unindent(char *s); + std::string read_and_unindent_block(long start, long end); + void analyse_callback(unsigned long code_crc, unsigned long tag_crc, int uid); + void analyse_code(unsigned long code_crc, unsigned long tag_crc, int uid); + int apply_callback(long block_end, long block_start, unsigned long code_crc, int uid); + int apply_code(long block_end, long block_start, unsigned long code_crc, int uid); + +public: + Fd_Mergeback(); + ~Fd_Mergeback(); + int merge_back(const std::string &s, const std::string &p, int task); + int ask_user_to_merge(const std::string &s, const std::string &p); + int analyse(); + int apply(); +}; + +extern int merge_back(const std::string &s, const std::string &p, int task); + + +#endif // _FLUID_MERGEBACK_H + +#endif diff --git a/fluid/app/shell_command.cxx b/fluid/app/shell_command.cxx new file mode 100644 index 000000000..df71a00f5 --- /dev/null +++ b/fluid/app/shell_command.cxx @@ -0,0 +1,1005 @@ +// +// FLUID main entry 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 +// + +// in progress: +// FLUID comes with example shell commands to build the current project file +// and run the project. This is accomplished by calling `fltk-config` on the +// files generated by FLUID, and by calling the executable directly. +// +// If the user wants more complex commands, he can add or modify them in the +// "Shell" settings panel. Modified shell commands are saved with the .fl +// file. + +// The Shell panel has a list of shell commands in the upper half. Under the +// list are buttons to add, duplicate, and delete shell commands. A popup +// menu offers import and export functionality and a list of sample scripts. +// We may want to add up and down buttons, so the user can change the +// order of commands. + +// Selecting any shell command in the list fills in and activates a list of +// options in the lower half of the panel. Those settings are: +// - Name: the name of the shell command in the list +// - Label: the label in the pulldown menu (could be the same as name?) +// - Shortcut: shortcut key to launch the command +// - Storage: where to store this shell command +// - Condition: pulldown menu to make the entry conditional for various +// target platforms, for example, a "Windows only" entry would only be added +// to the Shell menu on a Windows machine. Other options could be: +// - Linux only, macOS only, never (to make a list header!?), inactive? +// - Command: a multiline input for the actual shell command +// - Variables: a pulldown menu that insert variable names like $<sourcefile> +// - options to save project, code, and strings before running +// - test-run button + +// TODO: add @APPDIR@? +// TODO: get a macro to find `fltk-config` @FLTK_CONFIG@ +// TODO: add an input field so the user can insert their preferred file and path for fltk-config (user setting) +// `fltk-config` is actually tricky to find +// for live builds, we could check the program launch directory +// if we know where build/Xcode/bin/Debug/fluid is, we +// may or may not find ./build/Xcode/fltk-config +// on macOS with homebrew, we find /opt/homebrew/bin/fltk-config but the user +// can set their own install path. +// We can query the shell path, but that requires knowing the users shell (echo $SHELL). +// We can run the shell as a login shell with `-l`, so the user $PTH is set: /bin/bash -l -c 'fltk-config' +// The shell should output the path of the fltk-config that it found and why it is using that one. +// This can also output the fltk-config version. +// TODO: add a bunch of sensible sample shell commands +// TODO: when this new feature is used for the very first time, import two or three samples as initial user setting +// TODO: make the settings dialog resizable +// TODO: make g_shell_config static, not a pointer, but don't load anything in batch mode + +// FEATURE: Fd_Tool_Store icons are currently redundant with @file and @save and could be improved +// FEATURE: hostname, username, getenv support? +// FEATURE: add the files ./fluid.prefs and ./fluid.user.prefs as tool locations +// FEATURE: interpret compiler output, for example: clang, and highlight errors and warnings +// `.../shell_command.cxx:71:2: error: test` +// `71 | #error test` +// `clang++: error: no such file or directory: '.../shell_command.o'` +// would make the error message clickable in the shell window and could select the widget, +// open the matching editor in the widget panel, and highlight the line in SourceView. + +/* + Some ideas: + + default shell is in $SHELL on linux and macOS + + On macOS, we can write Apple Scripts: + + #!/usr/bin/env osascript + say "@BASENAME@" + + osascript <<EOD + say "spark" + EOD + + osascript <<EOD + tell application "Xcode" + build workspace document 1 + end tell + EOD + + powershell -c "$wshell = New-Object -ComObject wscript.shell; $wshell.SendKeys('^{ESCAPE}') + */ + +#include "app/shell_command.h" + +#include "app/fluid.h" +#include "io/file.h" +#include "panels/settings_panel.h" + +#include <FL/Fl_Double_Window.H> +#include <FL/Fl_Menu_Bar.H> +#include <FL/fl_message.H> +#include <FL/fl_string_functions.h> + +#include <errno.h> + +static std::string fltk_config_cmd; +static Fl_Process s_proc; + +/** + See if shell command is running (public) + */ +bool shell_command_running() { + return s_proc.desc() ? true : false; +} + +/** + Reads an entry from the group. A default value must be + supplied. The return value indicates if the value was available + (non-zero) or the default was used (0). + + \param[in] prefs preference group + \param[in] key name of entry + \param[out] value returned from preferences or default value if none was set + \param[in] defaultValue default value to be used if no preference was set + \return 0 if the default value was used + */ +char preferences_get(Fl_Preferences &prefs, const char *key, std::string &value, const std::string &defaultValue) { + char *v = NULL; + char ret = prefs.get(key, v, defaultValue.c_str()); + value = v; + ::free(v); + return ret; +} + +/** + Sets an entry (name/value pair). The return value indicates if there + was a problem storing the data in memory. However it does not + reflect if the value was actually stored in the preference file. + + \param[in] prefs preference group + \param[in] entry name of entry + \param[in] value set this entry to value (stops at the first nul character). + \return 0 if setting the value failed + */ +char preferences_set(Fl_Preferences &prefs, const char *key, const std::string &value) { + return prefs.set(key, value.c_str()); +} + + +/** \class Fl_Process + Launch an external shell command. + */ + +/** + Create a process manager + */ +Fl_Process::Fl_Process() { + _fpt= NULL; +} + +/** + Destroy the project manager. + */ +Fl_Process::~Fl_Process() { + // TODO: check what we need to do if a task is still running + if (_fpt) close(); +} + +/** + Open a process. + + \param[in] cmd the shell command that we want to run + \param[in] mode "r" or "w" for creating a stream that can read or write + \return a stream that is redirected from the shell command stdout + */ +FILE * Fl_Process::popen(const char *cmd, const char *mode) { +#if defined(_WIN32) && !defined(__CYGWIN__) + // PRECONDITIONS + if (!mode || !*mode || (*mode!='r' && *mode!='w') ) return NULL; + if (_fpt) close(); // close first before reuse + + ptmode = *mode; + pin[0] = pin[1] = pout[0] = pout[1] = perr[0] = perr[1] = INVALID_HANDLE_VALUE; + // stderr to stdout wanted ? + int fusion = (strstr(cmd,"2>&1") !=NULL); + + // Create windows pipes + if (!createPipe(pin) || !createPipe(pout) || (!fusion && !createPipe(perr) ) ) + return freeHandles(); // error + + // Initialize Startup Info + ZeroMemory(&si, sizeof(STARTUPINFO)); + si.cb = sizeof(STARTUPINFO); + si.dwFlags = STARTF_USESTDHANDLES; + si.hStdInput = pin[0]; + si.hStdOutput = pout[1]; + si.hStdError = fusion ? pout[1] : perr [1]; + + if ( CreateProcess(NULL, (LPTSTR) cmd,NULL,NULL,TRUE, + DETACHED_PROCESS,NULL,NULL, &si, &pi)) { + // don't need theses handles inherited by child process: + clean_close(pin[0]); clean_close(pout[1]); clean_close(perr[1]); + HANDLE & h = *mode == 'r' ? pout[0] : pin[1]; + _fpt = _fdopen(_open_osfhandle((fl_intptr_t) h,_O_BINARY),mode); + h= INVALID_HANDLE_VALUE; // reset the handle pointer that is shared + // with _fpt so we don't free it twice + } + + if (!_fpt) freeHandles(); + return _fpt; +#else + _fpt=::popen(cmd,mode); + return _fpt; +#endif +} + +/** + Close the current process. + */ +int Fl_Process::close() { +#if defined(_WIN32) && !defined(__CYGWIN__) + if (_fpt) { + fclose(_fpt); + clean_close(perr[0]); + clean_close(pin[1]); + clean_close(pout[0]); + _fpt = NULL; + return 0; + } + return -1; +#else + int ret = ::pclose(_fpt); + _fpt=NULL; + return ret; +#endif +} + +/** + non-null if file is open. + + \return the current file descriptor of the process' stdout + */ +FILE *Fl_Process::desc() const { + return _fpt; +} + +/** + Receive a single line from the current process. + + \param[out] line buffer to receive the line + \param[in] s size of the provided buffer + \return NULL if an error occurred, otherwise a pointer to the string + */ +char *Fl_Process::get_line(char * line, size_t s) const { + return _fpt ? fgets(line, (int)s, _fpt) : NULL; +} + +// returns fileno(FILE*): +// (file must be open, i.e. _fpt must be non-null) +// *FIXME* we should find a better solution for the 'fileno' issue +// non null if file is open +int Fl_Process::get_fileno() const { +#ifdef _MSC_VER + return _fileno(_fpt); // suppress MSVC warning +#else + return fileno(_fpt); +#endif +} + +#if defined(_WIN32) && !defined(__CYGWIN__) + +bool Fl_Process::createPipe(HANDLE * h, BOOL bInheritHnd) { + SECURITY_ATTRIBUTES sa; + sa.nLength = sizeof(sa); + sa.lpSecurityDescriptor = NULL; + sa.bInheritHandle = bInheritHnd; + return CreatePipe (&h[0],&h[1],&sa,0) ? true : false; +} + +FILE *Fl_Process::freeHandles() { + clean_close(pin[0]); clean_close(pin[1]); + clean_close(pout[0]); clean_close(pout[1]); + clean_close(perr[0]); clean_close(perr[1]); + return NULL; // convenient for error management +} + +void Fl_Process::clean_close(HANDLE& h) { + if (h!= INVALID_HANDLE_VALUE) CloseHandle(h); + h = INVALID_HANDLE_VALUE; +} + +#endif + + +/** + Prepare FLUID for running a shell command according to the command flags. + + \param[in] flags set various flags to save the project, code, and string before running the command + \return false if the previous command is still running + */ +static bool prepare_shell_command(int flags) { +// settings_window->hide(); + if (s_proc.desc()) { + fl_alert("Previous shell command still running!"); + return false; + } + if (flags & Fd_Shell_Command::SAVE_PROJECT) { + save_cb(0, 0); + } + if (flags & Fd_Shell_Command::SAVE_SOURCECODE) { + write_code_files(true); + } + if (flags & Fd_Shell_Command::SAVE_STRINGS) { + write_strings_cb(0, 0); + } + return true; +} + +/** + Called by the file handler when the command is finished. + */ +void shell_proc_done() { + shell_run_terminal->append("... END SHELL COMMAND ...\n"); + shell_run_button->activate(); + shell_run_window->label("FLUID Shell"); + fl_beep(); +} + +void shell_timer_cb(void*) { + if (!s_proc.desc()) { + shell_proc_done(); + } else { + Fl::add_timeout(0.25, shell_timer_cb); + } +} + +// Support the full piped shell command... +void shell_pipe_cb(FL_SOCKET, void*) { + char line[1024]=""; // Line from command output... + + if (s_proc.get_line(line, sizeof(line)) != NULL) { + // Add the line to the output list... + shell_run_terminal->append(line); + } else { + // End of file; tell the parent... + Fl::remove_timeout(shell_timer_cb); + Fl::remove_fd(s_proc.get_fileno()); + s_proc.close(); + shell_proc_done(); + } +} + +/** Find the script `fltk-config` that most closely relates to this version of FLUID. + This is not implemented yet. + */ +//static void find_fltk_config() { +// +//} + +static void expand_macro(std::string &cmd, const std::string ¯o, const std::string &content) { + for (int i=0;;) { + i = (int)cmd.find(macro, i); + if (i==(int)std::string::npos) break; + cmd.replace(i, macro.size(), content); + } +} + +static void expand_macros(std::string &cmd) { + expand_macro(cmd, "@BASENAME@", g_project.basename()); + expand_macro(cmd, "@PROJECTFILE_PATH@", g_project.projectfile_path()); + expand_macro(cmd, "@PROJECTFILE_NAME@", g_project.projectfile_name()); + expand_macro(cmd, "@CODEFILE_PATH@", g_project.codefile_path()); + expand_macro(cmd, "@CODEFILE_NAME@", g_project.codefile_name()); + expand_macro(cmd, "@HEADERFILE_PATH@", g_project.headerfile_path()); + expand_macro(cmd, "@HEADERFILE_NAME@", g_project.headerfile_name()); + expand_macro(cmd, "@TEXTFILE_PATH@", g_project.stringsfile_path()); + expand_macro(cmd, "@TEXTFILE_NAME@", g_project.stringsfile_name()); +// TODO: implement finding the script `fltk-config` for all platforms +// if (cmd.find("@FLTK_CONFIG@") != std::string::npos) { +// find_fltk_config(); +// expand_macro(cmd, "@FLTK_CONFIG@", fltk_config_cmd.c_str()); +// } + if (cmd.find("@TMPDIR@") != std::string::npos) + expand_macro(cmd, "@TMPDIR@", get_tmpdir()); +} + +/** + Show the terminal window where it was last positioned. + */ +void show_terminal_window() { + Fl_Preferences pos(fluid_prefs, "shell_run_Window_pos"); + int x, y, w, h; + pos.get("x", x, -1); + pos.get("y", y, 0); + pos.get("w", w, 640); + pos.get("h", h, 480); + if (x!=-1) { + shell_run_window->resize(x, y, w, h); + } + shell_run_window->show(); +} + +/** + Prepare for and run a shell command. + + \param[in] cmd the command that is sent to `/bin/sh -c ...` or `cmd.exe` on Windows machines + \param[in] flags various flags in preparation of the command + */ +void run_shell_command(const std::string &cmd, int flags) { + if (cmd.empty()) { + fl_alert("No shell command entered!"); + return; + } + + if (!prepare_shell_command(flags)) return; + + std::string expanded_cmd = cmd; + expand_macros(expanded_cmd); + + if ( ((flags & Fd_Shell_Command::DONT_SHOW_TERMINAL) == 0) + && (!shell_run_window->visible())) + { + show_terminal_window(); + } + + // Show the output window and clear things... + if (flags & Fd_Shell_Command::CLEAR_TERMINAL) + shell_run_terminal->printf("\033[2J\033[H"); + if (flags & Fd_Shell_Command::CLEAR_HISTORY) + shell_run_terminal->printf("\033[3J"); + shell_run_terminal->scrollbar->value(0); + shell_run_terminal->printf("\033[0;32m%s\033[0m\n", expanded_cmd.c_str()); + shell_run_window->label(expanded_cmd.c_str()); + + if (s_proc.popen((char *)expanded_cmd.c_str()) == NULL) { + shell_run_terminal->printf("\033[1;31mUnable to run shell command: %s\033[0m\n", + strerror(errno)); + shell_run_window->label("FLUID Shell"); + return; + } + shell_run_button->deactivate(); + + // if the function below does not for some reason, we will check periodically + // to see if the command is done + Fl::add_timeout(0.25, shell_timer_cb); + // this will tell us when the shell command is done + Fl::add_fd(s_proc.get_fileno(), shell_pipe_cb); +} + +/** + Create an empty shell command structure. + */ +Fd_Shell_Command::Fd_Shell_Command() +: shortcut(0), + storage(FD_STORE_USER), + condition(0), + flags(0), + shell_menu_item_(NULL) +{ +} + +/** + Copy the aspects of a shell command dataset into a new shell command. + + \param[in] rhs copy from this prototype + */ +Fd_Shell_Command::Fd_Shell_Command(const Fd_Shell_Command *rhs) +: name(rhs->name), + label(rhs->label), + shortcut(rhs->shortcut), + storage(rhs->storage), + condition(rhs->condition), + condition_data(rhs->condition_data), + command(rhs->command), + flags(rhs->flags), + shell_menu_item_(NULL) +{ +} + +/** + Create a default storage for a shell command and how it is accessible in FLUID. + + \param[in] name is used as a stand-in for the command name and label + */ +Fd_Shell_Command::Fd_Shell_Command(const std::string &in_name) +: name(in_name), + label(in_name), + shortcut(0), + storage(FD_STORE_USER), + condition(Fd_Shell_Command::ALWAYS), + command("echo \"Hello, FLUID!\""), + flags(Fd_Shell_Command::SAVE_PROJECT|Fd_Shell_Command::SAVE_SOURCECODE), + shell_menu_item_(NULL) +{ +} + +/** + Create a storage for a shell command and how it is accessible in FLUID. + + \param[in] in_name name of this command in the command list in the settings panel + \param[in] in_label label text in the main pulldown menu + \param[in] in_shortcut a keyboard shortcut that will also appear in the main menu + \param[in] in_storage storage location for this command + \param[in] in_condition commands can be hidden for certain platforms by setting a condition + \param[in] in_condition_data more details for future conditions, i.e. per user, per host, etc. + \param[in] in_command the shell command that we want to run + \param[in] in_flags some flags to tell FLUID to save the project, code, or strings before running the command + */ +Fd_Shell_Command::Fd_Shell_Command(const std::string &in_name, + const std::string &in_label, + Fl_Shortcut in_shortcut, + Fd_Tool_Store in_storage, + int in_condition, + const std::string &in_condition_data, + const std::string &in_command, + int in_flags) +: name(in_name), + label(in_label), + shortcut(in_shortcut), + storage(in_storage), + condition(in_condition), + condition_data(in_condition_data), + command(in_command), + flags(in_flags), + shell_menu_item_(NULL) +{ +} + +/** + Run this command now. + + Will open the Shell Panel and execute the command if no other command is + currently running. + */ +void Fd_Shell_Command::run() { + if (!command.empty()) + run_shell_command(command, flags); +} + +/** + Update the shell submenu in main menu with the shortcut and a copy of the label. + */ +void Fd_Shell_Command::update_shell_menu() { + if (shell_menu_item_) { + const char *old_label = shell_menu_item_->label(); // can be NULL + const char *new_label = label.c_str(); // never NULL + if (!old_label || (old_label && strcmp(old_label, new_label))) { + if (old_label) ::free((void*)old_label); + shell_menu_item_->label(fl_strdup(new_label)); + } + shell_menu_item_->shortcut(shortcut); + } +} + +/** + Check if the set condition is met. + + \return true if this command appears in the main menu + */ +bool Fd_Shell_Command::is_active() { + switch (condition) { + case ALWAYS: return true; + case NEVER: return false; +#ifdef _WIN32 + case MAC_ONLY: return false; + case UX_ONLY: return false; + case WIN_ONLY: return true; + case MAC_AND_UX_ONLY: return false; +#elif defined(__APPLE__) + case MAC_ONLY: return true; + case UX_ONLY: return false; + case WIN_ONLY: return false; + case MAC_AND_UX_ONLY: return true; +#else + case MAC_ONLY: return false; + case UX_ONLY: return true; + case WIN_ONLY: return false; + case MAC_AND_UX_ONLY: return true; +#endif + case USER_ONLY: return false; // TODO: get user name + case HOST_ONLY: return false; // TODO: get host name + case ENV_ONLY: { + const char *value = fl_getenv(condition_data.c_str()); + if (value && *value) return true; + return false; + } + } + return false; +} + +void Fd_Shell_Command::read(Fl_Preferences &prefs) { + int tmp; + preferences_get(prefs, "name", name, "<unnamed>"); + preferences_get(prefs, "label", label, "<no label>"); + prefs.get("shortcut", tmp, 0); + shortcut = (Fl_Shortcut)tmp; + prefs.get("storage", tmp, -1); + if (tmp != -1) storage = (Fd_Tool_Store)tmp; + prefs.get("condition", condition, ALWAYS); + preferences_get(prefs, "condition_data", condition_data, ""); + preferences_get(prefs, "command", command, ""); + prefs.get("flags", flags, 0); +} + +void Fd_Shell_Command::write(Fl_Preferences &prefs, bool save_location) { + preferences_set(prefs, "name", name); + preferences_set(prefs, "label", label); + if (shortcut != 0) prefs.set("shortcut", (int)shortcut); + if (save_location) prefs.set("storage", (int)storage); + if (condition != ALWAYS) prefs.set("condition", condition); + if (!condition_data.empty()) preferences_set(prefs, "condition_data", condition_data); + if (!command.empty()) preferences_set(prefs, "command", command); + if (flags != 0) prefs.set("flags", flags); +} + +void Fd_Shell_Command::read(class Fd_Project_Reader *in) { + const char *c = in->read_word(1); + if (strcmp(c, "{")!=0) return; // expecting start of group + storage = FD_STORE_PROJECT; + for (;;) { + c = in->read_word(1); + if (strcmp(c, "}")==0) break; // end of command list + else if (strcmp(c, "name")==0) + name = in->read_word(); + else if (strcmp(c, "label")==0) + label = in->read_word(); + else if (strcmp(c, "shortcut")==0) + shortcut = in->read_int(); + else if (strcmp(c, "condition")==0) + condition = in->read_int(); + else if (strcmp(c, "condition_data")==0) + condition_data = in->read_word(); + else if (strcmp(c, "command")==0) + command = in->read_word(); + else if (strcmp(c, "flags")==0) + flags = in->read_int(); + else + in->read_word(); // skip an unknown word + } +} + +void Fd_Shell_Command::write(class Fd_Project_Writer *out) { + out->write_string("\n command {"); + out->write_string("\n name "); out->write_word(name.c_str()); + out->write_string("\n label "); out->write_word(label.c_str()); + if (shortcut) out->write_string("\n shortcut %d", shortcut); + if (condition) out->write_string("\n condition %d", condition); + if (!condition_data.empty()) { + out->write_string("\n condition_data "); out->write_word(condition_data.c_str()); + } + if (!command.empty()) { + out->write_string("\n command "); out->write_word(command.c_str()); + } + if (flags) out->write_string("\n flags %d", flags); + out->write_string("\n }"); +} + + +/** + Manage a list of shell commands and their parameters. + */ +Fd_Shell_Command_List::Fd_Shell_Command_List() +: list(NULL), + list_size(0), + list_capacity(0), + shell_menu_(NULL) +{ +} + +/** + Release all shell commands and destroy this class. + */ +Fd_Shell_Command_List::~Fd_Shell_Command_List() { + clear(); +} + +/** + Return the shell command at the given index. + + \param[in] index must be between 0 and list_size-1 + \return a pointer to the shell command data + */ +Fd_Shell_Command *Fd_Shell_Command_List::at(int index) const { + return list[index]; +} + +/** + Clear all shell commands. + */ +void Fd_Shell_Command_List::clear() { + if (list) { + for (int i=0; i<list_size; i++) { + delete list[i]; + } + ::free(list); + list_size = 0; + list_capacity = 0; + list = 0; + } +} + +/** + remove all shell commands of the given storage location from the list. + */ +void Fd_Shell_Command_List::clear(Fd_Tool_Store storage) { + for (int i=list_size-1; i>=0; i--) { + if (list[i]->storage == storage) { + remove(i); + } + } +} + +/** + Read shell configuration from a preferences group. + */ +void Fd_Shell_Command_List::read(Fl_Preferences &prefs, Fd_Tool_Store storage) { + // import the old shell commands from previous user settings + if (&fluid_prefs == &prefs) { + int version; + prefs.get("shell_commands_version", version, 0); + if (version == 0) { + int save_fl, save_code, save_strings; + Fd_Shell_Command *cmd = new Fd_Shell_Command(); + cmd->storage = FD_STORE_USER; + cmd->name = "Sample Shell Command"; + cmd->label = "Sample Shell Command"; + cmd->shortcut = FL_ALT+'g'; + preferences_get(fluid_prefs, "shell_command", cmd->command, "echo \"Sample Shell Command\""); + fluid_prefs.get("shell_savefl", save_fl, 1); + fluid_prefs.get("shell_writecode", save_code, 1); + fluid_prefs.get("shell_writemsgs", save_strings, 0); + if (save_fl) cmd->flags |= Fd_Shell_Command::SAVE_PROJECT; + if (save_code) cmd->flags |= Fd_Shell_Command::SAVE_SOURCECODE; + if (save_strings) cmd->flags |= Fd_Shell_Command::SAVE_STRINGS; + add(cmd); + } + version = 1; + prefs.set("shell_commands_version", version); + } + Fl_Preferences shell_commands(prefs, "shell_commands"); + int n = shell_commands.groups(); + for (int i=0; i<n; i++) { + Fl_Preferences cmd_prefs(shell_commands, Fl_Preferences::Name(i)); + Fd_Shell_Command *cmd = new Fd_Shell_Command(); + cmd->storage = FD_STORE_USER; + cmd->read(cmd_prefs); + add(cmd); + } +} + +/** + Write shell configuration to a preferences group. + */ +void Fd_Shell_Command_List::write(Fl_Preferences &prefs, Fd_Tool_Store storage) { + Fl_Preferences shell_commands(prefs, "shell_commands"); + shell_commands.delete_all_groups(); + int index = 0; + for (int i=0; i<list_size; i++) { + if (list[i]->storage == FD_STORE_USER) { + Fl_Preferences cmd(shell_commands, Fl_Preferences::Name(index++)); + list[i]->write(cmd); + } + } +} + +/** + Read shell configuration from a project file. + */ +void Fd_Shell_Command_List::read(Fd_Project_Reader *in) { + const char *c = in->read_word(1); + if (strcmp(c, "{")!=0) return; // expecting start of group + clear(FD_STORE_PROJECT); + for (;;) { + c = in->read_word(1); + if (strcmp(c, "}")==0) break; // end of command list + else if (strcmp(c, "command")==0) { + Fd_Shell_Command *cmd = new Fd_Shell_Command(); + add(cmd); + cmd->read(in); + } else { + in->read_word(); // skip an unknown group + } + } +} + +/** + Write shell configuration to a project file. + */ +void Fd_Shell_Command_List::write(Fd_Project_Writer *out) { + int n_in_project_file = 0; + for (int i=0; i<list_size; i++) { + if (list[i]->storage == FD_STORE_PROJECT) + n_in_project_file++; + } + if (n_in_project_file > 0) { + out->write_string("\nshell_commands {"); + for (int i=0; i<list_size; i++) { + if (list[i]->storage == FD_STORE_PROJECT) + list[i]->write(out); + } + out->write_string("\n}"); + } +} + +/** + Add a previously created shell command to the end of the list. + + \param[in] cmd a pointer to the command that we want to add + */ +void Fd_Shell_Command_List::add(Fd_Shell_Command *cmd) { + if (list_size == list_capacity) { + list_capacity += 16; + list = (Fd_Shell_Command**)::realloc(list, list_capacity * sizeof(Fd_Shell_Command*)); + } + list[list_size++] = cmd; +} + +/** + Insert a newly created shell command at the given position in the list. + + \param[in] index must be between 0 and list_size-1 + \param[in] cmd a pointer to the command that we want to add + */ +void Fd_Shell_Command_List::insert(int index, Fd_Shell_Command *cmd) { + if (list_size == list_capacity) { + list_capacity += 16; + list = (Fd_Shell_Command**)::realloc(list, list_capacity * sizeof(Fd_Shell_Command*)); + } + ::memmove(list+index+1, list+index, (list_size-index)*sizeof(Fd_Shell_Command**)); + list_size++; + list[index] = cmd; +} + +/** + Remove and delete the command at the given index. + + \param[in] index must be between 0 and list_size-1 + */ +void Fd_Shell_Command_List::remove(int index) { + delete list[index]; + list_size--; + ::memmove(list+index, list+index+1, (list_size-index)*sizeof(Fd_Shell_Command**)); +} + +/** + This is called whenever the user clicks a shell command menu in the main menu. + + \param[in] u cast tp long to get the index of the shell command + */ +void menu_shell_cmd_cb(Fl_Widget*, void *u) { + long index = (long)(fl_intptr_t)u; + g_shell_config->list[index]->run(); +} + +/** + This is called when the user selects the menu to edit the shell commands. + It pops up the setting panel at the shell settings tab. + */ +void menu_shell_customize_cb(Fl_Widget*, void*) { + settings_window->show(); + w_settings_tabs->value(w_settings_shell_tab); +} + +/** + Rebuild the entire shell submenu from scratch and replace the old menu. + */ +void Fd_Shell_Command_List::rebuild_shell_menu() { + static Fl_Menu_Item *shell_submenu = NULL; + if (!shell_submenu) + shell_submenu = (Fl_Menu_Item*)main_menubar->find_item(menu_marker); + + int i, j, num_active_items = 0; + // count the active commands + for (i=0; i<list_size; i++) { + if (list[i]->is_active()) num_active_items++; + } + // allocate a menu item array + Fl_Menu_Item *mi = (Fl_Menu_Item*)::calloc(num_active_items+2, sizeof(Fl_Menu_Item)); + // set the menu item pointer for all active commands + for (i=j=0; i<list_size; i++) { + Fd_Shell_Command *cmd = list[i]; + if (cmd->is_active()) { + cmd->shell_menu_item_ = mi + j; + mi[j].callback(menu_shell_cmd_cb); + mi[j].argument(i); + cmd->update_shell_menu(); + j++; + } + } + if (j>0) mi[j-1].flags |= FL_MENU_DIVIDER; + mi[j].label(fl_strdup("Customize...")); + mi[j].shortcut(FL_ALT+'x'); + mi[j].callback(menu_shell_customize_cb); + // replace the old menu array with the new one + Fl_Menu_Item *mi_old = shell_menu_; + shell_menu_ = mi; + shell_submenu->user_data(shell_menu_); + // free all resources from the old menu + if (mi_old && (mi_old != default_menu)) { + for (i=0; ; i++) { + const char *label = mi_old[i].label(); + if (!label) break; + ::free((void*)label); + } + ::free(mi_old); + } +} + +/** + Tell the settings dialog to query this list and update its GUI elements. + */ +void Fd_Shell_Command_List::update_settings_dialog() { + if (w_settings_shell_tab) + w_settings_shell_tab->do_callback(w_settings_shell_tab, LOAD); +} + +/** + The default shell submenu in batch mode. + */ +Fl_Menu_Item Fd_Shell_Command_List::default_menu[] = { + { "Customize...", FL_ALT+'x', menu_shell_customize_cb }, + { NULL } +}; + +/** + Used to find the shell submenu within the main menu tree. + */ +void Fd_Shell_Command_List::menu_marker(Fl_Widget*, void*) { + // intentionally left empty +} + +/** + Export all selected shell commands to an external file. + + Verify that g_shell_config and w_settings_shell_list are not NULL. Open a + file chooser and export all items that are selected in w_settings_shell_list + into an external file. + */ +void Fd_Shell_Command_List::export_selected() { + if (!g_shell_config || (g_shell_config->list_size == 0)) return; + if (!w_settings_shell_list) return; + + Fl_Native_File_Chooser dialog; + dialog.title("Export selected shell commands:"); + dialog.type(Fl_Native_File_Chooser::BROWSE_SAVE_FILE); + dialog.filter("FLUID Files\t*.flcmd\n"); + dialog.directory(g_project.projectfile_path().c_str()); + dialog.preset_file((g_project.basename() + ".flcmd").c_str()); + if (dialog.show() != 0) return; + + Fl_Preferences file(dialog.filename(), "flcmd.fluid.fltk.org", NULL, (Fl_Preferences::Root)(Fl_Preferences::C_LOCALE|Fl_Preferences::CLEAR)); + Fl_Preferences shell_commands(file, "shell_commands"); + int i, index = 0, n = w_settings_shell_list->size(); + for (i = 0; i < n; i++) { + if (w_settings_shell_list->selected(i+1)) { + Fl_Preferences cmd(shell_commands, Fl_Preferences::Name(index++)); + g_shell_config->list[i]->write(cmd, true); + } + } +} + +/** + Import shell commands from an external file and add them to the list. + + Verify that g_shell_config and w_settings_shell_list are not NULL. Open a + file chooser and import all items. + */ +void Fd_Shell_Command_List::import_from_file() { + if (!g_shell_config || (g_shell_config->list_size == 0)) return; + if (!w_settings_shell_list) return; + + Fl_Native_File_Chooser dialog; + dialog.title("Import shell commands:"); + dialog.type(Fl_Native_File_Chooser::BROWSE_FILE); + dialog.filter("FLUID Files\t*.flcmd\n"); + dialog.directory(g_project.projectfile_path().c_str()); + dialog.preset_file((g_project.basename() + ".flcmd").c_str()); + if (dialog.show() != 0) return; + + Fl_Preferences file(dialog.filename(), "flcmd.fluid.fltk.org", NULL, Fl_Preferences::C_LOCALE); + Fl_Preferences shell_commands(file, "shell_commands"); + int i, n = shell_commands.groups(); + for (i = 0; i < n; i++) { + Fl_Preferences cmd_prefs(shell_commands, Fl_Preferences::Name(i)); + Fd_Shell_Command *cmd = new Fd_Shell_Command(); + cmd->storage = FD_STORE_USER; + cmd->read(cmd_prefs); + g_shell_config->add(cmd); + } + w_settings_shell_list->do_callback(w_settings_shell_list, LOAD); + w_settings_shell_cmd->do_callback(w_settings_shell_cmd, LOAD); + w_settings_shell_toolbox->do_callback(w_settings_shell_toolbox, LOAD); + g_shell_config->rebuild_shell_menu(); +} + +/** + A pointer to the list of shell commands if we are not in batch mode. + */ +Fd_Shell_Command_List *g_shell_config = NULL; + diff --git a/fluid/app/shell_command.h b/fluid/app/shell_command.h new file mode 100644 index 000000000..b45e2f8cc --- /dev/null +++ b/fluid/app/shell_command.h @@ -0,0 +1,148 @@ +// +// FLUID main entry 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 +// + +#ifndef _FLUID_SHELL_COMMAND_H +#define _FLUID_SHELL_COMMAND_H + +#include "app/fluid.h" + +#include <FL/Enumerations.H> + +#include <stdio.h> +#include <stdlib.h> +#include <string> +#if defined(_WIN32) && !defined(__CYGWIN__) +# include <direct.h> +# include <windows.h> +# include <io.h> +# include <fcntl.h> +# include <commdlg.h> +# include <FL/platform.H> +#else +# include <unistd.h> +#endif + +struct Fl_Menu_Item; +class Fl_Widget; +class Fl_Preferences; + +char preferences_get(Fl_Preferences &prefs, const char *key, std::string &value, const std::string &defaultValue); +char preferences_set(Fl_Preferences &prefs, const char *key, const std::string &value); + +void show_terminal_window(); +void run_shell_command(const std::string &cmd, int flags); +bool shell_command_running(void); + +class Fl_Process { +public: + Fl_Process(); + ~Fl_Process(); + + FILE *popen(const char *cmd, const char *mode="r"); + int close(); + + FILE * desc() const; + char * get_line(char * line, size_t s) const; + + int get_fileno() const; + +#if defined(_WIN32) && !defined(__CYGWIN__) +protected: + HANDLE pin[2], pout[2], perr[2]; + char ptmode; + PROCESS_INFORMATION pi; + STARTUPINFO si; + + static bool createPipe(HANDLE * h, BOOL bInheritHnd=TRUE); + +private: + FILE * freeHandles(); + static void clean_close(HANDLE& h); +#endif + +protected: + FILE * _fpt; +}; + +class Fd_Shell_Command { +public: + enum { ALWAYS, NEVER, MAC_ONLY, UX_ONLY, WIN_ONLY, MAC_AND_UX_ONLY, USER_ONLY, HOST_ONLY, ENV_ONLY }; // conditions + enum { SAVE_PROJECT = 1, SAVE_SOURCECODE = 2, SAVE_STRINGS = 4, SAVE_ALL = 7, + DONT_SHOW_TERMINAL = 8, CLEAR_TERMINAL = 16, CLEAR_HISTORY = 32 }; // flags + Fd_Shell_Command(); + Fd_Shell_Command(const Fd_Shell_Command *rhs); + Fd_Shell_Command(const std::string &in_name); + Fd_Shell_Command(const std::string &in_name, + const std::string &in_label, + Fl_Shortcut in_shortcut, + Fd_Tool_Store in_storage, + int in_condition, + const std::string &in_condition_data, + const std::string &in_command, + int in_flags); + std::string name; + std::string label; + Fl_Shortcut shortcut; + Fd_Tool_Store storage; + int condition; // always, hide, windows only, linux only, mac only, user, machine + std::string condition_data; // user name, machine name + std::string command; + int flags; // save_project, save_code, save_string, ... + Fl_Menu_Item *shell_menu_item_; + void run(); + void read(Fl_Preferences &prefs); + void write(Fl_Preferences &prefs, bool save_location = false); + void read(class Fd_Project_Reader*); + void write(class Fd_Project_Writer*); + void update_shell_menu(); + bool is_active(); +}; + +class Fd_Shell_Command_List { +public: + Fd_Shell_Command **list; + int list_size; + int list_capacity; + Fl_Menu_Item *shell_menu_; +public: + Fd_Shell_Command_List(); + ~Fd_Shell_Command_List(); + Fd_Shell_Command *at(int index) const; + void add(Fd_Shell_Command *cmd); + void insert(int index, Fd_Shell_Command *cmd); + void remove(int index); + void clear(); + void clear(Fd_Tool_Store store); +// void move_up(); +// void move_down(); +// int load(const std::string &filename); +// int save(const std::string &filename); + void read(Fl_Preferences &prefs, Fd_Tool_Store storage); + void write(Fl_Preferences &prefs, Fd_Tool_Store storage); + void read(class Fd_Project_Reader*); + void write(class Fd_Project_Writer*); + void rebuild_shell_menu(); + void update_settings_dialog(); + + static Fl_Menu_Item default_menu[]; + static void menu_marker(Fl_Widget*, void*); + static void export_selected(); + static void import_from_file(); +}; + +extern Fd_Shell_Command_List *g_shell_config; + +#endif // _FLUID_SHELL_COMMAND_H diff --git a/fluid/app/undo.cxx b/fluid/app/undo.cxx new file mode 100644 index 000000000..eb5001468 --- /dev/null +++ b/fluid/app/undo.cxx @@ -0,0 +1,263 @@ +// +// FLUID undo support 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 "app/undo.h" + +#include "app/fluid.h" +#include "io/file.h" +#include "nodes/Fl_Type.h" +#include "nodes/Fl_Widget_Type.h" +#include "widgets/widget_browser.h" + +#include <FL/Fl.H> +#include <FL/Fl_Window.H> +#include <FL/Fl_Preferences.H> +#include <FL/Fl_Menu_Bar.H> +#include <FL/fl_ask.H> +#include "tools/fluid_filename.h" +#include "../src/flstring.h" + +#if defined(_WIN32) && !defined(__CYGWIN__) +# include <io.h> +# include <windows.h> +# define getpid (int)GetCurrentProcessId +#else +# include <unistd.h> +#endif // _WIN32 && !__CYGWIN__ + + +// +// This file implements an undo system using temporary files; ideally +// we'd like to do this in memory, however the current data structures +// and design aren't well-suited... Instead, we save and restore +// checkpoint files. +// + +extern Fl_Window* the_panel; + +int undo_current = 0; // Current undo level in buffer +int undo_last = 0; // Last undo level in buffer +int undo_max = 0; // Maximum undo level used +int undo_save = -1; // Last undo level that was saved +static int undo_paused = 0; // Undo checkpointing paused? +int undo_once_type = 0; // Suspend further undos of the same type + + +// Return the undo filename. +// The filename is constructed in a static internal buffer and +// this buffer is overwritten by every call of this function. +// The return value is a pointer to this internal string. +static char *undo_filename(int level) { + static char undo_path[FL_PATH_MAX] = ""; // Undo path + static unsigned int undo_path_len = 0; // length w/o filename + + if (!undo_path_len) { + fluid_prefs.getUserdataPath(undo_path, sizeof(undo_path)); + undo_path_len = (unsigned int)strlen(undo_path); + } + + // append filename: "undo_PID_LEVEL.fl" + snprintf(undo_path + undo_path_len, + sizeof(undo_path) - undo_path_len - 1, + "undo_%d_%d.fl", getpid(), level); + return undo_path; +} + + +// Redo menu callback +void redo_cb(Fl_Widget *, void *) { + // int undo_item = main_menubar->find_index(undo_cb); + // int redo_item = main_menubar->find_index(redo_cb); + undo_once_type = 0; + + if (undo_current >= undo_last) { + fl_beep(); + return; + } + + undo_suspend(); + if (widget_browser) { + widget_browser->save_scroll_position(); + widget_browser->new_list(); + } + int reload_panel = (the_panel && the_panel->visible()); + if (!read_file(undo_filename(undo_current + 1), 0)) { + // Unable to read checkpoint file, don't redo... + widget_browser->rebuild(); + g_project.update_settings_dialog(); + undo_resume(); + return; + } + if (reload_panel) { + for (Fl_Type *t = Fl_Type::first; t; t=t->next) { + if (t->is_widget() && t->selected) + t->open(); + } + } + if (widget_browser) widget_browser->restore_scroll_position(); + + undo_current ++; + + // Update modified flag... + set_modflag(undo_current != undo_save); + widget_browser->rebuild(); + g_project.update_settings_dialog(); + + // Update undo/redo menu items... + // if (undo_current >= undo_last) Main_Menu[redo_item].deactivate(); + // Main_Menu[undo_item].activate(); + undo_resume(); +} + +// Undo menu callback +void undo_cb(Fl_Widget *, void *) { + // int undo_item = main_menubar->find_index(undo_cb); + // int redo_item = main_menubar->find_index(redo_cb); + undo_once_type = 0; + + if (undo_current <= 0) { + fl_beep(); + return; + } + + if (undo_current == undo_last) { + write_file(undo_filename(undo_current)); + } + + undo_suspend(); + // Undo first deletes all widgets which resets the widget_tree browser. + // Save the current scroll position, so we don't scroll back to 0 at undo. + // TODO: make the scroll position part of the .fl project file + if (widget_browser) { + widget_browser->save_scroll_position(); + widget_browser->new_list(); + } + int reload_panel = (the_panel && the_panel->visible()); + if (!read_file(undo_filename(undo_current - 1), 0)) { + // Unable to read checkpoint file, don't undo... + widget_browser->rebuild(); + g_project.update_settings_dialog(); + set_modflag(0, 0); + undo_resume(); + return; + } + if (reload_panel) { + for (Fl_Type *t = Fl_Type::first; t; t=t->next) { + if (t->is_widget() && t->selected) { + t->open(); + break; + } + } + } + // Restore old browser position. + // Ideally, we would save the browser position inside the undo file. + if (widget_browser) widget_browser->restore_scroll_position(); + + undo_current --; + + // Update modified flag... + set_modflag(undo_current != undo_save); + + // Update undo/redo menu items... + // if (undo_current <= 0) Main_Menu[undo_item].deactivate(); + // Main_Menu[redo_item].activate(); + widget_browser->rebuild(); + g_project.update_settings_dialog(); + undo_resume(); +} + +/** + \param[in] type set a new type, or set to 0 to clear the once_type without setting a checkpoint + \return 1 if the checkpoint was set, 0 if this is a repeating event + */ +int undo_checkpoint_once(int type) { + if (type == 0) { + undo_once_type = 0; + return 0; + } + if (undo_paused) return 0; + if (undo_once_type != type) { + undo_checkpoint(); + undo_once_type = type; + return 1; + } else { + // do not add more checkpoints for the same undo type + return 0; + } +} + +// Save current file to undo buffer +void undo_checkpoint() { + // printf("undo_checkpoint(): undo_current=%d, undo_paused=%d, modflag=%d\n", + // undo_current, undo_paused, modflag); + + // Don't checkpoint if undo_suspend() has been called... + if (undo_paused) return; + + // int undo_item = main_menubar->find_index(undo_cb); + // int redo_item = main_menubar->find_index(redo_cb); + undo_once_type = 0; + + // Save the current UI to a checkpoint file... + const char *filename = undo_filename(undo_current); + if (!write_file(filename)) { + // Don't attempt to do undo stuff if we can't write a checkpoint file... + perror(filename); + return; + } + + // Update the saved level... + if (modflag && undo_current <= undo_save) undo_save = -1; + else if (!modflag) undo_save = undo_current; + + // Update the current undo level... + undo_current ++; + undo_last = undo_current; + if (undo_current > undo_max) undo_max = undo_current; + + // Enable the Undo and disable the Redo menu items... + // Main_Menu[undo_item].activate(); + // Main_Menu[redo_item].deactivate(); +} + +// Clear undo buffer +void undo_clear() { + // int undo_item = main_menubar->find_index(undo_cb); + // int redo_item = main_menubar->find_index(redo_cb); + // Remove old checkpoint files... + for (int i = 0; i <= undo_max; i ++) { + fl_unlink(undo_filename(i)); + } + + // Reset current, last, and save indices... + undo_current = undo_last = undo_max = 0; + if (modflag) undo_save = -1; + else undo_save = 0; + + // Disable the Undo and Redo menu items... + // Main_Menu[undo_item].deactivate(); + // Main_Menu[redo_item].deactivate(); +} + +// Resume undo checkpoints +void undo_resume() { + undo_paused--; +} + +// Suspend undo checkpoints +void undo_suspend() { + undo_paused++; +} diff --git a/fluid/app/undo.h b/fluid/app/undo.h new file mode 100644 index 000000000..e8ffab891 --- /dev/null +++ b/fluid/app/undo.h @@ -0,0 +1,37 @@ +// +// FLUID undo definitions for the Fast Light Tool Kit (FLTK). +// +// Copyright 1998-2025 by Bill Spitzak and others. +// +// This library is free software. Distribution and use rights are outlined in +// the file "COPYING" which should have been included with this file. If this +// file is missing or damaged, see the license at: +// +// https://www.fltk.org/COPYING.php +// +// Please see the following page on how to report bugs and issues: +// +// https://www.fltk.org/bugs.php +// + +#ifndef undo_h +#define undo_h + +class Fl_Widget; + +#define kUndoWindowResize 1 + +extern int undo_current; // Current undo level in buffer +extern int undo_last; // Last undo level in buffer +extern int undo_save; // Last undo level that was saved +extern int undo_once_type; // Suspend further undos of the same type + +void redo_cb(Fl_Widget *, void *); // Redo menu callback +void undo_cb(Fl_Widget *, void *); // Undo menu callback +void undo_checkpoint(); // Save current file to undo buffer +int undo_checkpoint_once(int type); // Save undo buffer once until a different checkpoint type is called +void undo_clear(); // Clear undo buffer +void undo_resume(); // Resume undo checkpoints +void undo_suspend(); // Suspend undo checkpoints + +#endif // !undo_h |
