/* 
 * mxCmdUtils.c --
 *
 *	This file contains a collection of utility procedures that
 *	are used by many of the top-level command procedures for Mx.
 *
 * 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.  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/mx/RCS/mxCmdUtils.c,v 1.38 90/03/29 16:58:34 ouster Exp $ SPRITE (Berkeley)";
#endif not lint


#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <sx.h>

#include <alloca.h>

#include "mx.h"
#include "mxInt.h"

/*
 * Information used for parenthesis matching:
 */

char *(mxOpenParens[]) =  {"(", "{", "[", "<", NULL};
char *(mxCloseParens[]) = {")", "}", "]", ">", NULL};

/*
 * Information used for word-boundary finding:
 */

char mxWordMask[16] =  {0, 0, 0, 0, 0, 0, 0xff, 0x3,
	0xfe, 0xff, 0xff, 0x87, 0xfe, 0xff, 0xff, 0x7};

/*
 * Forward references to procedures defined later in this file:
 */

static void		MxCollapseMotion();

/*
 *----------------------------------------------------------------------
 *
 * MxCheckWritten --
 *
 *	See if closing a particular window will result in file modifications
 *	being lost.  If so, give the user a chance to either abort the
 *	command in progress or write out the file.
 *
 * Results:
 *	TCL_OK is returned if either a) changingAll is zero and there's
 *	another window open on mxwPtr's file, b) mxwPtr's file isn't
 *	dirty, or c) the user doesn't mind losing the changes to
 *	mxwPtr's file.  Otherwise, TCL_ERROR is returned and
 *	mxwPtr->interp->result will point to a message indicating why
 *	it's not safe to continue.
 *
 * Side effects:
 *	If the user asks for it, then the file will get written.
 *
 *----------------------------------------------------------------------
 */

