/* 
 * sxMenu.c --
 *
 *	This file implements pull-down menus using the facilities of the
 *	X window package.
 *
 * Copyright (C) 1986, 1988 Regents of the University of California.
 * Permission to use, copy, modify, and distribute this
 * software and its documentation for any purpose and without
 * fee is hereby granted, provided that the above copyright
 * notice appear in all copies.  The University of California
 * makes no representations about the suitability of this
 * software for any purpose.  It is provided "as is" without
 * express or implied warranty.
 */

#ifndef lint
static char rcsid[] = "$Header: /sprite/src/lib/sx/RCS/sxMenu.c,v 1.6 90/03/12 14:48:40 ouster Exp $ SPRITE (Berkeley)";
#endif not lint

#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <strings.h>
#include "sx.h"
#include "sxInt.h"

/*
 * Imports from library:
 */

char *malloc();

/*
 * The structure below is used internally to this file to describe
 * a menu entry.
 */

typedef struct {
    char *leftText;		/* Text to display left-justified in
				 * menu entry.  NULL means no text. */
    int leftSize;		/* # of characters in leftText. */
    int leftPos;		/* X position at which to draw text in menu. */
    char *centerText;		/* Text to display in center of menu
				 * entry. NULL means no centered text. */
    int centerSize;		/* Number of characters in centerText. */
    int centerPos;		/* X position at which to draw text in menu. */
    char *rightText;		/* Text to display right-justified in
				 * menu entry. NULL means no text. */
    int rightSize;		/* Number of characters in rightText. */
    int rightPos;		/* X position at which to draw text in menu. */
    unsigned long background;	/* Background color for menu entry. */
    void (*proc)();		/* Procedure to call when menu entry
				 * is selected. */
    ClientData clientData;	/* Client-supplied information to pass
				 * to proc. */
} MenuEntry;

/*
 * The structure below defines a pull-down menu.  It consists of a
 * number of menu entries plus a mask indicating which entries are
 * enabled:  disabled entries are displayed but do not respond to
 * button presses.  Two windows are used to display the menu:  one
 * for the menu title, and one for the menu itself (which is a child
 * of the root window and is only displayed when the menu is active).
 */

typedef struct Menu {
    Display *display;			/* Connection to X server. */
    char *name;				/* Name of menu (displayed in
					 * titleWindow). */
    Window titleWindow;			/* Window that is used to display menu
					 * name, and which user clicks on to
					 * pull menu down. *
    char *name;				/* Name of menu (displayed in
					 * titleWindow). */
    int nameWidth;			/* Size of menu name, in pixels. */
    Window menuWindow;			/* Window used to display menu
					 * entries. */
    struct MenuGroup *groupPtr;		/* Structure describing group of
					 * pulldown menus, all created in
					 * the same containing window. */
    XFontStruct *fontPtr;		/* Font to be used for this menu. */
    unsigned long foreground;		/* Foreground color for title and
					 * menu. */
    unsigned long background;		/* Background color for title. */
    GC gc;				/* Graphics context used for drawing
					 * in menu or menu title.  Only the
					 * font part never changes. */
    int numEntries;			/* Number of entries in menu. */
    MenuEntry *entryPtrs[SX_MAX_MENU_ENTRIES];
					/* Array of entries for this menu.
					 * Entry 0 is displayed at the top.
					 * NULL means entry doesn't exist.
					 * Only first numEntries are valid. */
    int mask;				/* Indicates which entries are enabled.
					 * 0 means disabled, 1 means enabled.
					 * The lowest-order bit corresponds to
					 * menu entry 0. */
    int titleWidth, titleHeight;	/* Dimensions of titleWindow. */
    int menuWidth;			/* Width of menuWindow in pixels,
					 * including border and shadow. */
    int menuHeight;			/* Height of menuWindow in pixels,
					 * including border and shadow. */
} Menu;

/*
 * The structure below defines a menu group, which consists of a bunch
 * of pulldown menus all lined up in a row in some containing window.
 */

typedef struct MenuGroup {
    Display *display;			/* Connection to X server. */
    Window w;				/* X window that contains the title
					 * windows for all the menus in
					 * this group. */
    Menu *menuPtrs[SX_MAX_MENUS];	/* Array of menus.  NULL means menu
					 * not defined.  All defined menus
					 * are together at beginning of
					 * array. */
} MenuGroup;

/*
 * Constants for displaying menus:
 *
 * MARGIN		Vertical spacing to leave around entries in menus.
 */

#define MARGIN 1

/*
 * The context below is used to map from X window ids to Menu and
 * MenuGroup structures.
 */

static XContext menuContext;
static XContext groupContext;

/*
 * The include's below are used to define the cursor used when a menu
 * is active.
 */

#include "bitmaps/star"
#include "bitmaps/starMask"

static Cursor cursor;

/*
 * If a menu is actually pulled down, the information below is used
 * to keep track of information about it.
 */

static int menuX, menuY;		/* Location of origin of pulled-down
					 * menu, in root coordinates. */
static int groupX, groupY;		/* Location of UL corner of leftmost
					 * menu in group of pulled-down menu,
					 * in root coordinates.  Used
					 * to determine when the pulled-down
					 * menu should be changed.  */
static Menu *selectedMenuPtr = NULL;	/* Selected menu (the one that's
					 * pulled down or that the mouse is
					 * over), or NULL if none selected. */
static int menuDown = 0;		/* 1 means a menu is actually
					 * pulled down. */
static int selectedEntry = -1;		/* Index of selected entry, or -1 if
					 * no entry is selected. */

static int init = 0;			/* 1 means initialized OK. */

/*
 * Forward references:
 */

static void	ComputeMenuLayout();
static void	MenuDeleteEntries();
static void	MenuEntryDisplay();
static void	MenuEventProc();
static void	MenuInit();
static void	MenuTitleEventProc();
static void	MenuTitleRedisplay();
static void	PullMenuDown();

