X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/mup/blobdiff_plain/cdb3c0882392596f814cf939cbfbd38adc6f2bfe..ddf6330b56bcfb657e0186b24b9b1422c51d3424:/mup/mupmate/Main.C diff --git a/mup/mupmate/Main.C b/mup/mupmate/Main.C new file mode 100644 index 0000000..906f6d4 --- /dev/null +++ b/mup/mupmate/Main.C @@ -0,0 +1,724 @@ +/* Copyright (c) 2006 by Arkkra Enterprises */ +/* All rights reserved */ + +// Code for the main window for Mupmate, a front end program for +// the Mup music publisher program from Arkkra Enterprises. +// It uses the FLTK toolkit for OS independence. + +// This file contains code for the toolbar and editor window, +// as well as general startup and showing the license. + +// We only support editing a single file at a time, so most classes +// as really effectively singletons, but the code is written to be +// able to support multiple instances, in case we ever want to do that. +// That means callback functions are always passed a pointer to +// a class instance as their second argument, and all they do is cast +// that to the appropriate type, and call the corresponding class method. + +// For the most part, widgets are only allocated when needed, but then +// stay around for the life of the process, in case they are needed again. + +// Callbacks are named with _cb suffix. +// Pointers are named with _p suffix, except for (char *) types +// that are pointing to text strings, which don't have any special suffix. + + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "globals.H" +#include "Main.H" +#include "Preferences.H" +#include "utils.H" + +#include +#ifdef OS_LIKE_WIN32 +#include "resource.h" +#else +#include +#ifdef OS_LIKE_UNIX +#include +#include "mup32.xpm" +#endif +#endif + +// Height of the tool bar on the main window +#define TOOLBAR_HEIGHT 30 + +// How often to blink the cursor. +#define BLINK_RATE 0.5 + +// If file indicating user has agreed to license terms doesn't exist, +// we ask them to agree. This is the text of the license. +extern const char * const license_text; + +//---------------------------------------------------------------------- + +// Define the toolbar and its submenus. +// Indented lines indicate the submenus. +// The & indicates shortcut key + +const char * File_label = "&File"; + const char * New_label = "&New"; + const char * NewFromTemplate_label = "New From &Template"; + const char * Open_label = "&Open..."; + const char * Save_label = "&Save"; + const char * SaveAs_label = "Save &As..."; + const char * Exit_label = "E&xit"; +const char * Edit_label = "&Edit"; + const char * Undo_label = "&Undo"; + const char * Cut_label = "Cu&t"; + const char * Copy_label = "&Copy"; + const char * Paste_label = "&Paste"; + const char * Delete_label = "&Delete"; + const char * Find_label = "&Find..."; + const char * FindNext_label = "Find &Next"; + const char * Replace_label = "&Replace..."; + const char * GoTo_label = "&Go To..."; + const char * SelectAll_label = "&Select All"; +const char * Run_label = "&Run"; + const char * Display_label = "&Display"; + const char * Play_label = "&Play"; + const char * WritePostScript_label = "&Write PostScript File"; + const char * WriteMIDI_label = "Write &MIDI File"; + const char * Options_label = "&Set Options..."; +const char * Config_label = "&Config"; + const char * FileLocations_label = "&File Locations..."; + const char * Preferences_label = "&Preferences..."; + const char * RegistrationForm_label = "&Registration Form..."; + const char * RegistrationKey_label = "Registration &Key..."; +const char * Help_label = "&Help"; + const char * UserGuide_label = "Mup &User's Guide"; + const char * StartupHints_label = "&Startup Hints"; + const char * AboutMupmate_label = "&About Mupmate"; + +Fl_Menu_Item Toolbar_menu[] = { + { File_label, 0, 0, 0, FL_SUBMENU }, + { New_label, FL_CTRL + 'n', File::New_cb }, + { NewFromTemplate_label, FL_CTRL + 't', File::NewFromTemplate_cb }, + { Open_label, FL_CTRL + 'o', File::Open_cb }, + { Save_label, FL_CTRL + 's', File::Save_cb }, + { SaveAs_label, 0, File::SaveAs_cb, 0, FL_MENU_DIVIDER }, + { Exit_label, 0, File::Exit_cb }, + { 0 }, + { Edit_label, 0, 0, 0, FL_SUBMENU }, + { Undo_label, FL_CTRL + 'z', Edit::Undo_cb, 0, FL_MENU_INACTIVE | FL_MENU_DIVIDER }, + { Cut_label, FL_CTRL + 'x', Edit::Cut_cb, 0, FL_MENU_INACTIVE }, + { Copy_label, FL_CTRL + 'c', Edit::Copy_cb, 0, FL_MENU_INACTIVE }, + { Paste_label, FL_CTRL + 'v', Edit::Paste_cb, 0, FL_MENU_INACTIVE }, + { Delete_label, FL_Delete, Edit::Delete_cb, 0, FL_MENU_INACTIVE | FL_MENU_DIVIDER }, + { Find_label, FL_CTRL + 'f', Edit::Find_cb, 0, FL_MENU_INACTIVE }, + { FindNext_label, FL_F + 3, Edit::FindNext_cb, 0, FL_MENU_INACTIVE }, + { Replace_label, FL_CTRL + 'h', Edit::Replace_cb, 0, FL_MENU_INACTIVE }, + { GoTo_label, FL_CTRL + 'g', Edit::GoTo_cb, 0, FL_MENU_DIVIDER }, + { SelectAll_label, FL_CTRL + 'a', Edit::SelectAll_cb, 0, FL_MENU_INACTIVE }, + { 0 }, + { Run_label, 0, 0, 0, FL_SUBMENU }, + { Display_label, 0, Run::Display_cb, 0, FL_MENU_INACTIVE }, + { Play_label, 0, Run::Play_cb, 0, FL_MENU_INACTIVE }, + { WritePostScript_label, 0, Run::WritePostScript_cb, 0, FL_MENU_INACTIVE }, + { WriteMIDI_label, 0, Run::WriteMIDI_cb, 0, FL_MENU_DIVIDER | FL_MENU_INACTIVE }, + { Options_label, 0, Run::Options_cb }, + { 0 }, + { Config_label, 0, 0, 0, FL_SUBMENU }, + { FileLocations_label, 0, Config::FileLocations_cb }, + { Preferences_label, 0, Config::Preferences_cb, 0, FL_MENU_DIVIDER }, + { RegistrationForm_label, 0, Config::RegistrationForm_cb }, + { RegistrationKey_label, 0, Config::RegistrationKey_cb }, + { 0 }, + { Help_label, 0, 0, 0, FL_SUBMENU }, + { UserGuide_label, 0, Help::Uguide_cb }, + { StartupHints_label, 0, Help::Startup_Hints_cb }, + { AboutMupmate_label, 0, Help::About_cb }, + { 0 }, + { 0 } +}; + + +//---------------------------------------------------------------------- + +// Linked list of main windows, in case we ever support more than one +// at a time. (Currently we don't, because we're not sure if it +// might be more confusing than useful.) +Main * Main::list_p; + + +// Constructor for main window. It contains toolbar and editor window. + +Main::Main(const char * title) + : Fl_Double_Window(Default_width, Default_height, title) +{ + xclass("mup"); +#ifdef OS_LIKE_WIN32 + icon((char *)LoadIcon(fl_display, MAKEINTRESOURCE(IDI_ICON))); +#else +#ifdef OS_LIKE_UNIX + fl_open_display(); + Pixmap p, mask; + XpmCreatePixmapFromData(fl_display, DefaultRootWindow(fl_display), + mup32_xpm, &p, &mask, NULL); + icon((char *)p); +#endif +#endif + // Try to use user's default foreground/background + Fl::get_system_colors(); + + void * data = 0; + + // Create class instances for each toolbar item + filemenu_p = new File(); + editmenu_p = new Edit(); + configmenu_p = new Config(); + helpmenu_p = new Help(); + runmenu_p = new Run(); + + // Add to list of windows + next = list_p; + list_p = this; + + // Create the toolbar and populate its menu items + toolbar_p = new Fl_Menu_Bar(0, 0, w(), TOOLBAR_HEIGHT); + int numitems = sizeof(Toolbar_menu) / sizeof(Toolbar_menu[0]); + for (int i = 0; i < numitems; i++) { + if (Toolbar_menu[i].text != 0) { + // As we move to each top-level menu item, + // keep a pointer to that item, which is then + // used as the argument to callback functions, + // so they know what object to act on. + if (strcmp(Toolbar_menu[i].text, File_label) == 0) { + data = (void *) filemenu_p; + } + else if (strcmp(Toolbar_menu[i].text, Edit_label) == 0) { + data = (void *) editmenu_p; + } + else if (strcmp(Toolbar_menu[i].text, Config_label) == 0) { + data = (void *) configmenu_p; + } + else if (strcmp(Toolbar_menu[i].text, Run_label) == 0) { + data = (void *) runmenu_p; + } + else if (strcmp(Toolbar_menu[i].text, Help_label) == 0) { + data = (void *) helpmenu_p; + } + } + Toolbar_menu[i].user_data(data); + } + toolbar_p->copy(Toolbar_menu); + + // Create and configure the editor window + editor_p = new Fl_Text_Editor(0, TOOLBAR_HEIGHT, w(), + h() - TOOLBAR_HEIGHT, ""); + editor_p->buffer( new Fl_Text_Buffer ); + + // Set font/size and arrange to be notified of changes in them + font_change_reg_p = new Font_change_registration(font_change_cb, + (void *) this); + + // Several objects need to be notified of changes in the + // editor window, so they can do things like gray-ungray menu items. + editor_p->buffer()->add_modify_callback(modify_cb, (void*) this); + editor_p->buffer()->add_modify_callback(File::modify_cb, + (void*) filemenu_p); + editor_p->buffer()->add_modify_callback(Edit::modify_cb, + (void*) editmenu_p); + + // Initialize state information. + have_selection = false; + can_paste = false; + prev_bufflength = 0; + // Undo is inactive until user does something that can be undone. + undo_active = false; + undo_active_on_next_change = true; + + // Arrange to make cursor blink + Fl::add_timeout(BLINK_RATE, blinker, this); + cursor_state = 1; + + // Let editor take as much space as is available + // if the user resizes the main window. + size_range(Min_width, Min_height, 0, 0); + resizable((Fl_Widget *) editor_p); + + // Other classes need to have access to editor and such + filemenu_p->set_editor(editor_p); + filemenu_p->set_parent(this); + editmenu_p->set_editor(editor_p); + runmenu_p->set_file(filemenu_p); + + // Arrange for destructor to free the new-ed child widgets + end(); + + show(); + + // Arrange for window manager closes to do Exit. + callback(atclose_cb, this); + when(FL_WHEN_NEVER); + +#ifdef OS_LIKE_UNIX + // Arrange for icon to be associated with window + XWMHints hints; + hints.flags = IconPixmapHint | IconMaskHint ; + hints.icon_pixmap = p; + hints.icon_mask = mask; + XSetWMHints(fl_display, fl_xid((Fl_Window *)this), &hints); +#endif +} + + +// Destructor for main window + +Main::~Main() +{ + delete font_change_reg_p; + font_change_reg_p = 0; + Fl::remove_timeout(blinker, this); + // Remove from list of Main windows + if (list_p == this) { + list_p = next; + } + else { + for (Main * m = list_p; m != 0; m = m->next) { + if (m->next == this) { + m->next = this->next; + break; + } + } + } + delete filemenu_p; + filemenu_p = 0; + delete editmenu_p; + editmenu_p = 0; + delete configmenu_p; + configmenu_p = 0; + delete helpmenu_p; + helpmenu_p = 0; + delete runmenu_p; + runmenu_p = 0; +} + + +// Callback for when user changes font/size + +void +Main::font_change_cb(void * data, Fl_Font font, unsigned char size) +{ + ((Main *)data)->font_change(font, size); +} + + +void +Main::font_change(Fl_Font font, unsigned char size) +{ + // Get shorter name for buffer, as we'll be using it a lot. + Fl_Text_Buffer * buffer_p = editor_p->buffer(); + + // Don't want this change to count as something that can be undone + buffer_p->canUndo(false); + + // We want to change the entire text buffer, so need to + // select its whole contents. If there was already a selection, + // save that and put it back when we are done + int sel_start, sel_end; + int had_selection; + int cursorplace = editor_p->insert_position(); + if ((had_selection = buffer_p->selected()) != 0) { + buffer_p->selection_position(&sel_start, &sel_end); + buffer_p->unselect(); + } + + // set new font and size + buffer_p->select(0, editor_p->buffer()->length() - 1); + editor_p->textfont(font); + editor_p->textsize(size); + buffer_p->unselect(); + + // Put selection and cursor back as they were before font change + if (had_selection) { + buffer_p->select(sel_start, sel_end); + } + editor_p->insert_position(cursorplace); + buffer_p->canUndo(true); +} + + +// Callback for when editor window changes. +// This arranges to gray/ungray toolbar menu items. + +void +Main::modify_cb(int, int, int, int, const char *, void * data) +{ + ((Main *)data)->modify(); +} + +void +Main::modify() +{ + int bufflength = editor_p->buffer()->length(); + // See if what changed is something we might care about. + if (editor_p->buffer()->selected() != have_selection || + editmenu_p->can_paste() != can_paste + || undo_active != undo_active_on_next_change + || bufflength < 2 || prev_bufflength == 0) { + + // Something changed, and we may need to + // gray or ungray menu items in response. + have_selection = editor_p->buffer()->selected(); + const Fl_Menu_Item * menu_p = toolbar_p->menu(); + Fl_Menu_Item * item_p; + // Walk through toolbar and submenus, checking + // if anything needs to be grayed/ungrayed. + for (int i = 0; i < toolbar_p->size(); i++) { + const char * mtext = toolbar_p->text(i); + if (mtext == 0) { + continue; + } + // Can only Copy, Cut, and Delete if something + // is selected. + if (strcmp(mtext, Copy_label) == 0 || + strcmp(mtext, Cut_label) == 0 || + strcmp(mtext, Delete_label) == 0) { + // have to un-const so we can (de)activate + item_p = (Fl_Menu_Item *) &(menu_p[i]); + if (have_selection) { + item_p->activate(); + } + else { + item_p->deactivate(); + } + } + + // Paste is different. It becomes active when there + // is something in clipboard, and never again becomes + // inactive. + if (strcmp(mtext, Paste_label) == 0 && + editmenu_p->can_paste()) { + ((Fl_Menu_Item *)&(menu_p[i]))->activate(); + can_paste = true; + } + + // Undo is also different. On first change of any + // kind it become active, and stays that way, + // except it gets reset on new file. + if (strcmp(mtext, Undo_label) == 0) { + if (undo_active && ! undo_active_on_next_change) { + ((Fl_Menu_Item *)&(menu_p[i]))->deactivate(); + undo_active = false; + undo_active_on_next_change = true; + } + else if ( ! undo_active && undo_active_on_next_change) { + ((Fl_Menu_Item *)&(menu_p[i]))->activate(); + undo_active = true; + } + } + + // Find and FindNext are inactive when file is empty, + // because obviously there is nothing to find. + // Similar for Replace and Select All. + // Also for all the Run things + if (strcmp(mtext, Find_label) == 0 || + strcmp(mtext, FindNext_label) == 0 || + strcmp(mtext, Replace_label) == 0 || + strcmp(mtext, SelectAll_label) == 0 || + strcmp(mtext, Display_label) == 0 || + strcmp(mtext, Play_label) == 0 || + strcmp(mtext, WritePostScript_label) == 0 || + strcmp(mtext, WriteMIDI_label) == 0) { + if (bufflength == 0) { + ((Fl_Menu_Item *)&(menu_p[i]))->deactivate(); + } + else { + ((Fl_Menu_Item *)&(menu_p[i]))->activate(); + } + } + } + } + prev_bufflength = bufflength; +} + + +// This method gets called when user starts working on a new file. +// It sets state information so we can know how to gray menu items properly. + +void +Main::begin_new_file() +{ + // transition Undo-ability state from true to false + undo_active = true; + undo_active_on_next_change = false; + editor_p->buffer()->call_modify_callbacks(); + runmenu_p->clean_up(); +} + + +// Handle some special cases. +// 1. By default fltk will exit the main window upon getting escape. +// That seems bad, since a vi user will be used to hitting escape all the +// time when editing, because that is always a "safe" thing to do, +// and if they did it here by mistake, they would lose all +// their text entry since the last save. So we ignore the escape in this window. +// I suppose we could ask if they really want to quit... +// 2. If user does cut or copy via keyboard accelerator, the normal code +// for ungraying the Paste button doesn't get called, so we catch that case +// here and ungray it. + +int +Main::handle_events(int e) +{ + // If escape is received while on main window, + // return 1 to show that we consumed the event. + if (e == FL_SHORTCUT && Fl::event_key() == FL_Escape) { + for (Main * m_p = list_p; m_p != 0; m_p = m_p->next) { + if (Fl::first_window() == m_p) { + return(1); + } + } + } + + // If user did cut or copy via cntl-c or cntl-x, + // arrange to ungray Paste. + if (e == FL_KEYUP && (Fl::event_state() & FL_CTRL) && + (Fl::event_key() == 'v' || Fl::event_key() == 'x')) { + for (Main * m_p = list_p; m_p != 0; m_p = m_p->next) { + if (Fl::first_window() == m_p) { + m_p->editmenu_p->set_can_paste(); + break; + } + } + } + + return(0); +} + + +// If user tries to close the main window via the window manager +// while having unsaved changes, we ask user if they want to save +// the changes first. + +CALL_BACK(Main, atclose) +{ + File::Exit_cb(0, filemenu_p); +} + + +// Blink the cursor. It can be hard to see if next to selected text if not +// blinking. We could potentially optimize to only do this while +// the window has focus, but it doesn't seem worth the complication... + +void +Main::blinker(void * data) +{ + Main * obj_p = (Main *) data; + // Put cursor into opposite of its current state + obj_p->cursor_state ^= 1; + obj_p->editor_p->show_cursor(obj_p->cursor_state); + // Reset timer to call ourselves again. + Fl::repeat_timeout(BLINK_RATE, blinker, data); +} + + +// Give user hints if we haven't already done that before. + +void +Main::hints(void) +{ + int did_startup; + (void) Preferences_p->get(Showed_startup_hints, did_startup, + Default_startup_hints_flag); + if ( ! did_startup) { + Help::Startup_Hints_cb(0, (void *) helpmenu_p); + } +} + +//---------------------------------------------------------------------- + +int +main(int argc, char **argv, const char **arge) +{ + // The arge value may get changed when we set new environment + // variables, so look up PATH first thing. + get_path(arge); + + // Uguide browser needs to show images + fl_register_images(); + +#ifndef OS_LIKE_WIN32 + // On Windows we use the native Open/Save As dialogs. + // On other platforms we use FLTK's, but add icon for Mup files. + Fl_File_Icon::load_system_icons(); + File::add_mup_icon(); +#endif + + // Try to get best hardware support for graphics + Fl::visual(FL_DOUBLE|FL_INDEX); + + // Get the user's preferences that persists across sessions + Preferences_p = new Fl_Preferences(Fl_Preferences::USER, + "arkkra.com", "mupmate"); + + // Enable tips when user hovers their mouse over a widget. + // If user doesn't like them, they can set delay to huge value. + Fl_Tooltip::enable(); + double tooltips_delay; + (void) Preferences_p->get(Tooltips_delay_preference, tooltips_delay, + Default_tooltips_delay); + Fl_Tooltip::delay(tooltips_delay); + + // Set $MUPPATH + char * val; + (void) Preferences_p->get(MUPPATH_location, val, + Default_MUPPATH_location); + set_muppath(val); + + // Tell Mup that it is being run via mupmate, + // so it can give more appropriate error messages. + putenv("MUPMATE=1"); + + // Create main window + Main *main_p = new Main("Mupmate"); + + // Ensure "escape" key doesn't kill main window, + // and make sure Paste ungraying works + Fl::add_handler(Main::handle_events); + + // Try to find some reasonable defaults for configuration items + // that aren't already set. + deduce_helper_locations(); + + // If magic file indicating license agreement isn't there, + // ask user to agree to Mup license. + if (access(magic_file(argv[0]), F_OK) != 0) { + // Show the license and get agreement before continuing + new License(main_p, magic_file(argv[0])); + } + else { + // Display the main window. + main_p->show(1, argv); + + // The first time, we show user some hints. + main_p->hints(); + } + + // Go to where user said they want to store their Mup files by default. + // Need to wait to do this until after we have deduced locations + // of executable, in case they were in current directory. + char * mup_dir; + (void) Preferences_p->get(Music_files_location, mup_dir, + Default_music_files_location); + if (strcmp(mup_dir, ".") != 0) { + if (chdir(mup_dir) != 0) { + char curr_dir[FL_PATH_MAX] = "current"; + char message[2 * FL_PATH_MAX + 100]; + (void *) getcwd(curr_dir, sizeof(curr_dir)); + sprintf(message, "Unable to change to folder\n" + "\"%s.\"\nStaying in \"%s\" folder.\n" + "Fix setting of \"Folder for Mup Files\"\n" + "in Config->File Locations.", + mup_dir, curr_dir); + fl_alert(message); + } + } + + // Expect 0 or 1 args. If 1, should be name of file to load + if (argc > 1) { + main_p->filemenu_p->load_file(argv[1]); + } + if (argc > 2) { + fl_alert("Only expecting one file; extra arguments are being ignored."); + } + + // Go into main event-handler loop + int exitvalue = Fl::run(); + Main::clean_exit(exitvalue); + /*NOTREACHED*/ + return(exitvalue); +} + + +// Clean up all the windows and their children and exit. + +void +Main::clean_exit(int exitval) +{ + Main * m_p; + Main * nextwin_p; + + for (m_p = list_p; m_p != 0; m_p = nextwin_p) { + nextwin_p = m_p->next; + delete m_p; + } + exit(exitval); +} + +//---------- class to show Mup license and get user's agreement + + +License::License(Main *m_p, const char * magic) + : Fl_Double_Window(Default_width, Default_height, "Mup License") +{ + // save passed-in info in object data + main_p = m_p; + magic_file_name = magic; + + // widget for displaying the license text + text_p = new Fl_Text_Display(20, 20, w() - 40, h() - 90); + resizable((Fl_Widget *) text_p); + text_p->buffer( new Fl_Text_Buffer () ); + text_p->textsize(18); + text_p->buffer()->text(license_text); + + i_agree_p = new Fl_Return_Button(100, h() - 50, 100, 30, "I Agree"); + i_agree_p->callback(IAgree_cb, this); + + cancel_p = new Fl_Button(w() - 200, h() - 50, 100, 30, "Cancel"); + cancel_p->callback(Cancel_cb, this); + cancel_p->shortcut(FL_Escape); + + show(); + + // Arrange for destructor to free the new-ed child widgets + end(); + + // Arrange for window manager closes to do Cancel. + callback(Cancel_cb, this); + when(FL_WHEN_NEVER); +} + + +License::~License() +{ +} + + +// Callback for when user clicks that they agree to the license. + +CALL_BACK(License, IAgree) +{ + // Create the magic file + int fd; + if ((fd = open(magic_file_name, O_WRONLY | O_CREAT, 0644)) < 0) { + fl_alert("Unable to create file indicating license agreement."); + } + else { + close(fd); + } + + hide(); + + // Bring up the normal main window + main_p->show(); + main_p->hints(); +} + +// Callback if user refuses to accept license. We just exit if they refuse. + +CALL_BACK(License, Cancel) +{ + exit(0); +}