/* 
 * sxButton.c --
 *
 *	This file implements buttons using the facilities of the X
 *	window package and the Sx dispatcher.  Buttons are rectangular
 *	windows with text displayed inside them, which light up when
 *	the cursor passes over them and cause client-supplied procedures
 *	to be invoked when buttoned.
 *
 * Copyright (C) 1986, 1987, 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/sxButton.c,v 1.6 89/05/12 13:16:06 ouster Exp $ SPRITE (Berkeley)";
#endif not lint

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

/*
 * Library imports:
 */

extern char *malloc();

/*
 * For each button there is one structure of the type defined below,
 * which is used to hold information about the button.
 */

typedef struct Button {
    Display *display;		/* Display containing button. */
    Window w;			/* Window corresponding to button. */
    int width, height;		/* Size of button window. */
    XFontStruct *fontPtr;	/* Info about font with which to draw text. */
    char *text;			/* String to display in button (dynamically
				 * allocated). */
    int textSize;		/* Number of non-NULL chars in text. */
    GC gc;			/* Graphics context used to draw stuff
				 * in the button window.  The foreground
				 * and background colors get changed to
				 * hold whatever's relevant at present. */
    unsigned long fg, bg;	/* Colors to use when cursor isn't over
				 * button. */
    unsigned long activeFg, activeBg;
				/* Colors for when cursor is over button. */
    int  active;		/* 1 means cursor is over button so
				 * display it using activeFg and activeBg
				 * instead of fg and bg. */
    void (*proc)();		/* Procedure to call when mouse button
				 * is pressed. */
    ClientData clientData;	/* Information to pass to proc. */
} Button;

/*
 * The context below is used to map from X window ids to Button structures.
 */

static XContext buttonContext;
static initialized = 0;

/*
 * The stuff below is used to define the cursors used for button windows.
 */

#include "bitmaps/dot"
#include "bitmaps/dotMask"
#include "bitmaps/star"
#include "bitmaps/starMask"

static Cursor dotCursor, starCursor;

/*
 * Forward references:
 */

static void	ButtonEventProc();
static void	ButtonInit();
static void	ButtonRedisplay();

/*
 *----------------------------------------------------------------------
 *
 * Sx_ButtonMake --
 *
 *	This procedure turns an ordinary window into a button window.
 *	If the window is already a button window, then its parameters
 *	are changed.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	From now on, this window is a button window.  The given text
 *	will be displayed centered in the window, in the given colors.
 *	Whenever the cursor passes over the window, the colors will
 *	change to the active ones.  When a mouse button is pressed
 *	over the window, the window will flash and the given procedure
 *	will be called.  It must have the following form:
 *	
 *	void
 *	proc(clientData, eventPtr)
 *	    ClientData clientData;
 *	    XButtonPressedEvent *eventPtr;
 *	{
 *	}
 *
 *	ClientData is just the parameter passed into Sx_ButtonMake;
 *	it can be used by the client in any fashion whatsoever, typically
 *	as a pointer to client-specific information about the button.
 *	EventPtr refers to the event describing the button press.
 *
 *----------------------------------------------------------------------
 */