/*
 *----------------------------------------------------------------------
 *
 * Sx_MenuCreate --
 *
 *	Create a new pulldown menu.
 *
 * Results:
 *	The return value is an X id for the window containing the
 *	pulldown menu's title.  This can be used to manipulate the
 *	menu (e.g. call XDestroyWindow to delete the menu).
 *
 * Side effects:
 *	A new pulldown menu is created and displayed.  It will have
 *	numEntries entries, as described by the entries in the entries[]
 *	array.  If there was already a menu by the given name in the
 *	given parent window, then the existing menu is replaced.
 *
 *	Later, when buttons are pressed over the entries in the pulldown
 *	menu, the client procedures indicated in entries[] will be called
 *	as follows:
 *
 *	void
 *	proc(clientData, entry, menuWindow)
 *	    ClientData clientData;
 *	    int entry;
 *	    Window menuWindow;
 *	{
 *	}
 *	
 *	The clientData parameter is the one passed in the entries[]
 *	element that was buttoned, entry is the index of the entry
 *	that was invoked, and menuWindow indicates the menu (it's
 *	the same as the value returned by this procedure).
 *
 *----------------------------------------------------------------------
 */

Window
Sx_MenuCreate(display, parent, name, numEntries, entries, fontPtr,
	foreground, background)
    Display *display;			/* Connection to X server. */
    Window parent;			/* Containing window in which to
					 * create menu.  If many menus are
					 * created in this window, their
					 * titles will be ordered from left
					 * to right in order of creation. */
    char *name;				/* Name of menu. */
    int numEntries;			/* Number of entries in menu.  Must be
					 * less than or equal to
					 * SX_MAX_MENU_ENTRIES. */
    Sx_MenuEntry entries[];		/* Entries:  must have numEntries
					 * values. */
    XFontStruct *fontPtr;		/* Font to use for displaying menu.
					 * NULL means use default font. */
    unsigned long foreground;		/* Foreground color to use to display
					 * menu title and menu entries. */
    unsigned long background;		/* Background color to use for title.
					 * Each menu entry indicates its own
					 * background color. */
{
    register MenuGroup *groupPtr;
    register Menu *menuPtr;
    register MenuEntry *entryPtr;
    register Sx_MenuEntry *paramPtr;
    Menu savedMenu;
    int i, index;
    caddr_t data;
    XGCValues gcValues;

    if (!init) {
	MenuInit(display);
    }

    if (fontPtr == NULL) {
	fontPtr = Sx_GetDefaultFont(display);
    }

    /*
     * See if there's already a MenuGroup set up for the parent window.
     * If not then create one.
     */

    if (XFindContext(display, parent, groupContext, &data) == 0) {
	groupPtr = (MenuGroup *) data;
    } else {
	groupPtr = (MenuGroup *) malloc(sizeof(MenuGroup));
	groupPtr->display = display;
	groupPtr->w = parent;
	for (i = 0; i < SX_MAX_MENUS; i++) {
	    groupPtr->menuPtrs[i] = NULL;
	}
	XSaveContext(display, parent, groupContext, (caddr_t) groupPtr);
    }

    /*
     * Figure out which entry to use (or replace).
     */

    for (index = 0; index < SX_MAX_MENUS; index++) {
	menuPtr = groupPtr->menuPtrs[index];
	if (menuPtr == NULL) {
	    break;
	}
	if (strcmp(menuPtr->name, name) == 0) {
	    break;
	}
    }
    if (index >= SX_MAX_MENUS) {
	Sx_Panic(display, "Sx_MenuCreate: tried to create too many menus.");
    }

    /*
     * If replacing, save old entries for deletion later.  If
     * creating from scratch, do once-only initialization.
     */

    if (menuPtr != NULL) {
	XFreeGC(display, menuPtr->gc);
	savedMenu = *menuPtr;
    } else {
	XSetWindowAttributes atts;

	savedMenu.numEntries = 0;
	menuPtr = (Menu *) malloc(sizeof(Menu));
	groupPtr->menuPtrs[index] = menuPtr;
	menuPtr->display = display;
	atts.background_pixmap = None;
	menuPtr->titleWindow = XCreateWindow(display, parent, 0, 0, 1, 1,
		0, CopyFromParent, InputOutput, CopyFromParent,
		CWBackPixel, &atts);
	menuPtr->name = (char *)
		malloc((unsigned) (strlen(name) + 1));
	(void) strcpy(menuPtr->name, name);
	menuPtr->nameWidth = XTextWidth(fontPtr, name, strlen(name));
	atts.save_under = True;
	atts.override_redirect = True;
	menuPtr->menuWindow = XCreateWindow(display,
		RootWindow(display, DefaultScreen(display)), 0, 0, 1, 1, 0,
	        CopyFromParent, InputOutput, CopyFromParent,
		CWBackPixmap|CWSaveUnder|CWOverrideRedirect,
		&atts);
	menuPtr->groupPtr = groupPtr;

	(void) Sx_HandlerCreate(display, menuPtr->titleWindow, ButtonPressMask
		|ButtonReleaseMask|EnterWindowMask|LeaveWindowMask
		|ExposureMask|StructureNotifyMask, MenuTitleEventProc,
		(ClientData) menuPtr);
	Sx_Pack(display, menuPtr->titleWindow, parent, SX_LEFT,
		menuPtr->nameWidth + XTextWidth(fontPtr, "mm", 2), 0,
		0, (unsigned long) 0, (Window) 0);
	(void) Sx_HandlerCreate(display, menuPtr->menuWindow,
		ExposureMask|ButtonReleaseMask|PointerMotionMask,
		MenuEventProc, (ClientData) menuPtr);
	XSaveContext(display, menuPtr->titleWindow, menuContext,
		(caddr_t) menuPtr);
    }

    /*
     * Do the initialization that's independent of whether this is a
     * replacement or creation.
     */
    
    menuPtr->fontPtr = fontPtr;
    menuPtr->foreground = foreground;
    menuPtr->background = background;
    gcValues.font = fontPtr->fid;
    menuPtr->gc = XCreateGC(display, menuPtr->titleWindow, GCFont, &gcValues);
    if (numEntries > SX_MAX_MENU_ENTRIES) {
	numEntries = SX_MAX_MENU_ENTRIES;
    }
    menuPtr->numEntries = numEntries;
    for (i = 0, paramPtr = entries; i < numEntries; i++, paramPtr++) {
	entryPtr = (MenuEntry *) malloc(sizeof(MenuEntry));
	menuPtr->entryPtrs[i] = entryPtr;
	if (paramPtr->leftText == NULL) {
	    entryPtr->leftText = NULL;
	    entryPtr->leftSize = 0;
	} else {
	    entryPtr->leftSize = strlen(paramPtr->leftText);
	    entryPtr->leftText = (char *)
		    malloc((unsigned) (entryPtr->leftSize + 1));
	    (void) strcpy(entryPtr->leftText, paramPtr->leftText);
	}
	if (paramPtr->centerText == NULL) {
	    entryPtr->centerText = NULL;
	    entryPtr->centerSize = 0;
	} else {
	    entryPtr->centerSize = strlen(paramPtr->centerText);
	    entryPtr->centerText = (char *)
		    malloc((unsigned) (entryPtr->centerSize + 1));
	    (void) strcpy(entryPtr->centerText, paramPtr->centerText);
	}
	if (paramPtr->rightText == NULL) {
	    entryPtr->rightText = NULL;
	    entryPtr->rightSize = 0;
	} else {
	    entryPtr->rightSize = strlen(paramPtr->rightText);
	    entryPtr->rightText = (char *)
		    malloc((unsigned) (entryPtr->rightSize + 1));
	    (void) strcpy(entryPtr->rightText, paramPtr->rightText);
	}
	if (paramPtr->background != -1) {
	    entryPtr->background = paramPtr->background;
	} else {
	    entryPtr->background = menuPtr->background;
	}
	entryPtr->proc = paramPtr->proc;
	entryPtr->clientData = paramPtr->clientData;
    }
    menuPtr->mask = (1<<numEntries) - 1;
    ComputeMenuLayout(menuPtr);

    /*
     * If we're replacing an existing menu, recycle the information in
     * its menu entries.  Do it now, rather than earlier, in case some
     * of the strings passed in for the new menu were actually part of
     * old menu.  This way they get copied before they're deleted.
     */

    MenuDeleteEntries(&savedMenu);

    return menuPtr->titleWindow;
}

