/* 
 * sxNotify.c --
 *
 *	This file provides a notification facility, whereby users
 *	are notified of various conditions and given a choice of
 *	options.  Notifiers are built using the facilities of
 *	the X window manager and the Sx dispatcher.
 *
 * 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 and that both that copyright
 * notice and this permission notice appear in supporting
 * documentation.  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/sxNotify.c,v 1.9 90/01/12 11:25:13 ouster Exp $ SPRITE (Berkeley)";
#endif not lint

#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <varargs.h>
#include "sx.h"
#include "sxInt.h"

/*
 * A notifier is a window that pops up and displays a message and
 * a set of options.  The message is displayed at the bottom of the
 * window, with the options spaced across the top of the window
 * in a collection of buttons.  The user reads the message and
 * clicks a mouse button over one of the options.
 */

typedef struct {
    Window w;			/* Window for button. */
    char *text;			/* Text to display for option. */
    int width;			/* Width of option button in pixels (includes
				 * text plus space on either side. */
} Option;

#define MAXOPTIONS 16

typedef struct {
    Display *display;		/* Connection to X server. */
    Window w;			/* Top-level window of notifier. */
    int numOptions;		/* Number of options actually used. */
    Option options[MAXOPTIONS];	/* Info about options. */
    int outerWidth, outerHeight;/* Outside dimensions of w, including space
				 * for shadowed box. */
    int width, height;		/* Inner width of area actually used for
				 * message and options. */
    int space;			/* Number of pixels to leave around edges
				 * of text. */
    int optionHeight;		/* Number of pixels occupied by the option
				 * area. */
    unsigned long fg, bg;	/* Colors to use for displaying. */
    XFontStruct *fontPtr;	/* Information about font for notifier. */
    GC gc;			/* Used for displaying. */
    char *message;		/* Message to be displayed. */
    int center;			/* 1 means center message, 0 means
				 * left-justify. */
} Notifier;

/*
 * Parameters controlling layout of notifier:
 *
 * SPACING:		spacing between lines of message.
 * OPTION_MARGIN	space around top and bottom of text in option buttons.
 * OPTION_BORDER:	thickness of borders around options.
 * WINDOW_BORDER:	thickness of border around notifier itself.
 */

#define SPACING 0
#define OPTION_MARGIN 2
#define OPTION_BORDER 1
#define WINDOW_BORDER 2

/*
 * Cursor for notifier windows:
 */

#include "bitmaps/dot"
#include "bitmaps/dotMask"
static Cursor cursor;

/*
 * Bitmap used to paint a fancy header area around the options.
 */

#include "bitmaps/notify"
static Pixmap header;

static int init = 0;

/*
 * The following variable is set to a value >= 0 by NotifierButtonProc
 * to signal that an option has been selected.
 */

static int selectedOption;

/*
 * Forward declarations:
 */

static void	NotifierButtonProc();
static void	NotifierEventProc();
static void	NotifierInit();
static char *	EndOfLine();

/*
 *----------------------------------------------------------------------
 *
 * Sx_Notify --
 *
 *	This procedure creates a notifier window displaying text and
 *	all the option strings.  It waits until one of the option
 *	strings has been buttoned with the mouse, then returns.  This
 *	procedure grabs the server, which prevents any other window
 *	activity until the user has selected an option.
 *
 * Results:
 *	The return value is the index of the option that was
 *	buttoned, where 0 corresponds to the first option in
 *	the list.
 *
 * Side effects:
 *	A window is created, then deleted.
 *
 *----------------------------------------------------------------------
 */
    /* VARARGS8 */
