/*
 *	$Source: /u1/X/xman/RCS/sxMessage.c,v $
 *	$Header: sxMessage.c,v 1.1 87/02/24 11:51:01 swick Exp $
 *
 * sxMessage.c
 *	This is a duplicate of sxNotify.c without the server freezing
 *	and with only one button (that has default text).  This is a very
 *	quick and very dirty hack of sxNotify.  If I thought it was a proper
 *	way to go, I'd spend time to clean it up.  The right thing to do
 *	is to compose the notifier from the button primitive and get real
 *	callbacks.
 *
 */

#ifndef lint
static char *rcsid_sxMessage_c = "$Header: sxMessage.c,v 1.1 87/02/24 11:51:01 swick Exp $";
#endif	lint

/* 
 * sxMessage.c --
 *
 *	This file provides a notification facility, whereby users
 *	are notified of various conditions.  Sx_Message returns immediately
 *	after posting the message window, permitting the application
 *	to continue.  Notifiers are built using the facilities of
 *	the X window manager and the Sx dispatcher.
 *
 * Copyright (C) 1986 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: sxMessage.c,v 1.1 87/02/24 11:51:01 swick Exp $ SPRITE (Berkeley)";
#endif not lint

#include <X/Xlib.h>
#include "sprite.h"
#include "char.h"
#include "io.h"
#include "string.h"
#include "sx.h"
#include "sxInt.h"
#include "varg.h"

/*
 * A notifier is a window that pops up and displays a message and
 * a set of options.  The message is displayed at the top of the
 * window, with the options spaced across the bottom of the window.
 * Each option is drawn in a box, much like a button.  The user
 * reads the message and clicks a mouse button over one of the
 * options.  This file does cursor tracking to see when the mouse
 * is over one of the options.
 */

typedef struct {
    char *text;			/* Text to display for option. */
    int x, y;			/* Location of option within notifier. */
    int width, height;		/* Dimensions of option, including border. */
} Option;

/*
 * 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 "cursors/dot"
#include "cursors/dotMask"
static Cursor cursor;

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

#include "notify.bits"
static Pixmap header;

/*
 * Display dimensions:
 */

static int displayWidth, displayHeight;

static Boolean init = FALSE;

static char *defaultButtonLabel = "Press to unpaste";

/* options */
#define MAXOPTIONS 1

typedef struct {
	Window w;		/* window id of message */
	Option options[MAXOPTIONS];	/* Space for options. */
	int nOptions;		/* number of options this time */
	int nLines;		/* 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 "space"). */
	int optionY;		/* Y-coord of top of option boxes. */
	int currentOption;	/* Option that the mouse is over, or -1. */
	Boolean center;		/* whether or not to center lines in w */
	int width;		/* width of notifier */
	int height;		/* height of notifier */
	char *message;		/* the text of the button */
	FontInfo *fontPtr;	/* text font for displaying message */
	int optionHeight;	/* Size of options. */
	int outerWidth, outerHeight;/* Outside dimensions of w, including space
				 * for shadowed box. */
	} Message;


/*
 * Forward declarations:
 */

void		handleEvent();
extern void	NotifierInit();
extern char *	EndOfLine();
extern void	OptionRedisplay();

/*
 *----------------------------------------------------------------------
 *
 * Sx_Message --
 *
 *	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.
 *
 * Results:
 *	none.
 *
 * Side effects:
 *	A window is created.  It will be deleted later when a button event
 *	happens to an identified region.
 *
 *----------------------------------------------------------------------
 */