/*
 *----------------------------------------------------------------------
 *
 * Sx_MenuSetMask --
 *
 *	Select which entries in a menu are enabled.
 *
 * Results:
 *	The return value is the previous value of the enabled mask
 *	for the menu.
 *
 * Side effects:
 *	After this call, only the entries given by ones in mask will be
 *	enabled in the menu given by window.  The low-order bit of mask
 *	corresponds to the first entry in the menu, etc.  Disabled menu
 *	entries are displayed differently, and are not sensitive to
 *	button pushes (i.e. the client procedure isn't invoked).  If
 *	every entry in a menu is disabled, then the menu title is
 *	displayed differently to indicate this fact.
 *
 *----------------------------------------------------------------------
 */

int
Sx_MenuSetMask(display, window, mask)
    Display *display;		/* Connection to X server. */
    Window window;		/* Window containing menu title (i.e. value
				 * returned by Sx_MenuCreate). */
    int mask;			/* New value for mask.  Low-order bit
				 * corresponds to first menu entry. */
{
    register Menu *menuPtr;
    int oldMask;
    caddr_t data;

    if (!init) {
	MenuInit(display);
    }

    if (XFindContext(display, window, menuContext, &data) != 0) {
	Sx_Panic(display, "Sx_MenuSetMask:  window parameter isn't a menu.");
    }

    menuPtr = (Menu *) data;
    oldMask = menuPtr->mask;
    menuPtr->mask = mask & ((1<<menuPtr->numEntries) - 1);
    if (menuPtr->mask == 0) {
	if (oldMask != 0) {
	    MenuTitleRedisplay(menuPtr);
	}
    } else if (oldMask == 0) {
	MenuTitleRedisplay(menuPtr);
    }
    return oldMask;
}

/*
 *----------------------------------------------------------------------
 *
 * Sx_MenuGetMask --
 *
 *	Find out which entries in a menu are enabled.
 *
 * Results:
 *	The return value is the current value of the enabled mask for
 *	the menu associated with window.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

int
Sx_MenuGetMask(display, window)
    Display *display;		/* Connection to X server. */
    Window window;		/* Window containing menu's title (i.e. return
				 * value from Sx_MenuCreate). */
{
    caddr_t data;

    if (!init) {
	MenuInit(display);
    }

    if (XFindContext(display, window, menuContext, &data) != 0) {
	Sx_Panic(display, "Sx_MenuGetMask:  window parameter isn't a menu.");
    }
    return ((Menu *) data)->mask;
}

/*
 *----------------------------------------------------------------------
 *
 * Sx_MenuReplaceEntry --
 *
 *	This procedure replaces a single entry in a pulldown menu.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The entryIndex'th entry in the menu associated with window
 *	is replaced with newEntry.
 *
 *----------------------------------------------------------------------
 */