#ifdef lint
int
Sx_Notify(display, parent, x, y, width, message, fontPtr, center, va_alist)
    Display *display;		/* Connection to X server. */
    Window parent;		/* Window in whose coordinates x and y are
				 * specified below. */
    int x, y;			/* Location of UL corner of notifier, in
				 * coords of parent.  If coords are < 0,
				 * then this procedure picks coords to
				 * center notifier. */
    int width;			/* Width of notifier, in pixels.  If 0,
				 * then this procedure picks a width that
				 * makes the notifier look pretty. */
    char *message;		/* Message to be displayed in notifier.
				 * May contain newlines to break into
				 * lines explicitly.  This procedure will
				 * automatically break lines at spaces
				 * to make sure that no line is longer
				 * than width pixels.  */
    XFontStruct *fontPtr;	/* Describes font in which to display text
				 * in the notifier.  If NULL, then a default
				 * font will be chosen. */
    int center;			/* 1 means center each line in the window,
				 * 0 means left-justify lines. */
    va_dcl			/* Additional arguments, each a pointer to
				 * a name to be displayed in a button,
				 * terminated by a NULL pointer. */
#else
int
Sx_Notify(va_alist)
    va_dcl			/* Actual arguments must be as shown for
				 * lint above. */
#endif
{
#ifndef lint
    Display *display;		/* Connection to X server. */
    Window parent;		/* Window in whose coordinates x and y are
				 * specified below. */
    int x, y;			/* Location of UL corner of notifier, in
				 * coords of parent.  If coords are < 0,
				 * then this procedure picks coords to
				 * center notifier. */
    int width;			/* Width of notifier, in pixels.  If 0,
				 * then this procedure picks a width that
				 * makes the notifier look pretty. */
    char *message;		/* Message to be displayed in notifier.
				 * May contain newlines to break into
				 * lines explicitly.  This procedure will
				 * automatically break lines at spaces
				 * to make sure that no line is longer
				 * than width pixels.  */
    XFontStruct *fontPtr;	/* Describes font in which to display text
				 * in the notifier.  If NULL, then a default
				 * font will be chosen. */
    int center;			/* 1 means center each line in the window,
				 * 0 means left-justify lines. */
#endif
    register Option *o;
    Notifier n;
    int numLines;		/* Number of lines in notifier, not including
				 * options. */
    int minOptionLength;	/* Total width of all options, assuming
				 * optionPad is 0. */
    int optionPad;		/* Amount of extra space to leave between
				 * options (in addition to "n.space"). */
    int optionY;		/* Y-coord of top of option boxes. */
    int buttonHeight;		/* Height of option buttons. */
    XWindowAttributes parentAtts;
				/* Information about parent. */
    XSetWindowAttributes childAtts;
				/* Used to set attributes of notifier. */
    int i, trialWidth, inc, tmp, displayWidth, displayHeight;
    va_list args;
    char *p;
    XGCValues values;

    /*
     * Collect the non-variable arguments:
     */

    va_start(args);
#ifndef lint
    display = va_arg(args, Display *);
    parent = va_arg(args, Window);
    x = va_arg(args, int);
    y = va_arg(args, int);
    width = va_arg(args, int);
    message = va_arg(args, char *);
    fontPtr = va_arg(args, XFontStruct *);
    center = va_arg(args, int);
#endif

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

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

    n.display = display;
    n.space = XTextWidth(fontPtr, "m", 1);
    n.fg = BlackPixel(display, DefaultScreen(display));
    n.bg = WhitePixel(display, DefaultScreen(display));
    n.fontPtr = fontPtr;
    n.message = message;
    n.center = center;
    displayWidth = DisplayWidth(display, DefaultScreen(display));
    displayHeight = DisplayHeight(display, DefaultScreen(display));

    /*
     * Fill in most of the info in the options array, and compute
     * how much screen space is needed for all the options if they're
     * placed side-to-side.  If I get to choose how wide the window
     * is, this will be a lower bound.
     */

    minOptionLength = 0;
    buttonHeight = fontPtr->ascent + fontPtr->descent + 2*OPTION_MARGIN;
    n.optionHeight = 2*buttonHeight + 2*OPTION_BORDER;
    optionY = buttonHeight/2 + SHADOW_TOP;
    for (n.numOptions = 0; n.numOptions < MAXOPTIONS; n.numOptions++) {
	o = &n.options[n.numOptions];
	o->text = va_arg(args, char *);
	if (o->text == NULL) {
	    break;
	}
	o->width = XTextWidth(fontPtr, o->text, strlen(o->text))
		+ 2*n.space;
	minOptionLength += o->width;
    }
    minOptionLength += (n.numOptions+1) * n.space;

    /*
     * Choose the width, if the caller didn't get one.  Start with
     * a wide width, and narrow it down until the window is about
     * 1.5 times as wide as it is high (within 10%).  If the caller
     * already chose a width, then just compute the height it needs.
     */

    n.width = width;
    if (n.width <= 0) {
	trialWidth = displayWidth/2;
    } else {
	trialWidth = n.width;
    }
    for (inc = trialWidth/2; ; inc /= 2) {
	p = message;
	for (numLines = 0, p = message; *p != 0; numLines++) {
	    p = EndOfLine(&n, p, trialWidth - 2*n.space, (int *) NULL);
	    while (*p == ' ') {
		p++;
	    }
	    if (*p == '\n') {
		p++;
	    }
	}
	n.height = n.optionHeight
		+ numLines*(fontPtr->ascent + fontPtr->descent + SPACING)
		+ 2*n.space;
	if (n.width > 0) {
	    break;
	}
	if (inc < 5) {
	    n.width = trialWidth;
	    break;
	}
	if ((n.height + n.height/2) > trialWidth) {
	    trialWidth += inc;
	} else {
	    trialWidth -= inc;
	}
	if (trialWidth < minOptionLength) {
	    trialWidth = minOptionLength;
	}
    }
    n.outerWidth = n.width + SHADOW_LEFT + SHADOW_RIGHT;
    n.outerHeight = n.height + SHADOW_TOP + SHADOW_BOTTOM;

    /*
     * Compute the final location of the notifier window, which will
     * be in RootWindow.  If the specified parent wasn't RootWindow,
     * the coordinate transformation will also be needed.  If the
     * caller didn't give a location, center the notifier in its
     * parent.
     */

    XGetWindowAttributes(display, parent, &parentAtts);
    if (x < 0) {
	x = (parentAtts.width - n.outerWidth)/2;
    }
    if (y < 0) {
	y = (parentAtts.height - n.outerHeight)/2;
    }
    if (parent != parentAtts.root) {
	int xOffset, yOffset;
	Window dummy;

	XTranslateCoordinates(display, parent, parentAtts.root, 0, 0,
		&xOffset, &yOffset, &dummy);
	x += xOffset;
	y += yOffset;
    }
    if ((x + n.outerWidth) > displayWidth) {
	x = displayWidth - n.outerWidth;
    }
    if ((y + n.outerHeight) > displayHeight) {
	y = displayHeight - n.outerHeight;
    }
    if (x < 0) {
	x = 0;
    }
    if (y < 0) {
	y = 0;
    }

    /*
     * Create a window for the notifier, plus one window for each button,
     * plus a graphics context.  Distribute extra space evenly among the
     * options.
     */

    childAtts.background_pixmap = None;
    childAtts.override_redirect = True;
    childAtts.cursor = cursor;
    n.w = XCreateWindow(display, parentAtts.root, x, y, n.outerWidth,
	    n.outerHeight, 0, CopyFromParent, InputOutput, CopyFromParent,
	    CWBackPixmap|CWOverrideRedirect|CWCursor, &childAtts);
    optionPad = (n.width - minOptionLength)/(n.numOptions+1);
    if (optionPad < 0) {
	optionPad = 0;
    }
    tmp = (n.width - minOptionLength - (n.numOptions+1)*optionPad)/2
	    + optionPad + n.space + SHADOW_LEFT;
    for (i = 0; i < n.numOptions; i++) {
	o = &n.options[i];
	o->w = Sx_ButtonCreate(display, n.w, tmp, optionY, o->width,
		buttonHeight, OPTION_BORDER, o->text, fontPtr,
		n.fg, n.bg, n.bg, n.fg,
		NotifierButtonProc, (ClientData) i);
	XMapWindow(display, o->w);
	tmp += o->width + optionPad + n.space;
    }
    Sx_HandlerCreate(display, n.w, ExposureMask, NotifierEventProc,
	    (ClientData) &n);
    values.foreground = n.fg;
    values.background = n.bg;
    values.font = fontPtr->fid;
    n.gc = XCreateGC(display, n.w, GCForeground|GCBackground|GCFont, &values);
    XMapWindow(display, n.w);

    /*
     * Grab the server and the pointer.  Don't do this right now:  problems
     * with the X11 grab mechanism (can't get all the right events).
     */
    
    /*
    i = XGrabPointer(display, n.w, False, ButtonPressMask|ButtonReleaseMask
	    |EnterWindowMask|LeaveWindowMask|ExposureMask,
	    GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
    if (i != 0) {
	Sx_Panic(display, "Sx_Notify couldn't grab pointer.");
    }
    XGrabServer(display);
    */

    /*
     * Process events until a button is pushed over an option.
     */
    
    selectedOption = -1;
    while (1) {
	XEvent event;

	XNextEvent(display, &event);

	/*
	 * Throw away button and keystroke events that aren't intended
	 * for the notifier or one of its buttons.  This is a temporary
	 * hack until server grabbing is done correctly.
	 */

	if ((event.type == ButtonPress) || (event.type == ButtonRelease)
		|| (event.type == KeyPress) || (event.type == KeyRelease)) {
	    int i;

	    if (event.xany.window != n.w) {
		for (i = 0; i < n.numOptions; i++) {
		    if (event.xany.window == n.options[i].w) {
			goto processEvent;
		    }
		}

		/*
		 * Special hack:  must release any grabs that might have been
		 * triggered automatically by the event.  I'm not sure how to
		 * solve this problem correctly, but it seems to me that there
		 * shouldn't have to be code here for this.
		 */

		XAllowEvents(display, AsyncPointer, CurrentTime);
		XAllowEvents(display, AsyncKeyboard, CurrentTime);
		continue;
	    }
	}

	processEvent:
	Sx_HandleEvent(&event);

	if (selectedOption >= 0) {
	    XDestroyWindow(display, n.w);
	    /*
	    XUngrabPointer(display, CurrentTime);
	    XUngrabServer(display);
	    */
	    XFlush(display);
	    return selectedOption;
	}
    }
}

