diff options
Diffstat (limited to 'fluid/shell_command.cxx')
| -rw-r--r-- | fluid/shell_command.cxx | 832 |
1 files changed, 709 insertions, 123 deletions
diff --git a/fluid/shell_command.cxx b/fluid/shell_command.cxx index cd764ca24..c557c6e54 100644 --- a/fluid/shell_command.cxx +++ b/fluid/shell_command.cxx @@ -1,7 +1,7 @@ // // FLUID main entry for the Fast Light Tool Kit (FLTK). // -// Copyright 1998-2021 by Bill Spitzak and others. +// 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 @@ -14,125 +14,122 @@ // 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: ad the files ./fluid.prefs and ./fluid.user.prefs as tool locations + +/* + 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 + */ + #include "shell_command.h" #include "fluid.h" +#include "file.h" #include "alignment_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 Fl_String fltk_config_cmd; static Fl_Process s_proc; -/// Shell settings in the .fl file -Shell_Settings shell_settings_windows = { }; -Shell_Settings shell_settings_linux = { }; -Shell_Settings shell_settings_macos = { }; - -/// Current shell command, stored in .fl file for each platform, and in app prefs -Fl_String g_shell_command; - -/// Save .fl file before running, stored in .fl file for each platform, and in app prefs -int g_shell_save_fl = 1; - -/// Save code file before running, stored in .fl file for each platform, and in app prefs -int g_shell_save_code = 1; - -/// Save strings file before running, stored in .fl file for each platform, and in app prefs -int g_shell_save_strings = 0; - -/// Use these settings from .fl files, stored in app prefs -int g_shell_use_fl_settings = 1; -/** - Read the default shell settings from the app preferences. +/** \class Fl_Process + Launch an external shell command. */ -void shell_prefs_get() -{ - fluid_prefs.get("shell_command", g_shell_command, "echo \"Custom Shell Command\""); - fluid_prefs.get("shell_savefl", g_shell_save_fl, 1); - fluid_prefs.get("shell_writecode", g_shell_save_code, 1); - fluid_prefs.get("shell_writemsgs", g_shell_save_strings, 0); - fluid_prefs.get("shell_use_fl", g_shell_use_fl_settings, 1); -} /** - Write the current shell settings to the app preferences. + Create a process manager */ -void shell_prefs_set() -{ - fluid_prefs.set("shell_command", g_shell_command); - fluid_prefs.set("shell_savefl", g_shell_save_fl); - fluid_prefs.set("shell_writecode", g_shell_save_code); - fluid_prefs.set("shell_writemsgs", g_shell_save_strings); - fluid_prefs.set("shell_use_fl", g_shell_use_fl_settings); +Fl_Process::Fl_Process() { + _fpt= NULL; } /** - Copy shell settings from the .fl buffer if use_fl_settings is set. + Destroy the project manager. */ -void shell_settings_read() -{ - if (g_shell_use_fl_settings==0) - return; -#if defined(_WIN32) - Shell_Settings &shell_settings = shell_settings_windows; -#elif defined(__APPLE__) - Shell_Settings &shell_settings = shell_settings_macos; -#else - Shell_Settings &shell_settings = shell_settings_linux; -#endif - g_shell_command = shell_settings.command; - g_shell_save_fl = ((shell_settings.flags&1)==1); - g_shell_save_code = ((shell_settings.flags&2)==2); - g_shell_save_strings = ((shell_settings.flags&4)==4); +Fl_Process::~Fl_Process() { + // TODO: check what we need to do if a task is still running + if (_fpt) close(); } /** - Copy current shell settings to the .fl buffer if use_fl_settings is set. - */ -void shell_settings_write() -{ - if (g_shell_use_fl_settings==0) - return; -#if defined(_WIN32) - Shell_Settings &shell_settings = shell_settings_windows; -#elif defined(__APPLE__) - Shell_Settings &shell_settings = shell_settings_macos; -#else - Shell_Settings &shell_settings = shell_settings_linux; -#endif - if (shell_settings.command) - free((void*)shell_settings.command); - shell_settings.command = NULL; - if (!g_shell_command.empty()) - shell_settings.command = fl_strdup(g_shell_command.c_str()); - shell_settings.flags = 0; - if (g_shell_save_fl) - shell_settings.flags |= 1; - if (g_shell_save_code) - shell_settings.flags |= 2; - if (g_shell_save_strings) - shell_settings.flags |= 4; -} + Open a process. -/** \class Fl_Process - \todo Explain. + \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 */ - -Fl_Process::Fl_Process() { - _fpt= NULL; -} - -Fl_Process::~Fl_Process() { - if (_fpt) close(); -} - -// FIXME: popen needs the UTF-8 equivalent fl_popen -// portable open process: FILE * Fl_Process::popen(const char *cmd, const char *mode) { #if defined(_WIN32) && !defined(__CYGWIN__) // PRECONDITIONS @@ -174,6 +171,9 @@ FILE * Fl_Process::popen(const char *cmd, const char *mode) { #endif } +/** + Close the current process. + */ int Fl_Process::close() { #if defined(_WIN32) && !defined(__CYGWIN__) if (_fpt) { @@ -192,11 +192,22 @@ int Fl_Process::close() { #endif } -// non-null if file is open +/** + 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; } @@ -237,31 +248,33 @@ void Fl_Process::clean_close(HANDLE& h) { #endif +/** + Prepare FLUID for running a shell command according to the command flags. -// Shell command support... - -static bool prepare_shell_command() { + \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 (g_shell_command.empty()) { - fl_alert("No shell command entered!"); - return false; - } - if (g_shell_save_fl) { + if (flags & Fd_Shell_Command::SAVE_PROJECT) { save_cb(0, 0); } - if (g_shell_save_code) { + if (flags & Fd_Shell_Command::SAVE_SOURCECODE) { write_code_files(); } - if (g_shell_save_strings) { + 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(); @@ -293,8 +306,56 @@ void shell_pipe_cb(FL_SOCKET, void*) { } } -void do_shell_command(Fl_Return_Button*, void*) { - if (!prepare_shell_command()) return; +/** 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(Fl_String &cmd, const Fl_String ¯o, const Fl_String &content) { + for (int i=0;;) { + i = cmd.find(macro, i); + if (i==Fl_String::npos) break; + cmd.replace(i, macro.size(), content); + } +} + +static void expand_macros(Fl_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@") != Fl_String::npos) { +// find_fltk_config(); +// expand_macro(cmd, "@FLTK_CONFIG@", fltk_config_cmd.c_str()); +// } + if (cmd.find("@TMPDIR@") != Fl_String::npos) + expand_macro(cmd, "@TMPDIR@", get_tmpdir()); +} + +/** + 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 Fl_String &cmd, int flags) { + if (cmd.empty()) { + fl_alert("No shell command entered!"); + return; + } + + if (!prepare_shell_command(flags)) return; + + Fl_String expanded_cmd = cmd; + expand_macros(expanded_cmd); if (!shell_run_window->visible()) { Fl_Preferences pos(fluid_prefs, "shell_run_Window_pos"); @@ -310,10 +371,10 @@ void do_shell_command(Fl_Return_Button*, void*) { } // Show the output window and clear things... - shell_run_terminal->printf("\033[0;32m%s\033[0m\n", g_shell_command.c_str()); - shell_run_window->label(g_shell_command.c_str()); + 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 *)g_shell_command.c_str()) == NULL) { + 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"); @@ -329,28 +390,553 @@ void do_shell_command(Fl_Return_Button*, void*) { } /** - Show a dialog box to run an external shell command. + 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 Fl_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 Fl_String &in_name, + const Fl_String &in_label, + Fl_Shortcut in_shortcut, + Fd_Tool_Store in_storage, + int in_condition, + const Fl_String &in_condition_data, + const Fl_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; + prefs.get("name", name, "<unnamed>"); + prefs.get("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); + prefs.get("condition_data", condition_data, ""); + prefs.get("command", command, ""); + prefs.get("flags", flags, 0); +} + +void Fd_Shell_Command::write(Fl_Preferences &prefs, bool save_location) { + prefs.set("name", name); + prefs.set("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()) prefs.set("condition_data", condition_data); + if (!command.empty()) prefs.set("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); + } + } +} - Copies the current settings into the dialog box. +/** + 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'; + fluid_prefs.get("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); + } +} - This dialog box offers a field for a command line and three check buttons - to generate and save various files before the command is run. +/** + 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); + } + } +} - If the fourth checkbox, "use settings in .fl design files" is checked, - all shell settings will be store in the current .fl file, and they will - be read and restored when the .fl is loaded again. +/** + 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 + } + } +} - Fluid will save different shell settings for different operating system as - it is common that a different OS requires a different shell command. +/** + 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}"); + } +} - Fluid comes with default shell settings. Pressing the "save as default" button - will store the current setting in the Fluid app settings and are used for new - designs, or if the "use settings..." box is not checked. +/** + Add a previously created shell command to the end of the list. - Fluid app settings are saved per user and per machine. + \param[in] cmd a pointer to the command that we want to add */ -void show_shell_window() { +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; + |