void
Sx_MenuReplaceEntry(display, window, entryIndex, newEntryPtr)
    Display *display;			/* Connection to X server. */
    Window window;			/* Window that's a pulldown menu
					 * title (i.e. window was a return
					 * value from Sx_MenuCreate). */
    int entryIndex;			/* Index of desired entry.  This
					 * entry must already exist. */
    register Sx_MenuEntry *newEntryPtr;	/* Pointer to new menu entry. */
{
    Menu *menuPtr;
    register MenuEntry *entryPtr;
    caddr_t data;

    if (!init) {
	MenuInit(display);
    }

    if (XFindContext(display, window, menuContext, &data) != 0) {
	Sx_Panic(display,
		"Sx_MenuReplaceEntry:  window parameter isn't a menu.");
    }
    menuPtr = (Menu *) data;

    entryPtr = menuPtr->entryPtrs[entryIndex];
    if (entryPtr->leftText != NULL) {
	free(entryPtr->leftText);
    }
    if (entryPtr->centerText != NULL) {
	free(entryPtr->centerText);
    }
    if (entryPtr->rightText != NULL) {
	free(entryPtr->rightText);
    }
    if (newEntryPtr->leftText == NULL) {
	entryPtr->leftText = NULL;
	entryPtr->leftSize = 0;
    } else {
	entryPtr->leftSize = strlen(newEntryPtr->leftText);
	entryPtr->leftText = (char *)
		malloc((unsigned) (entryPtr->leftSize + 1));
	(void) strcpy(entryPtr->leftText, newEntryPtr->leftText);
    }
    if (newEntryPtr->centerText == NULL) {
	entryPtr->centerText = NULL;
	entryPtr->centerSize = 0;
    } else {
	entryPtr->centerSize = strlen(newEntryPtr->centerText);
	entryPtr->centerText = (char *)
		malloc((unsigned) (entryPtr->centerSize + 1));
	(void) strcpy(entryPtr->centerText, newEntryPtr->centerText);
    }
    if (newEntryPtr->rightText == NULL) {
	entryPtr->rightText = NULL;
	entryPtr->rightSize = 0;
    } else {
	entryPtr->leftSize = strlen(newEntryPtr->leftText);
	entryPtr->leftText = (char *)
		malloc((unsigned) (entryPtr->leftSize + 1));
	(void) strcpy(entryPtr->leftText, newEntryPtr->leftText);
    }
    if (newEntryPtr->background != -1) {
	entryPtr->background = newEntryPtr->background;
    } else {
	entryPtr->background = menuPtr->background;
    }
    entryPtr->proc = newEntryPtr->proc;
    entryPtr->clientData = newEntryPtr->clientData;
    ComputeMenuLayout(menuPtr);
}

/*
 *----------------------------------------------------------------------
 *
 * Sx_MenuGetInfo --
 *
 *	This procedure returns a complete description of a particular
 *	menu.
 *
 * Results:
 *	The return value is a count of the number of entries in the
 *	menu, or -1 if window isn't a menu window.  Entries,
 *	*fontPtrPtr, *foregroundPtr, and *backgroundPtr all get
 *	filled in with the corresponding information as it was
 *	passed to Sx_MenuCreate or Sx_MenuReplaceEntry.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

int
Sx_MenuGetInfo(display, window, entries, fontPtrPtr, foregroundPtr,
	backgroundPtr)
    Display *display;			/* Connection to X server. */
    Window window;			/* Window corresponding to menu. */
    Sx_MenuEntry entries[];		/* Array of menu entries;  gets filled
					 * in by this procedure.  The string
					 * pointers will all refer to statics
					 * in the menu manager;  they will
					 * become invalid whenever the menu
					 * is modified or deleted.  If entries
					 * is NULL then no entry information
					 * is returned. */
    XFontStruct **fontPtrPtr;		/* Gets filled in with font info
					 * pointer (if NULL, then ignored). */
    unsigned long *foregroundPtr;	/* Gets filled in with menu's
					 * foreground color (if not NULL). */
    unsigned long *backgroundPtr;	/* Gets filled in with menu's
					 * background color (if not NULL). */
{
    register MenuEntry *srcPtr;
    register Sx_MenuEntry *dstPtr;
    register Menu *menuPtr;
    int i;
    caddr_t data;

    if (!init) {
	MenuInit(display);
    }

    if (XFindContext(display, window, menuContext, &data) != 0) {
	return -1;
    }
    menuPtr = (Menu *) data;

    if (fontPtrPtr != NULL) {
	*fontPtrPtr = menuPtr->fontPtr;
    }
    if (foregroundPtr != NULL) {
	*foregroundPtr = menuPtr->foreground;
    }
    if (backgroundPtr != NULL) {
	*backgroundPtr = menuPtr->background;
    }
    if (entries == NULL) {
	return menuPtr->numEntries;
    }

    for (i = 0, dstPtr = entries; i < menuPtr->numEntries; i++, dstPtr++) {
	srcPtr = menuPtr->entryPtrs[i];
	dstPtr->leftText = srcPtr->leftText;
	dstPtr->centerText = srcPtr->centerText;
	dstPtr->rightText = srcPtr->rightText;
	dstPtr->background = srcPtr->background;
	dstPtr->proc = srcPtr->proc;
	dstPtr->clientData = srcPtr->clientData;
    }
    return menuPtr->numEntries;
}

/*
 *----------------------------------------------------------------------
 *
 * Sx_MenuGetNames --
 *
 *	Given a parent window containing 0 or more menu children, return
 *	the names of all the children menus (and also their X window ids).
 *
 * Results:
 *	The return value is a count of the number of menus in parent,
 *	which may be zero.  The arrays "names" and "windows" get filled
 *	in with information about the menus in parent.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

int
Sx_MenuGetNames(display, parent, names, windows)
    Display *display;		/* Connection to X server. */
    Window parent;		/* Parent window, which ostensibly contains
				 * one or menus as subwindows. */
    char *(names[]);		/* Entries in this array get filled in with
				 * pointers to statically-allocated strings
				 * giving names of menus, unless names is NULL.
				 * Array must have SX_MAX_MENUS entries.
				 * Strings are statically allocated and are
				 * part of the menu manager;  they will become
				 * invalid when the corresponding menu is
				 * modified or deleted. */
    Window windows[];		/* Entries in this array get filled in with
				 * X window ids corresponding to names, unless
				 * windows is NULL. */
{
    register Menu *menuPtr;
    register MenuGroup *groupPtr;
    int i;
    caddr_t data;

    if (!init) {
	MenuInit(display);
    }

    if (XFindContext(display, parent, groupContext, &data) != 0) {
	return 0;
    }
    groupPtr = (MenuGroup *) data;

    for (i = 0; i < SX_MAX_MENUS; i++) {
	menuPtr = groupPtr->menuPtrs[i];
	if (menuPtr == NULL) {
	    break;
	}
	if (names != NULL) {
	    names[i] = menuPtr->name;
	}
	if (windows != NULL) {
	    windows[i] = menuPtr->titleWindow;
	}
    }
    return i;
}