void
Sx_ButtonMake(display, w, text, fontPtr, fg, bg, activeFg, activeBg,
	proc, clientData)
    Display *display;			/* Connection to server. */
    Window w;				/* Window that is to become a button
					 * window.  If this isn't already a
					 * button window, then it shouldn't
					 * be mapped.  If it's already a button
					 * window, its parameters will be
					 * changed and the window will be
					 * redisplayed if it's mapped. */
    char *text;				/* Text to display inside button.  May
					 * be NULL. */
    XFontStruct *fontPtr;		/* Font in which to display text.  If
					 * NULL, a default font is used. */
    unsigned long fg;			/* Color in which to display text when
					 * cursor isn't over button. */
    unsigned long bg;			/* Color for background of button,
					 * when cursor isn't over button. */
    unsigned long activeFg, activeBg;	/* Colors to use when cursor is over
					 * button. */
    void (*proc)();			/* Procedure to call when mouse button
					 * is pressed. */
    ClientData clientData;		/* Arbitrary value to pass to proc. */
{
    register Button *buttonPtr;
    caddr_t data;
    XGCValues values;

    if (!initialized) {
	ButtonInit(display);
    }

    /*
     * See if this window is already a button window.  If so, then
     * prepare it for re-allocation.  If not, then make a new Button
     * structure.
     */

    if (XFindContext(display, w, buttonContext, &data) == 0) {
	buttonPtr = (Button *) data;
	if (buttonPtr->text != NULL) {
	    free((char *) buttonPtr->text);
	}
	XFreeGC(display, buttonPtr->gc);
    } else {
	XSetWindowAttributes atts;
	Drawable dum1;
	int dum2;

	buttonPtr = (Button *) malloc(sizeof(Button));
	buttonPtr->display = display;
	buttonPtr->w = w;
	XGetGeometry(display, w, &dum1, &dum2, &dum2,
		&buttonPtr->width, &buttonPtr->height, &dum2, &dum2);
	buttonPtr->active = 0;
	(void) Sx_HandlerCreate(display, w, ExposureMask|EnterWindowMask|
		LeaveWindowMask|ButtonPressMask|ButtonReleaseMask|
		StructureNotifyMask, ButtonEventProc, (ClientData) buttonPtr);
	atts.background_pixmap = None;
	XChangeWindowAttributes(display, w, CWBackPixmap, &atts);
	XDefineCursor(display, w, dotCursor);
	XSaveContext(display, w, buttonContext, (caddr_t) buttonPtr);
    }

    /*
     * Initialize the contents of the button.
     */

    if (fontPtr == NULL) {
	fontPtr = Sx_GetDefaultFont(display);
    }
    buttonPtr->fontPtr = fontPtr;
    if (text != NULL) {
	buttonPtr->textSize = strlen(text);
	buttonPtr->text = malloc((unsigned) (buttonPtr->textSize + 1));
	buttonPtr->text = strcpy(buttonPtr->text, text);
    } else {
	buttonPtr->text = NULL;
	buttonPtr->textSize = 0;
    }
    values.font = fontPtr->fid;
    buttonPtr->gc = XCreateGC(display, w, GCFont, &values);
    buttonPtr->fg = fg;
    buttonPtr->bg = bg;
    buttonPtr->activeFg = activeFg;
    buttonPtr->activeBg = activeBg;
    buttonPtr->proc = proc;
    buttonPtr->clientData = clientData;

    ButtonRedisplay(buttonPtr);
}

/*
 *----------------------------------------------------------------------
 *
 * Sx_ButtonCreate --
 *
 *	Like Sx_ButtonMake, but creates a new window for the button
 *	rather than using an existing window.
 *
 * Results:
 *	The return value is the X id for a new window, which has
 *	location and size given by the parameters, and behaves
 *	just like a button window made by calling Sx_ButtonMake.
 *	The new window is not mapped.
 *
 * Side effects:
 *	The new window will behave like a button window.  See
 *	Sx_ButtonMake's documentation for details.
 *
 *----------------------------------------------------------------------
 */

Window
Sx_ButtonCreate(display, parent, x, y, width, height, borderWidth, text,
	fontPtr, fg, bg, activeFg, activeBg, proc, clientData)
    Display *display;			/* Connection to the server. */
    Window parent;			/* Window in which new button window
					 * is to be created. */
    int x, y;				/* Location of upper-left corner of
					 * new window's border, in coords of
					 * parent. */
    int width, height;			/* Inside dimensions of new window.  If
					 * either is zero, then the dimension
					 * is chosen to be just large enough to
					 * contain the text. */
    int borderWidth;			/* Border width for new window, in
					 * pixels. */
    char *text;				/* Text to display inside button.  May
					 * be NULL, in which case nothing
					 * appears inside the button. */
    XFontStruct *fontPtr;		/* Font in which to display text.  If
					 * NULL, a default font is used. */
    unsigned long fg;			/* Color in which to display border
					 * (and text when cursor isn't over
					 * button). */
    unsigned long bg;			/* Color for background of button, when
					 * cursor isn't over button. */
    unsigned long activeFg, activeBg;	/* Colors to use when cursor is over
					 * button. */
    void (*proc)();			/* Procedure to call when mouse button
					 * is pressed. */
    ClientData clientData;		/* Arbitrary value to pass to proc. */
{
    Window w;

    if (fontPtr == NULL) {
	fontPtr = Sx_GetDefaultFont(display);
    }
    if (height <= 0) {
	height = Sx_DefaultHeight(display, fontPtr);
    }
    if (width <= 0) {
	width = XTextWidth(fontPtr, text, strlen(text))
		+ XTextWidth(fontPtr, "  ", 2);
    }
    w = XCreateSimpleWindow(display, parent, x, y, width, height,
	    borderWidth, fg, bg);
    Sx_ButtonMake(display, w, text, fontPtr, fg, bg, activeFg, activeBg,
	    proc, clientData);
    return w;
}

/*
 *----------------------------------------------------------------------
 *
 * ButtonInit --
 *
 *	This procedure is called once only to initialize shared
 *	variables for the module.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Random initializations are performed.  See the code.
 *
 *----------------------------------------------------------------------
 */

