From 1985aefc0e502048f92b91beef87c0dfbe669fed Mon Sep 17 00:00:00 2001 From: Matthias Melcher Date: Fri, 7 Mar 2025 16:34:35 +0100 Subject: Restructuring Fluid source files. --- fluid/app/shell_command.cxx | 1005 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1005 insertions(+) create mode 100644 fluid/app/shell_command.cxx (limited to 'fluid/app/shell_command.cxx') 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 $ +// - 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 < +#include +#include +#include + +#include + +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, ""); + preferences_get(prefs, "label", 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=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; istorage = 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; istorage == 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; istorage == FD_STORE_PROJECT) + n_in_project_file++; + } + if (n_in_project_file > 0) { + out->write_string("\nshell_commands {"); + for (int i=0; istorage == 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; iis_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; iis_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; + -- cgit v1.2.3