/*
 *----------------------------------------------------------------------
 *
 * Sx_MenuGetWindow --
 *
 *	Given the parent window containing a menu, and the name of the
 *	menu, get the X window id corresponding to the menu.
 *
 * Results:
 *	The return value is the X window id of a menu subwindow of
 *	parent whose name (title) is as given.  If there isn't any
 *	such menu, NULL is returned.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

Window
Sx_MenuGetWindow(display, parent, name)
    Display *display;		/* Connection to X server. */
    Window parent;		/* Parent window, which ostensibly has one
				 * or more menu subwindows as children. */
    char *name;			/* Title of desired menu. */
{
    register Menu *menuPtr;
    register MenuGroup *groupPtr;
    int i;
    caddr_t data;

    if (!init) {
	MenuInit(display);
    }

    if (XFindContext(display, parent, groupContext, &data) != 0) {
	return NULL;
    }
    groupPtr = (MenuGroup *) data;

    for (i = 0; i < SX_MAX_MENUS; i++) {
	menuPtr = groupPtr->menuPtrs[i];
	if (menuPtr == NULL) {
	    break;
	}
	if (strcmp(name, menuPtr->name) == 0) {
	    return menuPtr->titleWindow;
	}
    }
    return NULL;
}

/*
 *----------------------------------------------------------------------
 *
 * MenuInit --
 *
 *	Performs various once-only initialization functions.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Random stuff gets initialized.  See code for details.
 *
 *----------------------------------------------------------------------
 */

static void
MenuInit(display)
    Display *display;		/* Connection to X server. */
{
    static XColor black = {0, 0, 0, 0, 0};
    static XColor white = {0, ~0, ~0, ~0, 0};
    Pixmap source, mask;
    Window root;


    root = RootWindow(display, DefaultScreen(display));
    source = XCreateBitmapFromData(display, root, star_bits,
	    star_width, star_height);
    mask = XCreateBitmapFromData(display, root, starMask_bits,
	    starMask_width, starMask_height);
    cursor = XCreatePixmapCursor(display, source, mask, &black, &white,
	    star_x_hot, star_y_hot);
    XFreePixmap(display, source);
    XFreePixmap(display, mask);

    groupContext = XUniqueContext();
    menuContext = XUniqueContext();

    init = 1;
}

/*
 *----------------------------------------------------------------------
 *
 * MenuTitleRedisplay--
 *
 *	Redisplay the title window for a menu.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The menu title for menu is redisplayed.
 *
 *----------------------------------------------------------------------
 */

static void
MenuTitleRedisplay(menuPtr)
    register Menu *menuPtr;		/* Menu to be redisplayed. */
{
    unsigned  long background, foreground;
    int x, y;

    /*
     * If this menu is currently selected, display it in reverse video.
     */
    
    if (menuPtr == selectedMenuPtr) {
	background = menuPtr->foreground;
	foreground = menuPtr->background;
    } else {
	background = menuPtr->background;
	foreground = menuPtr->foreground;
    }
    XSetForeground(menuPtr->display, menuPtr->gc, background);
    XFillRectangle(menuPtr->display, menuPtr->titleWindow, menuPtr->gc,
	    0, 0, menuPtr->titleWidth, menuPtr->titleHeight);
    XSetForeground(menuPtr->display, menuPtr->gc, foreground);
    x = (menuPtr->titleWidth - menuPtr->nameWidth)/2;
    y = (menuPtr->titleHeight + menuPtr->fontPtr->ascent
	    - menuPtr->fontPtr->descent)/2;
    XDrawString(menuPtr->display, menuPtr->titleWindow, menuPtr->gc,
	    x, y, menuPtr->name, strlen(menuPtr->name));
    
    /*
     * If the entire menu is disabled, display a bar through the title.
     */
    
    if (menuPtr->mask == 0) {
	XFillRectangle(menuPtr->display, menuPtr->titleWindow, menuPtr->gc,
		0, y - menuPtr->fontPtr->ascent/2, menuPtr->titleWidth-4, 2);
    }
}

/*
 *----------------------------------------------------------------------
 *
 * MenuTitleEventProc --
 *
 *	This procedure is called by the dispatcher when events that
 *	we're interested in occur in a menu's title window.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Depends on the event.  Could be anything from displaying the
 *	title to pulling down the menu to deleting the menu.
 *
 *----------------------------------------------------------------------
 */

static void
MenuTitleEventProc(menuPtr, eventPtr)
    register Menu *menuPtr;		/* Menu title for which event
					 * occurred. */
    register XEvent *eventPtr;		/* Describes what happened. */
{
    int i;

    if (eventPtr->type == Expose) {
	/*
	 * If there is a string of exposure events for pieces of the window,
	 * just wait and redisplay the whole thing on the last event.
	 */

	if (eventPtr->xexpose.count == 0) {
	    MenuTitleRedisplay(menuPtr);
	}
    } else if (eventPtr->type == ConfigureNotify) {
	menuPtr->titleWidth = eventPtr->xconfigure.width;
	menuPtr->titleHeight = eventPtr->xconfigure.height;
    } else if (eventPtr->type == EnterNotify) {
	/*
	 * Ignore enter and leave events when a menu is down;  they're
	 * handled by the pointer tracking code in MenuEventProc.
	 */
	
	if (menuDown) {
	    return;
	}
	selectedMenuPtr = menuPtr;
	MenuTitleRedisplay(menuPtr);
    } else if (eventPtr->type == LeaveNotify) {
	if ((selectedMenuPtr == menuPtr) && (!menuDown)) {
	    selectedMenuPtr = NULL;
	    MenuTitleRedisplay(menuPtr);
	}
    } else if (eventPtr->type == ButtonPress) {
	register Menu **menuPtrPtr;
	
	/*
	 * Time to pull a menu down.  Save the origin of the menu group
	 * window, for use later in mouse tracking.  Compute where the
	 * menu should go on the screen, then display it.
	 */

	groupX = eventPtr->xbutton.x_root - eventPtr->xbutton.x;
	for (menuPtrPtr = menuPtr->groupPtr->menuPtrs;
		(*menuPtrPtr != menuPtr) && (*menuPtrPtr != NULL);
		menuPtrPtr++) {
	    groupX -=(*menuPtrPtr)->titleWidth;
	}
	groupY = eventPtr->xbutton.y_root - eventPtr->xbutton.y;
	PullMenuDown(menuPtr, eventPtr->xbutton.x_root - eventPtr->xbutton.x,
		groupY + menuPtr->titleHeight);
	i = XGrabPointer(menuPtr->display, menuPtr->menuWindow, False,
		ButtonReleaseMask|PointerMotionMask, GrabModeAsync,
		GrabModeAsync, None, cursor, CurrentTime);
	if (i != 0) {
	    Sx_Panic(menuPtr->display, "Pull-down menu couldn't grab pointer.");
	}
	selectedEntry = -1;
	menuDown = 1;
    } else if (eventPtr->type == DestroyNotify) {
	register MenuGroup *groupPtr;
	int i;
    
	/*
	 * Free up everything associated with the menu itself.
	 */

	XDeleteContext(menuPtr->display, menuPtr->titleWindow, menuContext);
	XDestroyWindow(menuPtr->display, menuPtr->menuWindow);
	MenuDeleteEntries(menuPtr);
	free((char *) menuPtr->name);
	free((char *) menuPtr);
    
	/*
	 * Remove this menu from its group.  If the group is
	 * now empty, delete it also.
	 */
    
	groupPtr = menuPtr->groupPtr;
	for (i = 0; i < SX_MAX_MENUS; i++) {
	    if (groupPtr->menuPtrs[i] == menuPtr) {
		int j;
    
		groupPtr->menuPtrs[i] = NULL;
		for (j = i+1;
			(j < SX_MAX_MENUS) && (groupPtr->menuPtrs[j] != NULL);
			j++) {
		    groupPtr->menuPtrs[j-1] = groupPtr->menuPtrs[j];
		    groupPtr->menuPtrs[j] = NULL;
		}
	    }
	}
	if (groupPtr->menuPtrs[0] == NULL) {
	    XDeleteContext(groupPtr->display, groupPtr->w, groupContext);
	    free((char *) groupPtr);
	}
    }
}