/*
 *----------------------------------------------------------------------
 *
 * Sx_Panic --
 *
 *	This procedure is called when some totally unexpected and
 *	irremediable error has occurred.  It displays a message,
 *	waits for the user to acknowledge it, and terminates the
 *	process.
 *
 * Results:
 *	None.  Never returns.
 *
 * Side effects:
 *	The process dies a terrible, agonizing death.
 *
 *----------------------------------------------------------------------
 */

void
Sx_Panic(display, msg)
    Display *display;		/* Connection to X server. */
    char *msg;			/* Message explaining what happened. */
{
    char realMsg[2000];
    static int alreadyPanicked = 0;
    static char *panicMsg = NULL;
    int l;

    if (alreadyPanicked) {
	fprintf(stderr, "DOUBLE PANIC!!  ");
	l = strlen(panicMsg);
	if (panicMsg[l-1] == '.') {
	    fprintf(stderr, "%.*s", l-1, panicMsg);
	} else {
	    fputs(panicMsg, stderr);
	}
	l = strlen(msg);
	fprintf(stderr, ", then %c%.*s",
	    isupper(msg[0]) ? tolower(msg[0]) : msg[0],
	    l-2, msg+1);
	if (msg[l-1] != '.') {
	    fputc(msg[l-1], stderr);
	}
	fprintf(stderr, " while panicking.\n");
    } else {
	alreadyPanicked = 1;
	panicMsg = msg;
	(void) sprintf(realMsg, "PANIC!!\n\n%.1950s", msg);
	(void) Sx_Notify(display, RootWindow(display, DefaultScreen(display)),
		-1, -1, 0, realMsg, (XFontStruct *) NULL,
		1, "Kill Process", NULL);
    }
    exit(1);
}

