/*
 * 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};
  */
  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);
}