/*
 *----------------------------------------------------------------------
 *
 * MenuDeleteEntries --
 *
 *	Recycle all of the memory associated with entries for a given
 *	menu.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Memory gets reallocated.
 *
 *----------------------------------------------------------------------
 */

static void
MenuDeleteEntries(menuPtr)
    register Menu *menuPtr;	/* Menu whose entries are to be deleted. */
{
    register MenuEntry *entryPtr;
    int i;

    for (i = 0; i < menuPtr->numEntries; i++) {
	entryPtr = menuPtr->entryPtrs[i];
	if (entryPtr->leftText != NULL) {
	    free((char *) entryPtr->leftText);
	}
	if (entryPtr->centerText != NULL) {
	    free((char *) entryPtr->centerText);
	}
	if (entryPtr->rightText != NULL) {
	    free((char *) entryPtr->rightText);
	}
	free((char *) entryPtr);
    }
    menuPtr->numEntries = 0;
}

/*
 *----------------------------------------------------------------------
 *
 * ComputeMenuLayout --
 *
 *	This procedure scans all the entries in a menu and computes
 *	the overall size of the menu as well as where to position
 *	each text field of each entry.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Entries in the menu get modified to reflect the new layout.
 *	Menu->menuWindow also gets modified in size to reflect
 *	the new layout.
 *
 *----------------------------------------------------------------------
 */