int
Sx_Message(parent, x, y, width, message, fontPtr, center, buttonLabel)
    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. */
    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.  */
    FontInfo *fontPtr;		/* Describes font in which to display text
				 * in the notifier.  If NULL, then a default
				 * font will be chosen. */
    Boolean center;		/* TRUE means center each line in the window,
				 * FALSE means left-justify lines. */
    char *buttonLabel;		/* text to replace "Press to unpaste" */
{
    Message *Msg;		/* per-occurrance structure */
    int space;			/* Width of average character in font.  Used
				 * to determine how much space to leave around
				 * edges of message and other places. */
    WindowInfo info;		/* Holds dimensions of parent. */
    int i, trialWidth, inc, height, tmp;
    char *p;

    if (!init) {
	NotifierInit();
    }

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

    if ((Msg = (Message *)malloc( sizeof(Message) )) == NULL)
        Sx_Panic( "Sx_Message: could not allocate a new message structure" );

    /*
     * 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.
     */

    Msg->fontPtr = fontPtr;
    Msg->minOptionLength = 0;
    Msg->optionHeight = fontPtr->height + 2*OPTION_BORDER + 2*OPTION_MARGIN;
    space = fontPtr->width;

    Msg->nOptions = 1;
    Msg->options[0].text = (buttonLabel != NULL)
			     ? buttonLabel
			     : defaultButtonLabel;

    Msg->options[0].width = XStringWidth( Msg->options[0].text, fontPtr, 0, 0 )
    		       	    + 2*space + 2*OPTION_BORDER;

    Msg->options[0].height = Msg->optionHeight;
    Msg->minOptionLength += Msg->options[0].width;

    Msg->minOptionLength += (Msg->nOptions+1) * 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.
     */
    
    Msg->optionY = Msg->optionHeight/2 + SHADOW_TOP;
    if (width <= 0) {
	trialWidth = DisplayWidth()/2;
    } else {
	trialWidth = width;
    }
    for (inc = trialWidth/2; ; inc /= 2) {
	p = message;
	for (Msg->nLines = 0, p = message; *p != 0; Msg->nLines++) {
	    p = EndOfLine(p, fontPtr, trialWidth - 2*space, (int *) NULL);
	    while (*p == ' ') {
		p++;
	    }
	    if (*p == '\n') {
		p++;
	    }
	}
	height = 2*Msg->optionHeight + Msg->nLines*(fontPtr->height + SPACING)
		+ 2*space;
	if (width > 0) {
	    break;
	}
	if (inc < 5) {
	    width = trialWidth;
	    break;
	}
	if ((height + height/2) > trialWidth) {
	    trialWidth += inc;
	} else {
	    trialWidth -= inc;
	}
	if (trialWidth < Msg->minOptionLength) {
	    trialWidth = Msg->minOptionLength;
	}
    }
    Msg->outerWidth = width + SHADOW_LEFT + SHADOW_RIGHT;
    Msg->outerHeight = height + SHADOW_TOP + SHADOW_BOTTOM;

    /*
     * Fill in the rest of the option information, distributing the
     * extra space evenly between the options if there's any to spare.
     */

    Msg->optionPad = (width - Msg->minOptionLength)/(Msg->nOptions+1);
    if (Msg->optionPad < 0) {
	Msg->optionPad = 0;
    }
    tmp = (width - Msg->minOptionLength - (Msg->nOptions+1)*Msg->optionPad)/2
	    + Msg->optionPad + space + SHADOW_LEFT;

    Msg->options[0].x = tmp;
    Msg->options[0].y = Msg->optionY;

    Msg->message = message;
    Msg->width = width;
    Msg->height = height;
    Msg->center = center;

    /*
     * 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.
     */
    
    XQueryWindow(parent, &info);
    if (x < 0) {
	x = (info.width - Msg->outerWidth)/2;
    }
    if (y < 0) {
	y = (info.height - Msg->outerHeight)/2;
    }
    if (parent != RootWindow) {
	int xOffset, yOffset;
	Window dummy;

	XInterpretLocator(parent, &xOffset, &yOffset, &dummy, 0);
	x -= xOffset;
	y -= yOffset;
    }
    displayWidth = DisplayWidth();
    displayHeight = DisplayHeight();
    if ((x + Msg->outerWidth) > displayWidth) {
	x = displayWidth - Msg->outerWidth;
    }
    if ((y + Msg->outerHeight) > displayHeight) {
	y = displayHeight - Msg->outerHeight;
    }
    if (x < 0) {
	x = 0;
    }
    if (y < 0) {
	y = 0;
    }

    /*
     * Create a window for the notifier.
     */
    
    Msg->w = XCreateWindow(RootWindow, x, y, Msg->outerWidth, Msg->outerHeight,
    			   0, BlackPixmap, WhitePixmap);
    if (Msg->w == 0) {
	Sx_Panic("Sx_Message:  couldn't create window.");
    }
    XDefineCursor(Msg->w, cursor);
    XStoreName(Msg->w, "Message");
    XSelectInput(Msg->w, MouseMoved|ButtonPressed|ExposeWindow);

    Msg->currentOption = -1;

    Sx_HandlerCreate(Msg->w, MouseMoved|ButtonPressed|ExposeWindow,
		     handleEvent, (ClientData)Msg);

    XMapWindow(Msg->w);

}

