/*
 * Quick Menu Package -- An easy way to make simple (but nice) menus.
 *
 * The client programmer will generally use the GenMenu()/FreeMenu() 
 * interface which creates and frees a whole menu strip.  A menu strip
 * -- both the main strip and menu item strips -- is defined by a
 * NewMenu struct.  Each NewMenu struct (defined in qmenu.h) is just a
 * pointer to an array of strings and a pointer to an array of
 * NewMenu's.  The elements of the array of NewMenu's are paired with the
 * strings in the array and represent the sub menus for that string.
 * The string array is terminated with a null pointer.
 *
 * The package tries to do nice things, like automatically formatting
 * the menu strings into blocks with their command keys, and placing
 * markers pointing to subitem stips.  The strings for the menu items
 * can contain special control codes to control the optional 
 * characteristics of items.  Special codes come at the front of item
 * strings and are delimited with a special character (SPECIAL,
 * defined as '!' by default) and are listed briefly below:
 *
 *      c       Checkmark item
 *      t       Checkmark toggle
 *      +       Checkmark checked
 *      b       Highlight box
 *      n       Highlight none
 *      d       Disabled
 *      =C      Set command key to "C"
 *      0101... Set item exclude mask
 *
 * Any number of 1's and 0's after the special character will set the
 * Mutual exclude bits for that item in the order they occur.  So the
 * first 1 or 0 will set bit 0 of the exclude mask, the second will
 * set bit 1, etc.  Any unset will be 0.
 *
 * Additionally, if the item string is "-", the item will be 
 * a non-selectable horizontal line (a "rule") that can be used to 
 * visually group similar items in the menu strip.  There only need
 * to be NewMenu structs in the array for text menu items, not rules,
 * but don't forget to account for rules when counting MenuItem's
 * since they take a slot.
 *
 *
 * GenMenu() takes a pointer to a NewMenu struct and will create a main
 * Menu strip from the strings in the struct, with menu items from the
 * associated NewMenu's.  It returns a pointer to a Menu struct which is
 * the first element in the list for this strip.
 *
 * GenStrip() takes a pointer to an array of strings and creates a
 * top-level set of Menu structs for the main menu strip.
 *
 * FreeMenu() frees a Menu strip created with either GenMenu() or
 * GenStrip().
 *
 * GenItems() takes a pointer to a NewMenu struct and creates a single
 * list of MenuItem's from the description.  It also takes an X and Y
 * offset for all the elements in the list.
 *
 * FreeMItem() frees structures created by the above function.
 *
 * AttachSubMenu() takes a single MenuItem and a NewMenu struct and
 * attaches the MenuItems created from that NewMenu struct to the
 * parent MenuItem.  The parent should also have been created with
 * GenItems() in order to work correctly.  AttachSubMenu() places a
 * little maker on the parent item to show that it has subitems.  It
 * returns 1 for sucess and 0 for failure.
 *
 * -- WARNING:
 *
 * The client programmer is completely responsible for keeping the
 * menus within the bounds of the screen and for keeping within the
 * limits of Intuition.  The menu structures will not adjust as they
 * hit the borders of the screen (which might be a nice enhancement).
 * The strings in the NewMenu structures cannot be changed or deleted
 * while the Menus are in use, although they can be used to create
 * multiple Menus from the same strings.
 *
 **
 * This code can be used and modified freely.  Do let me know of any
 * improvements or enhancements you may add, and be sure to give credit
 * where credit is due (in the comments) if you redistribute this code.
 *
 *      Stuart Ferguson         1/89
 *      (shf@well.UUCP)
 */
#include <intuition/intuition.h>
#include "qmenu.h"

/*
 * Some useful macros for allocating and freeing structures and
 * arrays of structures.
 */
#define NEW(typ)        (typ*)AllocMem((long)sizeof(typ),0L)
#define FREI(p)         FreeMem(p,(long)sizeof(*p))

#define NEW_N(typ,n)    (typ*)AllocMem((long)((n)*sizeof(typ)),0L)
#define FREI_N(p,n)     FreeMem(p,(long)((n)*sizeof(*p)))

char * AllocMem();