int
MxCheckWritten(mxwPtr, changingAll)
    register MxWindow *mxwPtr;	/* Window that's about to be recycled. */
    int changingAll;		/* Non-zero means all of the windows on
				 * this window's files are about to be
				 * closed.  Zero means only mxwPtr is
				 * about to be closed. */
{
    static char *msg2 = "\"%.40s\" has been modified since the last time \
it was saved, and you're about to lose the changes.  What would you \
like to do?";
    static char *msg3 = "The file in this window has never been written \
to disk, and you're about to lose the changes.  In \
fact, the file doesn't even have a name.  What would you like to do?";
    char msg[300];

    if (*mxwPtr->fileInfoPtr->name == '\0') {
	return TCL_OK;
    }
    if (Mx_FileModified(mxwPtr->fileInfoPtr->file)
	    && (changingAll || ((mxwPtr->nextPtr == NULL)
	    && (mxwPtr == mxwPtr->fileInfoPtr->mxwPtr)))) {
	int option;
	if (*mxwPtr->fileInfoPtr->name != 0) {
	    sprintf(msg, msg2, mxwPtr->fileInfoPtr->name);
	    option = Sx_Notify(mxwPtr->display, mxwPtr->w, -1, -1, 0, msg,
		    mxwPtr->fontPtr, 1, "Save File", "Discard Changes",
		    "Skip command", (char *) NULL);
	    if (option == 0) {
		int result;

		result = Tcl_Eval(mxwPtr->interp, "write", 0, (char **) 0);

		/*
		 * If the file wasn't successfully written, then don't
		 * close the window.
		 */ 
		if (result != TCL_OK) {
		    return result;
		}
		return TCL_OK;
	    } else if (option == 1) {
		return TCL_OK;
	    }
	    mxwPtr->interp->result = "skipped command at your request";
	    return TCL_ERROR;
	} else {
	    option = Sx_Notify(mxwPtr->display, mxwPtr->w, -1, -1, 0, msg3,
		    mxwPtr->fontPtr, 1, "Discard Changes", "Skip command",
		    (char *) NULL);
	    if (option == 1) {
		mxwPtr->interp->result = "skipped command at your request";
		return TCL_ERROR;
	    }
	    return TCL_OK;
	}
    }
    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * MxKeyProc --
 *
 *	This procedure is invoked by the Sx dispatcher whenever a
 *	key is typed in an Mx window.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Can be almost arbitrary.  Depends completely on the key.
 *
 *----------------------------------------------------------------------
 */

void
MxKeyProc(mxwPtr, eventPtr)
    MxWindow *mxwPtr;			/* Keeps track of Mx information for
					 * window. */
    XKeyEvent *eventPtr;		/* Describes key that was pressed. */
{
    char keyString[20], *command, *p;
    int result, numBytes;
    int undo = 1;
    static char insertCommand[] = "insert abcdefghjkl";
    static char rawcmd[] = "!@";
    Window w;
    Display *display;

    if (mxwPtr->flags & DESTROYED) {
	return;
    }

    /*
     * Convert from raw key number to its ASCII equivalent, then pass
     * each equivalent character to the keystroke binder.  Process
     * any commands that are matched this way.
     */

    numBytes = XLookupString(eventPtr, keyString, 20, (KeySym *) NULL,
	    (XComposeStatus *) NULL);
    for (p = keyString ; numBytes > 0; numBytes--, p++) {
	if (eventPtr->state & ControlMask) {
	    *p &= 037;				/* Force control character. */
	}
	if (eventPtr->state & Mod1Mask) {
	    *p |= 0200;				/* Set "meta" bit. */
	}
	if (mxwPtr->quoteFlag) {
	    result = CMD_OK;
	    command = rawcmd;
	    mxwPtr->quoteFlag = 0;
	} else {
	    result = Cmd_MapKey(mxwPtr->cmdTable, *p, &command);
	}
	if (result == CMD_PARTIAL) {
	    continue;
	} else if (result == CMD_UNBOUND) {
	    char msg[100], msg2[30];
	    MxCvtToPrintable(command, 30, msg2);
	    sprintf(msg,
		    "The keystroke sequence \"%s\" has no function.",
		    msg2);
	    MxOutputMsg(mxwPtr, msg);
	    break;
	} else if (result == CMD_OK) {
	    if (*command == '!') {
		undo = 0;
		command++;
	    } else {
		Undo_Mark(mxwPtr->fileInfoPtr->log);
	    }

	    /*
	     * If the command is "@", then generate an insert command for
	     * the last character typed.
	     */

	    if ((command[0] == '@') && (command[1] == 0)) {
		int i = *p & 0xff;

		sprintf(insertCommand, "insert \\%o", i);
		command = insertCommand;
	    }

	    w = mxwPtr->w;
	    display = mxwPtr->display;
	    (void) MxDoCmd(mxwPtr, command);

	    /*
	     * Watch out!  The command could have destroyed the window.
	     */
    
	    if (MxGetMxWindow(display, w) != mxwPtr) {
		return;
	    }

	    if (undo) {
		Undo_Mark(mxwPtr->fileInfoPtr->log);
	    }
	}
    }
}

/*
 *----------------------------------------------------------------------
 *
 * MxMakeMenu --
 *
 *	Duplicate the command strings in an array of menu entries, then
 *	invoke Sx to create a menu.  It's needed in order to make sure
 *	that all of the command strings in all menus, even the initial
 *	default menus, are dynamically allocated.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Memory gets allocated, and a menu gets created.
 *
 *----------------------------------------------------------------------
 */

void
MxMakeMenu(mxwPtr, name, size, info)
    register MxWindow *mxwPtr;		/* Window in which to create menu. */
    char *name;				/* Name of menu. */
    int size;				/* Number of entries in menu. */
    Sx_MenuEntry info[];		/* Menu inforamtion. */
{
    Sx_MenuEntry entries[SX_MAX_MENU_ENTRIES];
    int i;

    for (i = 0; i < size; i++) {
	entries[i] = info[i];
	entries[i].clientData = (ClientData)
		malloc((unsigned) (strlen((char *) info[i].clientData) + 1));
	strcpy((char *) entries[i].clientData, (char *) info[i].clientData);
    }
    (void) Sx_MenuCreate(mxwPtr->display, mxwPtr->menuBar, name, size, entries,
	    mxwPtr->fontPtr, mxwPtr->foreground, mxwPtr->background);
}

/*
 *----------------------------------------------------------------------
 *
 * MxMenuProc --
 *
 *	This procedure is invoked whenever a menu command is invoked
 *	in a Mx window.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Whatever the command does.
 *
 *----------------------------------------------------------------------
 */

void
MxMenuProc(miPtr)
    MxMenuInfo *miPtr;		/* Gives Mx window and command to invoke. */
{
    if (miPtr->mxwPtr->flags & DESTROYED) {
	return;
    }
    Undo_Mark(miPtr->mxwPtr->fileInfoPtr->log);
    (void) MxDoCmd(miPtr->mxwPtr, miPtr->command);

    /*
     * Watch out!  The command could have destroyed the window.
     */

    if (!miPtr->mxwPtr->flags & DESTROYED) {
	Undo_Mark(miPtr->mxwPtr->fileInfoPtr->log);
    }
}

/*
 *----------------------------------------------------------------------
 *
 * MxGetMark --
 *
 *	Given a textual description of a position in a file, return
 *	an Mx_Position that describes the same point.  Marks are
 *	normally represented as two decimal numbers separated by
 *	any number of non-digits;  the first number is the line number
 *	and the second number is the character position within the
 *	line.  In addition, a number of keywords are recognized.  See
 *	the Mx man page (or the code below) for the keywords and what
 *	they mean.
 *
 * Results:
 *	The return value is TCL_OK if the mark was successfully converted
 *	to a position.  In this case, *posPtr will contain the position
 *	corresponding to mark.  If there was a problem (bad syntax for
 *	mark, or selection requested but doesn't exist in the file), TCL_ERROR
 *	is returned, an error message is left at mxwPtr->interp->result, and
 *	*posPtr is undefined.  If the position described by mark doesn't
 *	exist in the file (line number greater than # lines in file, or
 *	char number greater than # chars in the line), then it is rounded
 *	downward to the nearest existing position in the file.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

int
MxGetMark(mxwPtr, mark, posPtr)
    MxWindow *mxwPtr;			/* Window to whose file mark refers,
					 * and in which an error notifier
					 * should be displayed, if needed. */
    char *mark;				/* Textual description of position. */
    register Mx_Position *posPtr;	/* Where to store mark's position. */
{
    register MxFileInfo *fileInfoPtr = mxwPtr->fileInfoPtr;
    int nameLength;
    
    /*
     * Handle first the common case of a numerical mark specification.
     */
     
    if (isdigit(*mark)) {
	char *p;
	int length;

	posPtr->lineIndex = strtoul(mark, &p, 10);
	posPtr->lineIndex--;
	if (posPtr->lineIndex < 0) {
	    posPtr->lineIndex = 0;
	}
	while (!isdigit(*p)) {
	    if (*p == 0) {
		goto error;
	    }
	    p++;
	}
	posPtr->charIndex = atoi(p);
	if (Mx_GetLine(fileInfoPtr->file, posPtr->lineIndex, &length)
		== NULL) {
	    *posPtr = Mx_EndOfFile(fileInfoPtr->file);
	} else {
	    if (posPtr->charIndex >= length) {
		posPtr->charIndex = length-1;
	    }
	}
	return TCL_OK;
    }

    /*
     * Not a numeric specifier.  Now check for the predefined mark names.
     */

    nameLength = strlen(mark);
    if (strncmp(mark, "caret", nameLength) == 0) {
	*posPtr = fileInfoPtr->caretFirst;
	return TCL_OK;
    }
    if (strncmp(mark, "eof", nameLength) == 0) {
	*posPtr = Mx_EndOfFile(fileInfoPtr->file);
	return TCL_OK;
    }
    if (nameLength < 5) {
	nameLength = 5;
    }
    if (strncmp(mark, "sel.left", nameLength) == 0) {
	Mx_Position dummy;
	return MxGetSelRange(mxwPtr, posPtr, &dummy);
    }
    if (strncmp(mark, "sel.right", nameLength) == 0) {
	Mx_Position dummy;
	return MxGetSelRange(mxwPtr, &dummy, posPtr);
    }
    if (strncmp(mark, "top", nameLength) == 0) {
	*posPtr = mxwPtr->linePtr[0].position;
	return TCL_OK;
    }
    if (strncmp(mark, "center", nameLength) == 0) {
	*posPtr = mxwPtr->linePtr[mxwPtr->heightLines/2].position;
	return TCL_OK;
    }
    if (strncmp(mark, "bottom", nameLength) == 0) {
	*posPtr = mxwPtr->linePtr[mxwPtr->heightLines-1].position;
	return TCL_OK;
    }
    if (strncmp(mark, "eof", nameLength) == 0) {
	*posPtr = Mx_EndOfFile(mxwPtr->fileInfoPtr->file);
	return TCL_OK;
    }

    error:
    sprintf(mxwPtr->interp->result,
	    "\"%.100s\" doesn't make sense as a mark",
	    mark);
    return TCL_ERROR;
}

/*
 *----------------------------------------------------------------------
 *
 * MxCvtToPrintable --
 *
 *	Given a keystroke binding that may contain control characters
 *	and/or meta characters, this routine produces a printable version
 *	of the string.
 *
 * Results:
 *	Up to length characters are stored at *result (including the
 *	terminating NULL character).
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

void
MxCvtToPrintable(string, length, result)
    char *string;		/* Binding string to be converted. */
    int length;			/* No. of bytes available at result. */
    char *result;		/* Where to store printable form. */
{
    int chunkSize;
    char chunk[20];
    char *p;

    /*
     * Process the input string one character at a time to do the
     * conversion.
     */

    p = result;
    for ( ; *string != 0; string++) {
	int i;

	/*
	 * Figure out how to represent this particular character.
	 */

	i = *string & 0377;
	if (i <= 040) {
	    if (i == 033) {
		strcpy(chunk, "ESC");
	    } else if (i == '\n') {
		strcpy(chunk, "RET");
	    } else if (i == '\t') {
		strcpy(chunk, "TAB");
	    } else if (i == ' ') {
		strcpy(chunk, "SPACE");
	    } else {
		chunk[0] = 'C';
		chunk[1] = '-';
		chunk[2] = i - 1 + 'a';
		chunk[3] = 0;
	    }
	} else if (i < 0177) {
	    chunk[0] = i;
	    chunk[1] = 0;
	} else if (i == 0177) {
	    sprintf(chunk, "DEL");
	} else if ((i > 0240) && (i < 0377)) {
	    chunk[0] = 'M';
	    chunk[1] = '-';
	    chunk[2] = i & 0177;
	    chunk[3] = 0;
	} else {
	    sprintf(chunk, "%#x", i);
	}

	/*
	 * Add this chunk onto the result string (if it fits), with a
	 * preceding space if this isn't the first chunk.
	 */

	if (p != result) {
	    if (length < 1) {
		break;
	    }
	    *p = ' ';
	    p++;
	    length--;
	}
	chunkSize = strlen(chunk);
	if (length < chunkSize) {
	    strncpy(p, chunk, length);
	    p += length;
	    length = 0;
	    break;
	} else {
	    strcpy(p, chunk);
	    p += chunkSize;
	    length -= chunkSize;
	}
    }

    if (length == 0) {
	p--;
    }
    *p = 0;
}

/*
 *----------------------------------------------------------------------
 *
 * MxMarkParens --
 *
 *	See if position indicates a parenthesis (or part of one).  If it
 *	does, then change the parenthesis highlights for the file to mark
 *	the pair.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The parenthesis highlights for fileInfoPtr may be modified.  Only
 *	the 100 lines on either side of position are searched for a matching
 *	parenthesis, in order to save time when there's no match.
 *
 *----------------------------------------------------------------------
 */

void
MxMarkParens(fileInfoPtr, position)
    register MxFileInfo *fileInfoPtr;		/* File in which to look for
						 * matching parentheses. */
    Mx_Position position;			/* Position that may contain
						 * one parenthesis. */
{
    Mx_Position start, stop, open1, open2, close1, close2;

    start.lineIndex = position.lineIndex - 100;
    start.charIndex = 0;
    stop.lineIndex = position.lineIndex + 100;
    stop.charIndex = 0;
    if (Mx_SearchParen(fileInfoPtr->file, position, start, stop,
	    mxOpenParens, mxCloseParens, &open1, &open2,
	    &close1, &close2)) {
	if (fileInfoPtr->openParenPtr == NULL) {
	    fileInfoPtr->openParenPtr = MxHighlightCreate(fileInfoPtr->mxwPtr,
		    open1, open2, MX_UNDERLINE);
	    fileInfoPtr->closeParenPtr =
		    MxHighlightCreate(fileInfoPtr->mxwPtr, close1, close2,
		    MX_UNDERLINE);
	} else {
	    MxHighlightSetRange(fileInfoPtr->openParenPtr, open1,
		    open2);
	    MxHighlightSetRange(fileInfoPtr->closeParenPtr,
		    close1, close2);
	}
    }
}

/*
 *----------------------------------------------------------------------
 *
 * MxFocusProc --
 *
 *	This procedure is invoked by the Sx focus manager when an
 *	Mx file window gets or loses the focus.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The caret may be turned on or off.  Also, we remember that
 *	we've got the focus.
 *
 *----------------------------------------------------------------------
 */

void
MxFocusProc(mxwPtr, gotFocus)
    register MxWindow *mxwPtr;		/* Window that just got the focus. */
    int gotFocus;			/* 1 means window just got focus, 0
					 * means it just lost it. */
{
    if (gotFocus) {
	mxwPtr->flags |= FOCUS_WINDOW|NEEDS_UPDATE;
    } else {
	mxwPtr->flags &= ~FOCUS_WINDOW;
	MxCaretRedisplayText(mxwPtr->fileInfoPtr);
    }
}

/*
 *----------------------------------------------------------------------
 *
 * MxMouseProc --
 *
 *	Invoked by the Sx dispatcher whenever mouse-related events
 *	occur in an Mx or Tx window.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The caret may be turned on or turned off or moved.  The
 *	selection may change.
 *
 *----------------------------------------------------------------------
 */

void
MxMouseProc(mxwPtr, eventPtr)
    register MxWindow *mxwPtr;		/* Window to which event pertains. */
    register XButtonEvent *eventPtr;	/* What happened. */
{
    register MxFileInfo *fileInfoPtr = mxwPtr->fileInfoPtr;
    int x, y;
    Mx_Position pointer, first, last, anchor;
    static int dragCaret;		/* Should caret drag with pointer?
					 * Set when buttons go down. */
    static int dragSelection;	/* Should selection drag with pointer?
					 * Set when buttons go down. */
    static int button = -1;		/* The button that's currently down
					 * (if any). */
    static MxWindow *lastWindow;	/* Last window in which button was
					 * pressed. */
    static int lastX, lastY;		/* Last location where button was
					 * pressed. */
    static unsigned long lastTime;	/* Time when last button was pressed */
    static int repeatCount;		/* Number of times button has been
					 * pressed in the same place.  Cycles
					 * between 1 and 3. */
    static int anchorRight;		/* 1 means anchor point for
					 * selection is its right side.  0
					 * means it's the left side.  Can't
					 * save anchor position itself because
					 * the selection could float between
					 * button clicks. */
    static int scrollAction;	/* 1 means the shift key was down
					 * when the first button was
					 * pressed. */

    if ((eventPtr->subwindow != NULL)
	    && (eventPtr->subwindow != mxwPtr->fileWindow)) {
	return;
    }

    if (mxwPtr->flags & DESTROYED) {
	goto done;
    }

    /*
     * Collapse mouse motion events, so that when many motion events
     * pile up we immediately skip to the last one.
     */

    if (eventPtr->type == MotionNotify) {
	MxCollapseMotion(eventPtr);
    }

    /*
     * Before checking for "normal" button actions (for selection)
     * check for the shift key.  If it's down, then this is a scrolling
     * action.  Handle it right now and then return.
     */

    if ((eventPtr->state & ShiftMask) || scrollAction) {
	static int lastPosition;	/* The mouse y-position on which the
					 * current screen view is based. */
	if (eventPtr->type == ButtonPress) {
	    if (button != -1) {
		return;
	    }
	    button = eventPtr->button;
	    scrollAction = 1;
	    lastPosition = eventPtr->y;
	} else if (eventPtr->type == ButtonRelease) {
	    if (eventPtr->button == button) {
		scrollAction = 0;
		button = -1;
	    }
	} else if (eventPtr->type == MotionNotify) {
	    int delta, speed;
	    Mx_Position pos, eof;

	    if (eventPtr->state & Button1Mask) {
		speed = 4;
	    } else {
		speed = 15;
	    }
	    delta = ((eventPtr->y - lastPosition)*speed)
		/(mxwPtr->fontPtr->ascent + mxwPtr->fontPtr->descent);
	    lastPosition += (delta*(mxwPtr->fontPtr->ascent
		    + mxwPtr->fontPtr->descent))/speed;
	    if (delta < 0) {
		if (delta > -mxwPtr->heightLines) {
		    pos = mxwPtr->linePtr[-delta].position;
		} else {
		    pos = mxwPtr->linePtr[mxwPtr->heightLines-1].position;
		    delta += mxwPtr->heightLines;
		    pos.lineIndex -= delta;
		    pos.charIndex = 0;
		}
		eof = Mx_EndOfFile(mxwPtr->fileInfoPtr->file);
		if (MX_POS_LESS(eof, pos)) {
		    pos = eof;
		}
		MxGetInWindow(mxwPtr, pos, 0, 1);
	    } else if (delta > 0) {
		if (delta < mxwPtr->heightLines) {
		    pos = mxwPtr->linePtr[mxwPtr->heightLines
			    - 1 - delta].position;
		} else {
		    pos = mxwPtr->linePtr[mxwPtr->heightLines-1].position;
		    delta -= mxwPtr->heightLines - 1;
		    pos.lineIndex -= delta;
		    pos.charIndex = 0;
		    if (pos.lineIndex < 0) {
			pos.lineIndex = 0;
		    }
		}
		MxGetInWindow(mxwPtr, pos, mxwPtr->heightLines-1, 1);
	    }
	}
	return;
    }

    /*
     * (It's a little easier to see what you're pointing to if the
     * mouse doesn't have to be right on top of it to select it.
     * Offset the hot spot to accomplish this).
     */
    
    x = eventPtr->x - 2;
    y = eventPtr->y - 4;

    /*
     * When the left button goes up, search for a parenthesis matching
     * the current position, and highlight it.  If the button is anything
     * but the middle button, then save the commands invoked since the
     * last button press.
     */

    (void) MxFindPosition(mxwPtr, x, y, &pointer);
    if (eventPtr->type == ButtonRelease) {
	if (eventPtr->button == button) {
	    if (button == Button1) {
		MxMarkParens(fileInfoPtr, pointer);
	    }
	    if (button != Button2) {
		MxHistoryNext(mxwPtr, "history", 1);
	    }
	    button = -1;
	}
	Undo_Mark(mxwPtr->fileInfoPtr->log);
	return;
    }

    /*
     * When a button goes down, count how many consecutive clicks there
     * have been in the same place (if it's the left button), and decide
     * whether we're dragging the caret or the selection or both.
     */
    
    if (eventPtr->type == ButtonPress) {
	button = eventPtr->button;
	if (button == Button1) {
	    if ((lastWindow == mxwPtr) && ((eventPtr->time - lastTime) < 500)
		    && ((lastX + 1) >= x) && ((lastX - 1) <= x)
		    && ((lastY + 1) >= y) && ((lastY - 1) <= y)) {
		repeatCount += 1;
		if (repeatCount > 3) {
		    repeatCount = 1;
		}
	    } else {
		repeatCount = 1;
	    }
	    if (eventPtr->state & ControlMask) {
		dragCaret = 0;
		dragSelection = 1;
	    } else {
		dragCaret = 1;
		dragSelection = 0;
	    }
	    if (repeatCount != 1) {
		dragSelection = 1;
	    }
	} else if (button == Button3) {
	    dragSelection = 1;
	    if (eventPtr->state & Button1Mask) {
		dragCaret = 1;
	    } else {
		dragCaret = 0;
	    }
	} else {
	    button = -1;
	}
	lastX = x;
	lastY = y;
	lastTime = eventPtr->time;
	lastWindow = mxwPtr;
    }

    if (button == -1) {
	goto done;			/* We didn't see the down event. */
    }

    if (dragSelection) {

	/*
	 * There are three possibilities when setting the selection:
	 * drag a new selection; drag one edge of the selection with
	 * the caret as the other edge, and drag one edge with the
	 * original selection point as the other edge.
	 */

	if ((button == Button1) || dragCaret) {
	    first = last = anchor = pointer;
	} else {
	    Mx_Position dummy;

	    if ((fileInfoPtr != MxSelectedFile)
		    || (!(mxwPtr->flags & TX_WINDOW)
		    && !(eventPtr->state & ControlMask))) {
		anchor = fileInfoPtr->caretFirst;
		if (MX_POS_LESS(pointer, anchor)) {
		    anchor = Mx_Offset(fileInfoPtr->file, anchor, -1);
		}
	    } else if (anchorRight) {
		(void) MxGetSelRange(mxwPtr, &dummy, &anchor);
	    } else {
		(void) MxGetSelRange(mxwPtr, &anchor, &dummy);
	    }
	    if (MX_POS_LESS(pointer, anchor)) {
		first = pointer;
		last = anchor;
	    } else {
		first = anchor;
		last = pointer;
	    }
	}
	anchorRight = MX_POS_LESS(pointer, anchor);

	/*
	 * Depending on how many clicks there have been, round the selection up:
	 * 1st click:		no rounding
	 * 2nd click:		round to word boundary or parenthesized group
	 * 3rd click:		round out to line boundary
	 */

	switch (repeatCount) {
    
	    case 2: {
		Mx_Position tmp, oldFirst,  oldLast;
    
		tmp.lineIndex = tmp.charIndex = 0;
		oldFirst = first;
		oldLast = last;
		(void) Mx_SearchMask(fileInfoPtr->file, first, tmp,
			mxWordMask, &first);
		(void) Mx_SearchMask(fileInfoPtr->file, last,
			Mx_EndOfFile(fileInfoPtr->file), mxWordMask, &last);
		if ((button == Button1) && MX_POS_EQUAL(first, oldFirst)
			&& MX_POS_EQUAL(last, oldLast)) {
		    Mx_Position tmp1, tmp2;
		    
		    (void) Mx_SearchParen(fileInfoPtr->file, first,
			    Mx_ZeroPosition, Mx_EndOfFile(fileInfoPtr->file),
			    mxOpenParens, mxCloseParens, &first, &tmp1, &tmp2,
			    &last);
		}
		break;
	    }
    
	    case 3: {
		first.charIndex = 0;
		(void) Mx_GetLine(fileInfoPtr->file, last.lineIndex,
			&last.charIndex);
		last.charIndex -= 1;
		break;
	    }
	}
	MxSelectionSet(mxwPtr, first, last);
    }

    if (dragCaret) {
	if (!(mxwPtr->flags & TX_WINDOW)) {
	    if (dragSelection) {
		pointer = first;
	    }
	    MxCaretSetPosition(mxwPtr, pointer, 0);
	}
	Sx_Focus(mxwPtr->display, mxwPtr->fileWindow);
    }

    done:
    return;
}

/*
 *----------------------------------------------------------------------
 *
 * MxCreateWindow --
 *
 *	This utility procedure parses window geometries and creates
 *	top-level Mx and Tx windows.
 *
 * Results:
 *	The return value is the id of the window that was created.
 *	*widthPtr and *heightPtr get filled in with the window's
 *	initial width and height.
 *
 * Side effects:
 *	A window gets created and sizeHints get set up for the window
 *	manager.
 *
 *----------------------------------------------------------------------
 */

Window
MxCreateWindow(display, geometry, fontPtr, extraY, border, background,
	widthPtr, heightPtr)
    Display *display;			/* Connection to X server. */
    char *geometry;			/* User-specified geometry for windowm,
					 * or NULL. */
    XFontStruct *fontPtr;		/* Font that will be used in window. */
    int extraY;				/* Amount of extra vertical space to
					 * allow in addition to what's inside
					 * the file window.  Extra horizontal
					 * space will automatically be left for
					 * the scrollbar. */
    unsigned long border;		/* Color to use for border. */
    unsigned long background;		/* Color to use for background. */
    int *widthPtr;			/* Store window's initial width here */
    int *heightPtr;			/* Store initial height here. */
{
    XSizeHints sizeHints;
    int extraX, flags;
    Window window;

    extraX = Sx_ScrollbarWidth() + 1;
    sizeHints.flags = PResizeInc|PMinSize;
    sizeHints.x = sizeHints.y = 0;
    sizeHints.width_inc = fontPtr->max_bounds.width;
    sizeHints.height_inc = fontPtr->ascent + fontPtr->descent;
    sizeHints.min_width = extraX;
    sizeHints.min_height = extraY;
    sizeHints.width = 80*sizeHints.width_inc + extraX;
    sizeHints.height = 24*sizeHints.height_inc + extraY;
    if (geometry != NULL) {
	flags = XParseGeometry(geometry, &sizeHints.x, &sizeHints.y,
		&sizeHints.width, &sizeHints.height);
	if ((flags & (WidthValue|HeightValue)) == (WidthValue|HeightValue)) {
	    sizeHints.flags |= USSize;
	    sizeHints.width = sizeHints.width*sizeHints.width_inc + extraX;
	    sizeHints.height = sizeHints.height*sizeHints.height_inc + extraY;
	}
	if ((flags & (XValue|YValue)) == (XValue|YValue)) {
	    sizeHints.flags |= USPosition;
	    if (flags & XNegative) {
		sizeHints.x = DisplayWidth(display, DefaultScreen(display))
			+ sizeHints.x - sizeHints.width - 4;
	    }
	    if (flags & YNegative) {
		sizeHints.y = DisplayHeight(display, DefaultScreen(display))
			+ sizeHints.y - sizeHints.height - 4;
	    }
	}
    }
    window = XCreateSimpleWindow(display, DefaultRootWindow(display),
	    sizeHints.x, sizeHints.y, sizeHints.width, sizeHints.height,
	    2, border, background);
    XSetNormalHints(display, window, &sizeHints);
    *widthPtr = sizeHints.width;
    *heightPtr = sizeHints.height;
    return window;
}

/*
 *----------------------------------------------------------------------
 *
 * MxSetWMHints --
 *
 *	This is a utility procedure used during Tx and Mx window
 *	creation to set up icon and other information for a window
 *	manager.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The window manager hints get set for mxwPtr, and the iconPixmap
 *	field of mxwPtr gets filled in.
 *
 *----------------------------------------------------------------------
 */

void
MxSetWMHints(mxwPtr, iconName, defaultPixmap, iconX, iconY)
    register MxWindow *mxwPtr;		/* Window for which icon info is
					 * to get set up. */
    char *iconName;			/* Name of icon file, or NULL if none
					 * specified. */
    Pixmap defaultPixmap;		/* Default pixmap to use if iconName
					 * is NULL (may be None). */
    int iconX;				/* Desired x-coordinate for icon, or
					 * -1 if none specified. */
    int iconY;				/* Desired y-coordinate or -1. */
{
    char iconfile[200];		/* Buffer to hold a complete icon file name. */
    char hostname[100];		/* Buffer to hold the hostname. */
    int status, dummy;
    unsigned int height, width;
    XWMHints hints;
#ifndef ICONDIR
#define ICONDIR "/X/lib/icon"
#endif

    mxwPtr->iconPixmap = defaultPixmap;
    if ((iconName == NULL) && (defaultPixmap == None)) {
	iconName = XGetDefault(mxwPtr->display, "tx", "icon");
    }
    if (iconName != NULL) {

	/*
	 * If the icon name is "localhost", look in a standard directory
	 * for a file named after our host.
	 */

	if (strcmp(iconName, "localhost") == 0) {
	    char *dot;

	    gethostname(hostname, sizeof(hostname));
	    dot = strchr(hostname, '.');
	    if (dot != NULL) {
		*dot = '\0';
	    }
	    sprintf(iconfile, "%.100s/%s", ICONDIR, hostname);
	    iconName = iconfile;
	}
	status = XReadBitmapFile(mxwPtr->display, mxwPtr->w, iconName,
		&width, &height, &mxwPtr->iconPixmap, &dummy, &dummy);
	if (status != BitmapSuccess) {
	    mxwPtr->iconPixmap = NULL;
	}
    }

    hints.flags = InputHint;
    if (mxwPtr->iconPixmap != None) {
	hints.icon_pixmap = mxwPtr->iconPixmap;
	hints.flags |= IconPixmapHint;
    }
    hints.input = True;
    if ((iconX != -1) && (iconY != -1)) {
	hints.icon_x = iconX;
	hints.icon_y = iconY;
	hints.flags |= IconPositionHint;
    }
    XSetWMHints(mxwPtr->display, mxwPtr->w, &hints);
}

/*
 *----------------------------------------------------------------------
 *
 * MxCollapseMotion --
 *
 *	Given a MouseMotion event, this procedure searches the event
 *	queue for any other MouseMotion events.  If any are found that
 *	match the current one in all respects except mouse position,
 *	the pending event is eliminated and its position information
 *	is substituted into *eventPtr.
 *
 * Results:
 *	*EventPtr may be modified.
 *
 * Side effects:
 *	Collapsable mouse motion events are removed from the input
 *	queue.
 *
 *----------------------------------------------------------------------
 */

static int abortCollapse;

static void
MxCollapseMotion(eventPtr)
    XMotionEvent *eventPtr;	/* Event to try to collpase with others. */

{
    extern Bool CollapseProc();
    XEvent dummy;

    abortCollapse = 0;
    while (1) {
	if (XCheckIfEvent(eventPtr->display, &dummy, CollapseProc,
		(char *) eventPtr) == 0) {
	    return;
	}
    }
}

/*
 * Predicate procedure to see if any given event can be collapsed with
 * the given event.  If a non-matching MouseMotion event is found, abort
 * the rest of the search.
 */

	/* ARGSUSED */
Bool
CollapseProc(display, eventPtr, matchEventPtr)
    Display *display;			/* Connection to X server. */
    register XMotionEvent *eventPtr;	/* Potentially-collapsible event. */
    register XMotionEvent *matchEventPtr;
					/* Event to collapse into (the event
					 * passed to MxCollapseMotion). */
{
    if ((eventPtr->type != MotionNotify) || (abortCollapse)) {
	return 0;
    }
    if ((eventPtr->window != matchEventPtr->window)
	    || (eventPtr->root != matchEventPtr->root)
	    || (eventPtr->subwindow != matchEventPtr->subwindow)
	    || (eventPtr->state != matchEventPtr->state)
	    || (eventPtr->is_hint != matchEventPtr->is_hint)
	    || (eventPtr->same_screen != matchEventPtr->same_screen)) {
	abortCollapse = 1;
	return 0;
    }

    /*
     * Collapse the events by replacing info in the original event
     * with information from this later event.
     */

    matchEventPtr->time = eventPtr->time;
    matchEventPtr->x = eventPtr->x;
    matchEventPtr->y = eventPtr->y;
    matchEventPtr->x_root = eventPtr->x_root;
    matchEventPtr->y_root = eventPtr->y_root;
    return 1;
}

/*
 *----------------------------------------------------------------------
 *
 * MxDoCmd --
 *
 *	Execute a given Tcl command in a given window, and display
 *	error information if the command doesn't complete successfully.
 *
 * Results:
 *	Returns the result code from the command:  TCL_OK etc.
 *
 * Side effects:
 *	The command itself will probably have side effects, plus this
 *	procedure will display in the message window the command's
 *	result.
 *
 *----------------------------------------------------------------------
 */

int
MxDoCmd(mxwPtr, command)
    MxWindow *mxwPtr;		/* Context in which to execute command. */
    char *command;		/* Command to execute.  NULL means do
				 * nothing. */
{
    int result;
    char c;

    if (command == NULL) {
	return TCL_OK;
    }
    result = Tcl_Eval(mxwPtr->interp, command, 0, (char **) 0);
    if (result == TCL_OK) {
	if (*mxwPtr->interp->result != 0) {
	    MxOutputMsg(mxwPtr, mxwPtr->interp->result);
	}
	return result;
    }

    if (mxwPtr->flags & DESTROYED) {
	return result;
    }

    /*
     * Just for prettiness' sake, capitalize the first character of
     * the error message.
     */

    c = mxwPtr->interp->result[0];
    if ((c >= 'a') && (c <= 'z')) {
	char *msg;

	msg = alloca((unsigned) (strlen(mxwPtr->interp->result) + 1));
	strcpy(msg, mxwPtr->interp->result);
	msg[0] += 'A' - 'a';
	MxOutputMsg(mxwPtr, msg);
    } else {
	MxOutputMsg(mxwPtr, mxwPtr->interp->result);
    }
    return result;
}


/*
 *----------------------------------------------------------------------
 *
 * MxModTime --
 *
 *	Gets the time of the last modification of the file.
 *
 * Results:
 *	Returns the time of the last modification of the file.
 *	Returns 0 if the modification time can't be obtained,
 *	for instance if the file doesn't exist.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

time_t
MxLastMod(name)
    char *name;			/* File name */
{
    struct stat buf;		/* File status buffer */

    if (stat(name,&buf)<0) {
	return 0;
    }
    else {
	return buf.st_mtime;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * MxGetColor --
 *
 *	Parse a color spec and fill in a color value if we found one.
 *
 * Results:
 *	If colorName is non-NULL, find a color by that name and fill
 *	it in at *resultPtr if found.  If colorName is NULL and
 *	checkDefault is non-zero, then lookup (progName,defaultName)
 *	using the X default mechanism, and use it to find a color
 *	if possible.  If a color is found, it is stored at *resultPtr.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

void
MxGetColor(display, colorName, progName, defaultName, checkDefault, resultPtr)
    Display *display;		/* Connection to X server. */
    char *colorName;		/* Description of color, or NULL. */
    char *progName;		/* Program name to use for Xdefault lookup. */
    char *defaultName;		/* Property name to use for Xdefault lookup. */
    int checkDefault;		/* Zero means ignore progName and defaultName,
				 * non-zero means use 'em to locate a
				 * default value. */
    unsigned long *resultPtr;	/* Where to store color if found. */
{
    XColor color;

    if ((colorName == NULL) && checkDefault) {
	colorName = XGetDefault(display, progName, defaultName);
    }
    if (colorName == NULL) {
	return;
    }
    if (DisplayCells(display, DefaultScreen(display)) <= 2) {
	if ((strcmp(colorName, "black") == 0)
		|| (strcmp(colorName, "Black") == 0)) {
	    *resultPtr = BlackPixel(display, DefaultScreen(display));
	    return;
	} else if ((strcmp(colorName, "white") == 0)
		|| (strcmp(colorName, "White") == 0)) {
	    *resultPtr = WhitePixel(display, DefaultScreen(display));
	    return;
	} else {
	    return;
	}
    }
    if (XParseColor(display,
	    DefaultColormap(display, DefaultScreen(display)), colorName,
	    &color) == 0) {
	return;
    }
    if (XAllocColor(display, 
	    DefaultColormap(display, DefaultScreen(display)), &color) == 0) {
	return;
    }
    *resultPtr = color.pixel;
}
