/* 
 * tkFocus.c --
 *
 *	This file contains procedures that manage the input
 *	focus for Tk.
 *
 * Copyright (c) 1990-1994 The Regents of the University of California.
 * Copyright (c) 1994 Sun Microsystems, Inc.
 *
 * See the file "license.terms" for information on usage and redistribution
 * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
 */

static char sccsid[] = "@(#) tkFocus.c 1.14 94/12/23 10:25:46";

#include "tkInt.h"
#include "tkPort.h"

/*
 * For each top-level window that has ever received the focus, there
 * is a record of the following type:
 */

typedef struct TkFocusInfo {
    TkWindow *topLevelPtr;	/* Information about top-level window. */
    TkWindow *focusWinPtr;	/* The next time the focus comes to this
				 * top-level, it will be given to this
				 * window. */
    int flags;			/* OR-ed combination of bits;  see below
				 * for definitions. */
    struct TkFocusInfo *nextPtr;/* Next in list of all focus records for
				 * a given application. */
} FocusInfo;

/*
 * Flag bits for FocusInfo structures:
 *
 * GOT_FOCUS -		Non-zero means that focusWinPtr for this structure
 *			currently has the focus for its display.
 * IMPLICIT_FOCUS -	Non-zero means that we claimed the focus implicitly
 *			when the mouse entered the window, so we have to
 *			release the focus implicitly when the mouse leaves
 *			this window again.
 */

#define GOT_FOCUS	1
#define IMPLICIT_FOCUS	2

static int focusDebug = 0;

/*
 * Forward declarations for procedures defined in this file:
 */

static void		FocusCarefully _ANSI_ARGS_((TkWindow *winPtr));
static void		SetFocus _ANSI_ARGS_((TkWindow *winPtr));

/*
 *--------------------------------------------------------------
 *
 * Tk_FocusCmd --
 *
 *	This procedure is invoked to process the "focus" Tcl command.
 *	See the user documentation for details on what it does.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	See the user documentation.
 *
 *--------------------------------------------------------------
 */

int
Tk_FocusCmd(clientData, interp, argc, argv)
    ClientData clientData;	/* Main window associated with
				 * interpreter. */
    Tcl_Interp *interp;		/* Current interpreter. */
    int argc;			/* Number of arguments. */
    char **argv;		/* Argument strings. */
{
    Tk_Window tkwin = (Tk_Window) clientData;
    TkWindow *winPtr = (TkWindow *) clientData;
    TkWindow *newPtr, *focusWinPtr, *topLevelPtr;
    FocusInfo *focusPtr;
    char c;
    int length;

    /*
     * If invoked with no arguments, just return the current focus window.
     */

    if (argc == 1) {
	focusWinPtr = TkGetFocus(winPtr);
	if (focusWinPtr != NULL) {
	    interp->result = focusWinPtr->pathName;
	}
	return TCL_OK;
    }

    /*
     * If invoked with a single argument beginning with "." then focus
     * on that window.
     */

    if ((argc == 2) && (argv[1][0] == '.')) {
	newPtr = (TkWindow *) Tk_NameToWindow(interp, argv[1], tkwin);
	if (newPtr == NULL) {
	    return TCL_ERROR;
	}
	SetFocus(newPtr);
	return TCL_OK;
    }

    length = strlen(argv[1]);
    c = argv[1][1];
    if ((c == 'd') && (strncmp(argv[1], "-displayof", length) == 0)) {
	if (argc != 3) {
	    Tcl_AppendResult(interp, "wrong # args: should be \"",
		    argv[0], " -displayof window\"", (char *) NULL);
	    return TCL_ERROR;
	}
	newPtr = (TkWindow *) Tk_NameToWindow(interp, argv[2], tkwin);
	if (newPtr == NULL) {
	    return TCL_ERROR;
	}
	newPtr = TkGetFocus(newPtr);
	if (newPtr != NULL) {
	    interp->result = newPtr->pathName;
	}
    } else if ((c == 'l') && (strncmp(argv[1], "-lastfor", length) == 0)) {
	if (argc != 3) {
	    Tcl_AppendResult(interp, "wrong # args: should be \"",
		    argv[0], " -lastfor window\"", (char *) NULL);
	    return TCL_ERROR;
	}
	newPtr = (TkWindow *) Tk_NameToWindow(interp, argv[2], tkwin);
	if (newPtr == NULL) {
	    return TCL_ERROR;
	}
	for (topLevelPtr = newPtr; !(topLevelPtr->flags & TK_TOP_LEVEL);
		topLevelPtr = topLevelPtr->parentPtr)  {
	    /* Empty loop body. */
	}
	for (focusPtr = newPtr->mainPtr->focusPtr; focusPtr != NULL;
		focusPtr = focusPtr->nextPtr) {
	    if (focusPtr->topLevelPtr == topLevelPtr) {
		interp->result = focusPtr->focusWinPtr->pathName;
		return TCL_OK;
	    }
	}
	interp->result = topLevelPtr->pathName;
    } else {
	Tcl_AppendResult(interp, "bad option \"", argv[1],
		"\": must be -displayof or -lastfor", (char *) NULL);
	return TCL_ERROR;
    }
    return TCL_OK;
}