/*
 * Definitions for formatting the menus.  Glossary:
 *
 *      TEXTCOLOR       - color of the text items in the menus.
 *      MARKCOLOR       - color of the subitem marker.
 *      RULECOLOR       - color of the horizontal lines in the menus.
 *      RULEHEIGHT      - vertical thickness of the horizontal rules.
 *      RULEGAP         - vertical blank space around the rules.
 *      RULEMARGIN      - horizontal blank space around the rules.
 *      TEXTGAP         - vertical blank space around the text items.
 *      TEXTMARGIN      - horizontal blank space around the text items.
 *      MAINWID         - width of the text font on the main menu.
 *      MAINGAP         - spacing between items on the main menu strip.
 *      SUBINDENT       - overlap between items and their subitems.
 *      SUBDOWN         - vertical shift between items and their subitems.
 */
#define TEXTCOLOR       2
#define MARKCOLOR       3
#define RULECOLOR       0
#define RULEHEIGHT      2
#define RULEGAP         4
#define RULEMARGIN      10
#define TEXTGAP         2
#define TEXTMARGIN      4
#define MAINWID         10
#define MAINGAP         10
#define SUBINDENT       18
#define SUBDOWN         0

/*
 * Escape character for special control codes in text items strings.
 */
#define SPECIAL         '!'

/*
 * The extern "ta" is set by the client program for
 * the font to use for these menus.
 */
extern struct TextAttr ta;

/*
 * Generic templates to use for creating the dynamic menu structures. 
 */
static struct IntuiText
  generic_itext = {TEXTCOLOR, 0, JAM1, TEXTMARGIN/2, TEXTGAP/2, &ta,NULL,NULL},
  generic_smark = {MARKCOLOR, 0, JAM1, 0, TEXTGAP/2, &ta, (UBYTE *) ";", NULL};

static struct Menu
  generic_main = {NULL, 0, 0, 0, 10, MENUENABLED, NULL, NULL};

static struct MenuItem
  generic_mitem = {
         NULL, 0, 0, 0, 0,
         ITEMTEXT | ITEMENABLED | HIGHCOMP,
         0, NULL, NULL, 0, NULL, 0
};

/* Image struct with no imagery for the horizontal lines.
 */
static struct Image
  generic_hrule = {0, 0, 1, RULEHEIGHT, 2, NULL, 0, RULECOLOR, NULL};

static struct MenuItem
  generic_hitem = {
         NULL, RULEMARGIN/2, 0, 0, 0,
         ITEMENABLED | HIGHNONE,
         0, NULL, NULL, 0, NULL, 0
};


/*
 * Takes an array of strings and associated array of NewMenu structs
 * (as a single NewMenu struct) and constructs a menu strip from the
 * descripton.  This is the main high-level call that most clients
 * will make.
 */
struct Menu * GenMenu (nmen)
        register struct NewMenu *nmen;
{
        register short  i, ok, n;
        register struct Menu *mm;
        register struct MenuItem *mi;

        /* Count menus to be generated and create top level structure.
         */
        for (n = 0; nmen->str[n]; n++) ;
        mm = GenStrip (nmen->str);
        if (!mm) return NULL;

        /* Create the item strip for each main menu and attach to the
         * top level Menu structure.  Any failure causes the whole
         * thing to crumble to dust.
         */
        ok = 1;
        nmen = nmen->submenu;
        for (i = 0; i < n; i++) {
                if (nmen->str) {
                        mi = GenItems (nmen, 0L, 0L);
                        mm[i].FirstItem = mi;
                        ok &= (mi != NULL);
                }
                nmen ++;
        }
        if (!ok) {
                FreeMenu (mm);
                return NULL;
        }
        return mm;
}


/*
 * Generate a menu strip.  Just creates the top level Menu structures,
 * linked together and intialized.  Takes an array of pointers to
 * strings.
 */
struct Menu * GenStrip (str)
        char **str;
{
        register short  i, x, num;
        register struct Menu *mm, *m;

        /*
         * Create enough struct Menu's for the menu strip. 
         */
        for (num = 0; str[num]; num++) ;
        mm = NEW_N (struct Menu, num);
        if (!mm) return NULL;

        /*
         * Init the structures using the generic Menu struct as a template.
         * NOTE: the size of the font for these item labels is unknown for
         *       windows on the Workbench screen, so a size should be used
         *       that will work with 60 & 80 column fonts. 
         */
        x = 0;
        for (i = 0; i < num; i++) {
                m = &mm[i];
                *m = generic_main;
                m->LeftEdge = x;
                m->Width = strlen (str[i]) * MAINWID + TEXTMARGIN;
                x += m->Width + MAINGAP;
                m->MenuName = str[i];
                m->NextMenu = m + 1;
        }
        mm[num - 1].NextMenu = NULL;
        return mm;
}


