/* 
 * sxSelect.c --
 *
 *	This file implements a standard way of dealing with the
 *	current window selection.  It has two major features:
 *	1. Selections come in different formats:  a selection consists
 *	   of a format (which is a character string) and an arbitrary
 *	   bunch of bytes.
 *	2. The selection is accessed by callback procedures.  Rather
 *	   than storing the selection in a common place, the module
 *	   that "owns" the selection provides a procedure to call
 *	   whenever the contents of the selection are needed.
 *
 * Copyright (C) 1986, 1988 Regents of the University of California.
 * Permission to use, copy, modify, and distribute this
 * software and its documentation for any purpose and without
 * fee is hereby granted, provided that the above copyright
 * notice appear in all copies.  The University of California
 * makes no representations about the suitability of this
 * software for any purpose.  It is provided "as is" without
 * express or implied warranty.
 */

#ifndef lint
static char rcsid[] = "$Header: /sprite/src/lib/sx/RCS/sxSelect.c,v 1.3 89/05/08 17:12:53 ouster Exp $ SPRITE (Berkeley)";
#endif not lint

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

/*
 * The information below keeps track of the current selection, if it's
 * owned by this client.
 */

static int weveGotIt = 0;		/* 1 means either this client owns
					 * selection;  if selFetchProc is NULL,
					 * then there is no selection. */
static int (*selFetchProc)() = NULL;	/* Procedure to call to fetch
					 * selection, or NULL. */
static void (*selChangeProc)();		/* Procedure to call when selection
					 * changes. */
static ClientData selClientData;	/* Argument to pass to procedures. */

/*
 * Each client keeps a tiny transparent window, which is used solely
 * for the purpose of signalling between clients.  One client signals
 * another by unmapping its window;  this generates an UnmapWindow
 * event to goose the other client into action.
 */

static Display *signalDisplay;		/* Connection to X server. */
static Window signalWindow = 0;		/* Window that this client uses
					 * to receive signals.  0 means the
					 * window hasn't been created yet. */

/*
 * The atoms below are used to identify properties that are used to
 * hold information involved in passing the selection between clients.
 */

static Atom ownerProperty;	/* Holds 4-byte window id for current
				 * selection owner's signalWindow. */
static Atom requestProperty;	/* Holds request for selection information,
				 * if there is an unprocessed request. */
static Atom answerProperty;	/* Holds response from current selection owner,
				 * if there is an unprocessed answer. */
static Atom stringAtom;		/* Atom corresponding to "STRING". */

static int initialized = 0;

/*
 * Maximum number of bytes of selection that may be requested at once:
 */

#define BYTES_AT_ONCE 1024

/*
 * The following two data structures are used to pass selection
 * information between clients.  Request records are sent to
 * the current selection owner, and it responds with Answer
 * records.
 */

typedef struct {
    char desiredFormat[SX_FORMAT_SIZE];	/* Desired format for selection,
					 * null-terminated. */
    int firstByte;			/* First byte of selection to return.*/
    int numBytes;			/* How many bytes of selection to
					 * return.  A request size of -1
					 * means that selection ownership is
					 * switching to the requesting
					 * client. */
    Window signalWindow;		/* Where to signal when answer has
					 * been placed in ANSWER_BUFFER. */
} Request;

typedef struct {
    char actualFormat[SX_FORMAT_SIZE];	/* Actual format in which selection is
					 * being returned (null-terminated). */
    int numBytes;			/* Number of bytes of selection
					 * enclosed below. */
    char value[BYTES_AT_ONCE];		/* Selection contents. */
} Answer;

/*
 * Internal procedures referenced before they're defined:
 */

static void	SelClaim();
static void	SelInit();
static void	SelMakeSignalWindow();
static void	SelSignalProc();
static int	SelWaitForSignal();