/*
 *--------------------------------------------------------------
 *
 * TkFocusFilterEvent --
 *
 *	This procedure is invoked by Tk_HandleEvent when it encounters
 *	a FocusIn, FocusOut, Enter, or Leave event.
 *
 * Results:
 *	A return value of 1 means that Tk_HandleEvent should process
 *	the event normally (i.e. event handlers should be invoked).
 *	A return value of 0 means that this event should be ignored.
 *
 * Side effects:
 *	An additional event may be generated and processed.
 *
 *--------------------------------------------------------------
 */

int
TkFocusFilterEvent(winPtr, eventPtr)
    register TkWindow *winPtr;	/* Window that focus event is directed to. */
    XEvent *eventPtr;		/* FocusIn or FocusOut event. */
{
    FocusInfo *focusPtr;
    int delta;

    /*
     * If winPtr isn't a top-level window than just ignore the event.
     * If it is a top-level window, find its FocusInfo structure, if
     * there is one, and create a new one if there isn't one already.
     */

    if (!(winPtr->flags & TK_TOP_LEVEL)) {
	return 1;
    }
    for (focusPtr = winPtr->mainPtr->focusPtr; focusPtr != NULL;
	    focusPtr = focusPtr->nextPtr) {
	if (focusPtr->topLevelPtr == winPtr) {
	    break;
	}
    }
    if (focusPtr == NULL) {
	focusPtr = (FocusInfo *) ckalloc(sizeof(FocusInfo));
	focusPtr->topLevelPtr = focusPtr->focusWinPtr = winPtr;
	focusPtr->flags = 0;
	focusPtr->nextPtr = winPtr->mainPtr->focusPtr;
	winPtr->mainPtr->focusPtr = focusPtr;
    }

    if (eventPtr->type == FocusIn) {
	if (eventPtr->xfocus.detail != NotifyPointer) {
	    focusPtr->flags |= GOT_FOCUS;
	}

	/*
	 * If the focus arrives at a top-level window (e.g. because
	 * the mouse just moved into the window and the window manager
	 * gave the window the focus), redirect it to the appropriate
	 * child window unless the top-level window is the one that's
	 * supposed to have the focus.  It turns out that the focus
	 * can arrive either from above the top-level or below (e.g.,
	 * if the focus is in a descendant when you click, olvwm will
	 * move the focus back up to the top-level).
	 */

	if ((eventPtr->xfocus.detail == NotifyAncestor)
		|| (eventPtr->xfocus.detail == NotifyNonlinear)
		|| (eventPtr->xfocus.detail == NotifyInferior)) {

	    /*
	     * Special note: don't redirect the focus if we've recently
	     * issued a request to change the focus and the server hasn't
	     * seen that request yet.
	     */

	    delta = eventPtr->xfocus.serial - winPtr->mainPtr->focusSerial;
	    if ((focusPtr->focusWinPtr != winPtr) && (delta >= 0)) {
		if (focusDebug) {
		    printf("redirecting focus to %s\n", winPtr->pathName);
		}
		FocusCarefully(focusPtr->focusWinPtr);
	    }
	}
    } else if (eventPtr->type == FocusOut) {
	if ((eventPtr->xfocus.detail != NotifyPointer)
		&& (eventPtr->xfocus.detail != NotifyInferior)) {
	    focusPtr->flags &= ~GOT_FOCUS;
	}
    } else if (eventPtr->type == EnterNotify) {
	/*
	 * If there is no window manager, or if the window manager isn't
	 * moving the focus around (e.g. the disgusting "NoTitleFocus"
	 * option has been selected in twm), then we won't get FocusIn
	 * or FocusOut events.  Instead, the "focus" field will be set
	 * in an Enter event to indicate that we've already got the focus
	 * when then mouse enters the window (even though we didn't get
	 * a FocusIn event).  Watch for this and grab the focus when it
	 * happens.
	 */

	if (eventPtr->xcrossing.focus && !(focusPtr->flags & GOT_FOCUS)
		&& (eventPtr->xcrossing.detail != NotifyInferior)) {
	    /*
	     * Once again, be careful not to redirect the focus if there
	     * is already an outstanding focus change request made by us
	     * but not yet seen by the server.
	     */

	    delta = eventPtr->xfocus.serial - winPtr->mainPtr->focusSerial;
	    if (delta >= 0) {
		if (focusDebug) {
		    printf("implicit focus on %s\n", winPtr->pathName);
		}
		FocusCarefully(winPtr);
	    }
	    focusPtr->flags |= IMPLICIT_FOCUS;
	}
    } else if (eventPtr->type == LeaveNotify) {
	/*
	 * If the pointer just left a window for which we automatically
	 * claimed the focus on enter, release the focus again.
	 */

	if ((focusPtr->flags & IMPLICIT_FOCUS)
		&& (eventPtr->xcrossing.detail != NotifyInferior)) {
	    focusPtr->flags &= ~IMPLICIT_FOCUS;
	    XSetInputFocus(winPtr->display, PointerRoot, RevertToPointerRoot,
		    CurrentTime);
	}
    }
    return 1;
}

