--- /dev/null
+/* Copyright (c) 2006 by Arkkra Enterprises */
+/* All rights reserved */
+
+// This file includes methods related to the "Run" menu on the main toolbar.
+
+#include "Run.H"
+#include "Preferences.H"
+#include "globals.H"
+#include "Config.H"
+#include "defines.h"
+#include <FL/fl_ask.H>
+#include <FL/filename.H>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <ctype.h>
+#include <sys/stat.h>
+#ifdef OS_LIKE_UNIX
+#include <sys/wait.h>
+#include <errno.h>
+#endif
+
+// Message for when Mup fails, but we can't figure out why.
+const char * Unknown_Mup_failure = "Mup failed. Reason unknown.";
+
+// Describe use of numbers passed to -x option
+#define EXTRACT_NUM_DESCRIPTION "0 is used for pickup measure.\n" \
+ "Negative numbers are used to specify\n" \
+ "number of measures from the end.\n"
+
+// Tooltip is the same for all macro entries
+const char * macro_definition_tip =
+ "Your Mup input can use macros to vary characteristics\n"
+ "of the output for different runs.\n"
+ "Enter the name of a macro you want to define,\n"
+ "optionally followed by an equals sign and macro definition.\n"
+ "Macro names must consist of upper case letters,\n"
+ "numbers, and underscores, starting with an upper case letter.";
+
+//------ Class for user to set parameters to pass to Mup
+
+Run_parameters_dialog::Run_parameters_dialog(void)
+ : Fl_Double_Window(0, 0, 700, 350, "Mup Options")
+{
+ enable_combine_p = new Fl_Check_Button(20, 20, 165, 30,
+ "Enable Auto Multirest");
+ enable_combine_p->tooltip("Set whether to automatically combine\n"
+ "consecutive measures of rests into a multirest.");
+ enable_combine_p->callback(combine_cb, this);
+ enable_combine_p->when(FL_WHEN_CHANGED);
+
+ rest_combine_p = new Positive_Int_Input(370, 20, 40, 30,
+ "Min Measures to Combine");
+ rest_combine_p->tooltip("Minimum number of consecutive measures\n"
+ "of rest that will be combined into a multirest.");
+ // Only becomes active if auto-multirest combine becomes active
+ rest_combine_p->deactivate();
+
+ // -p firstpage
+ first_page_p = new Positive_Int_Input(300, 60, 50, 30, "First Page's Page Number");
+ first_page_p->tooltip("Set the page number to use\n"
+ "for the first page of music output.");
+
+ // -o pagelist
+ pages_p = new Fl_Group(30, 100, 380, 70, "Pages to Display");
+ pages_p->box(FL_ENGRAVED_BOX);
+ pages_p->align(FL_ALIGN_TOP_LEFT | FL_ALIGN_INSIDE);
+
+ all_p = new Fl_Check_Button(170, 105, 55, 20, "All");
+ all_p->type(FL_RADIO_BUTTON);
+ all_p->tooltip("You can restrict to displaying only a subset\n"
+ "of the pages of music, but this shows all pages.");
+ all_p->callback(selected_pages_cb, this);
+ all_p->when(FL_WHEN_CHANGED);
+
+ odd_p = new Fl_Check_Button(245, 105, 60, 20, "Odd");
+ odd_p->type(FL_RADIO_BUTTON);
+ odd_p->tooltip("This restricts to displaying only\n"
+ "odd numbered pages. This is generally\n"
+ "most useful for printing to a\n"
+ "single-sided printer.");
+ odd_p->callback(selected_pages_cb, this);
+ odd_p->when(FL_WHEN_CHANGED);
+
+ even_p = new Fl_Check_Button(320, 105, 70, 20, "Even");
+ even_p->type(FL_RADIO_BUTTON);
+ even_p->tooltip("This restricts to displaying only\n"
+ "even numbered pages. This is generally\n"
+ "most useful for printing to a\n"
+ "single-sided printer.");
+ even_p->callback(selected_pages_cb, this);
+ even_p->when(FL_WHEN_CHANGED);
+
+ selected_p = new Fl_Check_Button(50, 135, 85, 20, "Selected:");
+ selected_p->tooltip("This restricts to displaying only\n"
+ "the pages you list in the blank to the right.");
+ selected_p->type(FL_RADIO_BUTTON);
+ selected_p->callback(selected_pages_cb, this);
+ selected_p->when(FL_WHEN_CHANGED);
+
+ page_list_p = new Fl_Input(150, 130, 230, 30, "");
+ page_list_p->tooltip("List the specific pages you want displayed.\n"
+ "You can list individual pages separated by commas,\n"
+ "and/or ranges of pages separated by dashes.\n"
+ "Pages may be listed more than once and in any order.");
+ page_list_p->deactivate();
+ saved_page_list = 0;
+ pages_p->end();
+
+ // -s stafflist
+ staff_list_p = new Fl_Input(150, 180, 260, 30, "Staffs to Display/Play");
+ staff_list_p->tooltip("If you wish to display or play only a subset\n"
+ "of the staffs, or voices on a staff, list them here.\n"
+ "Staffs are specified by comma-separated numbers\n"
+ "or ranges, like 1,4 or 1-3,5-6. Staff numbers can be\n"
+ "followed by v1, v2, or v3 to limit to a single voice.\n");
+ saved_staff_list = 0;
+
+ // -x extract list
+ extract_begin_p = new Int_Input(150, 220, 95, 30, "Extract Measures");
+ extract_begin_p->tooltip("If you wish to display or play only selected\n"
+ "measures, enter the starting measure here.\n"
+ EXTRACT_NUM_DESCRIPTION);
+ extract_begin_p->callback(extract_cb, this);
+ extract_begin_p->when(FL_WHEN_CHANGED);
+ extract_end_p = new Int_Input(315, 220, 95, 30, "through");
+ extract_end_p->tooltip("If you wish to display or play only selected\n"
+ "measures, enter the ending measure here.\n"
+ EXTRACT_NUM_DESCRIPTION);
+ extract_end_p->deactivate();
+
+ // Macros
+ macros_group_p = new Fl_Group(430, 20, 250, 40 + MAX_MACROS * 40, "Macro Definitions");
+ macros_group_p->box(FL_ENGRAVED_BOX);
+ macros_group_p->align(FL_ALIGN_TOP | FL_ALIGN_INSIDE);
+ int m;
+ for (m = 0; m < MAX_MACROS; m++) {
+
+ macro_definitions_p[m] = new Fl_Input(450, 50 + m * 40, 210, 30, "");
+ macro_definitions_p[m]->tooltip(macro_definition_tip);
+
+ saved_macro_definitions[m] = 0;
+ }
+ macros_group_p->end();
+
+ save_p = new Fl_Return_Button(100, h() - 50, 80, 30, "Save");
+ save_p->callback(Save_cb, this);
+ save_p->when(FL_WHEN_RELEASE);
+
+ clear_form_p = new Fl_Button((w() - 150) / 2, h() - 50, 150, 30,
+ "Clear Options");
+ clear_form_p->callback(clear_form_cb, this);
+ clear_form_p->when(FL_WHEN_RELEASE);
+
+ cancel_p = new Fl_Button(w() - 180, h() - 50, 80, 30, "Cancel");
+ cancel_p->shortcut(FL_Escape);
+ cancel_p->callback(Cancel_cb, this);
+ cancel_p->when(FL_WHEN_RELEASE);
+
+ // Set everything to default values
+ clear_form();
+
+ // Arrange to clean up all the new-ed widgets in destructor.
+ end();
+
+ // Arrange for window manager closes to do Cancel.
+ callback(Cancel_cb, this);
+ when(FL_WHEN_NEVER);
+}
+
+
+Run_parameters_dialog::~Run_parameters_dialog(void)
+{
+ int m;
+ for (m = 0; m < MAX_MACROS; m++) {
+ if (saved_macro_definitions[m] != 0) {
+ delete saved_macro_definitions[m];
+ }
+ }
+}
+
+
+//---- Callback for when user changes enablement of rest combining,
+// to gray or ungray the field for how many measures to combine.
+
+CALL_BACK(Run_parameters_dialog, combine)
+{
+ if (enable_combine_p->value()) {
+ rest_combine_p->activate();
+ }
+ else {
+ rest_combine_p->value("");
+ rest_combine_p->deactivate();
+ }
+}
+
+
+//---- Callback for when user changes setting of start of measure extraction,
+// to gray or ungray the field for ending measure of extraction.
+
+CALL_BACK(Run_parameters_dialog, extract)
+{
+ if (extract_begin_p->size() > 0) {
+ extract_end_p->activate();
+ }
+ else {
+ extract_end_p->value("");
+ extract_end_p->deactivate();
+ }
+}
+
+//---- Callback for when user changes setting of page selection,
+// to gray or ungray the field for listing the specific pages to display
+
+CALL_BACK(Run_parameters_dialog, selected_pages)
+{
+ page_list_p->value("");
+ if (selected_p->value() == 1) {
+ page_list_p->activate();
+ }
+ else {
+ page_list_p->deactivate();
+ }
+}
+
+
+//----Callback for button for clearing the form
+
+CALL_BACK(Run_parameters_dialog, clear_form)
+{
+ // Set everything to default values
+ saved_enable_combine = false;
+ enable_combine_p->value(saved_enable_combine);
+
+ (void) sprintf(saved_combine_measures, "");
+ rest_combine_p->value(saved_combine_measures);
+
+ (void) sprintf(saved_first_page, "%d", MINFIRSTPAGE);
+ first_page_p->value(saved_first_page);
+
+ if (saved_page_list != 0) {
+ delete saved_page_list;
+ }
+ saved_page_list = new char[1];
+ saved_page_list[0] = '\0';
+ page_list_p->value(saved_page_list);
+ all_p->value(1);
+ saved_pages = ALL_PAGES;
+
+ if (saved_staff_list != 0) {
+ delete saved_staff_list;
+ }
+ saved_staff_list = new char[1];
+ saved_staff_list[0] = '\0';
+ staff_list_p->value(saved_staff_list);
+
+ saved_extract_begin[0] = '\0';
+ extract_begin_p->value(saved_extract_begin);
+ saved_extract_end[0] = '\0';
+ extract_end_p->value(saved_extract_end);
+
+ int m;
+ for (m = 0; m < MAX_MACROS; m++) {
+ macro_definitions_p[m]->value("");
+ if (saved_macro_definitions[m] != 0) {
+ delete saved_macro_definitions[m];
+ saved_macro_definitions[m] = 0;
+ }
+ }
+}
+
+
+//---- callback for when user clicks "Save" on Set Options form
+
+CALL_BACK(Run_parameters_dialog, Save)
+{
+ // -c rest combine option
+ bool error = false;
+ saved_enable_combine = enable_combine_p->value();
+ int num_meas = (int) atoi(rest_combine_p->value());
+ if (saved_enable_combine && (num_meas < MINRESTCOMBINE || num_meas > MAXRESTCOMBINE)) {
+ fl_alert("\"Min Measures to Combine\" must be between\n"
+ "%d and %d.", MINRESTCOMBINE, MAXRESTCOMBINE);
+ error = true;
+ }
+ else {
+ (void) strcpy(saved_combine_measures, rest_combine_p->value());
+ }
+
+ // -p first page option
+ int page_num = (int) atoi(first_page_p->value());
+ if (page_num < MINFIRSTPAGE || page_num > MAXFIRSTPAGE) {
+ fl_alert("\"First Page\" number must be between\n"
+ "%d and %d.", MINFIRSTPAGE, MAXFIRSTPAGE);
+ error = true;
+ }
+ else {
+ (void) strcpy(saved_first_page, first_page_p->value());
+ }
+
+ // We don't really fully parse and error check the -o argument,
+ // but make sure it at least is made up of only valid characters.
+ if (page_list_p->size() > 0) {
+ if (strspn(page_list_p->value(), "0123456789,- \t")
+ != page_list_p->size()) {
+ fl_alert("\"Pages to Display\" value is not valid.");
+ error = true;
+ }
+ else {
+ // Free existing, if any, and save new value.
+ if (saved_page_list != 0) {
+ delete saved_page_list;
+ }
+ saved_page_list = new char[page_list_p->size() + 1];
+ strcpy(saved_page_list, page_list_p->value());
+ }
+ }
+ else if (saved_page_list[0] != '\0') {
+ // Had a list before but not anymore.
+ // Null out the existing list.
+ delete saved_page_list;
+ saved_page_list = new char[1];
+ saved_page_list[0] = '\0';
+ }
+ if (all_p->value() == 1) {
+ saved_pages = ALL_PAGES;
+ }
+ if (odd_p->value() == 1) {
+ saved_pages = ODD_PAGES;
+ }
+ if (even_p->value() == 1) {
+ saved_pages = EVEN_PAGES;
+ }
+ if (selected_p->value() == 1) {
+ saved_pages = SELECTED_PAGES;
+ }
+ if (saved_pages == SELECTED_PAGES && page_list_p->size() == 0) {
+ fl_alert("You did not list which selected pages you want.");
+ error = true;
+ }
+
+ // Similar for staff list (-s option)
+ if (staff_list_p->size() > 0) {
+ if (strspn(staff_list_p->value(), "0123456789,-v \t")
+ != staff_list_p->size()) {
+ fl_alert("\"Staffs to Display/Play\" is not valid");
+ error = true;
+ }
+ else {
+ if (saved_staff_list != 0) {
+ delete saved_staff_list;
+ }
+ saved_staff_list = new char[staff_list_p->size() + 1];
+ strcpy(saved_staff_list, staff_list_p->value());
+ }
+ }
+ else if (saved_staff_list[0] != '\0') {
+ delete saved_staff_list;
+ saved_staff_list = new char[1];
+ saved_staff_list[0] = '\0';
+ }
+
+ // We can't really error check -x option values.
+ // Widget will have already constrained to integer,
+ // and we have no way to know what range is valid,
+ // since we don't know how many measures the song has.
+ // However, to keep things simple, we have fixed-size array of 8 bytes
+ // for saving value, so can only allow up to 6 digits plus sign.
+ if (abs(atoi(extract_begin_p->value())) >= 1000000) {
+ fl_alert("\"Extract Measures\" start value is out of range.");
+ error = true;
+ }
+ else {
+ (void) strcpy(saved_extract_begin, extract_begin_p->value());
+ }
+ if (abs(atoi(extract_end_p->value())) >= 1000000) {
+ fl_alert("\"Extract Measures\" end value is out of range.");
+ error = true;
+ }
+ else {
+ (void) strcpy(saved_extract_end, extract_end_p->value());
+ }
+
+ // Macros
+ int m;
+ int macsize;
+ bool changed;
+ for (m = 0; m < MAX_MACROS; m++) {
+ changed = false;
+ if ((macsize = macro_definitions_p[m]->size()) > 0) {
+ if (macro_error(macro_definitions_p[m]->value())) {
+ fl_alert("Macro %d has invalid format.", m + 1);
+ error = true;
+ }
+ else if (saved_macro_definitions[m] == 0) {
+ // no macro before, but is one now
+ changed = true;
+ }
+ else if (strcmp(macro_definitions_p[m]->value(),
+ saved_macro_definitions[m]) != 0) {
+ // A different macro value than before
+ delete saved_macro_definitions[m];
+ changed = true;
+ }
+ if (changed) {
+ saved_macro_definitions[m] = new char[macsize + 1];
+ (void) strcpy(saved_macro_definitions[m],
+ macro_definitions_p[m]->value());
+ }
+ }
+ else if (macsize == 0 && saved_macro_definitions[m] != 0) {
+ // Used to be a macro value, but not anymore
+ delete saved_macro_definitions[m];
+ saved_macro_definitions[m] = 0;
+ }
+ }
+
+
+ // If there were user errors, we leave the window up for user to
+ // correct the errors. User can, of course, cancel without correcting,
+ // in which case if they try to run, Mup will complain about the
+ // bad arguments.
+ if ( ! error ) {
+ hide();
+ }
+}
+
+
+//---- Callback for when user clicks "Cancel" on Set Options form
+
+CALL_BACK(Run_parameters_dialog, Cancel)
+{
+ // Put back all the previous values
+ enable_combine_p->value(saved_enable_combine);
+ rest_combine_p->value(saved_combine_measures);
+ first_page_p->value(saved_first_page);
+
+ // It seems setting a radio button via value(1) doesn't reset the
+ // others, so clear all, then set the one we want
+ all_p->value(0);
+ odd_p->value(0);
+ even_p->value(0);
+ selected_p->value(0);
+ switch (saved_pages) {
+ default: // default should be impossible
+ case ALL_PAGES:
+ all_p->value(1);
+ break;
+ case ODD_PAGES:
+ odd_p->value(1);
+ break;
+ case EVEN_PAGES:
+ even_p->value(1);
+ break;
+ case SELECTED_PAGES:
+ selected_p->value(1);
+ break;
+ }
+ page_list_p->value(saved_page_list);
+ staff_list_p->value(saved_staff_list);
+ extract_begin_p->value(saved_extract_begin);
+ extract_end_p->value(saved_extract_end);
+
+ int m;
+ for (m = 0; m < MAX_MACROS; m++) {
+ macro_definitions_p[m]->value( saved_macro_definitions[m] == 0
+ ? "" : saved_macro_definitions[m]);
+ }
+ hide();
+}
+
+// Return true if check of macro finds an error.
+
+bool
+Run_parameters_dialog::macro_error(const char * macro)
+{
+ // First character must be an upper case letter.
+ if ( ! isupper(macro[0])) {
+ return(true);
+ }
+ // Everything up to = or end of string, whichever comes first,
+ // must be upper case letter, digit, or underscore.
+ const char * m_p;
+ for (m_p = macro; *m_p != '\0' && *m_p != '='; m_p++) {
+ if ( ! isupper(*m_p) && ! isdigit(*m_p) && *m_p != '_') {
+ return(true);
+ }
+ }
+ return(false);
+}
+
+
+//--------class for Run menu -----------------------------------------------
+
+Run::Run()
+{
+ parameters_p = 0;
+ report_p = 0;
+#ifdef OS_LIKE_WIN32
+ display_child.hProcess = 0;
+ display_child.dwProcessId = 0;
+ MIDI_child.hProcess = 0;
+ MIDI_child.dwProcessId = 0;
+#else
+ display_child = 0;
+ MIDI_child = 0;
+#endif
+}
+
+Run::~Run()
+{
+ if (parameters_p != 0) {
+ delete parameters_p;
+ parameters_p = 0;
+ }
+ if (report_p != 0) {
+ delete report_p;
+ report_p = 0;
+ }
+ // Kill off any child processes
+ clean_up();
+}
+
+
+
+//------------callback for Display menu item
+
+CALL_BACK(Run, Display)
+{
+ Run_Mup(false, true);
+}
+
+
+//-----Callback for menu item to play MIDI file
+
+CALL_BACK(Run, Play)
+{
+ Run_Mup(true, true);
+}
+
+
+//---------callback for menu item for writing PostScript output
+
+CALL_BACK(Run, WritePostScript)
+{
+ Run_Mup(false, false);
+}
+
+//---------callback for menu item for writing MIDI output
+
+CALL_BACK(Run, WriteMIDI)
+{
+ Run_Mup(true, false);
+}
+
+//---- callback for menu item to collect Mup command line parameters from user
+
+CALL_BACK(Run, Options)
+{
+ if (parameters_p == 0) {
+ parameters_p = new Run_parameters_dialog();
+ }
+ parameters_p->show();
+}
+
+
+//--------- This lets Run class know the file name and editor buffer,
+// which it needs access to, so it can run Mup on its contents.
+
+void
+Run::set_file(File * file_info_p)
+{
+ file_p = file_info_p;
+}
+
+
+// Run Mup commands with user's parameters on the current file
+
+void
+Run::Run_Mup(bool midi, bool show_or_play)
+{
+
+ const char * mup_input = file_p->effective_filename();
+ // Make sure file has been written.
+ // The fiilename == 0 will be hit in the case where
+ // user did New From Template but made no changes.
+ if (file_p->unsaved_changes || file_p->filename == 0) {
+ int auto_save;
+ (void) Preferences_p->get(Auto_save_preference, auto_save,
+ Default_auto_save);
+ if (auto_save) {
+ file_p->Save(false);
+ if (file_p->unsaved_changes || file_p->filename == 0) {
+ // User probably canceled a Save that got
+ // turned into a SaveAs,
+ // or maybe Save failed. If not properly
+ // saved, we shouldn't try to process it.
+ return;
+ }
+ }
+ else {
+ bool hide_the_No;
+ const char * extra_text;
+ // If there is a previous version of the file,
+ // allow user to select "No," which means use that
+ // previous version rather than writing out current.
+ if (access(mup_input, F_OK) == 0) {
+ hide_the_No = false;
+ extra_text = "(If you select \"No,\" "
+ "the most recently saved version "
+ "of the Mup input file will be used.)";
+ }
+ else {
+ // No previous version
+ hide_the_No = true;
+ extra_text = "";
+ }
+ switch (file_p->save_changes_check(extra_text, hide_the_No)) {
+ default: // default case should be impossible
+ case Save_confirm_dialog::Cancel:
+ return;
+ case Save_confirm_dialog::No:
+ break;
+ case Save_confirm_dialog::Yes:
+ file_p->Save(false);
+ if (file_p->unsaved_changes
+ || file_p->filename == 0) {
+ // User probably canceled the Save,
+ // or maybe it failed. If not properly
+ // saved, we shouldn't try to process it.
+ return;
+ }
+ break;
+ }
+ }
+ // If was "Untitled" before, it is now saved under a good
+ // name, so get what that is.
+ mup_input = file_p->effective_filename();
+ }
+
+ // Get length of file name without the extension
+ int base_length = strlen(mup_input)
+ - strlen(fl_filename_ext(mup_input));
+
+ // Create output file name
+ char mup_output[base_length + (midi ? 5 : 4)];
+ strncpy(mup_output, mup_input, base_length);
+ strcpy(mup_output + base_length, (midi ? ".mid" : ".ps"));
+
+ // Create error file name
+ char mup_error[base_length + 5];
+ strncpy(mup_error, mup_input, base_length);
+ strcpy(mup_error + base_length, ".err");
+
+ // Get Mup command to use.
+ char * mup_command;
+ (void) Preferences_p->get(Mup_program_location, mup_command,
+ Default_Mup_program_location);
+ char full_location[FL_PATH_MAX];
+ if ( ! find_executable(mup_command, full_location)) {
+ fl_alert("Mup command not found.\n"
+ "Check Config > File Locations setting.");
+ return;
+ }
+
+ if (parameters_p == 0) {
+ // User hasn't set any parameters, so we will use all
+ // defaults. Creating the instance lets us deference it
+ // below, so we don't have to care whether user ever
+ // went to the Set Options page or not.
+ parameters_p = new Run_parameters_dialog();
+ }
+
+ // Build up list of arguments.
+ // array slots needed for args:
+ // 1 for Mup command itself
+ // 2 for -e and arg
+ // 2 for -f or -m and arg
+ // 2 for -c and arg
+ // 2 for -p and arg
+ // 2 for -o and arg
+ // 2 for -s and arg
+ // 2 for -x and arg
+ // 1 for Mup input file name
+ // 2 for each -D and its macro definition arg
+ // 1 for null terminator
+ const char * command[17 + 2 * MAX_MACROS];
+ command[0] = full_location;
+ command[1] = "-e";
+ command[2] = mup_error;
+ command[3] = (midi ? "-m" : "-f");
+ command[4] = mup_output;
+ int arg_offset = 5;
+
+ // rest combine
+ if (parameters_p->enable_combine_p->value() &&
+ parameters_p->rest_combine_p->size() > 0) {
+ command[arg_offset++] = "-c";
+ command[arg_offset++] = parameters_p->rest_combine_p->value();
+ }
+
+ // first page
+ if (parameters_p->first_page_p->size() > 0) {
+ command[arg_offset++] = "-p";
+ command[arg_offset++] = parameters_p->first_page_p->value();
+ }
+
+ // page list
+ switch (parameters_p->saved_pages) {
+ default: // default should be impossible
+ case Run_parameters_dialog::ALL_PAGES:
+ break;
+ case Run_parameters_dialog::ODD_PAGES:
+ command[arg_offset++] = "-o";
+ command[arg_offset++] = "odd";
+ break;
+ case Run_parameters_dialog::EVEN_PAGES:
+ command[arg_offset++] = "-o";
+ command[arg_offset++] = "even";
+ case Run_parameters_dialog::SELECTED_PAGES:
+ if (parameters_p->page_list_p->size() > 0) {
+ command[arg_offset++] = "-o";
+ command[arg_offset++] = parameters_p->page_list_p->value();
+ }
+ // Else they said selected, but then didn't list
+ // any particular pages. We treat like ALL_PAGES,
+ // since no pages is useless.
+ // We would have already given error message,
+ // but they must have ignored it.
+ break;
+ }
+
+ // staff list
+ if (parameters_p->staff_list_p->size() > 0) {
+ command[arg_offset++] = "-s";
+ command[arg_offset++] = parameters_p->staff_list_p->value();
+ }
+
+ // extract list
+ char xoption[parameters_p->extract_begin_p->size()
+ + parameters_p->extract_end_p->size() + 2];
+ if (parameters_p->extract_begin_p->size() > 0) {
+ command[arg_offset++] = "-x";
+ (void) strcpy(xoption, parameters_p->extract_begin_p->value());
+ if (parameters_p->extract_end_p->size() > 0) {
+ (void) strcat(xoption, ",");
+ (void) strcat(xoption, parameters_p->extract_end_p->value());
+ }
+ command[arg_offset++] = xoption;
+ }
+
+ // -D options
+ int m;
+ for (m = 0; m < MAX_MACROS; m++) {
+ if (parameters_p->saved_macro_definitions[m] != 0) {
+ command[arg_offset++] = "-D";
+ command[arg_offset++] = parameters_p->saved_macro_definitions[m];
+ }
+ }
+
+ // Mup input file name and null terminator
+ command[arg_offset++] = mup_input;
+ command[arg_offset++] = 0;
+
+ static bool set_mupquiet = false;
+ if ( ! set_mupquiet ) {
+ putenv("MUPQUIET=1");
+ set_mupquiet = true;
+ }
+
+ // Look up the right (dis)player program to use.
+ // On Windows we need this even if we are only writing the file,
+ // not actually (dis)playing. To keep the code simpler,
+ // we just always look it up.
+ char * player_command;
+ if (midi) {
+ (void) Preferences_p->get(MIDI_player_location,
+ player_command,
+ Default_MIDI_player_location);
+ }
+ else {
+ (void) Preferences_p->get(Viewer_location,
+ player_command,
+ Default_viewer_location);
+ }
+#ifdef OS_LIKE_WIN32
+ // Media player locks the file it plays, so if we had
+ // run it before, we have to make it release the lock
+ // before we run Mup. Also, if it was playing a different file
+ // before, it doesn't seem to want to play ours.
+ // So if the MIDI player of choice is the media player,
+ // we first try to make it close somewhat gracefully,
+ // and wait a second for that to complete.
+ // In any case we try to kill off any child MIDI players we
+ // know of. If the graceful close already worked,
+ // it should find the child already dead.
+ // If they are using some other MIDI player,
+ // we don't know what to do, so we'll just kill any child
+ // MIDI player we know spawned previously, if any.
+ // Would be nice to be able to do something less drastic than
+ // kill the process, but we're not sure what else would be effective.
+ if (midi) {
+ if (is_mplayer(player_command)) {
+ HWND window_handle;
+ if ((window_handle = FindWindow(0, "Windows Media Player")) != 0) {
+ SendMessage(window_handle, WM_CLOSE, 0, 0);
+ }
+ _sleep(1000);
+ }
+ if (has_MIDI_child()) {
+ kill_process(&MIDI_child, "MIDI player");
+ }
+ }
+ // Somewhat similarly, we kill off any prior displayer, unless
+ // the displayer is GSview, which we know we can pass -e argument
+ // to make it reuse existing instance, if any.
+ else if ( ! is_gsview(player_command) && has_display_child()) {
+ kill_process(&display_child, "display");
+ }
+#endif
+
+ // Run the command.
+ int ret = execute_command(command, 0, true);
+
+ // Report the errors, if any.
+ struct stat info;
+ if (stat(mup_error, &info) == 0) {
+ if (info.st_size > 0) {
+ if (report_p == 0) {
+ report_p = new Error_report();
+ }
+ report_p->loadfile(mup_error);
+ report_p->show();
+ }
+ unlink(mup_error);
+ }
+ else if (ret != 0) {
+ // Exited with error, but left no error file.
+ // Must have died badly (core dumped, execvp failed, etc)
+#if defined(WIFEXITED) && defined(WIFSIGNALED)
+ if (WIFEXITED(ret)) {
+ // Did exit(), so most likely exec failed
+ // due to bad path to Mup program,
+ // although we should have caught that above.
+ fl_alert("Mup exited with return code %d but no error output.\n"
+ "Check Config > File Locations setting.",
+ WEXITSTATUS(ret));
+ }
+ else if (WIFSIGNALED(ret)) {
+ // Probably core dump :-(
+ fl_alert("Mup exited due to signal %d.", WTERMSIG(ret));
+ } else {
+ fl_alert(Unknown_Mup_failure);
+ }
+#else // WIF... macros not defined
+ if (ret == -1) {
+#ifdef OS_LIKE_WIN32
+ // Look up the error reason to include in message.
+ DWORD format_retval;
+ LPVOID error_string = 0;
+ if (FormatMessage(
+ FORMAT_MESSAGE_ALLOCATE_BUFFER
+ | FORMAT_MESSAGE_FROM_SYSTEM
+ | FORMAT_MESSAGE_ARGUMENT_ARRAY,
+ 0, GetLastError(),
+ LANG_NEUTRAL, (LPTSTR)&error_string,
+ 0, 0)) {
+ fl_alert("Failed to execute Mup program:\n%s"
+ "Check settings in Config > File Locations.",
+ (char *) error_string);
+ LocalFree((HLOCAL)error_string);
+ }
+ else {
+ fl_alert(Unknown_Mup_failure);
+ }
+ }
+#else // not Windows
+ fl_alert(Unknown_Mup_failure);
+ }
+#endif
+ else {
+ fl_alert(Unknown_Mup_failure);
+ }
+#endif // WIF... macros
+ }
+
+ if (ret != 0) {
+ // Something went wrong. We should not go on to
+ // display/play even if user had asked for that.
+ return;
+ }
+
+ // We wrote the output file successfully.
+ // Hide any previous error window and
+ // check if we need to (dis)play the results.
+ if (report_p != 0) {
+ report_p->hide();
+ }
+ if ( ! show_or_play ) {
+ // User just asked to generate a file, not (dis)play it.
+ fl_message("%s output file\n\n %s\n\n"
+ "has been successfully created.",
+ (midi ? "MIDI" : "PostScript"), mup_output);
+ return;
+ }
+
+
+ if ( ! find_executable(player_command, full_location)) {
+ fl_alert("Unable to run %s command.\n"
+ "Check Config > File Locations setting.",
+ player_command);
+ return;
+ }
+
+#ifdef OS_LIKE_UNIX
+ // For MIDI, we try to kill the previous player
+ // if any, since we'll probably need the same
+ // sound card. Note that for Windows, we did this
+ // even before running Mup, since the player may lock
+ // the file so Mup would be unable to write it.
+ if (midi) {
+ kill_process(&MIDI_child, "MIDI player");
+ }
+
+ // If using gv as displayer and one already running on
+ // this file, it would be nice to get send it SIGHUP
+ // to make it re-read the file
+ // rather than starting a new one.
+ // But for other displayers, we don't know what signal
+ // might work, if any. And even with gv, if we send
+ // the signal, we have no way of knowing whether it
+ // actually worked. And there are lots of cases to
+ // consider. Suppose the displayer was gv,
+ // and they brought up an instance,
+ // then they changed to some other displayer,
+ // and then back to gv. Should we have killed off
+ // the first gv when bringing up the other viewer,
+ // of should we keep it up and reuse it after they
+ // change back? What if they kill the current
+ // instance just after we decided to re-use it?
+ // So just keep things simple for now. Always
+ // kill off any existing viewer we know of and
+ // start up a new one.
+ else { // displaying
+ kill_process(&display_child, "display");
+ }
+#endif
+
+ // Fill in the argv array for displayer/player
+ command[0] = full_location;
+#ifdef OS_LIKE_WIN32
+ // Media player appears to ignore its argument
+ // if it isn't a full path name, including drive.
+ // So ensure we have everything.
+ char fullpath[FL_PATH_MAX];
+ if (mup_output[1] != ':' ) {
+ // We don't have complete path.
+ // Get current directory including drive.
+ if (getcwd(fullpath, sizeof(fullpath)) == 0) {
+ fl_alert("Unable to determine current folder.");
+ return;
+ }
+ if (mup_output[0] != dir_separator()) {
+ // Relative path
+ (void) sprintf(fullpath + strlen(fullpath),
+ "%c%s", dir_separator(),
+ mup_output);
+ }
+ else {
+ // Was full path except for drive.
+ (void) strcpy(fullpath + 2, mup_output);
+ }
+ }
+ else {
+ // Can use existing path as is.
+ (void) strcpy(fullpath, mup_output);
+ }
+
+ if ( ! midi && is_gsview(player_command)) {
+ // For gsview, use -e to make it use
+ // existing instance if any.
+ command[1] = "-e";
+ command[2] = fullpath;
+ command[3] = '\0';
+ }
+ else {
+ command[1] = fullpath;
+ command[2] = '\0';
+
+ }
+#else
+ command[1] = mup_output;
+ command[2] = '\0';
+#endif
+
+ if (execute_command(command,
+ (midi ? &MIDI_child : &display_child)) != 0) {
+ fl_alert("Unable to run %s command.\n"
+ "Check settings under Config > File Locations.",
+ command[0]);
+ }
+}
+
+
+// Execute given command with the given argv.
+// If proc_info_p is zero, wait for the process to complete,
+// otherwise save information about the spawned process in what it points to,
+// so that the caller can keep track of it while it runs independently.
+// The hide_window parameter is only used for Windows and causes the
+// spawned process to be created with flags to not create a window,
+// This lets us use a console mode version of Mup, so traditional users
+// can continue to run Mup in a console without mupmate, but we can
+// run the same .exe without the annoyance of a console popping up.
+// Returns 0 on success, -1 on failure to create process,
+// or what the process returned if it had non-zero exit code,
+// and was waited for.
+
+int
+Run::execute_command(const char ** argv, Proc_Info *proc_info_p, bool hide_window)
+{
+ int ret = -1;
+
+#ifdef OS_LIKE_UNIX
+ pid_t child;
+ switch (child = fork()) {
+ case 0:
+ execvp(argv[0], (char * const *)argv);
+ // If here, the exec failed. Child must die.
+ exit(1);
+ case -1:
+ // failed
+ if (proc_info_p != 0) {
+ *proc_info_p = 0;
+ }
+ break;
+ default:
+ if (proc_info_p == 0) {
+ // wait for child to complete
+ if (waitpid(child, &ret, 0) < 0) {
+ ret = -1;
+ }
+ }
+ else {
+ *proc_info_p = child;
+ ret = 0;
+ }
+ break;
+ }
+#else
+
+#ifdef OS_LIKE_WIN32
+
+ // Convert the argv array into a string with each argument quoted.
+ // First calculate how much space is needed.
+ int a;
+ int length = 0;
+ bool has_quote = false; // to optimize normal case
+ for (a = 0; argv[a] != 0; a++) {
+ length += strlen(argv[a]);
+ // A quoted argv[0] won't work; just quote the other args.
+ if (a > 0) {
+ // Add space before the arg and quotes on each end.
+ length += 3;
+ const char * s;
+ // If embedded quote, add space for escaping it
+ for (s = argv[a]; *s != '\0'; s++) {
+ if (*s == '"') {
+ length++;
+ has_quote = true;
+ }
+ }
+ }
+ }
+
+ // Get space and fill in all the arguments, properly quoted.
+ char command[length + 1];
+ (void) strcpy(command, argv[0]);
+ char * dest = command + strlen(command);
+ for (a = 1; argv[a] != 0; a++) {
+ *dest++ = ' ';
+ *dest++ = '"';
+ if (has_quote) {
+ int i;
+ for (i = 0; argv[a][i] != '\0'; i++) {
+ if (argv[a][i] == '"') {
+ *dest++ = '\\';
+ }
+ *dest++ = argv[a][i];
+ }
+ }
+ else {
+ (void) strcpy(dest, argv[a]);
+ dest += strlen(dest);
+ }
+ *dest++ = '"';
+ }
+ *dest = '\0';
+
+ // Fill in information for starting up the process
+ PROCESS_INFORMATION process_info;
+ STARTUPINFO startup_info;
+ memset( &startup_info, 0, sizeof(startup_info));
+ startup_info.cb = sizeof(startup_info);
+ DWORD create_flags; // flags to control creation aspects
+ if (hide_window) {
+ startup_info.dwFlags = STARTF_USESHOWWINDOW;
+ startup_info.wShowWindow = SW_HIDE;
+ create_flags = CREATE_NO_WINDOW;
+ }
+ else {
+ create_flags = 0;
+ }
+
+ // Run the process
+ BOOL proc = CreateProcess(NULL, command, NULL, NULL,
+ TRUE, create_flags, NULL, NULL,
+ &startup_info, &process_info);
+
+ if (proc) {
+ // It was successfully created.
+ if (proc_info_p == 0) {
+ // wait for child to complete
+ DWORD result = WaitForSingleObject(
+ process_info.hProcess, INFINITE);
+ switch (result) {
+ case WAIT_FAILED:
+ case WAIT_ABANDONED:
+ case WAIT_TIMEOUT:
+ ret = -1;
+ break;
+ default:
+ GetExitCodeProcess(process_info.hProcess, &result);
+ ret = (int) result;
+ }
+ }
+ else {
+ *proc_info_p = process_info;
+ ret = 0;
+ }
+ }
+ else {
+ proc_info_p->hProcess = 0;
+ proc_info_p->dwProcessId = 0;
+ ret = -1;
+ }
+
+#else
+ fl_alert("Process execution only implemented\n"
+ "for Linux (and similar) and Windows so far...");
+ ret = -1;
+#endif
+
+#endif
+ return(ret);
+}
+
+
+// Kill the specified process, if it exists.
+// The description is used in error messages
+
+void
+Run::kill_process(const Proc_Info * const proc_info_p, const char * const description)
+{
+#ifdef OS_LIKE_UNIX
+ int exitstatus;
+ if (*proc_info_p == 0 || waitpid(*proc_info_p, &exitstatus, WNOHANG)
+ == *proc_info_p) {
+ // No process spawned or the one we had already died
+ return;
+ }
+ // Not clear how hard to try to kill process.
+ // SIGTERM should usually work, and is preferable if it works.
+ // We wait a little while and try SIGKILL if it hasn't died yet.
+ // We don't check for errors, because the
+ // only errors that should be possible are bad signal (should be
+ // impossible, since we hard-code SIGTERM), process doesn't exist
+ // (already dead, so no need to kill it), or bad permission (we
+ // spawned it ourself, so ought to have permission to kill it).
+ (void) kill(*proc_info_p, SIGTERM);
+ // Wait for up to 3 seconds for it to die
+ int w;
+ int ret;
+ for (w = 0; w < 3; w++) {
+ if ((ret = waitpid(*proc_info_p, &exitstatus, WNOHANG))
+ == *proc_info_p) {
+ // It died.
+ // *** Especially in the case of MIDI, there is a
+ // chance the player has already written stuff out
+ // to the device driver that it won't clear out when
+ // it dies, so that if we try to start a new one,
+ // the device will still be busy. But we have no way
+ // to know how long to wait, if at all, and don't
+ // want to always sleep a long time just to cover
+ // the case of trying to play again while in the middle
+ // of playing.
+ return;
+ }
+ if (ret == -1 && errno == ECHILD) {
+ // Child doesn't exist, so nothing to kill
+ return;
+ }
+ sleep(1);
+ }
+ // Okay. Resort to SIGKILL and wait 1 more second to let it die.
+ kill(*proc_info_p, SIGKILL);
+ sleep(1);
+#endif
+#ifdef OS_LIKE_WIN32
+ if (proc_info_p->hProcess == 0 || WaitForSingleObject(
+ proc_info_p->hProcess, 0) != WAIT_TIMEOUT) {
+ // No process spawned or the one we had must have already died.
+ return;
+ }
+ HANDLE handle;
+ if ( (handle = OpenProcess(PROCESS_TERMINATE,
+ FALSE, proc_info_p->dwProcessId)) == 0 ||
+ TerminateProcess(handle, 0) == 0) {
+ // Warn user we were unable to kill the old child.
+ DWORD format_retval;
+ LPVOID error_string = 0;
+ if (FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER
+ | FORMAT_MESSAGE_FROM_SYSTEM
+ | FORMAT_MESSAGE_ARGUMENT_ARRAY,
+ 0, GetLastError(),
+ LANG_NEUTRAL, (LPTSTR)&error_string,
+ 0, 0)) {
+ fl_alert("Unable to terminate %s process.\n%s",
+ description, (char *) error_string);
+ LocalFree((HLOCAL)error_string);
+ }
+ }
+ if (handle != 0) {
+ CloseHandle(handle);
+ }
+#endif
+}
+
+
+#ifdef OS_LIKE_WIN32
+// Return true if the given command is for Windows Media Player.
+
+bool
+Run::is_mplayer(const char * command)
+{
+ int length = strlen(command);
+ if (strcasecmp(command + length - 12, "wmplayer.exe") == 0 ||
+ strcasecmp(command + length - 11, "mplayer.exe") == 0 ||
+ strcasecmp(command + length - 8, "wmplayer") == 0 ||
+ strcasecmp(command + length - 7, "mplayer") == 0) {
+ return(true);
+ }
+ return(false);
+}
+
+
+// Return true if the given command is for GSview.
+
+bool
+Run::is_gsview(const char * command)
+{
+ int length = strlen(command);
+ if (strcasecmp(command + length - 12, "gsview32.exe") == 0 ||
+ strcasecmp(command + length - 10, "gsview.exe") == 0 ||
+ strcasecmp(command + length - 8, "gsview32") == 0 ||
+ strcasecmp(command + length - 6, "gsview") == 0) {
+ return(true);
+ }
+ return(false);
+}
+#endif
+
+
+// Called when user begins a new file, or when exiting,
+// to kill off child proceses that were spawned.
+// We start new processes for new file,
+// even if one already up for current file.
+// Not sure if that's what user wants, but...
+
+void
+Run::clean_up(void)
+{
+ kill_process(&display_child, "display");
+ kill_process(&MIDI_child, "MIDI player");
+}
+
+
+// Report whether we spawned a display child and think it is still alive.
+
+bool
+Run::has_display_child(void)
+{
+#ifdef OS_LIKE_WIN32
+ return(display_child.hProcess != 0);
+#else
+ return(display_child != 0);
+#endif
+}
+
+
+// Report whether we spawned a MIDI child and think it is still alive.
+
+bool
+Run::has_MIDI_child(void)
+{
+#ifdef OS_LIKE_WIN32
+ return(MIDI_child.hProcess != 0);
+#else
+ return(MIDI_child != 0);
+#endif
+}
+
+
+//------------ Class for displaying errors from Mup
+
+Error_report::Error_report(void)
+ : Fl_Double_Window(Default_width, Default_height, "Error report")
+{
+ text_p = new Fl_Text_Display(20, 20, w() - 40, h() - 90);
+ resizable((Fl_Widget *) text_p);
+ text_p->buffer( new Fl_Text_Buffer() );
+
+ // Set font/size and arrange to get notified of changes in them
+ font_change_reg_p = new Font_change_registration(
+ font_change_cb, (void *) this);
+
+ ok_p = new Fl_Return_Button(w() / 2 - 40, h() - 50, 80, 30, "OK");
+ ok_p->callback(OK_cb, this);
+ show();
+
+ // Arrange to clean up new-ed widgets in destructor.
+ end();
+}
+
+Error_report::~Error_report()
+{
+}
+
+
+// Load the error file (from -e of Mup) into window to show user.
+
+int
+Error_report::loadfile(const char * filename)
+{
+ return text_p->buffer()->loadfile(filename);
+}
+
+
+// Callback for when user clicks OK after reading Mup error report
+
+CALL_BACK(Error_report, OK)
+{
+ hide();
+}
+
+
+// Callback for change in font/size
+
+void
+Error_report::font_change_cb(void * data, Fl_Font font, unsigned char size)
+{
+ ((Error_report *)data)->font_change(font, size);
+}
+
+void
+Error_report::font_change(Fl_Font font, unsigned char size)
+{
+ text_p->textfont(font);
+ text_p->textsize(size);
+ text_p->redisplay_range(0, text_p->buffer()->length());
+}