/*
 *----------------------------------------------------------------------
 *
 * 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(string, fontPtr, width, extraPtr)
    register char *string;	/* String to be displayed. */
    register FontInfo *fontPtr;	/* Describes font in which string is 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. */
{
    char *lastWordBreak = NULL;
    int lastWidth, charSize;
    register char c;

    for (c = *string; c != 0; string++, c = *string) {
	if (Char_IsSpace(c)) {
	    lastWordBreak = string;
	    lastWidth = width;
	}
	if (c == '\n') {
	    goto done;
	}
	if ((c >= fontPtr->firstchar) && (c <= fontPtr->lastchar)) {
	    if (fontPtr->fixedwidth) {
		charSize = fontPtr->width;
	    } else {
		charSize = fontPtr->widths[c-fontPtr->firstchar];
	    }
	}
	width -= charSize;
	if (width < 0) {
	    if (lastWordBreak != NULL) {
		goto done;
	    }
	    width += charSize;
	    break;
	}
    }
    lastWordBreak = string;
    lastWidth = width;

    done:
    if (extraPtr != NULL) {
	*extraPtr = lastWidth;
    }
    return lastWordBreak;
}

/*
 *----------------------------------------------------------------------
 *
 * OptionRedisplay --
 *
 *	This procedure redraws a single option button.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Information gets drawn on the screen.
 *
 *----------------------------------------------------------------------
 */

static void
OptionRedisplay(w, optionPtr, fontPtr, reverse, space)
    Window w;			/* Window in which to draw option. */
    register Option *optionPtr;	/* Option to redraw. */
    FontInfo *fontPtr;		/* Font in which to draw. */
    Boolean reverse;		/* TRUE means draw option as white letters
				 * on black background.  FALSE means
				 * reverse. */
    int space;			/* Blank space to leave on each end of text. */
{
    if (reverse) {
	XPixSet(w, optionPtr->x, optionPtr->y, optionPtr->width,
		optionPtr->height, BlackPixel);
	XText(w, optionPtr->x + OPTION_BORDER + space,
		optionPtr->y + OPTION_BORDER + OPTION_MARGIN,
		optionPtr->text, String_Length(optionPtr->text), fontPtr->id,
		WhitePixel, BlackPixel);
    } else {
	XPixSet(w, optionPtr->x, optionPtr->y, optionPtr->width,
		optionPtr->height, WhitePixel);
	XPixSet(w, optionPtr->x, optionPtr->y, optionPtr->width,
		OPTION_BORDER, BlackPixel);
	XPixSet(w, optionPtr->x, optionPtr->y, OPTION_BORDER,
		optionPtr->height, BlackPixel);
	XPixSet(w, optionPtr->x + optionPtr->width - OPTION_BORDER,
		optionPtr->y, OPTION_BORDER, optionPtr->height, BlackPixel);
	XPixSet(w, optionPtr->x, optionPtr->y + optionPtr->height
		- OPTION_BORDER, optionPtr->width, OPTION_BORDER, BlackPixel);
	XText(w, optionPtr->x + OPTION_BORDER + space,
		optionPtr->y + OPTION_BORDER + OPTION_MARGIN,
		optionPtr->text, String_Length(optionPtr->text),
		fontPtr->id, BlackPixel, WhitePixel);
    }
}

/*
 *----------------------------------------------------------------------
 *
 * 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()
{
    Bitmap tmp;
    int width, height;

    cursor = XCreateCursor(dot_width, dot_height, dot_bits,
	    dotMask_bits, dot_x_hot, dot_y_hot,
	    BlackPixel, WhitePixel, GXcopy);

    header = 0;
    XQueryTileShape(notify_width, notify_height, &width, &height);
    if ((width <= 16) && (height <= 16)) {
	tmp = XStoreBitmap(width, height, notify_bits);
	if (tmp != 0) {
	    header = XMakePixmap(tmp, BlackPixel, WhitePixel);
	    XFreeBitmap(tmp);
	}
    }
    if (header == 0) {
	header = WhitePixmap;
    }

    displayWidth = DisplayWidth();
    displayHeight = DisplayHeight();

    if (cursor == 0) {
	Sx_Panic("Sx_Message: couldn't initialize cursor.");
    }

    init = TRUE;
}


/*
 *----------------------------------------------------------------------
 *
 * handleEvent --
 *
 *	This procedure is called by Sx to handle X events in the
 *	the message window.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Various Xlib calls to update the screen.
 *
 *----------------------------------------------------------------------
 */