/*
 * Attach a submenu to a MenuItem.  Takes the parent MenuItem and a
 * NewMenu structure that will be attached as a sub-menu.  
 * Attaches a ";" mark at the end of the parent item and calls
 * GenItems() to create the sub-menu strip.
 */
BOOL AttachSubMenu (mi, nmen)
        register struct MenuItem        *mi;
        struct NewMenu                  *nmen;
{
        register struct IntuiText *it;

        /* Create an IntuiText with the marker and position it
         * at the right edge of the item.
         */
        if (!(it = NEW (struct IntuiText))) return FALSE;
        *it = generic_smark;
        it->LeftEdge = mi->Width - IntuiTextLength (it) - 2;

        /* Create the subitem structure and attach to the main item.
         */
        if (!(mi->SubItem = GenItems
              (nmen, (LONG) (mi->Width - SUBINDENT), (LONG) SUBDOWN))) {
                FREI (it);
                return FALSE;
        }

        /* Only if it all worked attach the new text structure.
         */
        ((struct IntuiText *) mi->ItemFill)->NextText = it;
        return TRUE;
}


/*
 * Takes the given menu text item and skips past the special control
 * codes while adjusting the associated MenuItem appropriately.
 * Special codes are in the form of "!x", where "x" is:
 *
 *      c       Checkmark item
 *      t       Checkmark toggle
 *      b       Highlight box
 *      n       Highlight none
 *      d       Disabled
 *      +       Checkmark checked
 *      =C      Set command key to "C"
 *      1010... Set item exclude mask
 *
 * Takes the mostly defined MenuItem and diddles it.  Returns a pointer
 * to the item text with control codes stripped.
 */
static UBYTE * ProcessSpecialStuff (str, mi)
        char *str;
        struct MenuItem *mi;
{
        register LONG   x;
        register int    i;

        while (*str == SPECIAL) {
                switch (*++str) {
                    case 'c':
                        mi->Flags |= CHECKIT;
                        break;
                    case 't':
                        mi->Flags |= CHECKIT | MENUTOGGLE;
                        break;
                    case 'b':
                        mi->Flags &= ~HIGHFLAGS;
                        mi->Flags |= HIGHBOX;
                        break;
                    case 'n':
                        mi->Flags &= ~HIGHFLAGS;
                        mi->Flags |= HIGHNONE;
                        break;
                    case 'd':
                        mi->Flags &= ~ITEMENABLED;
                        break;
                    case '+':
                        mi->Flags |= CHECKIT | CHECKED;
                        break;
                    case '=':
                        mi->Flags |= COMMSEQ;
                        mi->Command = *++str;
                        break;
                    case '0':
                    case '1':
                        x = 0;
                        i = 0;
                        while (*str == '0' || *str == '1')
                                x += (*str++ - '0') << (i++);
                        mi->MutualExclude = x;
                        str--;
                        break;
                }
                str++;
        }
        return (UBYTE*) str;
}


/*
 * Construct a basic item list for a menu.  Takes a NewMenu structure
 * which contains a pointer to an array of pointers to strings and a 
 * pointer to an array of NewMenu structures.  The strings contain the
 * item text for each menu plus optional special control codes.  If the
 * string is "-", the item will be a horizontal rule rather than a text
 * item.  The NewMenu structures, if not NULL, are the sub-menu's for
 * each menu in the array.
 * "x" and "y" are the horizontal and vertical offsets of this set of
 * MenuItems.  These are set by AttachSubMenu() for positioning submenus
 * under their parent items.
 */