static void
ComputeMenuLayout(menuPtr)
    register Menu *menuPtr;		/* Menu to resize. */
{
    register MenuEntry *entryPtr;
    int leftMax, centerMax, rightMax;
    int i, length, space;

    /*
     * Compute the height of the menu.
     */

    menuPtr->menuHeight = menuPtr->numEntries*(menuPtr->fontPtr->ascent
	    + menuPtr->fontPtr->descent + 2*MARGIN) + SHADOW_TOP
	    + SHADOW_BOTTOM;

    /*
     * Compute the maximum width of any entry for each column, allowing
     * an extra one-space pad on each side of each entry.
     */
    
    leftMax = centerMax = rightMax = 0;
    space = XTextWidth(menuPtr->fontPtr, "m", 1);
    for (i = 0; i < menuPtr->numEntries; i++) {
	entryPtr = menuPtr->entryPtrs[i];
	if (entryPtr->leftText != NULL) {
	    length = XTextWidth(menuPtr->fontPtr, entryPtr->leftText,
		    strlen(entryPtr->leftText)) + 2*space;
	    if (length > leftMax) {
		leftMax = length;
	    }
	}
	if (entryPtr->centerText != NULL) {
	    length = XTextWidth(menuPtr->fontPtr, entryPtr->centerText,
		    strlen(entryPtr->centerText)) + 2*space;
	    if (length > centerMax) {
		centerMax = length;
	    }
	    entryPtr->centerPos = length;   /* Save to avoid recomputation. */
	}
	if (entryPtr->rightText != NULL) {
	    length = XTextWidth(menuPtr->fontPtr, entryPtr->rightText,
		    strlen(entryPtr->rightText)) + 2*space;
	    if (length > rightMax) {
		rightMax = length;
	    }
	    entryPtr->rightPos = length;   /* Save to avoid recomputation. */
	}
    }

    /*
     * Size the window so that none of the columns overlap (be sure
     * to leave space for the fancy border).  Then go back and fill
     * in the exact drawing position for each string.
     */
    
    menuPtr->menuWidth = leftMax + centerMax + rightMax +
	    SHADOW_LEFT + SHADOW_RIGHT;
    XResizeWindow(menuPtr->display, menuPtr->menuWindow, menuPtr->menuWidth,
	    menuPtr->menuHeight);
    
    for (i = 0; i < menuPtr->numEntries; i++) {
	entryPtr = menuPtr->entryPtrs[i];
	entryPtr->leftPos = SHADOW_LEFT + space;
	entryPtr->centerPos = SHADOW_LEFT + leftMax + space
		+ (centerMax - entryPtr->centerPos)/2;
	entryPtr->rightPos = menuPtr->menuWidth + space - entryPtr->rightPos
		- SHADOW_RIGHT;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * MenuEntryDisplay --
 *
 *	This procedure is called to display one entry of a menu.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The entry is displayed in its menu.
 *
 *----------------------------------------------------------------------
 */

static void
MenuEntryDisplay(menuPtr, index)
    register Menu *menuPtr;	/* Menu containing entry to display. */
    int index;			/* Index of entry within menu. */
{
    register MenuEntry *entryPtr;
    unsigned long background, foreground;
    int y, height;

    entryPtr = menuPtr->entryPtrs[index];
    if (index == selectedEntry) {
	background = menuPtr->foreground;
	foreground = entryPtr->background;
    } else {
	background = entryPtr->background;
	foreground = menuPtr->foreground;
    }
    height = menuPtr->fontPtr->ascent + menuPtr->fontPtr->descent + 2*MARGIN;
    y = index*height + SHADOW_LEFT;
    XSetForeground(menuPtr->display, menuPtr->gc, background);
    XFillRectangle(menuPtr->display, menuPtr->menuWindow, menuPtr->gc,
	    SHADOW_LEFT, y, menuPtr->menuWidth - (SHADOW_LEFT + SHADOW_RIGHT),
	    height);
    XSetForeground(menuPtr->display, menuPtr->gc, foreground);
    y += MARGIN + menuPtr->fontPtr->ascent - 1;
    if (entryPtr->leftText != NULL) {
	XDrawString(menuPtr->display, menuPtr->menuWindow, menuPtr->gc,
		entryPtr->leftPos, y, entryPtr->leftText, entryPtr->leftSize);
    }
    if (entryPtr->centerText != NULL) {
	XDrawString(menuPtr->display, menuPtr->menuWindow, menuPtr->gc,
		entryPtr->centerPos, y, entryPtr->centerText,
		entryPtr->centerSize);
    }
    if (entryPtr->rightText != NULL) {
	XDrawString(menuPtr->display, menuPtr->menuWindow, menuPtr->gc,
		entryPtr->rightPos, y, entryPtr->rightText,
		entryPtr->rightSize);
    }

    /*
     * If the entry is disabled, display a bar through it.
     */
    
    if (((1<<index) & menuPtr->mask) == 0) {
	XFillRectangle(menuPtr->display, menuPtr->menuWindow, menuPtr->gc, 
		SHADOW_LEFT + 2, y - menuPtr->fontPtr->ascent/2,
		menuPtr->menuWidth - (SHADOW_LEFT + SHADOW_RIGHT + 4), 2);
    }
}

/*
 *----------------------------------------------------------------------
 *
 * MenuFindTitle --
 *
 *	When tracking the mouse, this procedure determines if the
 *	mouse is in the title area of a menu.
 *
 * Results:
 *	The return value is a pointer to the menu over whose title
 *	the cursor is currently position, or NULL if the cursor
 *	isn't over any menu title in the caption containing menuPtr.
 *	If xPtr isn't NULL, *xPtr is filled in with the x-location
 *	(in RootWindow coords.) of the left edge of the returned
 *	title.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static Menu *
MenuFindTitle(menuPtr, eventPtr, xPtr)
    register Menu *menuPtr;		/* Menu entry.  The idea is to find
					 * out if some other menu in this
					 * one's group is selected. */
    register XMotionEvent *eventPtr;	/* X event giving current mouse
					 * location. */
    int *xPtr;
{
    register MenuGroup *groupPtr;
    int x, curX, y, i;

    /*
     * Compute mouse location relative to upper-left corner of the menu group.
     */

    x = eventPtr->x_root;
    y = eventPtr->y_root - groupY;

    /*
     * Make sure that y is within the range of this group.
     */

    if ((y < 0) || (y > menuPtr->titleHeight)) {
	return NULL;
    }

    /*
     * See which title's area it's in, if any.
     */
    
    if (x < groupX) {
	return NULL;
    }
    for (i = 0, curX = groupX, groupPtr = menuPtr->groupPtr;
	    i < SX_MAX_MENUS; i++, curX += menuPtr->titleWidth) {
	menuPtr = groupPtr->menuPtrs[i];
	if (menuPtr == NULL) {
	    return NULL;
	}
	if (x < curX + menuPtr->titleWidth) {
	    if (xPtr != NULL) {
		*xPtr = curX;
	    }
	    return menuPtr;
	}
    }
    return NULL;
}

/*
 *----------------------------------------------------------------------
 *
 * MenuEventProc --
 *
 *	This procedure is called by the dispatcher when events occur
 *	for a menu window.  This can only happen when the menu is
 *	being displayed, which means that a mouse button was pressed
 *	over a menu title and hasn't been released yet.  When this
 *	procedure is called, the mouse has been grabbed for the menu
 *	window.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	This procedure tracks the motion of the mouse and button
 *	releases.  It highlights the menu entry underneath the mouse
 *	(if the entry is enabled) and calls a client-supplied
 *	procedure if the mouse button is released over an entry.
 *	When the buttons are released, the menu is unmapped and
 *	mouse-grabbing stops.
 *
 *----------------------------------------------------------------------
 */

static void
MenuEventProc(menuPtr, eventPtr)
    register Menu *menuPtr;	/* Menu that is currently active. */
    XEvent *eventPtr;		/* Describes what just happened. */
{
    Menu *newPtr;

    /*
     * When we switch from one menu to another (or when the menu is
     * finally dropped), it's possible that "old" events (left over
     * from when the previous menu was down) may come in.  When they
     * do, throw them away.
     */
    
    if (menuPtr != selectedMenuPtr) {
	return;
    }

    if (eventPtr->type == Expose) {
	int i, width, height;

	/*
	 * In a string of expose events each giving a small region,
	 * ignore all but the last.
	 */

	if (eventPtr->xexpose.count > 0) {
	    return;
	}

	width = menuPtr->menuWidth - SHADOW_LEFT - SHADOW_RIGHT;
	height = menuPtr->menuHeight - SHADOW_TOP - SHADOW_BOTTOM;
    
	XSetForeground(menuPtr->display, menuPtr->gc, menuPtr->background);
	XFillRectangle(menuPtr->display, menuPtr->menuWindow, menuPtr->gc,
		0, 0, width, height);
	XSetForeground(menuPtr->display, menuPtr->gc, menuPtr->foreground);
	SxDrawShadow(menuPtr->display, menuPtr->menuWindow, menuPtr->gc,
		0, 0, width, height);
    
	for (i = 0; i < menuPtr->numEntries; i++) {
	    MenuEntryDisplay(menuPtr, i);
	}
    } else if (eventPtr->type == MotionNotify) {
	int index;

	/*
	 * Find the entry under the mouse (if any).  If it's enabled,
	 * then select it and unselect the previous one, if any.
	 */
	
	index = eventPtr->xmotion.y/(menuPtr->fontPtr->ascent
		+ menuPtr->fontPtr->descent + 2*MARGIN);
	if ((eventPtr->xmotion.y < 0) || (index >= menuPtr->numEntries)
		|| (eventPtr->xmotion.x < SHADOW_LEFT)
		|| (eventPtr->xmotion.x > (menuPtr->menuWidth - SHADOW_RIGHT))
		|| (((1<<index) & menuPtr->mask) == 0)) {
	    index = -1;
	}
	if (selectedEntry != index) {
	    int prev = selectedEntry;

	    selectedEntry = index;
	    if (index >= 0) {
		MenuEntryDisplay(menuPtr, index);
	    }
	    if (prev >= 0) {
		MenuEntryDisplay(menuPtr, prev);
	    }
	}

	/*
	 * If no entry is currently selected, see if the mouse has
	 * moved over the title of a different menu.  If so, then
	 * deselect the current menu and select the one whose title
	 * is under the cursor.
	 */
	
	if (index == -1) {
	    int x;

	    newPtr = MenuFindTitle(menuPtr, &eventPtr->xmotion, &x);
	    if ((newPtr != NULL) && (newPtr != menuPtr)) {
		selectedMenuPtr = newPtr;
		selectedEntry = -1;
		XUnmapWindow(menuPtr->display, menuPtr->menuWindow);
		MenuTitleRedisplay(menuPtr);
		PullMenuDown(newPtr, x, groupY + menuPtr->titleHeight);
		MenuTitleRedisplay(newPtr);
		x = XGrabPointer(newPtr->display, newPtr->menuWindow, False,
			ButtonReleaseMask|PointerMotionMask, GrabModeAsync,
			GrabModeAsync, None, cursor, CurrentTime);
		if (x != 0) {
		    Sx_Panic(menuPtr->display,
			    "Pull-down menu couldn't grab pointer.");
		}
	    }
	}
    } else if (eventPtr->type == ButtonRelease) {
	static int masks[] = {
	    0,
	    Button2Mask|Button3Mask|Button4Mask|Button5Mask,
	    Button1Mask|Button3Mask|Button4Mask|Button5Mask,
	    Button1Mask|Button2Mask|Button4Mask|Button5Mask,
	    Button1Mask|Button2Mask|Button3Mask|Button5Mask,
	    Button1Mask|Button2Mask|Button3Mask|Button4Mask
	};

	/*
	 * When all of the buttons have been released, then unmap
	 * the menu.  If an entry was selected at the time, flash it
	 * before erasing the menu, then call its action procedure
	 * after erasing the menu.  The computation of whether all
	 * buttons have been released is a little tricky because the
	 * "detail" value doesn't take into account the current event.
	 */

	if ((eventPtr->xbutton.state & masks[eventPtr->xbutton.button]) == 0) {
	    if (selectedEntry >= 0) {
		int i, index;

		index = selectedEntry;
		for (i = 0; i < 2; i++) {
		    selectedEntry = -1;
		    MenuEntryDisplay(menuPtr, index);
		    SxFlashWait(menuPtr->display);
		    selectedEntry = index;
		    MenuEntryDisplay(menuPtr, index);
		    SxFlashWait(menuPtr->display);
		}
	    }
	    selectedMenuPtr = NULL;
	    menuDown = 0;
	    XUngrabPointer(menuPtr->display, CurrentTime);
	    XUnmapWindow(menuPtr->display, menuPtr->menuWindow);
	    MenuTitleRedisplay(menuPtr);
	    XFlush(menuPtr->display);

	    if (selectedEntry >= 0) {
		MenuEntry *entry =
			menuPtr->entryPtrs[selectedEntry];
		(*entry->proc)(entry->clientData, selectedEntry,
			menuPtr->titleWindow);

		/*
		 * Be VERY CAREFUL after returning from the client procedure.
		 * It's possible that the client deleted the menu, leaving
		 * us with a dangling pointer.  The best thing here is to
		 * do nothing but return.
		 */
	    } else {
		newPtr = MenuFindTitle(menuPtr, &eventPtr->xmotion,
			(int *) NULL);
		if (newPtr != NULL) {
		    selectedMenuPtr = newPtr;
		    MenuTitleRedisplay(newPtr);
		}
	    }
	}
    }
}

/*
 *----------------------------------------------------------------------
 *
 * PullMenuDown --
 *
 *	This procedure does all the work of "pulling a menu down",
 *	i.e. making the menu visible on the screen.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The menu gets mapped onto the screen.
 *
 *----------------------------------------------------------------------
 */

static void
PullMenuDown(menuPtr, x, y)
    register Menu *menuPtr;	/* Menu to pull down.  There must not
				 * currently be a menu pulled down. */
    int x, y;			/* Screen coordinates at which to pull
				 * menu down. */
{
    int displayWidth, displayHeight, width, height;

    /*
     * Compute the location and dimensions of the new menu to be
     * displayed.  Float the menu up or to the left if that is
     * necessary to get it all on-screen.
     */

    displayWidth = DisplayWidth(menuPtr->display,
	    DefaultScreen(menuPtr->display));
    displayHeight = DisplayHeight(menuPtr->display,
	    DefaultScreen(menuPtr->display));
    width = menuPtr->menuWidth;
    if (width > displayWidth) {
	width = displayWidth;
    }
    height = menuPtr->menuHeight;
    if (height > displayHeight) {
	height = displayHeight;
    }
    if ((x+width) > displayWidth) {
	x = displayWidth - width;
    }
    if ((y+height) > displayHeight) {
	y = displayHeight - height;
    }
    if (x < 0) {
	x = 0;
    }
    if (y < 0) {
	y = 0;
    }
    menuX = x;
    menuY = y;

    /*
     * Save the previous pixels under the menu, then display the menu.
     */

    /* XGrabServer(); */
    XMoveWindow(menuPtr->display, menuPtr->menuWindow, x, y);
    XMapRaised(menuPtr->display, menuPtr->menuWindow);
}