/*
 *----------------------------------------------------------------------
 *
 * EndOfLine --
 *
 *	This procedure is used to figure out how much stuff will fit
 *	on one line of a notifier.  Assuming that the first character
 *	of string is to be at the beginning of a line, and that the
 *	line may be at most width pixels wide using fontPtr to display the
 *	text, this procedure determines	how much stuff will fit on the
 *	line.  The end of the line occurs either when a newline character
 *	appears, or at the end of a word if the next word would extend
 *	the line wider than width.  If a single word would more than fill
 *	the entire line, then the word is broken in the middle.
 *
 * Results:
 *	The return value is the address of the character just after the
 *	last one that fits on the line.  It can be a newline character,
 *	a space character, a NULL character (end of string), or even
 *	a regular character if a single word overflows the line.  If
 *	extraPtr is non-NULL, *extraPtr gets filled in with the number
 *	of unused pixels in this line.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static char *
EndOfLine(nPtr, string, width, extraPtr)
    Notifier *nPtr;		/* Information about notifier. */
    char *string;		/* String to be displayed. */
    int width;			/* Maximum number of pixels across line. */
    int *extraPtr;		/* If non-NULL, fill in the value pointed
				 * to with the number of pixels leftover
				 * in the line, or 0 if this line is
				 * width pixels wide. */
{
    register char *ptr = string;
    char *lastWordBreak;
    int remWidth, charSize;
    register char c;
    register XFontStruct *fontPtr = nPtr->fontPtr;

    if ((fontPtr->min_byte1 != 0) || (fontPtr->max_byte1 != 0)) {
	Sx_Panic(nPtr->display,
		"Notify.EndOfLine got font with too many characters.");
    }
    lastWordBreak = ptr;
    for (c = *ptr; c != 0; ptr++, c = *ptr) {
	if (isspace(c)) {
	    lastWordBreak = ptr;
	    remWidth = width;
	}
	if (c == '\n') {
	    goto done;
	}
	if (fontPtr->per_char == NULL) {
	    charSize = fontPtr->max_bounds.width;
	} else if ((c >= fontPtr->min_char_or_byte2)
		&& (c <= fontPtr->max_char_or_byte2)) {
	    charSize = fontPtr->per_char[c-fontPtr->min_char_or_byte2].width;
	} else {
	    charSize = fontPtr->min_bounds.width;
	}
	width -= charSize;
	if (width < 0) {
	    if (lastWordBreak == string) {
		lastWordBreak = ptr;
		remWidth = width + charSize;
		if (lastWordBreak == string) {
		    lastWordBreak += 1;
		    remWidth = 0;
		}
	    }
	    goto done;
	}
    }
    lastWordBreak = ptr;
    remWidth = width;

    done:
    if (extraPtr != NULL) {
	*extraPtr = remWidth;
    }
    return lastWordBreak;
}