/*
 *----------------------------------------------------------------------
 *
 * Sx_SelectionSet --
 *
 *	Change the selection.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The selection is changed.  All that happens is to save the
 *	name of the procedure to call when the selection is needed.
 *	FetchProc must be prepared to return part or all of the
 *	selection anytime it is called (up until the next time changeProc
 *	is called).  FetchProc has the following structure:
 *
 *	int
 *	fetchProc(clientData, desiredFormat, firstByte, numBytes,
 *	        valuePtr, formatPtr)
 *	    ClientData clientData;
 *	    char *desiredFormat;
 *	    int firstByte;
 *	    int numBytes;
 *	    char *valuePtr;
 *	    char *formatPtr;
 *	{
 *	}
 *
 *	In the calls to fetchProc, clientData will be a copy of
 *	the clientData parameter passed in here.  The other parameters,
 *	plus the return value, all have the same meanings as they do
 *	for Sx_SelectionGet.
 *
 *	ChangeProc is invoked in the following way:
 *
 *	void
 *	changeProc(clientData)
 *	    ClientData clientData;
 *	{
 *	}
 *
 *	The clientData parameter is identical to the clientData parameter
 *	passed in here.  ChangeProc will be invoked exactly once, the
 *	next time Sx_SelectionSet or Sx_SelectionClear is invoked.
 *
 *----------------------------------------------------------------------
 */

void
Sx_SelectionSet(display, fetchProc, changeProc, clientData)
    Display *display;		/* Connection to server. */
    int (*fetchProc)();		/* Procedure to invoke whenever the
				 * contents of the selection are needed. */
    void (*changeProc)();	/* Procedure to invoke the next time the
				 * selection is changed.  This procedure
				 * will be invoked exactly once. */
    ClientData clientData;	/* Arbitrary value to pass to above
				 * procedures. */
{
    if (!initialized) {
	SelInit(display);
    }

    if (!weveGotIt) {
	SelClaim(display);
    } else {
	if (selFetchProc != NULL) {
	    (*selChangeProc)(selClientData);
	}
    }
    selFetchProc = fetchProc;
    selChangeProc = changeProc;
    selClientData = clientData;
}

/*
 *----------------------------------------------------------------------
 *
 * Sx_SelectionClear --
 *
 *	Cancel any current selection.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	From now until the next call to Sx_SelectionSet, there will
 *	be no selection.
 *
 *----------------------------------------------------------------------
 */