/*
 *----------------------------------------------------------------------
 *
 * SetFocus --
 *
 *	This procedure is invoked to change the focus window for a
 *	given display in a given application.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Event handlers may be invoked to process the change of
 *	focus.
 *
 *----------------------------------------------------------------------
 */

static void
SetFocus(winPtr)
    TkWindow *winPtr;		/* Window that is to be the new focus for
				 * its display and application. */
{
    FocusInfo *focusPtr, *focusPtr2;
    TkWindow *topLevelPtr;
    int somebodyHadFocus;

    /*
     * Find the top-level window for winPtr, then find (or create)
     * a record for the top-level.  While searching through the list
     * of records, clear the GOT_FOCUS flag for any other top-level
     * on the same display, and remember whether this application
     * already had the focus for the display.
     */

    for (topLevelPtr = winPtr; !(topLevelPtr->flags & TK_TOP_LEVEL);
	    topLevelPtr = topLevelPtr->parentPtr)  {
	/* Empty loop body. */
    }
    focusPtr2 = NULL;
    somebodyHadFocus = 0;
    for (focusPtr = winPtr->mainPtr->focusPtr; focusPtr != NULL;
	    focusPtr = focusPtr->nextPtr) {
	if (focusPtr->topLevelPtr == topLevelPtr) {
	    if (focusPtr2 != NULL) {
		panic("SetFocus found two records for the same top-level");
	    }
	    focusPtr2 = focusPtr;
	} else if ((focusPtr->flags & GOT_FOCUS)
		&& (focusPtr->topLevelPtr->dispPtr == winPtr->dispPtr))  {
	    if (somebodyHadFocus) {
		panic("SetFocus found two focus windows for same display");
	    }
	    somebodyHadFocus = 1;
	    focusPtr->flags &= ~GOT_FOCUS;
	}
    }
    if (focusPtr2 != NULL) {
	focusPtr = focusPtr2;
	if ((focusPtr->focusWinPtr == winPtr)
		&& (focusPtr->flags & GOT_FOCUS)) {
	    return;
	}
    } else {
	focusPtr = (FocusInfo *) ckalloc(sizeof(FocusInfo));
	focusPtr->topLevelPtr = topLevelPtr;
	focusPtr->flags = 0;
	focusPtr->nextPtr = winPtr->mainPtr->focusPtr;
	winPtr->mainPtr->focusPtr = focusPtr;
    }

    /*
     * Reset the focus.
     */

    focusPtr->focusWinPtr = winPtr;
    focusPtr->flags |= GOT_FOCUS;
    Tk_MakeWindowExist((Tk_Window) winPtr);
    FocusCarefully(winPtr);
}