/*
 *----------------------------------------------------------------------
 *
 * NotifierInit --
 *
 *	This procedure is called once only to initialize shared
 *	variables for the module.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Random initializations get performed.  See the code for
 *	details.  If there's any problem, the procedure panics.
 *
 *----------------------------------------------------------------------
 */

static void
NotifierInit(display)
    Display *display;		/* Connection to the 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, dot_bits,
	    dot_width, dot_height);
    mask = XCreateBitmapFromData(display, root, dotMask_bits,
	    dotMask_width, dotMask_height);
    cursor = XCreatePixmapCursor(display, source, mask, &black, &white,
	    dot_x_hot, dot_y_hot);
    XFreePixmap(display, source);
    XFreePixmap(display, mask);

    header = XCreateBitmapFromData(display, root, notify_bits,
	    notify_width, notify_height);

    init = 1;
}

/*
 *----------------------------------------------------------------------
 *
 * NotifierEventProc --
 *
 *	This procedure is invoked by the Sx dispatcher when Exposure
 *	events occur on a notifier.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The notifier gets redisplayed.
 *
 *----------------------------------------------------------------------
 */

static void
NotifierEventProc(nPtr, eventPtr)
    register Notifier *nPtr;	/* Information about notifier. */
    XEvent *eventPtr;		/* What happened. */
{
    if (eventPtr->type == Expose) {
	register char *p;
	int y, spacing;

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

	/*
	 * Clear the background and draw the fancy stippling around
	 * the top of the window and the shadow along the right and
	 * bottom edges.
	 */

	XSetForeground(nPtr->display, nPtr->gc, nPtr->bg);
	XFillRectangle(nPtr->display, nPtr->w, nPtr->gc,
		SHADOW_LEFT, SHADOW_TOP, nPtr->width, nPtr->height);
	XSetForeground(nPtr->display, nPtr->gc, nPtr->fg);
	XSetStipple(nPtr->display, nPtr->gc, header);
	XSetFillStyle(nPtr->display, nPtr->gc, FillStippled);
	XFillRectangle(nPtr->display, nPtr->w, nPtr->gc,
		SHADOW_LEFT, SHADOW_TOP, nPtr->width, nPtr->optionHeight);
	XSetFillStyle(nPtr->display, nPtr->gc, FillSolid);
	y = SHADOW_TOP + nPtr->optionHeight - 1;
	XFillRectangle(nPtr->display, nPtr->w, nPtr->gc, SHADOW_LEFT,
		SHADOW_TOP + nPtr->optionHeight - 1, nPtr->width, 2);
	SxDrawShadow(nPtr->display, nPtr->w, nPtr->gc, 0, 0,
		nPtr->width, nPtr->height);

	/*
	 * Display the message, one line at a time.
	 */

	p = nPtr->message;
	spacing = SPACING + nPtr->fontPtr->ascent + nPtr->fontPtr->descent;
	for (y = SHADOW_TOP + nPtr->optionHeight + nPtr->space
		+ nPtr->fontPtr->ascent - 1; ; y += spacing) {
	    char *next;
	    int extraSpace;

	    if (*p == 0) {
		break;
	    }
	    next = EndOfLine(nPtr, p, nPtr->width - 2*nPtr->space,
		    &extraSpace);
	    if (nPtr->center) {
		extraSpace /= 2;
	    } else {
		extraSpace = 0;
	    }
	    if (next != p) {
		XDrawString(nPtr->display, nPtr->w, nPtr->gc,
		extraSpace + nPtr->space + SHADOW_LEFT, y, p, next-p);
	    }
	    
	    /*
	     * For lines not terminated by newline, eat up any white
	     * space at the beginning of the next line.  For lines
	     * terminated by newline, treat the space as significant.
	     */
	    
	    p = next;
	    while (*p == ' ') {
		p++;
	    }
	    if (*p == '\n') {
		p++;
	    }
	}
    }
}

/*
 *----------------------------------------------------------------------
 *
 * NotifierButtonProc --
 *
 *	This procedure is invoked when a button is pressed while the
 *	pointer is over one of the options in a notifier.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	It sets "selectedOption" to the option's index, as a signal
 *	to Sx_Notify.
 *
 *----------------------------------------------------------------------
 */

static void
NotifierButtonProc(optionIndex)
    int optionIndex;		/* Index of selected option. */
{
    selectedOption = optionIndex;
}