static void
ButtonInit(display)
    Display *display;
{
    static XColor black = {0, 0, 0, 0, 0};
    static XColor white = {0, ~0, ~0, ~0, 0};
    Pixmap source, mask;
    Window root;

    buttonContext = XUniqueContext();
    root = RootWindow(display, DefaultScreen(display));

    source = XCreateBitmapFromData(display, root, dot_bits,
	    dot_width, dot_height);
    mask = XCreateBitmapFromData(display, root, dotMask_bits,
	    dotMask_width, dotMask_height);
    dotCursor = XCreatePixmapCursor(display, source, mask, &black, &white,
	    dot_x_hot, dot_y_hot);
    XFreePixmap(display, source);
    XFreePixmap(display, mask);

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

    initialized = 1;
}

/*
 *----------------------------------------------------------------------
 *
 * ButtonRedisplay --
 *
 *	Redraw the information in a button window.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The button gets redrawn.
 *
 *----------------------------------------------------------------------
 */

static void
ButtonRedisplay(buttonPtr)
    register Button *buttonPtr;	/* Button to redisplay. */
{
    unsigned long fg, bg;
    int x, y, textWidth;

    if (buttonPtr->active) {
	fg = buttonPtr->activeFg;
	bg = buttonPtr->activeBg;
    } else {
	fg = buttonPtr->fg;
	bg = buttonPtr->bg;
    }
    XSetForeground(buttonPtr->display, buttonPtr->gc, bg);
    XFillRectangle(buttonPtr->display, buttonPtr->w, buttonPtr->gc, 0, 0,
	    buttonPtr->width, buttonPtr->height);
    if (buttonPtr->text != NULL) {
	textWidth = XTextWidth(buttonPtr->fontPtr, buttonPtr->text,
		buttonPtr->textSize);
	x = (buttonPtr->width - textWidth)/2;
	y = (buttonPtr->height + buttonPtr->fontPtr->ascent
		- buttonPtr->fontPtr->descent)/2;
	XSetForeground(buttonPtr->display, buttonPtr->gc, fg);
	XDrawString(buttonPtr->display, buttonPtr->w, buttonPtr->gc,
		x, y, buttonPtr->text, buttonPtr->textSize);
    }
}

/*
 *----------------------------------------------------------------------
 *
 * ButtonEventProc --
 *
 *	This procedure is invoked automatically by the Sx_ dispatcher
 * 	whenever an event occurs for a button window.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	This procedure performs most of the button-related side-effects,
 *	such as displaying the button in reverse video and calling the
 *	client's procedure when a mouse button is pressed.
 *
 *----------------------------------------------------------------------
 */

static void
ButtonEventProc(buttonPtr, eventPtr)
    register Button *buttonPtr;		/* Button for which it happened. */
    register XEvent *eventPtr;		/* Describes what happened. */
{
    int i;

    switch (eventPtr->type) {

	case Expose:
	    /*
	     * When expose events come in bunches, ignore all but the
	     * last in the bunch (the whole window gets redisplayed
	     * anyway).
	     */
	    
	    if (eventPtr->xexpose.count == 0) {
		ButtonRedisplay(buttonPtr);
	    }
	    return;

	case EnterNotify:
	    buttonPtr->active = 1;
	    ButtonRedisplay(buttonPtr);
	    return;

	case LeaveNotify:
	    buttonPtr->active = 0;
	    ButtonRedisplay(buttonPtr);
	    return;

	case ConfigureNotify:
	    buttonPtr->width = eventPtr->xconfigure.width;
	    buttonPtr->height = eventPtr->xconfigure.height;
	    return;

	case ButtonPress:
	    if ((eventPtr->xbutton.subwindow != None)
		    && (eventPtr->xbutton.subwindow != buttonPtr->w)) {
		return;
	    }
	    XDefineCursor(buttonPtr->display, buttonPtr->w, starCursor);

	    /*
	     * Flash the button before calling its action procedure.
	     */

	    for (i = 0; i < 2; i++) {
		buttonPtr->active = 0;
		ButtonRedisplay(buttonPtr);
		SxFlashWait(buttonPtr->display);
		buttonPtr->active = 1;
		ButtonRedisplay(buttonPtr);
		SxFlashWait(buttonPtr->display);
	    }
	    buttonPtr->proc(buttonPtr->clientData, eventPtr);

	    /*
	     * It's important that we don't do anything with the button
	     * after the procedure returns:  the procedure could have
	     * deleted the button, leaving us with a dangling pointer.
	     */

	    return;
	
	case ButtonRelease:

	    XDefineCursor(buttonPtr->display, buttonPtr->w, dotCursor);
	    return;

	case DestroyNotify:
	    XDeleteContext(buttonPtr->display, buttonPtr->w, buttonContext);
	    if (buttonPtr->text != NULL) {
		free(buttonPtr->text);
	    }
	    XFreeGC(buttonPtr->display, buttonPtr->gc);
	    free((char *) buttonPtr);
	    return;
    }
}