/*
 *----------------------------------------------------------------------
 *
 * TkGetFocus --
 *
 *	Given a window, this procedure returns the current focus
 *	window for its application and display.
 *
 * Results:
 *	The return value is a pointer to the window that currently
 *	has the input focus for the specified application and
 *	display, or NULL if none.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

TkWindow *
TkGetFocus(winPtr)
    TkWindow *winPtr;		/* Window that selects an application
				 * and a display. */
{
    FocusInfo *focusPtr;

    for (focusPtr = winPtr->mainPtr->focusPtr; focusPtr != NULL;
	    focusPtr = focusPtr->nextPtr) {
	if ((focusPtr->flags & GOT_FOCUS)
		&& (focusPtr->topLevelPtr->dispPtr == winPtr->dispPtr)) {
	    return focusPtr->focusWinPtr;
	}
    }
    return (TkWindow *) NULL;
}

/*
 *----------------------------------------------------------------------
 *
 * TkFocusDeadWindow --
 *
 *	This procedure is invoked when it is determined that
 *	a window is dead.  It cleans up focus-related information
 *	about the window.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Various things get cleaned up and recycled.
 *
 *----------------------------------------------------------------------
 */

void
TkFocusDeadWindow(winPtr)
    register TkWindow *winPtr;		/* Information about the window
					 * that is being deleted. */
{
    FocusInfo *focusPtr, *prevPtr;
    TkWindow *topLevelPtr;

    /*
     * Find the top-level window for winPtr, then look for a focus
     * record for its top-level.
     */

    for (topLevelPtr = winPtr; !(topLevelPtr->flags & TK_TOP_LEVEL);
	    topLevelPtr = topLevelPtr->parentPtr)  {
	/* Empty loop body. */
    }
    for (prevPtr = NULL, focusPtr = winPtr->mainPtr->focusPtr;
	    focusPtr != NULL;
	    prevPtr = focusPtr, focusPtr = focusPtr->nextPtr) {
	if (focusPtr->topLevelPtr != topLevelPtr) {
	    continue;
	}
	if (winPtr == topLevelPtr) {
	    /*
	     * The top-level window is the one being deleted: free
	     * the focus record and release the focus back to PointerRoot
	     * if we acquired it implicitly.
	     */

	    if (focusPtr->flags & IMPLICIT_FOCUS) {
		if (focusDebug) {
		    printf("releasing focus to root after %s died\n",
			    topLevelPtr->pathName);
		}
		XSetInputFocus(winPtr->display, PointerRoot,
			RevertToPointerRoot, CurrentTime);
	    }
	    if (prevPtr == NULL) {
		winPtr->mainPtr->focusPtr = focusPtr->nextPtr;
	    } else {
		prevPtr->nextPtr = focusPtr->nextPtr;
	    }
	    ckfree((char *) focusPtr);
	    break;
	} else if (focusPtr->focusWinPtr == winPtr) {
	    /*
	     * The deleted window had the focus for its top-level:
	     * move the focus to the top-level itself.
	     */

	    focusPtr->focusWinPtr = topLevelPtr;
	    if ((focusPtr->flags & GOT_FOCUS)
		    && !(topLevelPtr->flags & TK_ALREADY_DEAD)) {
		if (focusDebug) {
		    printf("forwarding focus to %s after %s died\n",
			    topLevelPtr->pathName, winPtr->pathName);
		}
		FocusCarefully(topLevelPtr);
	    }
	}
    }
}

/*
 *----------------------------------------------------------------------
 *
 * FocusCarefully --
 *
 *	Set the input focus to a particular window, ignoring any
 *	X errors that might occur (e.g., BadMatch errors that happen
 *	because the window isn't currently viewable).
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The input focus.
 *
 *----------------------------------------------------------------------
 */

static void
FocusCarefully(winPtr)
    TkWindow *winPtr;		/* Window that is to receive the focus. */
{
    Tk_ErrorHandler handler;

    handler = Tk_CreateErrorHandler(winPtr->display, -1, -1, -1,
	    (Tk_ErrorProc *) NULL, (ClientData) NULL);
    winPtr->mainPtr->focusSerial = NextRequest(winPtr->display);
    if (focusDebug) {
	printf("claiming focus for %s at %d\n", winPtr->pathName,
		(int) winPtr->mainPtr->focusSerial);
    }
    XSetInputFocus(winPtr->display, winPtr->window, RevertToParent,
	    CurrentTime);
    Tk_DeleteErrorHandler(handler);
}