void
Sx_SelectionClear(display)
    Display *display;		/* Connection to server. */
{
    if (!initialized) {
	SelInit(display);
    }

    if (!weveGotIt) {
	SelClaim(display);
    } else {
	if (selFetchProc != NULL) {
	    (*selChangeProc)(selClientData);
	}
	selFetchProc = NULL;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * Sx_SelectionGet --
 *
 *	Return part or all of the current selection.
 *
 * Results:
 *	If there is no selection at present, -1 is returned.  Otherwise,
 *	the return value indicates the number of bytes of selection
 *	information stored at *valuePtr (if it is less than numBytes,
 *	then the end of the selection was reached).  *ValuePtr gets
 *	filled in with bytes of the selection,  starting with the
 *	firstByte'th byte in the selection and containing either all
 *	the remaining bytes in the selection or numBytes bytes,
 *	whichever is less.  The selection will be returned in the
 *	closest possible form to what was requested with desiredFormat.
 *	The actual format in which the selection is returned will be
 *	placed at *formatPtr as an ASCII string containing not more
 *	than SX_FORMAT_SIZE characters (including the terminating 0).
 *
 * Side effects:
 *	None.
 *
 * Note:
 *	The formats of selections are completely up to the choice of the
 *	clients that manage them and will probably evolve over time.
 *	The one predefined format is "text":  in this case, the selection
 *	bytes consist of ASCII characters (not null-terminated).
 *
 *----------------------------------------------------------------------
 */

int
Sx_SelectionGet(display, desiredFormat, firstByte, numBytes, valuePtr,
    formatPtr)
    Display *display;		/* Connection to X server. */
    char *desiredFormat;	/* Form in which the selection should
				 * be returned, if possible. */
    int firstByte;		/* Index of the first byte of the selection
				 * that caller is interested in. */
    int numBytes;		/* Maximum number of bytes that may be
				 * stored at *valuePtr. */
    char *valuePtr;		/* Where to store selection information.
				 * Note: the information stored here
				 * will not necessarily be null-terminated.
				 * Depends on format of selection. */
    char *formatPtr;		/* Actual format of returned selection gets
				 * stored here, null-terminated.  Must be
				 * at least SX_FORMAT_SIZE bytes here. */
{
    Window owner;
    char *ownerBuffer;
    Request request;
    Answer *ansPtr;
    int totalCount, actualFormat, bytesAfter, numItems, result;
    Atom actualType;

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

    if (weveGotIt) {
	if (selFetchProc == NULL) {
	    return -1;
	}
	return (*selFetchProc)(selClientData, desiredFormat, firstByte,
		numBytes, valuePtr, formatPtr);
    }

    /*
     * Some other client has the selection.  Fetch it from them using
     * the cut buffers for communication.  Get at most BYTES_AT_ONCE
     * at a time.
     */

    SelMakeSignalWindow(display);
    result = XGetWindowProperty(display,
	    RootWindow(display, DefaultScreen(display)), ownerProperty, 0, 1,
	    False, stringAtom, &actualType, &actualFormat, &numItems,
	    &bytesAfter, &ownerBuffer);
    if (result != 0) {
	return -1;
    }
    if (ownerBuffer != NULL) {
	owner = *((Window *) ownerBuffer);
	XFree(ownerBuffer);
    }
    if ((actualType != stringAtom) || (numItems != sizeof(Window))) {
	return -1;
    }

    (void) strncpy(request.desiredFormat, desiredFormat,
	    SX_FORMAT_SIZE);
    request.desiredFormat[SX_FORMAT_SIZE-1] = 0;
    totalCount = 0;
    while (numBytes > 0) {
	request.firstByte = firstByte;
	request.numBytes = numBytes;
	if (numBytes > BYTES_AT_ONCE) {
	    request.numBytes = BYTES_AT_ONCE;
	}
	request.signalWindow = signalWindow;
	XChangeProperty(display, RootWindow(display, DefaultScreen(display)),
		requestProperty, stringAtom, 8, PropModeReplace,
		(unsigned char *) &request, sizeof(request));
	SxUnmapCarefully(display, owner);
	if (!SelWaitForSignal(display)) {
	    return -1;
	}
	result = XGetWindowProperty(display,
		RootWindow(display, DefaultScreen(display)), answerProperty,
		0, sizeof(Answer), True, stringAtom, &actualType,
		&actualFormat, &numItems, &bytesAfter, (char **) &ansPtr);
	if (result != 0) {
	    return -1;
	}
	if ((actualType != stringAtom) || (actualFormat != 8)
		|| (numItems < (sizeof(Answer) - BYTES_AT_ONCE))) {
	    if (ansPtr != NULL) {
		XFree((char *) ansPtr);
	    }
	    return -1;
	}
	(void) strncpy(formatPtr, ansPtr->actualFormat,
		SX_FORMAT_SIZE);
	formatPtr[SX_FORMAT_SIZE-1] = 0;
	if (ansPtr->numBytes > 0) {
	    bcopy(ansPtr->value, valuePtr, ansPtr->numBytes);
	    valuePtr += ansPtr->numBytes;
	    firstByte += ansPtr->numBytes;
	    totalCount += ansPtr->numBytes;
	    numBytes -= ansPtr->numBytes;
	}
	XFree((char *) ansPtr);

	if (ansPtr->numBytes < request.numBytes) {
	    if (ansPtr->numBytes < 0) {
		return -1;
	    } else {
		break;
	    }
	}
    }
    return totalCount;
}

/*
 *----------------------------------------------------------------------
 *
 * SelMakeSignalWindow --
 *
 *	Creates the dummy signalling window for this process, if it
 *	doesn't already exist.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static void
SelMakeSignalWindow(display)
    Display *display;		/* Connection to X server. */
{
    XSetWindowAttributes atts;

    if (signalWindow != 0) {
	return;
    }
    atts.background_pixmap = None;
    atts.override_redirect = True;
    signalDisplay = display;
    signalWindow = XCreateWindow(display,
	    RootWindow(display, DefaultScreen(display)), 0, 0, 1, 1,
	    0, CopyFromParent, InputOutput, CopyFromParent,
	    CWBackPixmap|CWOverrideRedirect, &atts);
    if (signalWindow == 0) {
	Sx_Panic(display,
		"Sx selection module couldn't create window for signalling.");
    }
    (void) Sx_HandlerCreate(display, signalWindow, StructureNotifyMask,
	    SelSignalProc, (ClientData) NULL);
    XMapWindow(display, signalWindow);
}

/*
 *----------------------------------------------------------------------
 *
 * SelSignalProc --
 *
 *	This procedure is invoked by the Sx scheduler whenever someone
 *	unmaps our signalWindow to request the selection contents.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Stores part of the selection in a cut buffer, and signals the
 *	requester by unmapping its signal window.
 *
 *----------------------------------------------------------------------
 */

	/* ARGSUSED */
static void
SelSignalProc(dummy, eventPtr)
    ClientData dummy;		/* Not used. */
    XUnmapEvent *eventPtr;	/* What happened. */
{
    Request *reqPtr;
    Answer answer;
    int numItems, bytesAfter, actualFormat, length, result;
    Atom actualType;

    if (eventPtr->type != UnmapNotify) {
	return;
    }

    /*
     * Remap the signal window (so we can receive more requests, then
     * grab the request and see what we're to do.
     */
    
    XMapWindow(signalDisplay, signalWindow);
    if (!weveGotIt) {
	return;
    }
    result = XGetWindowProperty(signalDisplay,
	    RootWindow(signalDisplay, DefaultScreen(signalDisplay)),
	    requestProperty, 0, sizeof(Request), True, stringAtom,
	    &actualType, &actualFormat, &numItems, &bytesAfter,
	    (char **) &reqPtr);
    if (result != 0) {
	return;
    }
    if ((actualType != stringAtom) || (actualFormat != 8)
	    || (numItems < (sizeof(Request)))) {
	if (reqPtr != NULL) {
	    XFree((char *) reqPtr);
	}
	return;
    }

    if (reqPtr->numBytes < 0) {
	/*
	 * Someone else is taking the selection.  If this client used
	 * to have it, then call the cleanup procedure.
	 */
	
	if (selFetchProc != NULL) {
	    (*selChangeProc)(selClientData);
	}
	selFetchProc = NULL;
	weveGotIt = 0;
	XFree((char *) reqPtr);
	return;
    }

    /*
     * Someone's requesting part of the selection.  If we've got the
     * selection, then call the client routine that will return selection
     * information, and store the information in a cut buffer (just in
     * case the requesting process forgot to terminate the format name,
     * do it here).
     */
    
    if (selFetchProc == NULL) {
	answer.numBytes = -1;
    } else {
	reqPtr->desiredFormat[SX_FORMAT_SIZE-1] = 0;
	length = reqPtr->numBytes;
	if (length > BYTES_AT_ONCE) {
	    length = BYTES_AT_ONCE;
	}
	answer.numBytes = (*selFetchProc)(selClientData, reqPtr->desiredFormat,
		reqPtr->firstByte, length, answer.value, answer.actualFormat);
	answer.actualFormat[SX_FORMAT_SIZE-1] = 0;
    }
    length = sizeof(Answer) - BYTES_AT_ONCE;
    if (answer.numBytes > 0) {
	length += answer.numBytes;
    }
    XChangeProperty(signalDisplay,
	    RootWindow(signalDisplay, DefaultScreen(signalDisplay)),
	    answerProperty, stringAtom, 8, PropModeReplace,
	    (unsigned char *) &answer, length);
    SxUnmapCarefully(signalDisplay, reqPtr->signalWindow);
    XFree((char *) reqPtr);
}

/*
 *----------------------------------------------------------------------
 *
 * SelClaim --
 *
 *	If we aren't the current selection owner, claim the selection
 *	for us.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The current selection owner is notified that we want the
 *	selection.  "weveGotIt" gets set to 1.
 *
 *----------------------------------------------------------------------
 */

static void
SelClaim(display)
    Display *display;		/* Connection to X server. */
{
    char *ownerBuffer;
    int result, actualFormat, numItems, bytesAfter;
    Atom actualType;
    Window owner;

    if (weveGotIt) {
	return;
    }
    weveGotIt = 1;
    SelMakeSignalWindow(display);

    /*
     * First, notify the previous owner of the selection, if any.
     */

    ownerBuffer = NULL;
    result = XGetWindowProperty(display,
	    RootWindow(display, DefaultScreen(display)), ownerProperty, 0, 1,
	    False, stringAtom, &actualType, &actualFormat, &numItems,
	    &bytesAfter, &ownerBuffer);
    if (ownerBuffer != NULL) {
	owner = *((Window *) ownerBuffer);
	XFree(ownerBuffer);
    }
    if ((result == 0) && (actualType == stringAtom)
	    && (numItems == sizeof(Window))) {
	Request request;

	if (owner == signalWindow) {
	    return;
	}
	request.firstByte = -1;
	request.numBytes = -1;
	XChangeProperty(display,
		RootWindow(display, DefaultScreen(display)),
		requestProperty, stringAtom, 8, PropModeReplace,
		(unsigned char *) &request, sizeof(request));
	SxUnmapCarefully(display, owner);
    }

    /*
     * Store us as the current owner of the selection.
     */

    XChangeProperty(display,
	    RootWindow(display, DefaultScreen(display)),
	    ownerProperty, stringAtom, 8, PropModeReplace,
	    (unsigned char *) &signalWindow, sizeof(Window));
}

/*
 *----------------------------------------------------------------------
 *
 * SelWaitForSignal --
 *
 *	This procedure is called to wait until either a) this client
 *	gets signalled (by someone unmapping its signalWindow) or
 *	b) a timeout occurs.
 *
 * Results:
 *	If a timeout occurred then 0 is returned.  If all's OK
 *	then 1 is returned.
 *
 * Side effects:
 *	The signalWindow gets temporarily unmapped, but this procedure
 *	immediately remaps it.
 *
 *----------------------------------------------------------------------
 */

static int
SelWaitForSignal(display)
    Display *display;		/* Connection to X server. */
{
    XEvent event;
    int mask, numFound;
    struct timeval timeout;

    while (1) {
	if (XCheckWindowEvent(display, signalWindow, -1, &event)) {
	    if (event.type == UnmapNotify) {
		break;
	    }
	    else continue;
	}

	/*
	 * Nothing there right now.  Wait for an event to come in, or
	 * for several seconds to elapse.
	 */

	timeout.tv_sec = 3;
	timeout.tv_usec = 0;
	mask = 1 << ConnectionNumber(display);
	numFound = select(ConnectionNumber(display) + 1,
		&mask, (int *) 0, (int *) 0, &timeout);
	if (numFound <= 0) {
	    return 0;
	}
    }

    /*
     * A signal came in:  remap the signalWindow, then return.
     */
    
    XMapWindow(display, signalWindow);
    return 1;
}

/*
 *----------------------------------------------------------------------
 *
 * SelInit --
 *
 *	Initialize information needed for selection.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Atoms and various other things get set up.
 *
 *----------------------------------------------------------------------
 */

static void
SelInit(display)
    Display *display;		/* Connection to server. */
{
    initialized = 1;

    ownerProperty = XInternAtom(display, "SelectionOwner", False);
    requestProperty = XInternAtom(display, "SelectionRequest", False);
    answerProperty = XInternAtom(display, "SelectionAnswer", False);
    stringAtom = XInternAtom(display, "STRING", False);
}