static void
handleEvent( clientData, eventPtr )
    ClientData		clientData;	/* ptr to Message structure */
    XMouseMovedEvent	*eventPtr;
{
	Message *Msg = (Message *)clientData;
	int i, y;
	int space = Msg->fontPtr->width;
	Window w = Msg->w;
	FontInfo *fontPtr = Msg->fontPtr;
	register Option *o;
	char *p;

	switch ((int) eventPtr->type) {

	    case MouseMoved:
		
		/*
		 * See if the mouse is over an option.  If so, display
		 * the option in reverse video.
		 */
		
		if ((eventPtr->y >= Msg->optionY) && (eventPtr->y <=
			Msg->optionY + Msg->options[0].height)) {
		    for (i = 0; i < Msg->nOptions; i++) {
			o = &Msg->options[i];
			if ((eventPtr->x < o->x) || (eventPtr->x > o->x + o->width)) {
			    continue;
			}
			goto newOption;
		    }
		}
		i = -1;
		newOption:
		if (i != Msg->currentOption) {
		    if (Msg->currentOption != -1) {
			OptionRedisplay(w,
				&Msg->options[Msg->currentOption],
				fontPtr, FALSE, space);
		    }
		    if (i != -1) {
			OptionRedisplay(Msg->w, &Msg->options[i], fontPtr,
					TRUE, space);
		    }
		    Msg->currentOption = i;
		}
		break;
	    
	    case ButtonPressed:
		if (Msg->currentOption >= 0) {
		    
		    /*
		     * Flash the button before returning.
		     */
		    
		    for (i = 0; i < 2; i++) {
			OptionRedisplay(w, &Msg->options[Msg->currentOption],
				fontPtr, FALSE, space);
			SxFlashWait();
			OptionRedisplay(w, &Msg->options[Msg->currentOption],
				fontPtr, TRUE, space);
			SxFlashWait();
		    }
		    free(Msg);
		    XDestroyWindow(w);
		    XFlush();
		    return;
		}
		break;
	    
	    case ExposeWindow:

		/*
		 * Clear the background and draw the shadow.
		 */

		XPixSet(w, 0, 0, Msg->outerWidth, Msg->outerHeight, WhitePixel);
		SxDrawShadow(w, 0, 0, Msg->width, Msg->height, BlackPixel, 0);

		/*
		 * Display the message, one line at a time.
		 */
    
		p = Msg->message;
		for (y = 2*Msg->optionHeight + space + SHADOW_TOP; ;
			y += SPACING + fontPtr->height) {
		    char *next;
		    int extraSpace;

		    if (*p == 0) {
			break;
		    }
		    next = EndOfLine(p, fontPtr, Msg->width - 2*space,
			    &extraSpace);
		    if (Msg->center) {
			extraSpace /= 2;
		    } else {
			extraSpace = 0;
		    }
		    if (next != p) {
			XText(w, extraSpace + space + SHADOW_LEFT, y, p,
				next-p, fontPtr->id, BlackPixel, WhitePixel);
		    }
		    
		    /*
		     * 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++;
		    }
		}
		
		/*
		 * Display the options.
		 */
		
		XTileSet(w, SHADOW_LEFT, SHADOW_TOP, Msg->width,
			2*Msg->optionHeight, header);
		XLine(w, SHADOW_LEFT, 2*Msg->optionHeight + SHADOW_TOP - 1,
			Msg->width, 2*Msg->optionHeight + SHADOW_TOP - 1, 1, 1,
			BlackPixel, GXcopy, AllPlanes);
		for (i = 0; i < Msg->nOptions; i++) {
		    OptionRedisplay(w, &Msg->options[i], fontPtr,
			    i == Msg->currentOption, space);
		}
		break;
	}
}