struct MenuItem *GenItems (nmen, x, y)
        struct NewMenu *nmen;
        LONG    x, y;
{
        register struct MenuItem *mi, *cmi;
        register struct IntuiText *itext;
        struct Image   *img;
        register short  i, len, max;
        short           n;
        struct NewMenu *sub;

        /* Count menu items (n) and allocate an array for the strip.
         */
        for (n = 0; nmen->str[n]; n++) ;
        if (!(mi = NEW_N (struct MenuItem, n))) return NULL;

        /* Counts the number of rules in the menu ("-" strings)
         * and allocates the structures for the lines and the text items.
         */
        max = 0;
        for (i = 0; i < n; i++) max += (*nmen->str[i] == '-');
        if (n - max)
                if (!(itext = NEW_N (struct IntuiText, n - max))) {
                        FREI_N (mi, n);
                        return NULL;
                }
        if (max)
                if (!(img = NEW_N (struct Image, max))) {
                        FREI_N (mi, n);
                        if (n - max) FREI_N (itext, n - max);
                        return NULL;
                }

        /* Loop through text menu items and initialize the
         * associated IntuiText structures.  Compute the maximum
         * width of the menu taking command keys into account while
         * assigning all the other parts of the text MenuItem's.
         */
        max = 0;
        for (i = 0; i < n; i++) {
                if (*nmen->str[i] == '-') continue;     /* skip rules */

                /* Init the text MenuItem to point to the assocd IntuiText.
                 */
                cmi = &mi[i];
                *cmi = generic_mitem;
                cmi->ItemFill = (APTR) itext;

                /* Init the IntuiText and adjust the MenuItem from the
                 * flags set in the menu text string.
                 */
                *itext = generic_itext;
                itext->IText = ProcessSpecialStuff (nmen->str[i], cmi);

                /* Make a first cut at measuring the length of the item.
                 */
                len = IntuiTextLength (itext) + TEXTMARGIN;

                /* If command key set, add to length.
                 */
                if (cmi->Flags & COMMSEQ) len += COMMWIDTH + MAINWID;

                /* If this is a checkmark item, shift the text over to
                 * make room and add that to the length.
                 * Compute the max length.
                 */
                if (cmi->Flags & CHECKIT) {
                        itext->LeftEdge += CHECKWIDTH;
                        len += CHECKWIDTH;
                }
                if (len > max) max = len;
                itext ++;
        }

        /* Secondary assignment loop.  Position the text MenuItems and
         * init the horizontal lines.
         */
        for (i = 0; i < n; i++) {
                cmi = &mi[i];

                if (*nmen->str[i] != '-') {
                        cmi->LeftEdge = x;
                        cmi->TopEdge = y;
                        cmi->Width = max;
                        cmi->Height = ta.ta_YSize + TEXTGAP;
                        y += cmi->Height;
                } else {

                        /* Rule items point to their Image structure
                         * and are just a little narrower than the
                         * menu itself.
                         */
                        *img = generic_hrule;
                        img->Width = max - RULEMARGIN;
                        *cmi = generic_hitem;
                        cmi->TopEdge = y + RULEGAP/2;
                        y += RULEHEIGHT + RULEGAP;
                        cmi->ItemFill = (APTR) img;
                        img ++;
                }
                cmi->NextItem = cmi + 1;
        }
        mi[n - 1].NextItem = NULL;

        /* Attach submenu's, if any.
         */
        if (!(sub = nmen->submenu)) return mi;

        /* Use "max" as a flag for the success of the attachments.
         */
        max = 1;
        for (i = 0; i < n; i++) {
                if (*nmen->str[i] == '-') continue;
                if (sub->str)
                        max &= AttachSubMenu (&mi[i], sub);
                sub ++;
        }
        if (!max) {
                FreeMItem (mi);
                return NULL;
        }
        return mi;
}


/*
 * Free a Menu structure created by GenStrip() that has items 
 * created with GenItems().
 */
void FreeMenu (mm)
        struct Menu    *mm;
{
        register short  i;
        register struct Menu *t;

        i = 0;
        for (t = mm; t; t = t->NextMenu) {
                if (t->FirstItem) FreeMItem (t->FirstItem);
                i++;
        }
        FREI_N (mm, i);
}


/*
 * Free a MenuItem structure created by GenItems().
 */
void FreeMItem (mi)
        register struct MenuItem *mi;
{
        register short  nit, nimg;
        register struct MenuItem *c;
        register struct IntuiText *it = NULL, *it1;
        struct Image   *img = NULL;

        /* Scan the MenuItem structures and count the number of images
         * and IntuiText structures.  Find the pointer to the first of
         * each structure in the set.  That will be the first element
         * in the array that was allocated as a unit.
         */
        nit = nimg = 0;
        for (c = mi; c; c = c->NextItem) {
                if (c->SubItem) FreeMItem (c->SubItem);
                if (c->Flags & ITEMTEXT) {
                        it1 = (struct IntuiText *) c->ItemFill;
                        if (!it) it = it1;
                        nit++;

                        /* Free the subitem marker, if any.
                         */
                        if (it1->NextText) FREI (it1->NextText);
                } else {
                        if (!img) img = (struct Image *) c->ItemFill;
                        nimg++;
                }
        }

        /* Free the arrays of structures of images and texts, as
         * well as the main array of MenuItem structures themselves.
         */
        if (nit) FREI_N (it, nit);
        if (nimg) FREI_N (img, nimg);
        FREI_N (mi, nit + nimg);
}
